Charts I: components 101-120
This commit is contained in:
116 files changed
+7825
-3
No files matched your search
+146
@@ -0,0 +1,146 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/internal"
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// ChartSlice is one share of a PieChart or FunnelChart.
|
||||
type ChartSlice struct {
|
||||
Label string
|
||||
Value float64
|
||||
}
|
||||
|
||||
// PieChartOptions configure a PieChart.
|
||||
type PieChartOptions struct {
|
||||
Frame ChartFrame
|
||||
// Donut cuts out the middle and writes the total there.
|
||||
Donut bool
|
||||
// MinAngle merges two or more narrower slices into Other.
|
||||
MinAngle float64
|
||||
Format func(v float64) string
|
||||
}
|
||||
|
||||
// PieChart shows shares as slices labeled with percents.
|
||||
func PieChart(c *ui.Context, slices []ChartSlice, opts PieChartOptions) ChartResult {
|
||||
if opts.MinAngle < 0 || opts.MinAngle >= 360 {
|
||||
panic("mujica: PieChart MinAngle must be in [0, 360)")
|
||||
}
|
||||
ch := newChart(c, opts.Frame)
|
||||
k := Tokens(c)
|
||||
pal := internal.Palette(k)
|
||||
size := FontSize(c, theme.CaptionSize)
|
||||
parts := chartMemo(ch, "parts", [2]any{chartID(slices), opts.MinAngle}, func() []ChartSlice {
|
||||
return pieMerge(slices, opts.MinAngle, msg(c, "pie_chart.other"))
|
||||
})
|
||||
total := 0.0
|
||||
for _, s := range parts {
|
||||
total += s.Value
|
||||
}
|
||||
n := len(parts)
|
||||
if total == 0 {
|
||||
n = 0
|
||||
}
|
||||
keys := make([]chartKey, len(parts))
|
||||
for i, s := range parts {
|
||||
keys[i] = chartKey{Name: s.Label, Color: pal[i%len(pal)], Shape: i}
|
||||
}
|
||||
// angle returns slice i's start and end, clockwise.
|
||||
angle := func(i int) (float64, float64) {
|
||||
a := 0.0
|
||||
for j := range i {
|
||||
a += parts[j].Value
|
||||
}
|
||||
return 2 * math.Pi * a / total, 2 * math.Pi * (a + parts[i].Value) / total
|
||||
}
|
||||
geom := func(r ui.Rect) (cx, cy, r0, r1 float32) {
|
||||
r1 = min(r.W, r.H)/2 - 8
|
||||
if opts.Donut {
|
||||
r0 = r1 * 0.58
|
||||
}
|
||||
return r.X + r.W/2, r.Y + r.H/2, r0, r1
|
||||
}
|
||||
share := func(i int) string { return fmt.Sprintf("%.1f%%", 100*parts[i].Value/total) }
|
||||
spec := chartSpec{n: n, keys: keys}
|
||||
spec.readout = func(i int) string {
|
||||
return parts[i].Label + " · " + chartValue(c, parts[i].Value, opts.Format) + " · " + share(i)
|
||||
}
|
||||
spec.pick = func(r ui.Rect, x, y float32) int {
|
||||
cx, cy, r0, r1 := geom(r)
|
||||
d := float32(math.Hypot(float64(x-cx), float64(y-cy)))
|
||||
if d < r0 || d > r1 {
|
||||
return -1
|
||||
}
|
||||
a := math.Atan2(float64(x-cx), float64(cy-y))
|
||||
if a < 0 {
|
||||
a += 2 * math.Pi
|
||||
}
|
||||
for i := range parts {
|
||||
if a0, a1 := angle(i); a >= a0 && a < a1 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
spec.draw = func(p *ui.Painter, r ui.Rect, active int) {
|
||||
cx, cy, r0, r1 := geom(r)
|
||||
for i := range parts {
|
||||
a0, a1 := angle(i)
|
||||
if a1 <= a0 {
|
||||
continue
|
||||
}
|
||||
grow := float32(0)
|
||||
if i == active {
|
||||
grow = 6
|
||||
}
|
||||
p.FillPath(internal.Sector(cx, cy, r0, r1+grow, a0, a1), keys[i].Color)
|
||||
p.StrokePath(internal.Sector(cx, cy, r0, r1+grow, a0, a1), 1.5, k.Surface)
|
||||
// Wide slices carry their shape and percent.
|
||||
if a1-a0 > 0.38 {
|
||||
mx, my := internal.Polar(cx, cy, (r0+r1)/2+float32(chartBool(r0 == 0))*r1*0.12, (a0+a1)/2)
|
||||
ink := chartInk(k, keys[i].Color)
|
||||
internal.Marker(p, i, mx, my-8, 9, ink)
|
||||
chartText(p, share(i), mx, my+2, size, ink, 0.5, 0)
|
||||
}
|
||||
}
|
||||
if opts.Donut {
|
||||
chartText(p, chartValue(c, total, opts.Format), cx, cy, FontSize(c, theme.H3Size), k.Text, 0.5, 0.75)
|
||||
chartText(p, msg(c, "pie_chart.total"), cx, cy+4, size, k.TextMuted, 0.5, 0)
|
||||
}
|
||||
}
|
||||
res, _ := ch.build(spec)
|
||||
return res
|
||||
}
|
||||
|
||||
// pieMerge folds two or more narrow slices into other.
|
||||
func pieMerge(slices []ChartSlice, minAngle float64, other string) []ChartSlice {
|
||||
total := 0.0
|
||||
for _, s := range slices {
|
||||
if s.Value < 0 || math.IsNaN(s.Value) || math.IsInf(s.Value, 0) || s.Label == "" {
|
||||
panic("mujica: PieChart slices need a label and a finite value ≥ 0")
|
||||
}
|
||||
total += s.Value
|
||||
}
|
||||
var keep []ChartSlice
|
||||
small, rest := 0, 0.0
|
||||
for _, s := range slices {
|
||||
if total > 0 && s.Value/total*360 < minAngle {
|
||||
small++
|
||||
rest += s.Value
|
||||
}
|
||||
}
|
||||
for _, s := range slices {
|
||||
if small < 2 || total == 0 || s.Value/total*360 >= minAngle {
|
||||
keep = append(keep, s)
|
||||
}
|
||||
}
|
||||
if small >= 2 {
|
||||
keep = append(keep, ChartSlice{Label: other, Value: rest})
|
||||
}
|
||||
return keep
|
||||
}
|
||||
Reference in new issue
Block a user