Files
MujicaUI/funnel_chart.go
T

86 lines
2.4 KiB
Go

package mujica
import (
"fmt"
"math"
"github.com/egoist/mygo/ui"
"mujicaui/internal"
"mujicaui/theme"
)
// FunnelChartOptions configure a FunnelChart.
type FunnelChartOptions struct {
Frame ChartFrame
Format func(v float64) string
}
// FunnelChart narrows by stage, labeled with conversion rates.
func FunnelChart(c *ui.Context, stages []ChartSlice, opts FunnelChartOptions) ChartResult {
top := 0.0
for _, s := range stages {
if s.Label == "" || s.Value < 0 || math.IsNaN(s.Value) || math.IsInf(s.Value, 0) {
panic("mujica: FunnelChart stages need a label and a finite value ≥ 0")
}
top = max(top, s.Value)
}
ch := newChart(c, opts.Frame)
k := Tokens(c)
col := internal.Palette(k)[0]
size := FontSize(c, theme.CaptionSize)
n := len(stages)
if top == 0 {
n = 0
}
pct := func(a, b float64) string {
if b == 0 {
return "—"
}
return fmt.Sprintf("%.1f%%", 100*a/b)
}
rate := func(i int) string {
if i == 0 {
return pct(stages[0].Value, stages[0].Value)
}
return pct(stages[i].Value, stages[i-1].Value)
}
geom := func(r ui.Rect) (ui.Rect, internal.Band) {
plot := ui.Rect{X: r.X + 8, Y: r.Y + 4, W: r.W * 0.58, H: r.H - 8}
return plot, internal.Band{N: len(stages), R0: plot.Y, R1: plot.Y + plot.H, Pad: 0.08}
}
spec := chartSpec{n: n}
spec.readout = func(i int) string {
s := stages[i]
return fmt.Sprintf(msg(c, "funnel_chart.readout"), s.Label, chartValue(c, s.Value, opts.Format), rate(i), pct(s.Value, stages[0].Value))
}
spec.pick = func(r ui.Rect, x, y float32) int {
plot, band := geom(r)
if y < plot.Y || y >= plot.Y+plot.H || x < r.X || x > r.X+r.W {
return -1
}
return min(len(stages)-1, int((y-plot.Y)/band.Step()))
}
spec.draw = func(p *ui.Painter, r ui.Rect, active int) {
plot, band := geom(r)
cx := plot.X + plot.W/2
width := func(i int) float32 { return max(plot.W*float32(stages[min(i, len(stages)-1)].Value/top), 2) }
for i, s := range stages {
y0, y1 := band.Start(i), band.Start(i)+band.Width()
w0, w1 := width(i), width(i+1)
var shape ui.Path
shape.MoveTo(cx-w0/2, y0).LineTo(cx+w0/2, y0).LineTo(cx+w1/2, y1).LineTo(cx-w1/2, y1).Close()
p.FillPath(&shape, col)
if i == active {
p.StrokePath(&shape, 2, k.Text)
}
ly := (y0 + y1) / 2
lx := plot.X + plot.W + 16
chartText(p, s.Label+" · "+chartValue(c, s.Value, opts.Format), lx, ly, size, k.Text, 0, 1)
chartText(p, rate(i), lx, ly+2, size, k.TextMuted, 0, 0)
}
}
res, _ := ch.build(spec)
return res
}