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

+75 -1
View File
@@ -1,3 +1,77 @@
package main
var charts1Pages = []page{}
import (
"fmt"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"mujicaui/theme"
)
const c1Category = "Charts I"
const c1Keys = "Tab focuses the chart (legend check boxes first); arrows move between points and the readout is announced; Home/End jump; Escape clears."
var charts1Pages = []page{
{ID: "101", Name: "LineChart", ZhName: "折线图", Category: c1Category, Demo: c1LineDemo, Code: c1LineCode, Keys: c1Keys + " Space on a legend entry hides or shows its series."},
{ID: "102", Name: "AreaChart", ZhName: "面积图", Category: c1Category, Demo: c1AreaDemo, Code: c1AreaCode, Keys: c1Keys},
{ID: "103", Name: "BarChart", ZhName: "柱状图", Category: c1Category, Demo: c1BarDemo, Code: c1BarCode, Keys: c1Keys + " Arrows step bar by bar."},
{ID: "104", Name: "PieChart", ZhName: "饼图", Category: c1Category, Demo: c1PieDemo, Code: c1PieCode, Keys: c1Keys},
{ID: "105", Name: "ScatterChart", ZhName: "散点图", Category: c1Category, Demo: c1ScatterDemo, Code: c1ScatterCode, Keys: c1Keys + " Enter or Space selects the point; drag a box to select."},
{ID: "106", Name: "RadarChart", ZhName: "雷达图", Category: c1Category, Demo: c1RadarDemo, Code: c1RadarCode, Keys: c1Keys},
{ID: "107", Name: "HeatmapChart", ZhName: "热力图", Category: c1Category, Demo: c1HeatDemo, Code: c1HeatCode, Keys: "Tab focuses the map; Left/Right move along a row, Up/Down along a column; Home/End jump; Escape clears."},
{ID: "108", Name: "CalendarHeatmap", ZhName: "日历热力图", Category: c1Category, Demo: c1CalendarDemo, Code: c1CalendarCode, Keys: "Left/Right move a week, Up/Down a day; Home/End jump to the year's ends; the year buttons take Enter or Space."},
{ID: "109", Name: "Histogram", ZhName: "直方图", Category: c1Category, Demo: c1HistogramDemo, Code: c1HistogramCode, Keys: c1Keys + " Arrows step bin by bin."},
{ID: "110", Name: "BoxPlot", ZhName: "箱线图", Category: c1Category, Demo: c1BoxDemo, Code: c1BoxCode, Keys: c1Keys + " Arrows step group by group."},
{ID: "111", Name: "WaterfallChart", ZhName: "瀑布图", Category: c1Category, Demo: c1WaterfallDemo, Code: c1WaterfallCode, Keys: c1Keys},
{ID: "112", Name: "FunnelChart", ZhName: "漏斗图", Category: c1Category, Demo: c1FunnelDemo, Code: c1FunnelCode, Keys: c1Keys + " Arrows step stage by stage."},
{ID: "113", Name: "BulletChart", ZhName: "子弹图", Category: c1Category, Demo: c1BulletDemo, Code: c1BulletCode, Keys: c1Keys + " Arrows read the value, the target, then each range."},
{ID: "114", Name: "Sparkline", ZhName: "迷你图", Category: c1Category, Demo: c1SparkDemo, Code: c1SparkCode, Keys: "Tab focuses the sparkline; Left/Right read points; Home/End jump; Escape shows the last value again."},
{ID: "115", Name: "Gauge", ZhName: "仪表盘", Category: c1Category, Demo: c1GaugeDemo, Code: c1GaugeCode, Keys: c1Keys + " Arrows read the value, then each segment."},
{ID: "116", Name: "Treemap", ZhName: "矩形树图", Category: c1Category, Demo: c1TreemapDemo, Code: c1TreemapCode, Keys: c1Keys + " Enter or Space drills into a branch; Backspace goes back; Back and the breadcrumb take Tab."},
{ID: "117", Name: "Sunburst", ZhName: "旭日图", Category: c1Category, Demo: c1SunburstDemo, Code: c1SunburstCode, Keys: c1Keys + " Enter or Space drills in; Backspace goes back."},
{ID: "118", Name: "SankeyChart", ZhName: "桑基图", Category: c1Category, Demo: c1SankeyDemo, Code: c1SankeyCode, Keys: c1Keys + " Reading a node or link highlights every path through it."},
{ID: "119", Name: "NetworkGraph", ZhName: "网络图", Category: c1Category, Demo: c1NetworkDemo, Code: c1NetworkCode, Keys: c1Keys + " Shift+arrows move the node; +/- zoom; 0 resets the view; drag nodes or the background."},
{ID: "120", Name: "RealtimeChart", ZhName: "实时图表", Category: c1Category, Demo: c1RealtimeDemo, Code: c1RealtimeCode, Keys: "Tab reaches Pause, the legend and the chart; chart keys as in LineChart."},
}
var c1Statuses = []string{"Ready", "Loading", "Failed", "Empty"}
// c1Frame shows the status picker and returns the frame.
func c1Frame(c *ui.Context, label string, status *string) (mujica.ChartFrame, bool) {
ui.Select(c, status, c1Statuses).Width(160).AlignSelf(ui.Start)
f := mujica.ChartFrame{Label: label}
switch *status {
case "Loading":
f.Status = mujica.DataLoading
case "Failed":
f.Status, f.Error = mujica.DataFailed, "The archive did not answer"
}
return f, *status != "Empty"
}
// c1Section lays out a title, controls, then the chart.
func c1Section(c *ui.Context, title string, controls, body func()) {
ui.Column(c).Gap(12).Children(func() {
ui.Text(c, title).Font(theme.SerifFont).FontSize(mujica.FontSize(c, theme.H3Size))
ui.Row(c).Gap(16).Wrap().AlignItems(ui.Center).Children(controls)
ui.Column(c).Width(560).Children(body)
})
}
// c1Log records a chart's events in the gallery log.
func c1Log(log func(string), name string, r mujica.ChartResult) {
if r.Changed() {
log(fmt.Sprintf("%s: point %d", name, r.Point))
}
if s, ok := r.Toggled(); ok {
log(name + ": toggled " + s)
}
if r.ValueChanged() {
log(name + ": value changed")
}
if r.Retried() {
log(name + ": retry")
}
}
+174
View File
@@ -0,0 +1,174 @@
package main
// Snippets are the bodies of the Examples in example_charts1_test.go.
const c1LineCode = `series := []mujica.ChartSeries{
{Name: "Pews", Values: []float64{3, 5, math.NaN(), 4, 8}},
{Name: "Candles", Values: []float64{5, 2, 6, 7, 1}},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.LineChart(c, series, mujica.LineChartOptions{Frame: mujica.ChartFrame{Label: "Chapel"}})
}
ui.Render(view, 400, 300, 1)`
const c1AreaCode = `series := []mujica.ChartSeries{
{Name: "Matins", Values: []float64{3, 4, 6, 5}},
{Name: "Vespers", Values: []float64{2, 3, 2, 4}},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.AreaChart(c, series, mujica.AreaChartOptions{Frame: mujica.ChartFrame{Label: "Attendance"}, Stacked: true})
}
ui.Render(view, 400, 300, 1)`
const c1BarCode = `categories := []string{"Nave", "Choir", "Crypt"}
series := []mujica.ChartSeries{{Name: "Gold", Values: []float64{4, -2, 3}}, {Name: "Silver", Values: []float64{2, 5, 1}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.BarChart(c, categories, series, mujica.BarChartOptions{Frame: mujica.ChartFrame{Label: "Tithes"}, Labels: true})
}
ui.Render(view, 400, 300, 1)`
const c1PieCode = `slices := []mujica.ChartSlice{{Label: "Wax", Value: 50}, {Label: "Oil", Value: 30}, {Label: "Salt", Value: 4}, {Label: "Ash", Value: 3}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.PieChart(c, slices, mujica.PieChartOptions{Frame: mujica.ChartFrame{Label: "Stores"}, Donut: true, MinAngle: 20})
}
ui.Render(view, 400, 300, 1)`
const c1ScatterCode = `var selected []mujica.ScatterKey
groups := []mujica.ScatterGroup{{Name: "Monks", Points: []mujica.ScatterPoint{{X: 1, Y: 2, Size: 4}, {X: 3, Y: 5, Size: 9}}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ScatterChart(c, &selected, groups, mujica.ScatterChartOptions{Frame: mujica.ChartFrame{Label: "Choir"}, Bubbles: true})
}
ui.Render(view, 400, 300, 1)`
const c1RadarCode = `axes := []string{"Voice", "Latin", "Chant", "Organ"}
series := []mujica.ChartSeries{{Name: "Agnès", Values: []float64{8, 6, 9, 4}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.RadarChart(c, axes, series, mujica.RadarChartOptions{Frame: mujica.ChartFrame{Label: "Skills"}})
}
ui.Render(view, 400, 300, 1)`
const c1HeatCode = `rows, cols := []string{"Mon", "Tue"}, []string{"Matins", "Lauds", "Vespers"}
values := [][]float64{{1, 4, 9}, {3, math.NaN(), 7}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.HeatmapChart(c, rows, cols, values, mujica.HeatmapChartOptions{Frame: mujica.ChartFrame{Label: "Hours"}})
}
ui.Render(view, 400, 300, 1)`
const c1CalendarCode = `year := 2026
values := map[mujica.Date]float64{{Year: 2026, Month: time.March, Day: 4}: 5}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.CalendarHeatmap(c, &year, values, mujica.CalendarHeatmapOptions{Frame: mujica.ChartFrame{Label: "Prayers"}})
}
ui.Render(view, 400, 300, 1)`
const c1HistogramCode = `values := []float64{1, 2, 2, 3, 4, 5, 9, 10}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Histogram(c, values, mujica.HistogramOptions{Frame: mujica.ChartFrame{Label: "Ages"}, BinWidth: 5})
}
ui.Render(view, 400, 300, 1)`
const c1BoxCode = `groups := []mujica.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 5, 40}}, {Name: "Nave", Values: []float64{4, 5, 6, 7}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.BoxPlot(c, groups, mujica.BoxPlotOptions{Frame: mujica.ChartFrame{Label: "Hymns"}})
}
ui.Render(view, 400, 300, 1)`
const c1WaterfallCode = `steps := []mujica.WaterfallStep{
{Label: "Tithes", Value: 12},
{Label: "Repairs", Value: -5},
{Label: "Year", Kind: mujica.WaterfallTotal},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.WaterfallChart(c, steps, mujica.WaterfallChartOptions{Frame: mujica.ChartFrame{Label: "Ledger"}})
}
ui.Render(view, 400, 300, 1)`
const c1FunnelCode = `stages := []mujica.ChartSlice{{Label: "Visitors", Value: 1000}, {Label: "Novices", Value: 400}, {Label: "Vows", Value: 100}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.FunnelChart(c, stages, mujica.FunnelChartOptions{Frame: mujica.ChartFrame{Label: "Order"}})
}
ui.Render(view, 400, 300, 1)`
const c1BulletCode = `target := 250.0
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.BulletChart(c, 270, mujica.BulletChartOptions{Frame: mujica.ChartFrame{Label: "Alms"}, Target: &target, Bands: []float64{150, 225, 300}})
}
ui.Render(view, 400, 300, 1)`
const c1SparkCode = `values := []float64{3, 5, 4, 7, 6, 9}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Sparkline(c, values, mujica.SparklineOptions{Frame: mujica.ChartFrame{Label: "Bells"}, Kind: mujica.SparklineArea})
}
ui.Render(view, 400, 300, 1)`
const c1GaugeCode = `segments := []mujica.GaugeSegment{{To: 60, Label: "Calm", Tone: mujica.GaugeGood}, {To: 100, Label: "Crowded", Tone: mujica.GaugeDanger}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Gauge(c, 72, mujica.GaugeOptions{Frame: mujica.ChartFrame{Label: "Nave"}, Max: 100, Unit: "%", Segments: segments})
}
ui.Render(view, 400, 300, 1)`
const c1TreemapCode = `var path []string
root := mujica.ChartNode{Name: "Abbey", Children: []mujica.ChartNode{
{Name: "Library", Children: []mujica.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}},
{Name: "Kitchen", Value: 25},
}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Treemap(c, &path, root, mujica.TreemapOptions{Frame: mujica.ChartFrame{Label: "Rooms"}})
}
ui.Render(view, 400, 300, 1)`
const c1SunburstCode = `var path []string
root := mujica.ChartNode{Name: "Abbey", Children: []mujica.ChartNode{
{Name: "Library", Children: []mujica.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}},
{Name: "Kitchen", Value: 25},
}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Sunburst(c, &path, root, mujica.SunburstOptions{Frame: mujica.ChartFrame{Label: "Rooms"}})
}
ui.Render(view, 400, 300, 1)`
const c1SankeyCode = `nodes := []mujica.SankeyNode{{ID: "tithe", Label: "Tithes"}, {ID: "fund", Label: "Fund"}, {ID: "roof", Label: "Roof"}}
links := []mujica.SankeyLink{{From: "tithe", To: "fund", Value: 60}, {From: "fund", To: "roof", Value: 50}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.SankeyChart(c, nodes, links, mujica.SankeyChartOptions{Frame: mujica.ChartFrame{Label: "Money"}})
}
ui.Render(view, 400, 300, 1)`
const c1NetworkCode = `var state mujica.NetworkGraphState
nodes := []mujica.GraphNode{{ID: "a", Label: "Abbot"}, {ID: "b", Label: "Prior"}, {ID: "c", Label: "Novice"}}
edges := []mujica.GraphEdge{{From: "a", To: "b"}, {From: "b", To: "c"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.NetworkGraph(c, &state, nodes, edges, mujica.NetworkGraphOptions{Frame: mujica.ChartFrame{Label: "House"}, Force: true})
}
ui.Render(view, 400, 300, 1)`
const c1RealtimeCode = `buf := mujica.NewRealtimeBuffer(600, "Bell")
start := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
for i := range 60 {
buf.Append(start.Add(time.Duration(i)*time.Second), float64(i%7))
}
var state mujica.RealtimeChartState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.RealtimeChart(c, &state, buf, mujica.RealtimeChartOptions{Frame: mujica.ChartFrame{Label: "Sound"}, Window: 30 * time.Second})
}
ui.Render(view, 400, 300, 1)`
+137
View File
@@ -0,0 +1,137 @@
package main
import (
"fmt"
"math"
"strings"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
var c1Abbey = mujica.ChartNode{Name: "Abbey", Children: []mujica.ChartNode{
{Name: "Library", Children: []mujica.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}, {Name: "Letters", Value: 8}}},
{Name: "Kitchen", Value: 25},
{Name: "Garden", Children: []mujica.ChartNode{{Name: "Herbs", Value: 12}, {Name: "Vines", Value: 8}}},
{Name: "Chapel", Children: []mujica.ChartNode{{Name: "Nave", Value: 18}, {Name: "Crypt", Value: 6}}},
}}
func c1TreemapDemo(c *ui.Context, log func(string)) {
type state struct {
status string
path []string
}
s := ui.Local(c.Root(), "116", func() state { return state{status: "Ready"} })
c1Section(c, "Treemap", func() {}, func() {
frame, show := c1Frame(c, "Rooms", &s.status)
root := c1Abbey
if !show {
root = mujica.ChartNode{Name: "Abbey"}
s.path = nil
}
r := mujica.Treemap(c, &s.path, root, mujica.TreemapOptions{Frame: frame})
c1Log(log, "Treemap", r)
if r.ValueChanged() {
log("Treemap: path /" + strings.Join(s.path, "/"))
}
})
}
func c1SunburstDemo(c *ui.Context, log func(string)) {
type state struct {
status string
path []string
depth float64
}
s := ui.Local(c.Root(), "117", func() state { return state{status: "Ready", depth: 2} })
c1Section(c, "Sunburst", func() {
ui.Text(c, fmt.Sprintf("%.0f rings", s.depth))
ui.StepSlider(c, &s.depth, 1, 3, 1).Width(120)
}, func() {
frame, show := c1Frame(c, "Rooms", &s.status)
frame.Height = 300
root := c1Abbey
if !show {
root = mujica.ChartNode{Name: "Abbey"}
s.path = nil
}
r := mujica.Sunburst(c, &s.path, root, mujica.SunburstOptions{Frame: frame, Depth: int(s.depth)})
c1Log(log, "Sunburst", r)
if r.ValueChanged() {
log("Sunburst: path /" + strings.Join(s.path, "/"))
}
})
}
func c1SankeyDemo(c *ui.Context, log func(string)) {
type state struct{ status string }
s := ui.Local(c.Root(), "118", func() state { return state{status: "Ready"} })
c1Section(c, "SankeyChart", func() {}, func() {
frame, show := c1Frame(c, "Money", &s.status)
nodes := []mujica.SankeyNode{{ID: "tithe", Label: "Tithes"}, {ID: "alms", Label: "Alms"}, {ID: "fund", Label: "Fund"}, {ID: "roof", Label: "Roof"}, {ID: "bread", Label: "Bread"}, {ID: "bells", Label: "Bells"}}
links := []mujica.SankeyLink{{From: "tithe", To: "fund", Value: 60}, {From: "alms", To: "fund", Value: 30}, {From: "alms", To: "bread", Value: 10}, {From: "fund", To: "roof", Value: 50}, {From: "fund", To: "bells", Value: 40}}
if !show {
links = nil
}
c1Log(log, "SankeyChart", mujica.SankeyChart(c, nodes, links, mujica.SankeyChartOptions{Frame: frame}))
})
}
func c1NetworkDemo(c *ui.Context, log func(string)) {
type state struct {
status string
force bool
graph mujica.NetworkGraphState
}
s := ui.Local(c.Root(), "119", func() state { return state{status: "Ready", force: true} })
c1Section(c, "NetworkGraph", func() {
ui.Checkbox(c, &s.force, "Force layout")
if mujica.Button(c, "Reset view", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.graph.ResetView()
log("NetworkGraph: reset view")
}
}, func() {
frame, show := c1Frame(c, "House", &s.status)
nodes := []mujica.GraphNode{{ID: "a", Label: "Abbot", X: 0, Y: 0}, {ID: "b", Label: "Prior", X: 10, Y: 0}, {ID: "c", Label: "Cellarer", X: 10, Y: 10}, {ID: "d", Label: "Novice", X: 0, Y: 10}, {ID: "e", Label: "Cantor", X: 5, Y: 5}}
edges := []mujica.GraphEdge{{From: "a", To: "b"}, {From: "a", To: "c"}, {From: "c", To: "d"}, {From: "b", To: "e"}, {From: "e", To: "d"}}
if !show {
nodes, edges = nil, nil
}
c1Log(log, "NetworkGraph", mujica.NetworkGraph(c, &s.graph, nodes, edges, mujica.NetworkGraphOptions{Frame: frame, Force: s.force}))
})
}
func c1RealtimeDemo(c *ui.Context, log func(string)) {
type state struct {
status string
feed bool
buf *mujica.RealtimeBuffer
next time.Time
tick int
view mujica.RealtimeChartState
}
s := ui.Local(c.Root(), "120", func() state {
return state{status: "Ready", feed: true, buf: mujica.NewRealtimeBuffer(600, "Bell", "Organ")}
})
// The demo appends a sample every 500 ms while feeding.
if s.feed && !c.Now().Before(s.next) {
t := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC).Add(time.Duration(s.tick) * 500 * time.Millisecond)
x := float64(s.tick)
s.buf.Append(t, 5+3*math.Sin(x/6), 4+2*math.Sin(x/4+1))
s.tick++
s.next = c.Now().Add(500 * time.Millisecond)
}
if s.feed {
c.After(s.next.Sub(c.Now()))
}
c1Section(c, "RealtimeChart", func() { ui.Checkbox(c, &s.feed, "Feed data") }, func() {
frame, _ := c1Frame(c, "Sound", &s.status)
r := mujica.RealtimeChart(c, &s.view, s.buf, mujica.RealtimeChartOptions{Frame: frame, Window: 20 * time.Second})
c1Log(log, "RealtimeChart", r)
if r.ValueChanged() {
log(fmt.Sprintf("RealtimeChart: paused=%v", s.view.Paused))
}
})
}
+197
View File
@@ -0,0 +1,197 @@
package main
import (
"fmt"
"math"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func c1Series(gap bool) []mujica.ChartSeries {
a := []float64{3, 5, 4, 6, 8, 7, 9, 11}
if gap {
a = []float64{3, 5, math.NaN(), 6, 8, 7, 9, 11}
}
return []mujica.ChartSeries{
{Name: "Matins", Values: a},
{Name: "Vespers", Values: []float64{2, 3, 5, 4, 6, 9, 8, 7}},
{Name: "Compline", Values: []float64{1, 2, 2, 3, 2, 4, 5, 4}},
}
}
func c1LineDemo(c *ui.Context, log func(string)) {
type state struct {
status string
gap, time bool
big bool
}
s := ui.Local(c.Root(), "101", func() state { return state{status: "Ready", gap: true} })
c1Section(c, "LineChart", func() {
ui.Checkbox(c, &s.gap, "Missing value")
ui.Checkbox(c, &s.time, "Time axis")
ui.Checkbox(c, &s.big, "100,000 points")
}, func() {
frame, show := c1Frame(c, "Attendance", &s.status)
series, opts := c1Series(s.gap), mujica.LineChartOptions{Frame: frame}
if s.big {
series = []mujica.ChartSeries{{Name: "Wave", Values: c1Wave()}}
}
if s.time {
t0 := time.Date(2026, 10, 5, 6, 0, 0, 0, time.UTC)
opts.Times = make([]time.Time, len(series[0].Values))
for i := range opts.Times {
opts.Times[i] = t0.Add(time.Duration(i) * time.Hour)
}
}
if !show {
series = nil
opts.Times = nil
}
c1Log(log, "LineChart", mujica.LineChart(c, series, opts))
})
}
var c1WaveData []float64
// c1Wave is 100,000 points, made once.
func c1Wave() []float64 {
if c1WaveData == nil {
c1WaveData = make([]float64, 100_000)
for i := range c1WaveData {
c1WaveData[i] = math.Sin(float64(i)/900) + 0.3*math.Sin(float64(i)/37)
}
}
return c1WaveData
}
func c1AreaDemo(c *ui.Context, log func(string)) {
type state struct {
status string
stacked bool
base float64
}
s := ui.Local(c.Root(), "102", func() state { return state{status: "Ready", stacked: true} })
c1Section(c, "AreaChart", func() {
ui.Checkbox(c, &s.stacked, "Stacked")
ui.Text(c, fmt.Sprintf("Baseline %.0f", s.base))
ui.StepSlider(c, &s.base, -4, 4, 1).Width(140)
}, func() {
frame, show := c1Frame(c, "Attendance", &s.status)
series := c1Series(false)
if !show {
series = nil
}
c1Log(log, "AreaChart", mujica.AreaChart(c, series, mujica.AreaChartOptions{Frame: frame, Stacked: s.stacked, Baseline: s.base}))
})
}
func c1BarDemo(c *ui.Context, log func(string)) {
type state struct {
status string
horizontal, stacked, labels bool
}
s := ui.Local(c.Root(), "103", func() state { return state{status: "Ready", labels: true} })
c1Section(c, "BarChart", func() {
ui.Checkbox(c, &s.horizontal, "Horizontal")
ui.Checkbox(c, &s.stacked, "Stacked")
ui.Checkbox(c, &s.labels, "Labels")
}, func() {
frame, show := c1Frame(c, "Tithes", &s.status)
cats := []string{"Nave", "Choir", "Crypt", "Bells"}
series := []mujica.ChartSeries{{Name: "Gold", Values: []float64{4, -2, 3, 5}}, {Name: "Silver", Values: []float64{2, 5, 1, -1}}}
if !show {
series = nil
}
c1Log(log, "BarChart", mujica.BarChart(c, cats, series, mujica.BarChartOptions{Frame: frame, Horizontal: s.horizontal, Stacked: s.stacked, Labels: s.labels}))
})
}
func c1PieDemo(c *ui.Context, log func(string)) {
type state struct {
status string
donut bool
min float64
}
s := ui.Local(c.Root(), "104", func() state { return state{status: "Ready", donut: true, min: 20} })
c1Section(c, "PieChart", func() {
ui.Checkbox(c, &s.donut, "Donut")
ui.Text(c, fmt.Sprintf("Merge under %.0f°", s.min))
ui.StepSlider(c, &s.min, 0, 40, 5).Width(140)
}, func() {
frame, show := c1Frame(c, "Stores", &s.status)
slices := []mujica.ChartSlice{{Label: "Wax", Value: 50}, {Label: "Oil", Value: 30}, {Label: "Myrrh", Value: 12}, {Label: "Salt", Value: 4}, {Label: "Ash", Value: 4}}
if !show {
slices = nil
}
c1Log(log, "PieChart", mujica.PieChart(c, slices, mujica.PieChartOptions{Frame: frame, Donut: s.donut, MinAngle: s.min}))
})
}
func c1ScatterDemo(c *ui.Context, log func(string)) {
type state struct {
status string
bubbles bool
selected []mujica.ScatterKey
}
s := ui.Local(c.Root(), "105", func() state { return state{status: "Ready", bubbles: true} })
c1Section(c, "ScatterChart", func() {
ui.Checkbox(c, &s.bubbles, "Bubbles")
ui.Text(c, fmt.Sprintf("%d selected", len(s.selected)))
}, func() {
frame, show := c1Frame(c, "Choir", &s.status)
groups := []mujica.ScatterGroup{
{Name: "Monks", Points: []mujica.ScatterPoint{{X: 1, Y: 2, Size: 4}, {X: 3, Y: 5, Size: 9}, {X: 6, Y: 4, Size: 1}, {X: 7, Y: 8, Size: 6}}},
{Name: "Nuns", Points: []mujica.ScatterPoint{{X: 2, Y: 7, Size: 16}, {X: 8, Y: 9, Size: 2}, {X: 5, Y: 2, Size: 8}}},
}
if !show {
groups = nil
}
r := mujica.ScatterChart(c, &s.selected, groups, mujica.ScatterChartOptions{Frame: frame, Bubbles: s.bubbles})
c1Log(log, "ScatterChart", r)
if r.ValueChanged() {
log(fmt.Sprintf("ScatterChart: %d selected", len(s.selected)))
}
})
}
func c1RadarDemo(c *ui.Context, log func(string)) {
type state struct{ status string }
s := ui.Local(c.Root(), "106", func() state { return state{status: "Ready"} })
c1Section(c, "RadarChart", func() {}, func() {
frame, show := c1Frame(c, "Skills", &s.status)
series := []mujica.ChartSeries{{Name: "Agnès", Values: []float64{8, 6, 9, 4, 7}}, {Name: "Clare", Values: []float64{5, 9, 6, 8, 3}}}
if !show {
series = nil
}
c1Log(log, "RadarChart", mujica.RadarChart(c, []string{"Voice", "Latin", "Chant", "Organ", "Bells"}, series, mujica.RadarChartOptions{Frame: frame}))
})
}
func c1HeatDemo(c *ui.Context, log func(string)) {
type state struct {
status string
gaps bool
}
s := ui.Local(c.Root(), "107", func() state { return state{status: "Ready", gaps: true} })
c1Section(c, "HeatmapChart", func() { ui.Checkbox(c, &s.gaps, "Empty cells") }, func() {
frame, show := c1Frame(c, "Hours", &s.status)
rows, cols := []string{"Mon", "Tue", "Wed", "Thu"}, []string{"Matins", "Lauds", "Terce", "Vespers", "Compline"}
values := make([][]float64, len(rows))
for r := range rows {
values[r] = make([]float64, len(cols))
for col := range cols {
values[r][col] = float64((r*7 + col*5) % 11)
if s.gaps && (r+col)%6 == 1 {
values[r][col] = math.NaN()
}
}
}
if !show {
rows, values = nil, nil
}
c1Log(log, "HeatmapChart", mujica.HeatmapChart(c, rows, cols, values, mujica.HeatmapChartOptions{Frame: frame}))
})
}
+171
View File
@@ -0,0 +1,171 @@
package main
import (
"fmt"
"math"
"strconv"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func c1CalendarDemo(c *ui.Context, log func(string)) {
type state struct {
status, start string
year int
}
s := ui.Local(c.Root(), "108", func() state { return state{status: "Ready", start: "Monday", year: 2026} })
c1Section(c, "CalendarHeatmap", func() { ui.Select(c, &s.start, []string{"Locale", "Sunday", "Monday", "Saturday"}) }, func() {
frame, show := c1Frame(c, "Prayers", &s.status)
frame.Height = 160
values := map[mujica.Date]float64{}
if show {
d := mujica.Date{Year: s.year, Month: time.January, Day: 1}
for i := range 366 {
if (i*37)%5 != 0 {
values[d.AddDays(i)] = float64((i * 13) % 9)
}
}
}
start := map[string]mujica.WeekStart{"Locale": mujica.WeekStartLocale, "Sunday": mujica.WeekStartSunday, "Monday": mujica.WeekStartMonday, "Saturday": mujica.WeekStartSaturday}[s.start]
r := mujica.CalendarHeatmap(c, &s.year, values, mujica.CalendarHeatmapOptions{Frame: frame, WeekStart: start})
c1Log(log, "CalendarHeatmap", r)
if r.ValueChanged() {
log("CalendarHeatmap: year " + strconv.Itoa(s.year))
}
})
}
func c1HistogramDemo(c *ui.Context, log func(string)) {
type state struct {
status string
bins float64
cumulative bool
values []float64
}
s := ui.Local(c.Root(), "109", func() state {
v := make([]float64, 500)
for i := range v {
v[i] = 50 + 12*math.Sin(float64(i)*1.7)*math.Cos(float64(i)*0.31)
}
return state{status: "Ready", bins: 10, values: v}
})
c1Section(c, "Histogram", func() {
ui.Text(c, fmt.Sprintf("%.0f bins", s.bins))
ui.StepSlider(c, &s.bins, 3, 30, 1).Width(140)
ui.Checkbox(c, &s.cumulative, "Cumulative")
}, func() {
frame, show := c1Frame(c, "Ages", &s.status)
values := s.values
if !show {
values = nil
}
c1Log(log, "Histogram", mujica.Histogram(c, values, mujica.HistogramOptions{Frame: frame, Bins: int(s.bins), Cumulative: s.cumulative}))
})
}
func c1BoxDemo(c *ui.Context, log func(string)) {
type state struct{ status string }
s := ui.Local(c.Root(), "110", func() state { return state{status: "Ready"} })
c1Section(c, "BoxPlot", func() {}, func() {
frame, show := c1Frame(c, "Hymns", &s.status)
groups := []mujica.BoxGroup{
{Name: "Choir", Values: []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 24}},
{Name: "Nave", Values: []float64{4, 5, 5, 6, 7, 8}},
{Name: "Crypt", Values: []float64{12, 14, 15, 18, 22, 25}},
}
if !show {
groups = nil
}
c1Log(log, "BoxPlot", mujica.BoxPlot(c, groups, mujica.BoxPlotOptions{Frame: frame}))
})
}
func c1WaterfallDemo(c *ui.Context, log func(string)) {
type state struct{ status string }
s := ui.Local(c.Root(), "111", func() state { return state{status: "Ready"} })
c1Section(c, "WaterfallChart", func() {}, func() {
frame, show := c1Frame(c, "Ledger", &s.status)
steps := []mujica.WaterfallStep{
{Label: "Tithes", Value: 12}, {Label: "Repairs", Value: -5}, {Label: "Q1", Kind: mujica.WaterfallSubtotal},
{Label: "Alms", Value: 4}, {Label: "Bread", Value: -2}, {Label: "Year", Kind: mujica.WaterfallTotal},
}
if !show {
steps = nil
}
c1Log(log, "WaterfallChart", mujica.WaterfallChart(c, steps, mujica.WaterfallChartOptions{Frame: frame}))
})
}
func c1FunnelDemo(c *ui.Context, log func(string)) {
type state struct{ status string }
s := ui.Local(c.Root(), "112", func() state { return state{status: "Ready"} })
c1Section(c, "FunnelChart", func() {}, func() {
frame, show := c1Frame(c, "Order", &s.status)
stages := []mujica.ChartSlice{{Label: "Visitors", Value: 1000}, {Label: "Novices", Value: 400}, {Label: "Vows", Value: 100}, {Label: "Priors", Value: 12}}
if !show {
stages = nil
}
c1Log(log, "FunnelChart", mujica.FunnelChart(c, stages, mujica.FunnelChartOptions{Frame: frame}))
})
}
func c1BulletDemo(c *ui.Context, log func(string)) {
type state struct {
status string
value, target float64
vertical bool
}
s := ui.Local(c.Root(), "113", func() state { return state{status: "Ready", value: 270, target: 250} })
c1Section(c, "BulletChart", func() {
ui.Text(c, fmt.Sprintf("Value %.0f", s.value))
ui.StepSlider(c, &s.value, 0, 300, 10).Width(140)
ui.Checkbox(c, &s.vertical, "Vertical")
}, func() {
frame, _ := c1Frame(c, "Alms", &s.status)
if s.vertical {
frame.Height = 200
}
target := s.target
c1Log(log, "BulletChart", mujica.BulletChart(c, s.value, mujica.BulletChartOptions{Frame: frame, Target: &target, Bands: []float64{150, 225, 300}, Vertical: s.vertical}))
})
}
func c1SparkDemo(c *ui.Context, log func(string)) {
type state struct{ status string }
s := ui.Local(c.Root(), "114", func() state { return state{status: "Ready"} })
c1Section(c, "Sparkline", func() {}, func() {
frame, show := c1Frame(c, "Bells", &s.status)
values := []float64{3, 5, 4, math.NaN(), 7, 6, 9, 8, 11}
if !show {
values = nil
}
ui.Row(c).Gap(24).Wrap().Children(func() {
for _, k := range []mujica.SparklineKind{mujica.SparklineLine, mujica.SparklineBar, mujica.SparklineArea} {
f := frame
f.Label = fmt.Sprintf("Bells %d", k+1)
c1Log(log, f.Label, mujica.Sparkline(c, values, mujica.SparklineOptions{Frame: f, Kind: k}))
}
})
})
}
func c1GaugeDemo(c *ui.Context, log func(string)) {
type state struct {
status string
value float64
full bool
}
s := ui.Local(c.Root(), "115", func() state { return state{status: "Ready", value: 72} })
c1Section(c, "Gauge", func() {
ui.Text(c, fmt.Sprintf("Value %.0f", s.value))
ui.StepSlider(c, &s.value, 0, 100, 1).Width(140)
ui.Checkbox(c, &s.full, "Full circle")
}, func() {
frame, _ := c1Frame(c, "Nave", &s.status)
segments := []mujica.GaugeSegment{{To: 60, Label: "Calm", Tone: mujica.GaugeGood}, {To: 85, Label: "Busy", Tone: mujica.GaugeWarning}, {To: 100, Label: "Crowded", Tone: mujica.GaugeDanger}}
c1Log(log, "Gauge", mujica.Gauge(c, s.value, mujica.GaugeOptions{Frame: frame, Max: 100, Unit: "%", Segments: segments, Full: s.full}))
})
}
+71
View File
@@ -0,0 +1,71 @@
package main
import (
"slices"
"strings"
"testing"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
// c1Demo runs page id's demo with an event log.
func c1Demo(t *testing.T, id string) (*ui.Tester, *[]string) {
t.Helper()
i := slices.IndexFunc(charts1Pages, func(p page) bool { return p.ID == id })
if i < 0 {
t.Fatalf("no page %s", id)
}
events := &[]string{}
tt := ui.NewTester(func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
ui.Scroll(c).Fill().Padding(24).Children(func() {
charts1Pages[i].Demo(c, func(s string) { *events = append(*events, s) })
})
}, 900, 760)
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
return tt, events
}
// c1Labels names each demo's chart for the keys.
var c1Labels = map[string]string{
"101": "Attendance", "102": "Attendance", "103": "Tithes", "104": "Stores", "105": "Choir", "106": "Skills",
"107": "Hours", "108": "Prayers", "109": "Ages", "110": "Hymns", "111": "Ledger", "112": "Order", "113": "Alms",
"114": "Bells 1", "115": "Nave", "116": "Rooms", "117": "Rooms", "118": "Money", "119": "House", "120": "Sound",
}
func TestCharts1DemosLogReadouts(t *testing.T) {
for _, p := range charts1Pages {
if p.Category != "Charts I" || p.Code == "" || p.Keys == "" {
t.Fatalf("%s: incomplete page", p.ID)
}
tt, events := c1Demo(t, p.ID)
tabUntil(t, tt, c1Labels[p.ID])
tt.Key(0, ui.KeyRight)
if len(*events) == 0 || !strings.HasSuffix((*events)[len(*events)-1], "point 0") {
t.Errorf("%s: keyboard readout not logged: %q", p.ID, *events)
}
}
}
func TestCharts1TreemapDrillLogs(t *testing.T) {
tt, events := c1Demo(t, "116")
tabUntil(t, tt, "Rooms")
tt.Key(0, ui.KeyRight)
tt.Key(0, ui.KeyEnter)
if !slices.Contains(*events, "Treemap: path /Library") {
t.Fatalf("drill not logged: %q", *events)
}
}
func tabUntil(t *testing.T, tt *ui.Tester, label string) {
t.Helper()
for range 40 {
if tt.Focused(label) {
return
}
tt.Key(0, ui.KeyTab)
}
t.Fatalf("Tab never reached %q", label)
}