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 }