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

+160
View File
@@ -0,0 +1,160 @@
package mujica
import (
"fmt"
"math"
"slices"
"github.com/egoist/mygo/ui"
"mujicaui/internal"
"mujicaui/theme"
)
// BoxGroup is one named sample of a BoxPlot.
type BoxGroup struct {
Name string
Values []float64
}
// BoxPlotOptions configure a BoxPlot.
type BoxPlotOptions struct {
Frame ChartFrame
Format func(v float64) string
}
// boxStats are quartiles, whisker ends and outliers.
type boxStats struct {
q1, median, q3, lo, hi float64
outliers []float64
n int
}
// boxSummary summarizes finite values; n is 0 without any.
func boxSummary(values []float64) boxStats {
var v []float64
for _, x := range values {
if !math.IsNaN(x) && !math.IsInf(x, 0) {
v = append(v, x)
}
}
if len(v) == 0 {
return boxStats{}
}
slices.Sort(v)
q := func(p float64) float64 {
at := p * float64(len(v)-1)
i := int(at)
if i+1 >= len(v) {
return v[i]
}
return v[i] + (at-float64(i))*(v[i+1]-v[i])
}
s := boxStats{q1: q(0.25), median: q(0.5), q3: q(0.75), n: len(v)}
fence := 1.5 * (s.q3 - s.q1)
s.lo, s.hi = s.q1, s.q3
for _, x := range v {
if x < s.q1-fence || x > s.q3+fence {
s.outliers = append(s.outliers, x)
continue
}
s.lo, s.hi = min(s.lo, x), max(s.hi, x)
}
return s
}
// BoxPlot compares groups by quartile box, whiskers and outlier points.
func BoxPlot(c *ui.Context, groups []BoxGroup, opts BoxPlotOptions) ChartResult {
ch := newChart(c, opts.Frame)
k := Tokens(c)
pal := internal.Palette(k)
size := FontSize(c, theme.CaptionSize)
ids := ""
for _, g := range groups {
if g.Name == "" {
panic("mujica: BoxPlot groups need names")
}
ids += chartID(g.Values) + ";"
}
stats := chartMemo(ch, "stats", ids, func() []boxStats {
out := make([]boxStats, len(groups))
for i, g := range groups {
out[i] = boxSummary(g.Values)
}
return out
})
lo, hi, n := math.Inf(1), math.Inf(-1), 0
for _, s := range stats {
if s.n > 0 {
n++
lo, hi = min(lo, s.lo, slices.Min(append([]float64{s.lo}, s.outliers...))), max(hi, s.hi, slices.Max(append([]float64{s.hi}, s.outliers...)))
}
}
var step float64
if n > 0 {
lo, hi, step = internal.Nice(lo, hi, 4)
} else {
groups = nil
}
val := func(v float64) string { return chartValue(c, v, opts.Format) }
geom := func(r ui.Rect) (ui.Rect, internal.Band, internal.Linear) {
plot := chartInset(r, 52, 26)
return plot, internal.Band{N: len(groups), R0: plot.X, R1: plot.X + plot.W, Pad: 0.45},
internal.Linear{D0: lo, D1: hi, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)}
}
spec := chartSpec{n: len(groups)}
spec.readout = func(i int) string {
s := stats[i]
if s.n == 0 {
return groups[i].Name + " · " + msg(c, "chart.missing")
}
return fmt.Sprintf(msg(c, "box_plot.readout"), groups[i].Name, val(s.median), val(s.q1), val(s.q3), val(s.lo), val(s.hi), len(s.outliers))
}
spec.pick = func(r ui.Rect, x, y float32) int {
plot, band, _ := geom(r)
if !plot.Contains(x, y) {
return -1
}
return min(len(groups)-1, int((x-plot.X)/band.Step()))
}
spec.draw = func(p *ui.Painter, r ui.Rect, active int) {
plot, band, ys := geom(r)
internal.AxisY(p, plot, ys, internal.Ticks(lo, hi, step), func(v float64) string { return internal.FormatTick(v, step) }, size, k)
var xs []float32
var names []string
for i, g := range groups {
xs, names = append(xs, band.Center(i)), append(names, g.Name)
}
internal.AxisX(p, plot, xs, names, size, k)
for i, s := range stats {
if s.n == 0 {
continue
}
col := pal[i%len(pal)]
x, w := band.Start(i), band.Width()
cx := internal.Snap(p, x+w/2)
whisker := func(a, b float64) {
var path ui.Path
path.MoveTo(cx, ys.Map(a)).LineTo(cx, ys.Map(b))
p.StrokePath(&path, 1.5, k.TextMuted)
p.Line(cx-w/4, ys.Map(b), cx+w/4, ys.Map(b), 1.5, k.TextMuted)
}
whisker(s.q3, s.hi)
whisker(s.q1, s.lo)
box := ui.Rect{X: x, Y: ys.Map(s.q3), W: w, H: max(ys.Map(s.q1)-ys.Map(s.q3), 1)}
p.Fill(box, col.Alpha(0.22), 2)
p.Stroke(box, col, 2, 1.5)
p.Line(x, ys.Map(s.median), x+w, ys.Map(s.median), 2.5, k.Text)
for _, o := range s.outliers {
var dot ui.Path
dot.Circle(cx, internal.Snap(p, ys.Map(o)), 3.5)
p.StrokePath(&dot, 1.5, col)
}
if i == active {
p.Stroke(ui.Rect{X: x - 6, Y: plot.Y, W: w + 12, H: plot.H}, k.Text, 3, 1.5)
}
}
}
res, _ := ch.build(spec)
return res
}