WIP components (as built)
This commit is contained in:
98 files changed
+2746
-1204
No files matched your search
@@ -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"
|
||||
}
|
||||
@@ -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}))
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user