prototyped svg reports
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package charts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func ExampleBar() {
|
||||
mu := NewBar(
|
||||
BarValue{
|
||||
Label: "label: 101",
|
||||
Value: 101,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: -09",
|
||||
Value: -99,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: 201",
|
||||
Value: 201,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: -199",
|
||||
Value: -199,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: 301",
|
||||
Value: 301,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: -299",
|
||||
Value: -299,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: 401",
|
||||
Value: 401,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: -399",
|
||||
Value: -399,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: 501",
|
||||
Value: 501,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: -499",
|
||||
Value: -499,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: 601",
|
||||
Value: 601,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: -599",
|
||||
Value: -599,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: 701",
|
||||
Value: 701,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: -699",
|
||||
Value: -699,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: 801",
|
||||
Value: 801,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: -799",
|
||||
Value: -799,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: 901",
|
||||
Value: 901,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: -899",
|
||||
Value: -899,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: 1001",
|
||||
Value: 1001,
|
||||
},
|
||||
BarValue{
|
||||
Label: "label: -999",
|
||||
Value: -999,
|
||||
},
|
||||
).SVG().GetMarkup()
|
||||
|
||||
os.WriteFile("./test.svg", []byte(mu), 666)
|
||||
fmt.Println(mu)
|
||||
// Output: <svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="height: auto; width: 100%" viewBox="0 0 100 100"><rect x="0" y="45" width="4.75" height="55" class="fill-accent"/><rect x="5" y="55" width="4.75" height="45" class="fill-accent"/><rect x="10" y="40" width="4.75" height="60" class="fill-accent"/><rect x="15" y="60" width="4.75" height="40" class="fill-accent"/><rect x="20" y="35" width="4.75" height="65" class="fill-accent"/><rect x="25" y="65" width="4.75" height="35" class="fill-accent"/><rect x="30" y="30" width="4.75" height="70" class="fill-accent"/><rect x="35" y="70" width="4.75" height="30" class="fill-accent"/><rect x="40" y="25" width="4.75" height="75" class="fill-accent"/><rect x="45" y="75" width="4.75" height="25" class="fill-accent"/><rect x="50" y="20" width="4.75" height="80" class="fill-accent"/><rect x="55" y="80" width="4.75" height="20" class="fill-accent"/><rect x="60" y="15" width="4.75" height="85" class="fill-accent"/><rect x="65" y="85" width="4.75" height="15" class="fill-accent"/><rect x="70" y="10" width="4.75" height="90" class="fill-accent"/><rect x="75" y="90" width="4.75" height="10" class="fill-accent"/><rect x="80" y="5" width="4.75" height="95" class="fill-accent"/><rect x="85" y="95" width="4.75" height="5" class="fill-accent"/><rect x="90" y="0" width="4.75" height="100" class="fill-accent"/><rect x="95" y="100" width="4.75" height="0" class="fill-accent"/></svg>
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package charts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func ExampleLineChart() {
|
||||
c := NewLineChart(100, 100,
|
||||
LineValue{
|
||||
Value: 1,
|
||||
},
|
||||
LineValue{
|
||||
Value: 2,
|
||||
},
|
||||
LineValue{
|
||||
Value: 4,
|
||||
},
|
||||
LineValue{
|
||||
Value: 8,
|
||||
},
|
||||
LineValue{
|
||||
Value: 16,
|
||||
},
|
||||
LineValue{
|
||||
Value: 32,
|
||||
},
|
||||
).
|
||||
Min(0).
|
||||
Max(40).
|
||||
Foreground("purple").
|
||||
Background("pink").
|
||||
SVG().
|
||||
GetMarkup()
|
||||
|
||||
output := []byte(`<html><body>` + c + `</body></html>`)
|
||||
os.WriteFile("./line_chart.html", []byte(output), 0666)
|
||||
fmt.Println(c)
|
||||
// Output: <svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="height: auto; width: 100%" viewBox="0 0 100 100"></svg>
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package charts
|
||||
|
||||
import "ruben/inventory2/server/ui/svg"
|
||||
|
||||
func getChartOrientationTransform(height float64) svg.Transform {
|
||||
return svg.Transform{
|
||||
svg.TransformScale{
|
||||
X: 1,
|
||||
Y: -1,
|
||||
},
|
||||
svg.TransformTranslate{
|
||||
Y: -height,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func upsideDownTransform(x, y float64) svg.Transform {
|
||||
return svg.Transform{
|
||||
svg.TransformTranslate{
|
||||
X: x,
|
||||
Y: y,
|
||||
},
|
||||
svg.TransformScale{
|
||||
X: 1,
|
||||
Y: -1,
|
||||
},
|
||||
svg.TransformTranslate{
|
||||
X: -x,
|
||||
Y: -y,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="height: auto; width: 100%" viewBox="0 0 100 100"><rect x="0" y="45" width="4.75" height="55" class="fill-accent"/><rect x="5" y="55" width="4.75" height="45" class="fill-accent"/><rect x="10" y="40" width="4.75" height="60" class="fill-accent"/><rect x="15" y="60" width="4.75" height="40" class="fill-accent"/><rect x="20" y="35" width="4.75" height="65" class="fill-accent"/><rect x="25" y="65" width="4.75" height="35" class="fill-accent"/><rect x="30" y="30" width="4.75" height="70" class="fill-accent"/><rect x="35" y="70" width="4.75" height="30" class="fill-accent"/><rect x="40" y="25" width="4.75" height="75" class="fill-accent"/><rect x="45" y="75" width="4.75" height="25" class="fill-accent"/><rect x="50" y="20" width="4.75" height="80" class="fill-accent"/><rect x="55" y="80" width="4.75" height="20" class="fill-accent"/><rect x="60" y="15" width="4.75" height="85" class="fill-accent"/><rect x="65" y="85" width="4.75" height="15" class="fill-accent"/><rect x="70" y="10" width="4.75" height="90" class="fill-accent"/><rect x="75" y="90" width="4.75" height="10" class="fill-accent"/><rect x="80" y="5" width="4.75" height="95" class="fill-accent"/><rect x="85" y="95" width="4.75" height="5" class="fill-accent"/><rect x="90" y="0" width="4.75" height="100" class="fill-accent"/><rect x="95" y="100" width="4.75" height="0" class="fill-accent"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -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