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), ) }