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

237 lines
3.7 KiB
Go

package charts
import (
"ruben/inventory2/server/ui/svg"
"slices"
)
type (
LineChart struct {
width float64
height float64
foreground string
background string
values []LineValue
max *float64
min *float64
}
LineValue struct {
Label string
Value float64
}
)
func NewLineChart(width, height float64, vs ...LineValue) *LineChart {
return &LineChart{
width: width,
height: height,
values: vs,
}
}
func (c *LineChart) Max(n float64) *LineChart {
c.max = &n
return c
}
func (c *LineChart) Min(n float64) *LineChart {
c.min = &n
return c
}
func (c *LineChart) Foreground(fg string) *LineChart {
c.foreground = fg
return c
}
func (c *LineChart) Background(bg string) *LineChart {
c.background = bg
return c
}
func (c *LineChart) SVG() svg.MarshalerReader {
var (
minVal float64
maxVal float64
)
fg := "black"
if c.foreground != "" {
fg = c.foreground
}
bg := c.background
if c.min != nil {
minVal = *c.min
} else if len(c.values) > 0 {
minVal = slices.MinFunc(c.values, compareLineValues).Value
}
if c.max != nil {
maxVal = *c.max
} else if len(c.values) > 0 {
maxVal = slices.MaxFunc(c.values, compareLineValues).Value
}
svgBg := bg
if svgBg == "" {
svgBg = "auto"
}
s := svg.NewSVG().
Attr(
svg.NewLength(c.width).
AsWidth(),
svg.NewLength(c.height).
AsHeight(),
svg.Style{
// "width": "100%",
// "height": "100%",
"background": svgBg,
},
/*
svg.ViewBox{
X: 0,
Y: minVal,
Width: float64(len(c.values) - 1),
Height: maxVal - minVal,
},
*/
svg.ViewBox{
X: 0,
Y: 0,
Width: c.width,
Height: c.height,
},
svg.PreserveAspectRatio{},
)
g := new(svg.G).
Attr(
// getChartOrientationTransform(c.height),
svg.Transform{
// scale down to provide padding
svg.TransformTranslate{
X: 0.05 * c.width,
Y: 0.05 * c.height,
},
svg.TransformScale{
X: 0.9,
Y: 0.9,
},
// flip
svg.TransformScale{
X: 1,
Y: -1,
},
svg.TransformTranslate{
Y: -c.height,
},
},
)
// lines
pts := make(svg.Points, len(c.values))
for i, p := range c.values {
pts[i] = svg.Point{
X: (c.width / float64(len(c.values)-1)) * float64(i),
Y: ((p.Value - minVal) * c.height) / (maxVal - minVal),
}
}
g = g.Child(
new(svg.Polyline).Attr(
pts,
svg.NewLength(2).
Unit(svg.Px).
AsStrokeWidth(),
svg.VectorEffectNonScalingStroke,
svg.Stroke(fg),
svg.Fill("none"),
),
)
// points
if len(c.values) > 0 {
r := 1.0 / float64(len(c.values))
rx := r
ry := r
for i, p := range c.values {
// dot on the line graph
x := pts[i].X
y := pts[i].Y
ell := new(svg.Ellipse).
Attr(
svg.NewLength(x).
AsCX(),
svg.NewLength(y).
AsCY(),
svg.Percentage(rx).
AsRX(),
svg.Percentage(ry).
AsRY(),
svg.Stroke(fg),
svg.NewLength(2).
Unit(svg.Px).
AsStrokeWidth(),
svg.VectorEffectNonScalingStroke,
)
if bg != "" {
ell = ell.Attr(
svg.Fill(bg),
)
} else {
ell = ell.Attr(
svg.Fill(fg),
)
}
g = g.Child(
ell,
// label
upsideDownCenteredText(p.Label, x, y).
Attr(
svg.NewLength(c.height*0.05).
Unit(svg.Px).
AsFontSize(),
),
)
}
}
return s.Child(g)
}
func compareLineValues(a, b LineValue) int {
if a.Value < b.Value {
return -1
}
if a.Value > b.Value {
return 1
}
return 0
}
func upsideDownCenteredText(txt string, x, y float64) *svg.Text {
return svg.NewText(txt).
Attr(
svg.TextAnchorMiddle,
svg.DominantBaselineMiddle,
svg.NewLength(x).
AsX(),
svg.NewLength(y).
AsY(),
upsideDownTransform(x, y),
)
}