Files
inventory-plus-plus/server/ui/charts/bar.go
T
2026-07-18 18:24:37 -06:00

160 lines
3.1 KiB
Go

package charts
import (
"fmt"
"slices"
"strings"
"ruben/inventory2/server/ui/svg"
)
type (
// TODO: rename to BarChart
Bar struct {
values []BarValue
classes barChartClasses
}
BarValue struct {
Label string
Value float64
}
barChartClasses struct {
svg []string
bar []string
}
)
func NewBar(vs ...BarValue) *Bar {
return &Bar{
values: vs,
}
}
func (c *Bar) WithSVGClass(classes ...string) *Bar {
c.classes.svg = append(c.classes.svg, classes...)
return c
}
func (c *Bar) WithBarClass(classes ...string) *Bar {
c.classes.bar = append(c.classes.bar, classes...)
return c
}
func (c *Bar) SVG() svg.MarshalerReader {
const (
minX = 0
minY = 0
maxX = 100
maxY = 100
rangeX = maxX - minX
rangeY = maxY - minY
)
s := svg.NewSVG().
Attr(
svg.Style{
"width": "100%",
"height": "auto",
"padding": "1em",
"border-width": "2px",
},
svg.ViewBox{
X: minX,
Y: minY,
Width: maxX - minX,
Height: maxY - minY,
},
svg.Class(
strings.Join(append(
[]string{"rounded-lg", "border-border"},
c.classes.bar...,
), " "),
),
)
if len(c.classes.svg) != 0 {
s = s.Attr(svg.Class(strings.Join(c.classes.svg, " ")))
}
if len(c.values) > 0 {
minV := slices.MinFunc(c.values, func(a, b BarValue) int {
if a.Value < b.Value {
return -1
}
if a.Value > b.Value {
return 1
}
return 0
})
maxV := slices.MaxFunc(c.values, func(a, b BarValue) int {
if a.Value < b.Value {
return -1
}
if a.Value > b.Value {
return 1
}
return 0
})
rangeV := maxV.Value - minV.Value
spacePerBar := float64(rangeX) / float64(len(c.values))
barPadding := 0.025 * spacePerBar
barWidth := spacePerBar - (2 * barPadding)
fontSize := barWidth / 2
for i, v := range c.values {
scaledV := (v.Value - minV.Value) * rangeY / rangeV
barX := float64(i)*spacePerBar + barPadding
s = s.Child(
new(svg.Rect).
Attr(
svg.NewLength(barX).AsX(),
svg.NewLength(maxY-scaledV).AsY(),
svg.NewLength(barWidth).AsWidth(),
svg.NewLength(scaledV).AsHeight(),
svg.Class(
strings.Join(append(
[]string{"fill-accent"},
c.classes.bar...,
), " "),
),
),
svg.NewText(fmt.Sprint(v.Value)).
Attr(
svg.NewLength(barX+(barWidth/2)).AsX(),
svg.NewLength(maxY-scaledV+fontSize).AsY(),
svg.Fill("var(--accent-foreground)"),
svg.TextAnchorMiddle,
// svg.NewLength(fontSize).AsFontSize(),
svg.Style{
"font-size": fmt.Sprintf("%vpx", fontSize),
},
),
)
if v.Label != "" {
x := barX + (barWidth / 2)
y := maxY - fontSize
s = s.Child(
svg.NewText(v.Label).
Attr(
svg.NewLength(x).AsX(),
svg.NewLength(y).AsY(),
svg.Fill("var(--accent-foreground)"),
//svg.NewLength(fontSize).AsFontSize(),
svg.Style{
"font-size": fmt.Sprintf("%vpx", fontSize),
},
svg.DominantBaselineCentral,
svg.TransformRotate(-90).About(x, y),
),
)
}
}
}
return s
}