This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
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),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user