WIP components (as built)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 15:43:43 -04:00
1 parent c236ea7ece
commit 33e924f2b1
98 files changed
+2746 -1204

No files matched your search

+68 -1
View File
@@ -1,3 +1,70 @@
package main
var charts2Pages = []page{}
import (
"fmt"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
const c2Category = "Charts II"
var charts2Pages = []page{
{ID: "121", Name: "ChartLegend", ZhName: "图例", Category: c2Category, Demo: c2LegendDemo, Code: c2ChartLegendCode, Keys: "Tab enters; arrows move; Space toggles; Enter expands."},
{ID: "122", Name: "ChartTooltip", ZhName: "读数浮层", Category: c2Category, Demo: c2TooltipDemo, Code: c2ChartTooltipCode, Keys: c1Keys},
{ID: "123", Name: "ChartCrosshair", ZhName: "十字光标", Category: c2Category, Demo: c2CrosshairDemo, Code: c2ChartCrosshairCode, Keys: c1Keys},
{ID: "124", Name: "ChartAxis", ZhName: "坐标轴", Category: c2Category, Demo: c2AxisDemo, Code: c2ChartAxisCode, Keys: c1Keys},
{ID: "125", Name: "ChartGrid", ZhName: "图表网格", Category: c2Category, Demo: c2GridDemo, Code: c2ChartGridCode, Keys: c1Keys},
{ID: "126", Name: "ChartBrush", ZhName: "区间刷选", Category: c2Category, Demo: c2BrushDemo, Code: c2ChartBrushCode, Keys: "Tab: start, window, end; arrows step; PageUp/Down step ten; Home/End reach bounds."},
{ID: "127", Name: "ChartAnnotation", ZhName: "图表标注", Category: c2Category, Demo: c2AnnotationDemo, Code: c2ChartAnnotationCode, Keys: c1Keys + " Alt+PageUp/Down selects a movable note; Alt+arrows move it."},
{ID: "128", Name: "ChartExport", ZhName: "图表导出", Category: c2Category, Demo: c2ExportDemo, Code: c2ChartExportCode, Keys: "Tab reaches Export PNG; Enter or Space renders bytes in the current theme."},
{ID: "129", Name: "ViolinPlot", ZhName: "小提琴图", Category: c2Category, Demo: c2ViolinDemo, Code: c2ViolinPlotCode, Keys: c1Keys},
{ID: "130", Name: "ParallelCoordinates", ZhName: "平行坐标", Category: c2Category, Demo: c2ParallelDemo, Code: c2ParallelCoordinatesCode, Keys: c1Keys + " Alt+PageUp/Down selects an axis; Alt+Up/Down adjusts its lower bound; Alt+Shift+Up/Down adjusts its upper bound; Alt+Delete clears."},
{ID: "131", Name: "ChordDiagram", ZhName: "弦图", Category: c2Category, Demo: c2ChordDemo, Code: c2ChordDiagramCode, Keys: c1Keys},
{ID: "132", Name: "SparkBar", ZhName: "迷你柱", Category: c2Category, Demo: c2SparkBarDemo, Code: c2SparkBarCode, Keys: "Tab focuses bars; Left/Right read points; Home/End jump; Escape restores the last value."},
{ID: "133", Name: "TrendIndicator", ZhName: "趋势指示", Category: c2Category, Demo: c2TrendDemo, Code: c2TrendIndicatorCode, Keys: "Change and Invert controls use the keyboard; the indicator is read as text."},
{ID: "138", Name: "DensityPlot", ZhName: "密度图", Category: c2Category, Demo: c2DensityDemo, Code: c2DensityPlotCode, Keys: c1Keys + " Space on legend entries hides or shows a group."},
{ID: "140", Name: "ChartEmptyState", ZhName: "图表占位", Category: c2Category, Demo: c2EmptyDemo, Code: c2ChartEmptyStateCode, Keys: "Tab reaches Retry; Enter or Space retries."},
}
func c2Check(c *ui.Context, value *bool, label string, log func(string)) {
if mujica.Checkbox(c, value, label, mujica.CheckboxOptions{}).Changed() {
log(fmt.Sprintf("%s = %v", label, *value))
}
}
func c2Number(c *ui.Context, value *float64, label string, lo, hi, step float64, log func(string)) {
ui.Row(c).Gap(8).AlignItems(ui.Center).Children(func() {
ui.Text(c, label)
r := mujica.NumberInput(c, value, mujica.NumberInputOptions{Label: label, Min: lo, Max: hi, Step: step})
r.Element.Width(150)
if r.Changed() {
log(fmt.Sprintf("%s = %g", label, *value))
}
})
}
func c2Select(c *ui.Context, value *string, label string, choices []string, log func(string)) {
items := make([]mujica.SelectOption[string], len(choices))
for i, s := range choices {
items[i] = mujica.SelectOption[string]{Value: s, Label: s}
}
r := mujica.Select(c, value, items, mujica.SelectOptions{Label: label})
r.Element.Width(160)
if r.Changed() {
log(label + " = " + *value)
}
}
func c2Frame(c *ui.Context, label string, status *string, log func(string)) (mujica.ChartFrame, bool) {
c2Select(c, status, "Status", c1Statuses, log)
f := mujica.ChartFrame{Label: label}
if *status == "Loading" {
f.Status = mujica.DataLoading
}
if *status == "Failed" {
f.Status, f.Error = mujica.DataFailed, "The archive did not answer"
}
return f, *status != "Empty"
}
+145
View File
@@ -0,0 +1,145 @@
package main
const c2ChartLegendCode = `hidden := map[string]bool{}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
k := mujica.Tokens(c)
items := []mujica.ChartLegendItem{{Name: "Pews", Color: k.Info}, {Name: "Candles", Color: k.Success, Shape: 1}}
mujica.ChartLegend(c, &hidden, items, mujica.ChartLegendOptions{MaxItems: 1})
}
ui.Render(view, 400, 200, 1)`
const c2ChartTooltipCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
k := mujica.Tokens(c)
mujica.ChartTooltip(c, 40, 40, []mujica.ChartTooltipLine{{Text: "Lauds"}, {Text: "Pews 5", Color: k.Info}, {Text: "Candles 2", Color: k.Success, Shape: 1}})
}
ui.Render(view, 400, 200, 1)`
const c2ChartCrosshairCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Max: 10})
y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100})
hair := mujica.ChartCrosshair(c, mujica.ChartCrosshairOptions{X: &x, Y: &y})
ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) {
plot := ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60}
x.Draw(p, plot)
y.Draw(p, plot)
hair.Draw(p, plot, x.Map(plot, 5), y.Map(plot, 70))
})
}
ui.Render(view, 400, 300, 1)`
const c2ChartAxisCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
axis := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Kind: mujica.AxisLog, Min: 1, Max: 1000})
ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) {
axis.Draw(p, ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60})
})
}
ui.Render(view, 400, 300, 1)`
const c2ChartGridCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Max: 10})
y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100})
grid := mujica.ChartGrid(c, mujica.ChartGridOptions{X: &x, Y: &y, Minor: 2})
ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) {
plot := ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60}
grid.Draw(p, plot)
x.Draw(p, plot)
y.Draw(p, plot)
})
}
ui.Render(view, 400, 300, 1)`
const c2ChartBrushCode = `value := [2]float64{20, 60}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ChartBrush(c, &value, mujica.ChartBrushOptions{Label: "Months", Max: 100, Step: 1, Values: []float64{2, 4, 3, 8, 5}})
}
ui.Render(view, 400, 160, 1)`
const c2ChartAnnotationCode = `notes := []mujica.ChartNote{{Kind: mujica.NoteHLine, Value: 50, Text: "Goal", Draggable: true}, {Kind: mujica.NoteBand, Value: 6, End: 8, Text: "Lent"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Max: 10})
y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100})
annotation := mujica.ChartAnnotation(c, notes, mujica.ChartAnnotationOptions{X: &x, Y: &y})
box := ui.Box(c).Fill().Focusable().Label("Annotations")
b := box.Bounds()
annotation.Drag(box, ui.Rect{X: 60, Y: 20, W: b.W - 80, H: b.H - 60})
box.Draw(func(p *ui.Painter, r ui.Rect) {
plot := ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60}
x.Draw(p, plot)
y.Draw(p, plot)
annotation.Draw(p, plot)
})
}
ui.Render(view, 400, 300, 1)`
const c2ChartExportCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
data, err := mujica.ChartExport(c, func(c *ui.Context) {
mujica.SparkBar(c, []float64{3, -2, 5}, mujica.SparkBarOptions{Frame: mujica.ChartFrame{Label: "Alms"}})
}, mujica.ChartExportOptions{Width: 300, Height: 100, Scale: 2})
if err != nil {
panic(err)
}
bitmap, err := ui.DecodeBitmap(data)
if err != nil {
panic(err)
}
ui.Image(c, bitmap).Size(300, 100)
}
ui.Render(view, 400, 160, 1)`
const c2SparkBarCode = `values := []float64{3, -2, 4, 1, -3}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.SparkBar(c, values, mujica.SparkBarOptions{Frame: mujica.ChartFrame{Label: "Alms"}, BarWidth: 10})
}
ui.Render(view, 400, 100, 1)`
const c2TrendIndicatorCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.TrendIndicator(c, 12.5, mujica.TrendIndicatorOptions{Precision: 1, Unit: "%", Note: "vs Lent"})
}
ui.Render(view, 400, 100, 1)`
const c2ChartEmptyStateCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ChartEmptyState(c, mujica.DataFailed, mujica.ChartEmptyStateOptions{Height: 180, Error: "The archive did not answer"})
}
ui.Render(view, 400, 240, 1)`
const c2ViolinPlotCode = `groups := []mujica.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 7, 9}}, {Name: "Nave", Values: []float64{2, 4, 5, 6, 8}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ViolinPlot(c, groups, mujica.ViolinPlotOptions{Frame: mujica.ChartFrame{Label: "Hymns"}, Bandwidth: 1})
}
ui.Render(view, 400, 300, 1)`
const c2ParallelCoordinatesCode = `filters := map[string][2]float64{}
axes := []string{"Age", "Hymns", "Vigils"}
rows := [][]float64{{20, 3, 9}, {35, 8, 2}, {50, 5, 5}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ParallelCoordinates(c, &filters, axes, rows, mujica.ParallelCoordinatesOptions{Frame: mujica.ChartFrame{Label: "Monks"}, Names: []string{"Agnes", "Clare", "Hild"}})
}
ui.Render(view, 400, 300, 1)`
const c2ChordDiagramCode = `names := []string{"Nave", "Choir", "Crypt"}
matrix := [][]float64{{0, 5, 2}, {4, 0, 1}, {1, 2, 0}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ChordDiagram(c, names, matrix, mujica.ChordDiagramOptions{Frame: mujica.ChartFrame{Label: "Paths"}})
}
ui.Render(view, 400, 300, 1)`
const c2DensityPlotCode = `groups := []mujica.BoxGroup{{Name: "Matins", Values: []float64{1, 2, 2, 3, 7}}, {Name: "Vespers", Values: []float64{3, 4, 5, 6, 6}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.DensityPlot(c, groups, mujica.DensityPlotOptions{Frame: mujica.ChartFrame{Label: "Ages"}, Bandwidth: 1, Points: 128})
}
ui.Render(view, 400, 300, 1)`
@@ -0,0 +1,105 @@
package main
import (
"fmt"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func c2ViolinDemo(c *ui.Context, log func(string)) { c2DistributionDemo(c, log, "129", "ViolinPlot") }
func c2DensityDemo(c *ui.Context, log func(string)) { c2DistributionDemo(c, log, "138", "DensityPlot") }
func c2DistributionDemo(c *ui.Context, log func(string), id, name string) {
type state struct {
status string
bandwidth float64
disabled bool
groups []mujica.BoxGroup
}
s := ui.Local(c.Root(), id, func() state {
return state{status: "Ready", groups: []mujica.BoxGroup{
{Name: "Matins", Values: []float64{1, 2, 2, 3, 4, 5, 8, 9}},
{Name: "Vespers", Values: []float64{3, 4, 5, 6, 6, 7, 8, 11}},
}}
})
c1Section(c, name, func() {
c2Number(c, &s.bandwidth, "Bandwidth", 0, 5, 0.5, log)
c2Check(c, &s.disabled, "Disabled", log)
}, func() {
f, show := c2Frame(c, name, &s.status, log)
groups := s.groups
if !show {
groups = nil
}
ui.Column(c).Disabled(s.disabled).Children(func() {
if id == "129" {
c1Log(log, name, mujica.ViolinPlot(c, groups, mujica.ViolinPlotOptions{Frame: f, Bandwidth: s.bandwidth}))
} else {
c1Log(log, name, mujica.DensityPlot(c, groups, mujica.DensityPlotOptions{Frame: f, Bandwidth: s.bandwidth}))
}
})
})
}
func c2ParallelDemo(c *ui.Context, log func(string)) {
type state struct {
status string
disabled bool
filters map[string][2]float64
rows [][]float64
}
s := ui.Local(c.Root(), "130", func() state {
return state{status: "Ready", filters: map[string][2]float64{}, rows: [][]float64{{20, 3, 9}, {35, 8, 2}, {50, 5, 5}, {65, 1, 4}}}
})
c1Section(c, "ParallelCoordinates", func() {
c2Check(c, &s.disabled, "Disabled", log)
if mujica.Button(c, "Clear filters", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() {
s.filters = map[string][2]float64{}
log("ParallelCoordinates: clear filters")
}
}, func() {
f, show := c2Frame(c, "ParallelCoordinates", &s.status, log)
rows := s.rows
names := []string{"Agnes", "Clare", "Hild", "Bede"}
if !show {
rows = nil
names = nil
}
ui.Column(c).Disabled(s.disabled).Children(func() {
c1Log(log, "ParallelCoordinates", mujica.ParallelCoordinates(c, &s.filters, []string{"Age", "Hymns", "Vigils"}, rows, mujica.ParallelCoordinatesOptions{Frame: f, Names: names}))
})
ui.Text(c, fmt.Sprintf("Filters: %v", s.filters))
})
}
func c2ChordDemo(c *ui.Context, log func(string)) {
type state struct {
status string
disabled bool
amount float64
matrix [][]float64
}
s := ui.Local(c.Root(), "131", func() state {
return state{status: "Ready", amount: 5, matrix: [][]float64{{0, 5, 2}, {4, 0, 1}, {1, 2, 0}}}
})
c1Section(c, "ChordDiagram", func() {
old := s.amount
c2Number(c, &s.amount, "Nave to Choir", 0, 20, 1, log)
if old != s.amount {
s.matrix = [][]float64{{0, s.amount, 2}, {4, 0, 1}, {1, 2, 0}}
}
c2Check(c, &s.disabled, "Disabled", log)
}, func() {
f, show := c2Frame(c, "ChordDiagram", &s.status, log)
names, matrix := []string{"Nave", "Choir", "Crypt"}, s.matrix
if !show {
names = nil
matrix = nil
}
ui.Column(c).Disabled(s.disabled).Children(func() {
c1Log(log, "ChordDiagram", mujica.ChordDiagram(c, names, matrix, mujica.ChordDiagramOptions{Frame: f}))
})
})
}
+138
View File
@@ -0,0 +1,138 @@
package main
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"mujicaui/internal"
)
type c2PartsState struct {
status, kind string
disabled, crosshair, draggable, fixed bool
minor float64
notes []mujica.ChartNote
}
func c2TooltipDemo(c *ui.Context, log func(string)) { c2PartsDemo(c, log, "122", "ChartTooltip") }
func c2CrosshairDemo(c *ui.Context, log func(string)) { c2PartsDemo(c, log, "123", "ChartCrosshair") }
func c2AxisDemo(c *ui.Context, log func(string)) { c2PartsDemo(c, log, "124", "ChartAxis") }
func c2GridDemo(c *ui.Context, log func(string)) { c2PartsDemo(c, log, "125", "ChartGrid") }
func c2AnnotationDemo(c *ui.Context, log func(string)) { c2PartsDemo(c, log, "127", "ChartAnnotation") }
func c2PartsDemo(c *ui.Context, log func(string), id, name string) {
s := ui.Local(c.Root(), id, func() c2PartsState {
return c2PartsState{status: "Ready", kind: "Linear", crosshair: true, minor: 2, draggable: true, notes: []mujica.ChartNote{
{Kind: mujica.NoteHLine, Value: 50, Text: "Goal"},
{Kind: mujica.NoteVLine, Value: 3, Text: "Advent"},
{Kind: mujica.NoteBand, Value: 6, End: 8, Text: "Lent"},
{Kind: mujica.NoteText, Value: 1, End: 90, Text: "Feast"},
}}
})
c1Section(c, name, func() {
c2Check(c, &s.disabled, "Disabled", log)
switch id {
case "123":
c2Check(c, &s.crosshair, "Crosshair", log)
case "124":
c2Select(c, &s.kind, "Axis kind", []string{"Linear", "Log", "Time", "Category"}, log)
c2Check(c, &s.fixed, "Fixed ticks", log)
case "125":
c2Number(c, &s.minor, "Minor", 0, 8, 1, log)
case "127":
c2Check(c, &s.draggable, "Draggable", log)
}
}, func() {
f, show := c2Frame(c, name, &s.status, log)
ui.Column(c).Disabled(s.disabled).Children(func() {
c1Log(log, name, c2PartsChart(c, f, show, s, id, log))
})
if id == "127" {
ui.Text(c, fmt.Sprintf("Goal %.1f · Advent %.1f · Lent %.1f–%.1f · Feast %.1f, %.1f", s.notes[0].Value, s.notes[1].Value, s.notes[2].Value, s.notes[2].End, s.notes[3].Value, s.notes[3].End))
}
})
}
func c2PartsChart(c *ui.Context, frame mujica.ChartFrame, show bool, s *c2PartsState, id string, log func(string)) mujica.ChartResult {
xo := mujica.ChartAxisOptions{Max: 10}
yo := mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100}
pts := [][2]float64{{2, 30}, {5, 80}, {8, 55}}
if id == "124" {
switch s.kind {
case "Log":
yo.Kind, yo.Min = mujica.AxisLog, 1
case "Time":
xo.Kind = mujica.AxisTime
xo.From = time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
xo.To = xo.From.Add(10 * time.Hour)
for i := range pts {
pts[i][0] = float64(xo.From.Unix()) + pts[i][0]*3600
}
case "Category":
xo.Kind, xo.Categories = mujica.AxisCategory, []string{"Nave", "Choir", "Crypt"}
for i := range pts {
pts[i][0] = float64(i)
}
}
if s.fixed {
yo.Ticks = []float64{10, 50, 90}
}
}
x, y := mujica.ChartAxis(c, xo), mujica.ChartAxis(c, yo)
grid := mujica.ChartGrid(c, mujica.ChartGridOptions{Y: &y, Minor: int(s.minor)})
hair := mujica.ChartCrosshair(c, mujica.ChartCrosshairOptions{X: &x, Y: &y})
var notes []mujica.ChartNote
if id == "127" {
notes = s.notes
for i := range notes {
notes[i].Draggable = s.draggable
}
}
annotation := mujica.ChartAnnotation(c, notes, mujica.ChartAnnotationOptions{X: &x, Y: &y})
plotOf := func(r ui.Rect) ui.Rect {
return ui.Rect{X: r.X + 64, Y: r.Y + 16, W: max(1, r.W-100), H: max(1, r.H-56)}
}
at := func(r ui.Rect, i int) (float32, float32) {
plot := plotOf(r)
return x.Map(plot, pts[i][0]), y.Map(plot, pts[i][1])
}
marks := len(pts)
if !show {
marks = 0
}
colors := internal.Palette(mujica.Tokens(c))
return mujica.Chart(c, frame, mujica.ChartSpec{
Marks: marks,
Pick: func(r ui.Rect, px, py float32) int {
return hair.Snap(len(pts), func(i int) (float32, float32) { return at(r, i) }, px, py)
},
Anchor: at,
Readout: func(i int) string { return fmt.Sprintf("x %s · y %s", x.Label(pts[i][0]), y.Label(pts[i][1])) },
Tooltip: func(i int) []mujica.ChartTooltipLine {
return []mujica.ChartTooltipLine{{Text: x.Label(pts[i][0])}, {Text: "Pews " + y.Label(pts[i][1]), Color: colors[0]}, {Text: "Candles " + y.Label(pts[i][1]/2), Color: colors[1], Shape: 1}}
},
Input: func(el *ui.Element, r ui.Rect) {
if annotation.Drag(el, plotOf(r)) {
log("ChartAnnotation: moved")
}
},
Draw: func(p *ui.Painter, r ui.Rect, active int) {
plot := plotOf(r)
grid.Draw(p, plot)
x.Draw(p, plot)
y.Draw(p, plot)
annotation.Draw(p, plot)
for i := range pts {
px, py := at(r, i)
internal.Marker(p, i, px, py, 10, colors[i])
}
if id == "123" && s.crosshair && active >= 0 {
px, py := at(r, active)
hair.Draw(p, plot, px, py)
}
},
})
}
+194
View File
@@ -0,0 +1,194 @@
package main
import (
"fmt"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"mujicaui/internal"
)
func c2LegendDemo(c *ui.Context, log func(string)) {
type state struct {
hidden map[string]bool
vertical, disabled bool
max float64
}
s := ui.Local(c.Root(), "121", func() state { return state{hidden: map[string]bool{}, max: 3} })
c1Section(c, "ChartLegend", func() {
c2Check(c, &s.vertical, "Vertical", log)
c2Check(c, &s.disabled, "Disabled", log)
c2Number(c, &s.max, "Max items", 0, 6, 1, log)
}, func() {
pal := internal.Palette(mujica.Tokens(c))
names := []string{"Matins", "Lauds", "Prime", "Terce", "Sext", "None"}
items := make([]mujica.ChartLegendItem, len(names))
for i, n := range names {
items[i] = mujica.ChartLegendItem{Name: n, Color: pal[i], Shape: i % 5}
}
ui.Column(c).Disabled(s.disabled).Children(func() {
r := mujica.ChartLegend(c, &s.hidden, items, mujica.ChartLegendOptions{Vertical: s.vertical, MaxItems: int(s.max)})
if expanded, ok := r.Expanded(); ok {
log(fmt.Sprintf("ChartLegend: expanded %v", expanded))
}
if n, ok := r.Toggled(); ok {
log("ChartLegend: toggled " + n)
}
})
ui.Text(c, fmt.Sprintf("Hidden: %v", s.hidden))
})
}
func c2BrushDemo(c *ui.Context, log func(string)) {
type state struct {
value [2]float64
disabled bool
step float64
}
s := ui.Local(c.Root(), "126", func() state { return state{value: [2]float64{20, 60}, step: 1} })
c1Section(c, "ChartBrush", func() {
c2Check(c, &s.disabled, "Disabled", log)
c2Number(c, &s.step, "Step", 1, 10, 1, log)
}, func() {
vals := []float64{1, 4, 2, 7, 3, 8, 4, 9}
r := mujica.ChartBrush(c, &s.value, mujica.ChartBrushOptions{Label: "Months", Max: 100, Step: s.step, Values: vals, Disabled: s.disabled})
if r.Changed() {
log(fmt.Sprintf("ChartBrush: range %g–%g", s.value[0], s.value[1]))
}
x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Min: s.value[0], Max: s.value[1]})
y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 10})
xs := []float64{0, 15, 30, 45, 60, 75, 90, 100}
var visible []int
for i, v := range xs {
if v >= s.value[0] && v <= s.value[1] {
visible = append(visible, i)
}
}
plotOf := func(r ui.Rect) ui.Rect { return ui.Rect{X: r.X + 40, Y: r.Y + 8, W: max(1, r.W-60), H: max(1, r.H-40)} }
at := func(r ui.Rect, i int) (float32, float32) {
plot := plotOf(r)
return x.Map(plot, xs[visible[i]]), y.Map(plot, vals[visible[i]])
}
c1Log(log, "Zoom preview", mujica.Chart(c, mujica.ChartFrame{Label: "Zoom preview", Height: 140}, mujica.ChartSpec{
Marks: len(visible), Anchor: at,
Pick: func(r ui.Rect, px, py float32) int {
return internal.Nearest(len(visible), func(i int) (float32, float32) { return at(r, i) }, px, py, 24, false)
},
Readout: func(i int) string { return fmt.Sprintf("x %g · y %g", xs[visible[i]], vals[visible[i]]) },
Draw: func(p *ui.Painter, r ui.Rect, active int) {
plot := plotOf(r)
x.Draw(p, plot)
y.Draw(p, plot)
for i := range visible {
px, py := at(r, i)
internal.Marker(p, 0, px, py, 8, mujica.Tokens(c).AccentText)
}
},
}))
})
}
func c2ExportDemo(c *ui.Context, log func(string)) {
type state struct {
width, height, scale float64
disabled bool
bitmap *ui.Bitmap
bytes int
error string
}
s := ui.Local(c.Root(), "128", func() state { return state{width: 400, height: 240, scale: 2} })
c1Section(c, "ChartExport", func() {
c2Number(c, &s.width, "Width", 200, 600, 50, log)
c2Number(c, &s.height, "Height", 160, 400, 40, log)
c2Number(c, &s.scale, "Scale", 1, 3, 0.5, log)
c2Check(c, &s.disabled, "Disabled", log)
}, func() {
if mujica.Button(c, "Export PNG", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() {
data, err := mujica.ChartExport(c, func(c *ui.Context) {
mujica.BarChart(c, []string{"Nave", "Choir", "Crypt"}, []mujica.ChartSeries{{Name: "Pews", Values: []float64{3, 5, 2}}}, mujica.BarChartOptions{Frame: mujica.ChartFrame{Label: "Exported chart", Height: float32(s.height) - 80}})
}, mujica.ChartExportOptions{Width: int(s.width), Height: int(s.height), Scale: float32(s.scale)})
if err == nil {
s.bitmap, err = ui.DecodeBitmap(data)
}
if err != nil {
s.error = err.Error()
log("ChartExport: " + s.error)
} else {
s.error = ""
s.bytes = len(data)
log(fmt.Sprintf("ChartExport: %d bytes", s.bytes))
}
}
if s.error != "" {
ui.Text(c, s.error).TextColor(mujica.Tokens(c).Danger).Role(ui.RoleStatus)
}
if s.bitmap != nil {
ui.Image(c, s.bitmap).Size(float32(s.width), float32(s.height))
ui.Text(c, fmt.Sprintf("PNG: %d bytes", s.bytes))
}
})
}
func c2SparkBarDemo(c *ui.Context, log func(string)) {
type state struct {
status string
width float64
disabled bool
}
s := ui.Local(c.Root(), "132", func() state { return state{status: "Ready", width: 10} })
c1Section(c, "SparkBar", func() {
c2Number(c, &s.width, "Bar width", 2, 20, 1, log)
c2Check(c, &s.disabled, "Disabled", log)
}, func() {
f, show := c2Frame(c, "Alms", &s.status, log)
vals := []float64{3, -2, 4, 1, -3}
if !show {
vals = nil
}
ui.Column(c).Disabled(s.disabled).Children(func() {
c1Log(log, "SparkBar", mujica.SparkBar(c, vals, mujica.SparkBarOptions{Frame: f, BarWidth: float32(s.width)}))
})
})
}
func c2TrendDemo(c *ui.Context, log func(string)) {
type state struct {
change, precision float64
invert bool
}
s := ui.Local(c.Root(), "133", func() state { return state{change: 12.5, precision: 1} })
c1Section(c, "TrendIndicator", func() {
c2Number(c, &s.change, "Change", -100, 100, 0.5, log)
c2Number(c, &s.precision, "Precision", 0, 3, 1, log)
c2Check(c, &s.invert, "Invert", log)
}, func() {
mujica.TrendIndicator(c, s.change, mujica.TrendIndicatorOptions{Precision: int(s.precision), Unit: "%", Note: "vs Lent", Invert: s.invert})
ui.Row(c).Gap(24).Children(func() {
mujica.TrendIndicator(c, 3, mujica.TrendIndicatorOptions{})
mujica.TrendIndicator(c, -3, mujica.TrendIndicatorOptions{})
mujica.TrendIndicator(c, 0, mujica.TrendIndicatorOptions{})
})
})
}
func c2EmptyDemo(c *ui.Context, log func(string)) {
type state struct {
status string
height float64
disabled bool
}
s := ui.Local(c.Root(), "140", func() state { return state{status: "Failed", height: 180} })
c1Section(c, "ChartEmptyState", func() {
c2Number(c, &s.height, "Height", 160, 320, 40, log)
c2Check(c, &s.disabled, "Disabled", log)
}, func() {
f, _ := c2Frame(c, "ChartEmptyState", &s.status, log)
ui.Column(c).Disabled(s.disabled).Border(1, mujica.Tokens(c).Border).Children(func() {
if mujica.ChartEmptyState(c, f.Status, mujica.ChartEmptyStateOptions{Height: float32(s.height), Error: f.Error}).Changed() {
log("ChartEmptyState: retry")
s.status = "Loading"
}
})
})
}
+111
View File
@@ -0,0 +1,111 @@
package main
import (
"slices"
"strings"
"testing"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func c2Demo(t *testing.T, id string) (*ui.Tester, *[]string) {
t.Helper()
i := slices.IndexFunc(charts2Pages, func(p page) bool { return p.ID == id })
if i < 0 {
t.Fatalf("missing 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() { charts2Pages[i].Demo(c, func(s string) { *events = append(*events, s) }) })
}, 900, 760)
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
return tt, events
}
func TestCharts2ExistingSet(t *testing.T) {
want := []string{"121", "122", "123", "124", "125", "126", "127", "128", "129", "130", "131", "132", "133", "138", "140"}
var ids []string
for _, p := range charts2Pages {
ids = append(ids, p.ID)
if p.Category != "Charts II" || p.Code == "" || p.Keys == "" {
t.Fatalf("incomplete page %s", p.ID)
}
c2Demo(t, p.ID)
}
if !slices.Equal(ids, want) {
t.Fatalf("page IDs %v", ids)
}
}
func TestCharts2ReadoutsAndControlsLog(t *testing.T) {
for _, id := range []string{"122", "123", "124", "125", "127", "129", "130", "131", "132", "138"} {
tt, events := c2Demo(t, id)
p := charts2Pages[slices.IndexFunc(charts2Pages, func(p page) bool { return p.ID == id })]
label := p.Name
if id == "132" {
label = "Alms"
}
tabUntil(t, tt, label)
tt.Key(0, ui.KeyRight)
if !slices.Contains(*events, p.Name+": point 0") {
t.Errorf("%s readout events %q", id, *events)
}
if err := tt.Click("Disabled"); err != nil {
t.Fatal(err)
}
if !slices.Contains(*events, "Disabled = true") {
t.Errorf("%s disabled events %q", id, *events)
}
}
}
func TestCharts2LegendBrushAnnotationAndRetry(t *testing.T) {
tt, events := c2Demo(t, "121")
if err := tt.Click("+3 more"); err != nil {
t.Fatal(err)
}
if err := tt.Click("Matins"); err != nil {
t.Fatal(err)
}
if !slices.Contains(*events, "ChartLegend: expanded true") || !slices.Contains(*events, "ChartLegend: toggled Matins") {
t.Fatalf("legend events %q", *events)
}
tt, events = c2Demo(t, "126")
tabUntil(t, tt, "Months: 20 to 60")
tt.Key(0, ui.KeyRight)
if !slices.Contains(*events, "ChartBrush: range 21–61") {
t.Fatalf("brush events %q", *events)
}
tt, events = c2Demo(t, "127")
tabUntil(t, tt, "ChartAnnotation")
tt.Key(ui.Alt, ui.KeyUp)
if !slices.Contains(*events, "ChartAnnotation: moved") || !tt.HasText("Goal 51.0 · Advent 3.0 · Lent 6.0–8.0 · Feast 1.0, 90.0") {
t.Fatalf("annotation events %q text %q", *events, tt.Texts())
}
tt, events = c2Demo(t, "140")
if err := tt.Click("Retry"); err != nil {
t.Fatal(err)
}
if !slices.Contains(*events, "ChartEmptyState: retry") || !tt.HasText("Loading") {
t.Fatalf("retry events %q", *events)
}
}
func TestCharts2ExportAndTrendControls(t *testing.T) {
tt, events := c2Demo(t, "128")
tabUntil(t, tt, "Export PNG")
tt.Key(0, ui.KeyEnter)
if len(*events) == 0 || !strings.HasPrefix((*events)[len(*events)-1], "ChartExport: ") || !strings.HasSuffix((*events)[len(*events)-1], " bytes") {
t.Fatalf("export events %q", *events)
}
tt, events = c2Demo(t, "133")
if err := tt.Click("Invert"); err != nil {
t.Fatal(err)
}
if !slices.Contains(*events, "Invert = true") {
t.Fatalf("trend events %q", *events)
}
}