Charts I: components 101-120

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 14:34:31 -04:00
1 parent 6f0c02d339
commit efee19c7d4
116 files changed
+7825 -3

No files matched your search

+155
View File
@@ -0,0 +1,155 @@
package mujica
import (
"fmt"
"log/slog"
"math"
"slices"
"strings"
"github.com/egoist/mygo/ui"
"mujicaui/internal"
"mujicaui/theme"
)
// SunburstOptions configure a Sunburst.
type SunburstOptions struct {
Frame ChartFrame
// Depth is the number of rings; 0 means 3.
Depth int
Format func(v float64) string
}
// sunArc is one drawn segment of a ring.
type sunArc struct {
path []string
node ChartNode
ring int
a0, a1 float64
color int
}
// Sunburst draws descendants of *path as rings; branches drill in.
func Sunburst(c *ui.Context, path *[]string, root ChartNode, opts SunburstOptions) ChartResult {
if opts.Depth < 0 {
panic("mujica: Sunburst Depth must be ≥ 0")
}
depth := opts.Depth
if depth == 0 {
depth = 3
}
chartCheckTree(root)
chartDescend(root, *path)
k := Tokens(c)
u := Density(c).Unit()
size := FontSize(c, theme.CaptionSize)
pal := internal.Palette(k)
var res ChartResult
wrap := ui.Column(c).Gap(u * 2).MinWidth(0)
wrap.Children(func() {
res.value = chartPath(c, root.Name, path, wrap.IsDisabled())
node := chartDescend(root, *path)
sum := node.total()
var arcs []sunArc
var walk func(n ChartNode, at []string, ring int, a0, a1 float64, color int)
walk = func(n ChartNode, at []string, ring int, a0, a1 float64, color int) {
total, width := n.total(), a1-a0
for i, ch := range n.Children {
v := ch.total()
if v <= 0 {
continue
}
span := width * v / total
col := color
if ring == 0 {
col = i
}
arc := sunArc{path: append(slices.Clone(at), ch.Name), node: ch, ring: ring, a0: a0, a1: a0 + span, color: col}
arcs = append(arcs, arc)
if ring+1 < depth {
walk(ch, arc.path, ring+1, arc.a0, arc.a1, col)
}
a0 += span
}
}
if sum > 0 {
walk(node, nil, 0, 0, 2*math.Pi, 0)
}
geom := func(r ui.Rect) (cx, cy, hole, ring float32) {
rad := min(r.W, r.H)/2 - 6
hole = rad * 0.3
return r.X + r.W/2, r.Y + r.H/2, hole, (rad - hole) / float32(depth)
}
ch := newChart(c, opts.Frame)
spec := chartSpec{n: len(arcs)}
spec.readout = func(i int) string {
a := arcs[i]
s := fmt.Sprintf("%s · %s · %.1f%%", strings.Join(a.path, " › "), chartValue(c, a.node.total(), opts.Format), 100*a.node.total()/sum)
if len(a.node.Children) > 0 {
s += " · " + msg(c, "chart.opens")
}
return s
}
spec.pick = func(r ui.Rect, x, y float32) int {
cx, cy, hole, ring := geom(r)
d := float32(math.Hypot(float64(x-cx), float64(y-cy)))
a := math.Atan2(float64(x-cx), float64(cy-y))
if a < 0 {
a += 2 * math.Pi
}
for i, arc := range arcs {
r0 := hole + ring*float32(arc.ring)
if d >= r0 && d < r0+ring && a >= arc.a0 && a < arc.a1 {
return i
}
}
return -1
}
spec.activate = func(i int) {
if len(arcs[i].node.Children) > 0 {
*path = append(slices.Clone(*path), arcs[i].path...)
res.value = true
slog.Debug("mujica: sunburst drill", "path", strings.Join(*path, "/"))
}
}
// color fades outer rings, keeping 3:1 against Surface.
color := func(a sunArc) ui.Color {
col := pal[a.color%len(pal)]
if faded := col.Mix(k.Surface, 0.18*float32(a.ring)); theme.Contrast(faded, k.Surface) >= 3 {
return faded
}
return col
}
spec.draw = func(p *ui.Painter, r ui.Rect, active int) {
cx, cy, hole, ring := geom(r)
for i, a := range arcs {
r0 := hole + ring*float32(a.ring)
col := color(a)
p.FillPath(internal.Sector(cx, cy, r0, r0+ring, a.a0, a.a1), col)
p.StrokePath(internal.Sector(cx, cy, r0, r0+ring, a.a0, a.a1), 1.5, k.Surface)
if i == active {
p.StrokePath(internal.Sector(cx, cy, r0+2, r0+ring-2, a.a0, a.a1), 2, chartInk(k, col))
}
}
// Labels go on top where arcs are long enough.
for _, a := range arcs {
r0 := hole + ring*float32(a.ring)
col := color(a)
if mid := float64(r0+ring/2) * (a.a1 - a.a0); mid > 44 {
x, y := internal.Polar(cx, cy, r0+ring/2, (a.a0+a.a1)/2)
chartText(p, chartFit(p, a.node.Name, min(float32(mid)*0.8, ring*1.25), size), x, y, size, chartInk(k, col), 0.5, 0.5)
}
}
chartText(p, chartFit(p, node.Name, hole*2-8, size), cx, cy, size, k.TextMuted, 0.5, 1)
chartText(p, chartValue(c, sum, opts.Format), cx, cy+2, FontSize(c, theme.BodySize), k.Text, 0.5, 0)
}
r, plot := ch.build(spec)
if plot != nil && !plot.IsDisabled() && plot.Shortcut(0, ui.KeyBackspace) && len(*path) > 0 {
*path, res.value = (*path)[:len(*path)-1], true
}
res.Point, res.changed, res.retry = r.Point, r.changed, r.retry
})
res.Element = wrap
return res
}