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 }