This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
package charts
|
||||
|
||||
import (
|
||||
"ruben/inventory2/server/ui/svg"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
TimeLineChart struct {
|
||||
width float64
|
||||
height float64
|
||||
foreground string
|
||||
background string
|
||||
values []TimeLineValue
|
||||
max *float64
|
||||
min *float64
|
||||
}
|
||||
TimeLineValue struct {
|
||||
Label string
|
||||
Time time.Time
|
||||
Value float64
|
||||
}
|
||||
)
|
||||
|
||||
func NewTimeLineChart(width, height float64, vs ...TimeLineValue) *TimeLineChart {
|
||||
return &TimeLineChart{
|
||||
width: width,
|
||||
height: height,
|
||||
values: validValues(vs),
|
||||
}
|
||||
}
|
||||
|
||||
func validValues(vs []TimeLineValue) []TimeLineValue {
|
||||
values := make([]TimeLineValue, 0, len(vs))
|
||||
for _, v := range vs {
|
||||
if !v.Time.IsZero() {
|
||||
values = append(values, v)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) Max(n float64) *TimeLineChart {
|
||||
c.max = &n
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) Min(n float64) *TimeLineChart {
|
||||
c.min = &n
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) Foreground(fg string) *TimeLineChart {
|
||||
c.foreground = fg
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) Background(bg string) *TimeLineChart {
|
||||
c.background = bg
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) SVG() svg.MarshalerReader {
|
||||
return svg.NewSVG().
|
||||
Attr(
|
||||
svg.NewLength(c.width).
|
||||
AsWidth(),
|
||||
svg.NewLength(c.height).
|
||||
AsHeight(),
|
||||
svg.Style{
|
||||
"background": c.getOuterSVGBackground(),
|
||||
},
|
||||
svg.ViewBox{
|
||||
X: 0,
|
||||
Y: 0,
|
||||
Width: c.width,
|
||||
Height: c.height,
|
||||
},
|
||||
svg.PreserveAspectRatio{},
|
||||
).
|
||||
Child(
|
||||
new(svg.G).
|
||||
Attr(
|
||||
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.85, // a little more padding on the bottom
|
||||
},
|
||||
|
||||
// flip
|
||||
svg.TransformScale{
|
||||
X: 1,
|
||||
Y: -1,
|
||||
},
|
||||
svg.TransformTranslate{
|
||||
Y: -c.height,
|
||||
},
|
||||
},
|
||||
).
|
||||
Child(c.getChildren()...),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) getOuterSVGBackground() string {
|
||||
if c.background != "" {
|
||||
return c.background
|
||||
}
|
||||
return "auto"
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) getChildren() []svg.GChildren {
|
||||
pts := c.getPoints()
|
||||
|
||||
return append(
|
||||
append(
|
||||
[]svg.GChildren{
|
||||
new(svg.Polyline).Attr(
|
||||
pts,
|
||||
svg.NewLength(2).
|
||||
Unit(svg.Px).
|
||||
AsStrokeWidth(),
|
||||
svg.VectorEffectNonScalingStroke,
|
||||
svg.Stroke(c.getStroke()),
|
||||
svg.Fill("none"),
|
||||
),
|
||||
},
|
||||
c.buildDots(pts)...,
|
||||
),
|
||||
c.buildLabels(pts)...,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) getPoints() svg.Points {
|
||||
minVal, _ := c.getMinVal()
|
||||
maxVal, _ := c.getMaxVal()
|
||||
|
||||
minTime, maxTime := c.getMinAndMaxUnixTimes()
|
||||
|
||||
pts := make(svg.Points, len(c.values))
|
||||
for i, p := range c.values {
|
||||
pts[i] = svg.Point{
|
||||
X: c.width * float64(p.Time.UnixNano()-minTime) / float64(maxTime-minTime),
|
||||
Y: ((p.Value - minVal) * c.height) / (maxVal - minVal),
|
||||
}
|
||||
}
|
||||
|
||||
return pts
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) getMinVal() (float64, bool) {
|
||||
if c.min != nil {
|
||||
return *c.min, true
|
||||
}
|
||||
if values := c.values; len(values) > 0 {
|
||||
return slices.MinFunc(values, compareTimeLineValues).Value, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) getMaxVal() (float64, bool) {
|
||||
if c.max != nil {
|
||||
return *c.max, true
|
||||
}
|
||||
if len(c.values) > 0 {
|
||||
return slices.MaxFunc(c.values, compareTimeLineValues).Value, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func compareTimeLineValues(a, b TimeLineValue) int {
|
||||
if a.Value < b.Value {
|
||||
return -1
|
||||
}
|
||||
if a.Value > b.Value {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) getMinAndMaxUnixTimes() (minTime, maxTime int64) {
|
||||
if len(c.values) > 0 {
|
||||
return slices.MinFunc(c.values, compareTimeLineTimeValues).Time.UnixNano(),
|
||||
slices.MaxFunc(c.values, compareTimeLineTimeValues).Time.UnixNano()
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func compareTimeLineTimeValues(a, b TimeLineValue) int {
|
||||
at := a.Time
|
||||
bt := b.Time
|
||||
an := at.UnixNano()
|
||||
bn := bt.UnixNano()
|
||||
if at.IsZero() {
|
||||
an = 0
|
||||
}
|
||||
if bt.IsZero() {
|
||||
bn = 0
|
||||
}
|
||||
return int(an - bn)
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) buildDots(pts svg.Points) []svg.GChildren {
|
||||
if len(c.values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
chn := make([]svg.GChildren, len(c.values))
|
||||
for i, p := range pts {
|
||||
chn[i] = c.buildDot(p)
|
||||
}
|
||||
|
||||
return chn
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) buildDot(p svg.Point) *svg.Circle {
|
||||
return new(svg.Circle).
|
||||
Attr(
|
||||
svg.NewLength(p.X).
|
||||
AsCX(),
|
||||
svg.NewLength(p.Y).
|
||||
AsCY(),
|
||||
svg.NewLength(0.01*min(c.width, c.height)).
|
||||
AsR(),
|
||||
svg.Stroke(c.getStroke()),
|
||||
svg.NewLength(2).
|
||||
Unit(svg.Px).
|
||||
AsStrokeWidth(),
|
||||
svg.VectorEffectNonScalingStroke,
|
||||
c.getDotFill(),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) getDotFill() svg.Fill {
|
||||
if c.background != "" {
|
||||
return svg.Fill(c.background)
|
||||
} else {
|
||||
return svg.Fill(c.getStroke())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) buildLabels(pts svg.Points) []svg.GChildren {
|
||||
if len(c.values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
chn := make([]svg.GChildren, len(c.values))
|
||||
for i, p := range pts {
|
||||
chn[i] = c.buildLabel(c.values[i], p)
|
||||
}
|
||||
|
||||
return chn
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) buildLabel(v TimeLineValue, p svg.Point) *svg.Element[svg.SVGTag, svg.SVGAttribute, svg.SVGChildren] {
|
||||
return svg.NewSVG().
|
||||
Attr(
|
||||
svg.NewLength(p.X).
|
||||
AsX(),
|
||||
svg.NewLength(p.Y).
|
||||
AsY(),
|
||||
svg.NewLength(24).
|
||||
Unit(svg.Px).
|
||||
AsWidth(),
|
||||
svg.NewLength(24).
|
||||
Unit(svg.Px).
|
||||
AsHeight(),
|
||||
svg.Class("group"),
|
||||
svg.Style{
|
||||
"overflow": "visible",
|
||||
"fill": "var(--accent-foreground)",
|
||||
},
|
||||
svg.ViewBox{
|
||||
X: 0,
|
||||
Y: 0,
|
||||
Width: 1,
|
||||
Height: 1,
|
||||
},
|
||||
svg.PreserveAspectRatio{
|
||||
Align: &svg.AlignValue{
|
||||
X: svg.AlignMid,
|
||||
Y: svg.AlignMid,
|
||||
},
|
||||
},
|
||||
).
|
||||
Child(
|
||||
c.buildAlwaysDisplayedLabelText(v, p),
|
||||
new(svg.Rect).
|
||||
Attr(
|
||||
svg.NewLength(0.1).
|
||||
AsStrokeWidth(),
|
||||
svg.Fill("var(--muted)"),
|
||||
svg.Stroke("var(--accent-foreground)"),
|
||||
svg.NewLength(2.5).
|
||||
AsHeight(),
|
||||
svg.NewLength(-5).
|
||||
AsX(),
|
||||
svg.NewLength(-3).
|
||||
AsY(),
|
||||
svg.NewLength(10).
|
||||
AsWidth(),
|
||||
svg.Class("not-group-hover:hidden"),
|
||||
),
|
||||
c.buildHoverLabelText(v, p),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) buildAlwaysDisplayedLabelText(v TimeLineValue, p svg.Point) *svg.Element[svg.GTag, svg.GAttribute, svg.GChildren] {
|
||||
return c.buildLabelText(v, p)
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) buildHoverLabelText(v TimeLineValue, p svg.Point) *svg.Element[svg.GTag, svg.GAttribute, svg.GChildren] {
|
||||
labelText := c.buildLabelText(v, p).
|
||||
Attr(
|
||||
svg.Class("not-group-hover:hidden"),
|
||||
)
|
||||
|
||||
if v.Time.IsZero() {
|
||||
return labelText
|
||||
}
|
||||
|
||||
return labelText.Child(
|
||||
svg.NewText(v.Time.Format("Jan _2 3:04:05 PM")).
|
||||
Attr(
|
||||
svg.TextAnchorMiddle,
|
||||
svg.DominantBaselineMiddle,
|
||||
svg.Style{
|
||||
"font-size": "1px",
|
||||
},
|
||||
svg.Class("not-group-hover:hidden"),
|
||||
|
||||
svg.NewLength(0).
|
||||
AsX(),
|
||||
svg.NewLength(2.5).
|
||||
AsY(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *TimeLineChart) buildLabelText(v TimeLineValue, p svg.Point) *svg.Element[svg.GTag, svg.GAttribute, svg.GChildren] {
|
||||
return new(svg.G).
|
||||
Attr(
|
||||
svg.NewLength(0).
|
||||
AsX(),
|
||||
svg.NewLength(0).
|
||||
AsY(),
|
||||
upsideDownTransform(0, 0),
|
||||
).
|
||||
Child(
|
||||
svg.NewText(v.Label).
|
||||
Attr(
|
||||
svg.TextAnchorMiddle,
|
||||
svg.DominantBaselineMiddle,
|
||||
svg.Style{
|
||||
"font-size": "1px",
|
||||
},
|
||||
|
||||
svg.NewLength(0).
|
||||
AsX(),
|
||||
svg.NewLength(1.25).
|
||||
AsY(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
func (c *TimeLineChart) buildLabelText(v TimeLineValue, p svg.Point) *svg.Element[svg.GTag, svg.GAttribute, svg.GChildren] {
|
||||
labelText := new(svg.G).
|
||||
Attr(
|
||||
svg.NewLength(0).
|
||||
AsX(),
|
||||
svg.NewLength(0).
|
||||
AsY(),
|
||||
upsideDownTransform(0, 0),
|
||||
).
|
||||
Child(
|
||||
svg.NewText(v.Label).
|
||||
Attr(
|
||||
svg.TextAnchorMiddle,
|
||||
svg.DominantBaselineMiddle,
|
||||
svg.Style{
|
||||
"font-size": "1px",
|
||||
},
|
||||
|
||||
svg.NewLength(0).
|
||||
AsX(),
|
||||
svg.NewLength(1.25).
|
||||
AsY(),
|
||||
),
|
||||
)
|
||||
|
||||
if v.Time.IsZero() {
|
||||
return labelText
|
||||
}
|
||||
|
||||
return labelText.Child(
|
||||
svg.NewText(v.Time.Format("Jan _2 3:04:05 PM")).
|
||||
Attr(
|
||||
svg.TextAnchorMiddle,
|
||||
svg.DominantBaselineMiddle,
|
||||
svg.Style{
|
||||
"font-size": "1px",
|
||||
},
|
||||
svg.Class("not-group-hover:hidden"),
|
||||
|
||||
svg.NewLength(0).
|
||||
AsX(),
|
||||
svg.NewLength(2.5).
|
||||
AsY(),
|
||||
),
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
func (c *TimeLineChart) getStroke() string {
|
||||
if c.foreground != "" {
|
||||
return c.foreground
|
||||
}
|
||||
return "black"
|
||||
}
|
||||
Reference in New Issue
Block a user