| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "math" |
| 6 | "strings" |
| 7 | ) |
| 8 | |
| 9 | func buildPentagonSVG(sweetness, acidity, tannin, alcohol, body int, size float64, showLabels bool) string { |
| 10 | cx, cy := size/2.0, size/2.0 |
| 11 | margin := 5.0 |
| 12 | if showLabels { |
| 13 | margin = 30.0 |
| 14 | } |
| 15 | r := size/2.0 - margin |
| 16 | scores := []int{sweetness, acidity, tannin, alcohol, body} |
| 17 | labels := []string{"Sweetness", "Acidity", "Tannin", "Alcohol", "Body"} |
| 18 | angles := make([]float64, 5) |
| 19 | for i := range angles { |
| 20 | angles[i] = (-90.0 + 72.0*float64(i)) * math.Pi / 180.0 |
| 21 | } |
| 22 | |
| 23 | var b strings.Builder |
| 24 | fmt.Fprintf(&b, `<svg viewBox="0 0 %g %g" width="100%%" xmlns="http://www.w3.org/2000/svg">`, size, size) |
| 25 | |
| 26 | for _, pct := range []float64{0.2, 0.4, 0.6, 0.8} { |
| 27 | parts := make([]string, 5) |
| 28 | for i, a := range angles { |
| 29 | parts[i] = fmt.Sprintf("%.1f,%.1f", cx+r*pct*math.Cos(a), cy+r*pct*math.Sin(a)) |
| 30 | } |
| 31 | fmt.Fprintf(&b, `<polygon points="%s" fill="none" stroke="white" stroke-opacity="0.12" stroke-width="0.75"/>`, strings.Join(parts, " ")) |
| 32 | } |
| 33 | |
| 34 | outline := make([]string, 5) |
| 35 | for i, a := range angles { |
| 36 | outline[i] = fmt.Sprintf("%.1f,%.1f", cx+r*math.Cos(a), cy+r*math.Sin(a)) |
| 37 | } |
| 38 | fmt.Fprintf(&b, `<polygon points="%s" fill="none" stroke="white" stroke-opacity="0.25" stroke-width="1"/>`, strings.Join(outline, " ")) |
| 39 | |
| 40 | for _, a := range angles { |
| 41 | fmt.Fprintf(&b, `<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="white" stroke-opacity="0.12" stroke-width="0.75"/>`, |
| 42 | cx, cy, cx+r*math.Cos(a), cy+r*math.Sin(a)) |
| 43 | } |
| 44 | |
| 45 | dataPoints := make([][2]float64, 5) |
| 46 | for i, s := range scores { |
| 47 | d := float64(s) / 5.0 * r |
| 48 | dataPoints[i] = [2]float64{cx + d*math.Cos(angles[i]), cy + d*math.Sin(angles[i])} |
| 49 | } |
| 50 | parts := make([]string, 5) |
| 51 | for i, p := range dataPoints { |
| 52 | parts[i] = fmt.Sprintf("%.1f,%.1f", p[0], p[1]) |
| 53 | } |
| 54 | fmt.Fprintf(&b, `<polygon points="%s" fill="white" fill-opacity="0.08" stroke="white" stroke-width="1.5"/>`, strings.Join(parts, " ")) |
| 55 | |
| 56 | for _, p := range dataPoints { |
| 57 | fmt.Fprintf(&b, `<circle cx="%.1f" cy="%.1f" r="2.5" fill="white"/>`, p[0], p[1]) |
| 58 | } |
| 59 | |
| 60 | if showLabels { |
| 61 | for i, label := range labels { |
| 62 | a := angles[i] |
| 63 | labelDist := r + 18.0 |
| 64 | lx := cx + labelDist*math.Cos(a) |
| 65 | ly := cy + labelDist*math.Sin(a) + 3.5 |
| 66 | fmt.Fprintf(&b, `<text x="%.1f" y="%.1f" fill="white" fill-opacity="0.5" font-size="9" font-family="Commit Mono, monospace" text-anchor="middle">%s</text>`, lx, ly, label) |
| 67 | } |
| 68 | } |
| 69 | b.WriteString("</svg>") |
| 70 | return b.String() |
| 71 | } |
| 72 | |
| 73 | func buildBarsSVG(clarity, colorIntensity, aromaIntensity, noseComplexity int, width float64) string { |
| 74 | barHeight := 4.0 |
| 75 | rowHeight := 22.0 |
| 76 | sectionGap := 14.0 |
| 77 | labelWidth := 100.0 |
| 78 | trackLeft := labelWidth + 4.0 |
| 79 | trackWidth := width - trackLeft - 10.0 |
| 80 | headerSize := 9.0 |
| 81 | |
| 82 | type attr struct { |
| 83 | Label string |
| 84 | Score int |
| 85 | } |
| 86 | sections := []struct { |
| 87 | Name string |
| 88 | Attrs []attr |
| 89 | }{ |
| 90 | {"Appearance", []attr{{"Clarity", clarity}, {"Intensity", colorIntensity}}}, |
| 91 | {"Nose", []attr{{"Aroma", aromaIntensity}, {"Complexity", noseComplexity}}}, |
| 92 | } |
| 93 | totalRows := 0 |
| 94 | for _, s := range sections { |
| 95 | totalRows += len(s.Attrs) |
| 96 | } |
| 97 | totalHeight := float64(len(sections))*(headerSize+8.0) + float64(totalRows)*rowHeight + sectionGap |
| 98 | |
| 99 | var b strings.Builder |
| 100 | fmt.Fprintf(&b, `<svg viewBox="0 0 %g %g" width="100%%" xmlns="http://www.w3.org/2000/svg">`, width, totalHeight) |
| 101 | y := 4.0 |
| 102 | for si, sec := range sections { |
| 103 | if si > 0 { |
| 104 | y += sectionGap |
| 105 | } |
| 106 | fmt.Fprintf(&b, `<text x="0" y="%.1f" fill="white" fill-opacity="0.4" font-size="%g" font-family="Commit Mono, monospace" text-transform="uppercase" letter-spacing="1">%s</text>`, |
| 107 | y+headerSize, headerSize, sec.Name) |
| 108 | y += headerSize + 8.0 |
| 109 | for _, a := range sec.Attrs { |
| 110 | barY := y + (rowHeight-barHeight)/2.0 |
| 111 | fillW := float64(a.Score) / 5.0 * trackWidth |
| 112 | fmt.Fprintf(&b, `<text x="0" y="%.1f" fill="white" fill-opacity="0.5" font-size="9" font-family="Commit Mono, monospace">%s</text>`, |
| 113 | y+rowHeight/2.0+3.0, a.Label) |
| 114 | fmt.Fprintf(&b, `<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="2" fill="white" fill-opacity="0.08"/>`, |
| 115 | trackLeft, barY, trackWidth, barHeight) |
| 116 | if fillW > 0 { |
| 117 | fmt.Fprintf(&b, `<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="2" fill="white" fill-opacity="0.6"/>`, |
| 118 | trackLeft, barY, fillW, barHeight) |
| 119 | } |
| 120 | y += rowHeight |
| 121 | } |
| 122 | } |
| 123 | b.WriteString("</svg>") |
| 124 | return b.String() |
| 125 | } |