mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-04 04:23:02 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
package scorecard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/yourorg/devour/internal/quality"
|
||||
)
|
||||
|
||||
type Dimension struct {
|
||||
Name string
|
||||
Score float64
|
||||
Strict float64
|
||||
Count int
|
||||
}
|
||||
|
||||
type ScorecardData struct {
|
||||
ProjectName string
|
||||
Version string
|
||||
OverallScore float64
|
||||
StrictScore float64
|
||||
Grade string
|
||||
FindingsTotal int
|
||||
FindingsOpen int
|
||||
LastScan time.Time
|
||||
Dimensions []Dimension
|
||||
FindByType map[string]int
|
||||
FindByTier map[string]int
|
||||
}
|
||||
|
||||
func Generate(data *ScorecardData, outputPath string) error {
|
||||
width := 780 * Scale
|
||||
leftPanelWidth := 260 * Scale
|
||||
frameInset := 5 * Scale
|
||||
|
||||
rowCount := len(data.Dimensions)
|
||||
if rowCount < 4 {
|
||||
rowCount = 4
|
||||
}
|
||||
cols := 2
|
||||
rowsPerCol := (rowCount + cols - 1) / cols
|
||||
rowH := 20 * Scale
|
||||
tableContentH := 14*Scale + 4*Scale + 6*Scale + rowsPerCol*rowH
|
||||
contentH := max(tableContentH+28*Scale, 150*Scale)
|
||||
height := 12*Scale + contentH
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
dc := NewDrawContext(img, Scale)
|
||||
|
||||
dc.FillBackground(BG)
|
||||
dc.DrawDoubleFrame(0, 0, width-1, height-1, FRAME, BORDER, 2*Scale, 1)
|
||||
|
||||
contentTop := frameInset + Scale
|
||||
contentBot := height - frameInset - Scale
|
||||
contentMidY := (contentTop + contentBot) / 2
|
||||
dividerX := leftPanelWidth
|
||||
|
||||
drawLeftPanel(dc, data, frameInset+11*Scale, dividerX-11*Scale, contentTop+4*Scale, contentBot-4*Scale)
|
||||
dc.DrawVertRuleWithOrnament(dividerX, contentTop+12*Scale, contentBot-12*Scale, contentMidY, BORDER, ACCENT)
|
||||
drawRightPanel(dc, data, dividerX+11*Scale, width-frameInset-11*Scale, contentTop+4*Scale, contentBot-4*Scale)
|
||||
|
||||
dir := filepath.Dir(outputPath)
|
||||
if dir != "" {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := png.Encode(f, img); err != nil {
|
||||
return fmt.Errorf("failed to encode PNG: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func drawLeftPanel(dc *DrawContext, data *ScorecardData, lpLeft, lpRight, lpTop, lpBot int) {
|
||||
lpCenter := (lpLeft + lpRight) / 2
|
||||
panelWidth := lpRight - lpLeft
|
||||
panelHeight := lpBot - lpTop
|
||||
|
||||
dc.DrawRoundedRect(lpLeft, lpTop, panelWidth, panelHeight, 4*Scale, BGScore)
|
||||
dc.DrawRect(lpLeft, lpTop, lpRight, lpBot, BORDER, 1)
|
||||
|
||||
versionText := "version unknown"
|
||||
if data.Version != "" {
|
||||
versionText = "v" + data.Version
|
||||
}
|
||||
fontVersion := GetFont()
|
||||
versionW, _, versionOffY := dc.TextBounds(versionText, fontVersion)
|
||||
versionY := lpTop + 12*Scale - versionOffY
|
||||
dc.DrawText(versionText, lpCenter-versionW/2, versionY, fontVersion, DIM)
|
||||
|
||||
title := "DEVOUR SCORE"
|
||||
fontTitle := GetFont()
|
||||
titleW, titleH, _ := dc.TextBounds(title, fontTitle)
|
||||
titleY := lpTop + 28*Scale
|
||||
dc.DrawText(title, lpCenter-titleW/2, titleY, fontTitle, TEXT)
|
||||
|
||||
ruleY := titleY + titleH + 7*Scale
|
||||
dc.DrawRuleWithOrnament(ruleY, lpLeft+28*Scale, lpRight-28*Scale, lpCenter, BORDER, ACCENT)
|
||||
|
||||
scoreText := FmtScore(data.OverallScore)
|
||||
fontBig := GetFont()
|
||||
scoreW, scoreH, scoreOffY := dc.TextBounds(scoreText, fontBig)
|
||||
scoreY := ruleY + 6*Scale + 7*Scale - scoreOffY
|
||||
scoreColor := GetScoreColor(int(data.OverallScore))
|
||||
dc.DrawText(scoreText, lpCenter-scoreW/2, scoreY, fontBig, scoreColor)
|
||||
|
||||
strictLabel := "strict"
|
||||
strictValue := FmtScore(data.StrictScore) + "%"
|
||||
fontStrictLabel := GetFont()
|
||||
fontStrictVal := GetFont()
|
||||
labelW, _, labelOffY := dc.TextBounds(strictLabel, fontStrictLabel)
|
||||
valueW, _, valueOffY := dc.TextBounds(strictValue, fontStrictVal)
|
||||
gap := 5 * Scale
|
||||
strictY := scoreY + scoreH + 6*Scale
|
||||
strictX := lpCenter - (labelW+gap+valueW)/2
|
||||
dc.DrawText(strictLabel, strictX, strictY-labelOffY, fontStrictLabel, DIM)
|
||||
strictColor := GetScoreColorMuted(int(data.StrictScore))
|
||||
dc.DrawText(strictValue, strictX+labelW+gap, strictY-valueOffY, fontStrictVal, strictColor)
|
||||
|
||||
projectName := data.ProjectName
|
||||
if projectName == "" {
|
||||
projectName = "project"
|
||||
}
|
||||
fontProject := GetFont()
|
||||
projectW, projectH, _ := dc.TextBounds(projectName, fontProject)
|
||||
pillPadX := 8 * Scale
|
||||
pillPadY := 3 * Scale
|
||||
pillHeight := projectH + 2*pillPadY
|
||||
pillTop := strictY + projectH + 8*Scale
|
||||
pillLeft := lpCenter - projectW/2 - pillPadX
|
||||
pillRight := lpCenter + projectW/2 + pillPadX
|
||||
dc.DrawRoundedRect(pillLeft, pillTop, pillRight-pillLeft, pillHeight, 3*Scale, BG)
|
||||
dc.DrawRect(pillLeft, pillTop, pillRight, pillTop+pillHeight, BORDER, 1)
|
||||
projectY := pillTop + pillPadY
|
||||
dc.DrawText(projectName, lpCenter-projectW/2, projectY, fontProject, DIM)
|
||||
}
|
||||
|
||||
func drawRightPanel(dc *DrawContext, data *ScorecardData, tableX1, tableX2, tableTop, tableBot int) {
|
||||
fontRow := GetFont()
|
||||
fontStrict := GetFont()
|
||||
rowCount := len(data.Dimensions)
|
||||
|
||||
cols := 2
|
||||
rowsPerCol := (rowCount + cols - 1) / cols
|
||||
gridGap := 8 * Scale
|
||||
gridWidth := (tableX2 - tableX1 - gridGap) / cols
|
||||
rowH := 20 * Scale
|
||||
|
||||
for colIndex := 0; colIndex < cols; colIndex++ {
|
||||
gridX1 := tableX1 + colIndex*(gridWidth+gridGap)
|
||||
gridX2 := gridX1 + gridWidth
|
||||
|
||||
dc.DrawRoundedRect(gridX1, tableTop, gridWidth, tableBot-tableTop, 4*Scale, BGTable)
|
||||
dc.DrawRect(gridX1, tableTop, gridX2, tableBot, BORDER, 1)
|
||||
|
||||
nameColWidth := 120 * Scale
|
||||
valueColGap := 4 * Scale
|
||||
valueColWidth := 34 * Scale
|
||||
totalContentWidth := nameColWidth + valueColGap + valueColWidth + valueColGap + valueColWidth
|
||||
blockLeft := gridX1 + (gridWidth-totalContentWidth)/2
|
||||
nameColX := blockLeft
|
||||
healthColX := nameColX + nameColWidth + valueColGap
|
||||
strictColX := healthColX + valueColWidth + valueColGap + 4*Scale
|
||||
|
||||
thisColRows := rowsPerCol
|
||||
if colIndex == 1 && rowCount%2 != 0 {
|
||||
thisColRows = rowsPerCol - 1
|
||||
}
|
||||
if colIndex*rowsPerCol+thisColRows > rowCount {
|
||||
thisColRows = rowCount - colIndex*rowsPerCol
|
||||
}
|
||||
|
||||
contentHeight := thisColRows * rowH
|
||||
contentTop := (tableTop+tableBot)/2 - contentHeight/2
|
||||
|
||||
_, rowTextH, rowTextOff := dc.TextBounds("Xg", fontRow)
|
||||
|
||||
startIdx := colIndex * rowsPerCol
|
||||
for rowIdx := 0; rowIdx < thisColRows; rowIdx++ {
|
||||
dimIdx := startIdx + rowIdx
|
||||
if dimIdx >= rowCount {
|
||||
break
|
||||
}
|
||||
dim := data.Dimensions[dimIdx]
|
||||
|
||||
bandTop := contentTop + rowIdx*rowH
|
||||
|
||||
if rowIdx%2 == 1 {
|
||||
dc.FillRect(gridX1+1, bandTop, gridWidth-2, rowH, BGRowAlt)
|
||||
}
|
||||
|
||||
textY := bandTop + (rowH-rowTextH)/2 - rowTextOff + Scale
|
||||
|
||||
maxNameWidth := nameColWidth - 2*Scale
|
||||
name := dc.TruncateText(dim.Name, maxNameWidth, fontRow)
|
||||
dc.DrawText(name, nameColX, textY, fontRow, TEXT)
|
||||
|
||||
score := dim.Score
|
||||
if score == 0 {
|
||||
score = 100
|
||||
}
|
||||
scoreText := FmtScore(score) + "%"
|
||||
dc.DrawText(scoreText, healthColX, textY, fontRow, GetScoreColor(int(score)))
|
||||
|
||||
strict := dim.Strict
|
||||
if strict == 0 {
|
||||
strict = score
|
||||
}
|
||||
strictText := FmtScore(strict) + "%"
|
||||
_, strictTextH, strictOff := dc.TextBounds(strictText, fontStrict)
|
||||
strictY := bandTop + (rowH-strictTextH)/2 - strictOff
|
||||
dc.DrawText(strictText, strictColX, strictY, fontStrict, GetScoreColorMuted(int(strict)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FromQualityState creates ScorecardData from quality state
|
||||
func FromQualityState(state *quality.State, projectName, version string) *ScorecardData {
|
||||
data := &ScorecardData{
|
||||
ProjectName: projectName,
|
||||
Version: version,
|
||||
FindingsTotal: len(state.Findings),
|
||||
LastScan: state.LastScan,
|
||||
FindByType: make(map[string]int),
|
||||
FindByTier: make(map[string]int),
|
||||
}
|
||||
|
||||
// Get score from scorecard
|
||||
if state.Scorecard != nil {
|
||||
data.OverallScore = float64(state.Scorecard.TotalScore)
|
||||
data.StrictScore = float64(state.Scorecard.StrictScore)
|
||||
data.FindByType = state.Scorecard.FindingsByType
|
||||
data.FindByTier = make(map[string]int)
|
||||
for sev, count := range state.Scorecard.FindingsByTier {
|
||||
data.FindByTier[fmt.Sprintf("T%d", sev)] = count
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate grade
|
||||
data.Grade = GetScoreGrade(int(data.OverallScore))
|
||||
|
||||
// Count open findings
|
||||
for _, f := range state.Findings {
|
||||
if f.Status == quality.StatusOpen {
|
||||
data.FindingsOpen++
|
||||
}
|
||||
}
|
||||
|
||||
// Build dimensions from findings by type
|
||||
data.Dimensions = buildDimensions(state)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// buildDimensions builds dimension list from quality state
|
||||
func buildDimensions(state *quality.State) []Dimension {
|
||||
dims := []Dimension{}
|
||||
|
||||
byType := make(map[string]*Dimension)
|
||||
|
||||
for _, f := range state.Findings {
|
||||
if f.Status == quality.StatusOpen {
|
||||
if _, exists := byType[f.Type]; !exists {
|
||||
byType[f.Type] = &Dimension{
|
||||
Name: formatDimensionName(f.Type),
|
||||
Score: 100,
|
||||
Count: 0,
|
||||
}
|
||||
}
|
||||
byType[f.Type].Count++
|
||||
byType[f.Type].Score -= float64(f.Severity)
|
||||
if byType[f.Type].Score < 0 {
|
||||
byType[f.Type].Score = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, dim := range byType {
|
||||
dim.Strict = dim.Score
|
||||
dims = append(dims, *dim)
|
||||
}
|
||||
|
||||
sort.Slice(dims, func(i, j int) bool {
|
||||
return dims[i].Count > dims[j].Count
|
||||
})
|
||||
|
||||
if len(dims) > 12 {
|
||||
dims = dims[:12]
|
||||
}
|
||||
|
||||
return dims
|
||||
}
|
||||
|
||||
// formatDimensionName formats a dimension name for display
|
||||
func formatDimensionName(name string) string {
|
||||
// Map internal names to display names
|
||||
nameMap := map[string]string{
|
||||
"complexity": "Complexity",
|
||||
"duplication": "Duplication",
|
||||
"naming": "Naming",
|
||||
"security": "Security",
|
||||
"dead_code": "Dead Code",
|
||||
"unused_import": "Unused Import",
|
||||
"unused_var": "Unused Variable",
|
||||
"god_component": "God Component",
|
||||
"mixed_concerns": "Mixed Concerns",
|
||||
"test_coverage": "Test Coverage",
|
||||
}
|
||||
|
||||
if display, ok := nameMap[name]; ok {
|
||||
return display
|
||||
}
|
||||
|
||||
if len(name) > 0 {
|
||||
return string(name[0]-32) + name[1:]
|
||||
}
|
||||
return name
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package scorecard
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
type DrawContext struct {
|
||||
Img *image.RGBA
|
||||
Scale int
|
||||
}
|
||||
|
||||
func NewDrawContext(img *image.RGBA, scale int) *DrawContext {
|
||||
return &DrawContext{Img: img, Scale: scale}
|
||||
}
|
||||
|
||||
func (dc *DrawContext) S(v int) int {
|
||||
return v * dc.Scale
|
||||
}
|
||||
|
||||
func (dc *DrawContext) FillRect(x, y, w, h int, c color.RGBA) {
|
||||
for dy := 0; dy < h; dy++ {
|
||||
for dx := 0; dx < w; dx++ {
|
||||
px, py := x+dx, y+dy
|
||||
if px >= 0 && px < dc.Img.Bounds().Dx() && py >= 0 && py < dc.Img.Bounds().Dy() {
|
||||
dc.Img.Set(px, py, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *DrawContext) DrawRect(x1, y1, x2, y2 int, c color.RGBA, width int) {
|
||||
for i := 0; i < width; i++ {
|
||||
dc.DrawHLine(x1, y1+i, x2, c)
|
||||
dc.DrawHLine(x1, y2-i, x2, c)
|
||||
dc.DrawVLine(x1+i, y1, y2, c)
|
||||
dc.DrawVLine(x2-i, y1, y2, c)
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *DrawContext) DrawHLine(x1, y, x2 int, c color.RGBA) {
|
||||
if y < 0 || y >= dc.Img.Bounds().Dy() {
|
||||
return
|
||||
}
|
||||
if x1 > x2 {
|
||||
x1, x2 = x2, x1
|
||||
}
|
||||
for x := x1; x <= x2; x++ {
|
||||
if x >= 0 && x < dc.Img.Bounds().Dx() {
|
||||
dc.Img.Set(x, y, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *DrawContext) DrawVLine(x, y1, y2 int, c color.RGBA) {
|
||||
if x < 0 || x >= dc.Img.Bounds().Dx() {
|
||||
return
|
||||
}
|
||||
if y1 > y2 {
|
||||
y1, y2 = y2, y1
|
||||
}
|
||||
for y := y1; y <= y2; y++ {
|
||||
if y >= 0 && y < dc.Img.Bounds().Dy() {
|
||||
dc.Img.Set(x, y, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *DrawContext) DrawRoundedRect(x, y, w, h, r int, c color.RGBA) {
|
||||
dc.FillRect(x+r, y, w-2*r, h, c)
|
||||
dc.FillRect(x, y+r, w, h-2*r, c)
|
||||
for dy := -r; dy <= 0; dy++ {
|
||||
for dx := -r; dx <= 0; dx++ {
|
||||
if dx*dx+dy*dy >= r*r {
|
||||
continue
|
||||
}
|
||||
dc.Img.Set(x+r+dx, y+r+dy, c)
|
||||
dc.Img.Set(x+w-r-1-dx, y+r+dy, c)
|
||||
dc.Img.Set(x+r+dx, y+h-r-1-dy, c)
|
||||
dc.Img.Set(x+w-r-1-dx, y+h-r-1-dy, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *DrawContext) DrawRoundedRectWithOutline(x, y, w, h, r int, fill, outline color.RGBA, outlineWidth int) {
|
||||
dc.DrawRoundedRect(x, y, w, h, r, fill)
|
||||
rr := r - outlineWidth
|
||||
if rr < 0 {
|
||||
rr = 0
|
||||
}
|
||||
for i := 0; i < outlineWidth; i++ {
|
||||
ri := r - i
|
||||
if ri < 0 {
|
||||
ri = 0
|
||||
}
|
||||
dc.DrawHLine(x+ri, y+i, x+w-ri-1, outline)
|
||||
dc.DrawHLine(x+ri, y+h-i-1, x+w-ri-1, outline)
|
||||
dc.DrawVLine(x+i, y+ri, y+h-ri-1, outline)
|
||||
dc.DrawVLine(x+w-i-1, y+ri, y+h-ri-1, outline)
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *DrawContext) DrawDiamond(cx, cy, size int, c color.RGBA) {
|
||||
for dy := -size; dy <= size; dy++ {
|
||||
for dx := -size; dx <= size; dx++ {
|
||||
if abs(dx)+abs(dy) <= size {
|
||||
px, py := cx+dx, cy+dy
|
||||
if px >= 0 && px < dc.Img.Bounds().Dx() && py >= 0 && py < dc.Img.Bounds().Dy() {
|
||||
dc.Img.Set(px, py, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *DrawContext) DrawRuleWithOrnament(y, x1, x2, cx int, lineColor, ornamentColor color.RGBA) {
|
||||
gap := dc.S(8)
|
||||
dc.DrawHLine(x1, y, cx-gap, lineColor)
|
||||
dc.DrawHLine(cx+gap, y, x2, lineColor)
|
||||
dc.DrawDiamond(cx, y, dc.S(3), ornamentColor)
|
||||
}
|
||||
|
||||
func (dc *DrawContext) DrawVertRuleWithOrnament(x, y1, y2, cy int, lineColor, ornamentColor color.RGBA) {
|
||||
gap := dc.S(8)
|
||||
dc.DrawVLine(x, y1, cy-gap, lineColor)
|
||||
dc.DrawVLine(x, cy+gap, y2, lineColor)
|
||||
dc.DrawDiamond(x, cy, dc.S(3), ornamentColor)
|
||||
}
|
||||
|
||||
func (dc *DrawContext) DrawText(text string, x, y int, face font.Face, c color.RGBA) {
|
||||
d := font.Drawer{
|
||||
Dst: dc.Img,
|
||||
Src: &image.Uniform{c},
|
||||
Face: face,
|
||||
Dot: fixed.Point26_6{X: fixed.I(x), Y: fixed.I(y)},
|
||||
}
|
||||
d.DrawString(text)
|
||||
}
|
||||
|
||||
func (dc *DrawContext) DrawCenteredText(text string, cx, y int, face font.Face, c color.RGBA) {
|
||||
advance := font.MeasureString(face, text)
|
||||
x := cx - (advance.Ceil() / 2)
|
||||
dc.DrawText(text, x, y, face, c)
|
||||
}
|
||||
|
||||
func (dc *DrawContext) DrawRightAlignedText(text string, rx, y int, face font.Face, c color.RGBA) {
|
||||
advance := font.MeasureString(face, text)
|
||||
x := rx - advance.Ceil()
|
||||
dc.DrawText(text, x, y, face, c)
|
||||
}
|
||||
|
||||
func (dc *DrawContext) FillBackground(c color.RGBA) {
|
||||
draw.Draw(dc.Img, dc.Img.Bounds(), &image.Uniform{c}, image.Point{}, draw.Src)
|
||||
}
|
||||
|
||||
func (dc *DrawContext) DrawDoubleFrame(x1, y1, x2, y2 int, outerColor, innerColor color.RGBA, outerWidth, innerWidth int) {
|
||||
dc.DrawRect(x1, y1, x2, y2, outerColor, outerWidth)
|
||||
innerX1 := x1 + outerWidth + 2
|
||||
innerY1 := y1 + outerWidth + 2
|
||||
innerX2 := x2 - outerWidth - 2
|
||||
innerY2 := y2 - outerWidth - 2
|
||||
dc.DrawRect(innerX1, innerY1, innerX2, innerY2, innerColor, innerWidth)
|
||||
}
|
||||
|
||||
func (dc *DrawContext) TextWidth(text string, face font.Face) int {
|
||||
return font.MeasureString(face, text).Ceil()
|
||||
}
|
||||
|
||||
func (dc *DrawContext) TextBounds(text string, face font.Face) (width, height, offsetY int) {
|
||||
advance := font.MeasureString(face, text)
|
||||
width = advance.Ceil()
|
||||
metrics := face.Metrics()
|
||||
height = (metrics.Ascent + metrics.Descent).Ceil()
|
||||
offsetY = -metrics.Ascent.Ceil()
|
||||
return
|
||||
}
|
||||
|
||||
func (dc *DrawContext) TruncateText(text string, maxWidth int, face font.Face) string {
|
||||
if dc.TextWidth(text, face) <= maxWidth {
|
||||
return text
|
||||
}
|
||||
ellipsis := "…"
|
||||
ellipsisWidth := dc.TextWidth(ellipsis, face)
|
||||
for len(text) > 0 {
|
||||
text = text[:len(text)-1]
|
||||
if dc.TextWidth(text, face)+ellipsisWidth <= maxWidth {
|
||||
return text + ellipsis
|
||||
}
|
||||
}
|
||||
return ellipsis
|
||||
}
|
||||
|
||||
func GetFont() font.Face {
|
||||
return basicfont.Face7x13
|
||||
}
|
||||
|
||||
func FmtScore(score float64) string {
|
||||
if score == float64(int(score)) {
|
||||
return strconv.Itoa(int(score))
|
||||
}
|
||||
return strconv.FormatFloat(score, 'f', 1, 64)
|
||||
}
|
||||
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package scorecard
|
||||
|
||||
import "image/color"
|
||||
|
||||
// Scale for retina/high-DPI rendering
|
||||
const Scale = 2
|
||||
|
||||
// Theme colors for the scorecard badge - warm earth-tone palette
|
||||
var (
|
||||
// BG is the main background (warm cream)
|
||||
BG = color.RGBA{R: 247, G: 240, B: 228, A: 255}
|
||||
// BGScore is the score panel background
|
||||
BGScore = color.RGBA{R: 240, G: 232, B: 217, A: 255}
|
||||
// BGTable is the table background
|
||||
BGTable = color.RGBA{R: 240, G: 233, B: 220, A: 255}
|
||||
// BGRowAlt is the alternate row background
|
||||
BGRowAlt = color.RGBA{R: 234, G: 226, B: 212, A: 255}
|
||||
// TEXT is the main text color (dark brown)
|
||||
TEXT = color.RGBA{R: 58, G: 48, B: 38, A: 255}
|
||||
// DIM is the dimmed text color (warm gray)
|
||||
DIM = color.RGBA{R: 138, G: 122, B: 102, A: 255}
|
||||
// BORDER is the inner border color (warm tan)
|
||||
BORDER = color.RGBA{R: 192, G: 176, B: 152, A: 255}
|
||||
// ACCENT is the accent color (warm brown)
|
||||
ACCENT = color.RGBA{R: 148, G: 112, B: 82, A: 255}
|
||||
// FRAME is the outer frame color (warm tan)
|
||||
FRAME = color.RGBA{R: 172, G: 152, B: 126, A: 255}
|
||||
)
|
||||
|
||||
// Score grade colors - gradient from sage to rose
|
||||
var (
|
||||
// GradeA is for scores 90-100% (deep sage green)
|
||||
GradeA = color.RGBA{R: 68, G: 120, B: 68, A: 255}
|
||||
// GradeB is for scores 70-89% (olive green)
|
||||
GradeB = color.RGBA{R: 120, G: 140, B: 72, A: 255}
|
||||
// GradeC is for scores 50-69% (yellow-green)
|
||||
GradeC = color.RGBA{R: 145, G: 155, B: 80, A: 255}
|
||||
// GradeD is for scores 30-49% (mustard)
|
||||
GradeD = color.RGBA{R: 180, G: 150, B: 70, A: 255}
|
||||
// GradeF is for scores 0-29% (dusty rose)
|
||||
GradeF = color.RGBA{R: 170, G: 110, B: 90, A: 255}
|
||||
)
|
||||
|
||||
// Muted score colors for strict column (pastel orange/peach shades)
|
||||
var (
|
||||
// GradeAMuted is muted version of GradeA
|
||||
GradeAMuted = color.RGBA{R: 195, G: 160, B: 115, A: 255} // light sandy peach
|
||||
// GradeBMuted is muted version of GradeB
|
||||
GradeBMuted = color.RGBA{R: 200, G: 148, B: 100, A: 255} // warm apricot
|
||||
// GradeCMuted is muted version of GradeC
|
||||
GradeCMuted = color.RGBA{R: 195, G: 125, B: 95, A: 255} // soft coral
|
||||
// GradeDMuted is muted version of GradeD
|
||||
GradeDMuted = color.RGBA{R: 190, G: 130, B: 100, A: 255}
|
||||
// GradeFMuted is muted version of GradeF
|
||||
GradeFMuted = color.RGBA{R: 185, G: 120, B: 100, A: 255}
|
||||
)
|
||||
|
||||
// Severity colors for findings
|
||||
var (
|
||||
SeverityT1Color = color.RGBA{R: 100, G: 180, B: 255, A: 255}
|
||||
SeverityT2Color = color.RGBA{R: 255, G: 200, B: 100, A: 255}
|
||||
SeverityT3Color = color.RGBA{R: 255, G: 140, B: 80, A: 255}
|
||||
SeverityT4Color = color.RGBA{R: 255, G: 80, B: 80, A: 255}
|
||||
)
|
||||
|
||||
func GetGradeColor(grade string) color.RGBA {
|
||||
switch grade {
|
||||
case "A":
|
||||
return GradeA
|
||||
case "B":
|
||||
return GradeB
|
||||
case "C":
|
||||
return GradeC
|
||||
case "D":
|
||||
return GradeD
|
||||
default:
|
||||
return GradeF
|
||||
}
|
||||
}
|
||||
|
||||
func GetGradeColorMuted(grade string) color.RGBA {
|
||||
switch grade {
|
||||
case "A":
|
||||
return GradeAMuted
|
||||
case "B":
|
||||
return GradeBMuted
|
||||
case "C":
|
||||
return GradeCMuted
|
||||
case "D":
|
||||
return GradeDMuted
|
||||
default:
|
||||
return GradeFMuted
|
||||
}
|
||||
}
|
||||
|
||||
func GetScoreGrade(score int) string {
|
||||
switch {
|
||||
case score >= 90:
|
||||
return "A"
|
||||
case score >= 70:
|
||||
return "B"
|
||||
case score >= 50:
|
||||
return "C"
|
||||
case score >= 30:
|
||||
return "D"
|
||||
default:
|
||||
return "F"
|
||||
}
|
||||
}
|
||||
|
||||
func GetScoreColor(score int) color.RGBA {
|
||||
return GetGradeColor(GetScoreGrade(score))
|
||||
}
|
||||
|
||||
func GetScoreColorMuted(score int) color.RGBA {
|
||||
return GetGradeColorMuted(GetScoreGrade(score))
|
||||
}
|
||||
|
||||
func GetSeverityColor(severity int) color.RGBA {
|
||||
switch severity {
|
||||
case 1:
|
||||
return SeverityT1Color
|
||||
case 2:
|
||||
return SeverityT2Color
|
||||
case 3:
|
||||
return SeverityT3Color
|
||||
case 4:
|
||||
return SeverityT4Color
|
||||
default:
|
||||
return DIM
|
||||
}
|
||||
}
|
||||
|
||||
func ScaleValue(v int) int {
|
||||
return v * Scale
|
||||
}
|
||||
Reference in New Issue
Block a user