WIP components (as built)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 15:51:57 -04:00
1 parent 73df90b9b1
commit b4798a6287
110 files changed
+3975 -273

No files matched your search

+67 -37
View File
@@ -3,6 +3,7 @@ package mujica
import (
"fmt"
"math"
"slices"
"github.com/egoist/mygo/ui"
@@ -11,7 +12,6 @@ import (
"mujicaui/theme"
)
// AllocationItem is an asset class's value and target weight in percent.
type AllocationItem struct {
Name string
Value float64
@@ -20,13 +20,12 @@ type AllocationItem struct {
// AssetAllocationChartOptions configure an AssetAllocationChart.
type AssetAllocationChartOptions struct {
Frame ChartFrame
// Tolerance is the drift in points that warns; zero means 5.
Frame ChartFrame
Version int
Tolerance float64
Format func(v float64) string
}
// AssetAllocationChart shows holdings as a donut and lists each weight against its target.
func AssetAllocationChart(c *ui.Context, items []AllocationItem, opts AssetAllocationChartOptions) ChartResult {
if opts.Tolerance < 0 {
panic("mujica: AssetAllocationChart Tolerance is negative")
@@ -35,62 +34,93 @@ func AssetAllocationChart(c *ui.Context, items []AllocationItem, opts AssetAlloc
if tol == 0 {
tol = 5
}
total, targets := 0.0, 0.0
for _, it := range items {
if it.Name == "" || it.Value < 0 || it.Target < 0 || math.IsNaN(it.Value) || math.IsNaN(it.Target) {
panic("mujica: AllocationItem needs a Name and values ≥ 0")
}
total += it.Value
targets += it.Target
}
if len(items) > 0 && math.Abs(targets-100) > 0.01 && targets != 0 {
panic(fmt.Sprintf("mujica: AllocationItem targets sum to %v, not 100", targets))
}
k := Tokens(c)
u := Density(c).Unit()
pal := internal.Palette(k)
var res ChartResult
row := ui.Row(c).Gap(u * 6).AlignItems(ui.Start).Wrap()
row.Children(func() {
slices := make([]ChartSlice, len(items))
for i, it := range items {
slices[i] = ChartSlice{Label: it.Name, Value: it.Value}
memo := ui.Local(row, "mujica.allocation", func() allocationSlices { return allocationSlices{} })
if id := chartID(items) + fmt.Sprint(opts.Version) + msg(c, "pie_chart.other"); id != memo.id {
total, targets := 0.0, 0.0
for _, it := range items {
if it.Name == "" || it.Value < 0 || it.Target < 0 || math.IsNaN(it.Value) || math.IsNaN(it.Target) || math.IsInf(it.Value, 0) || math.IsInf(it.Target, 0) {
panic("mujica: AllocationItem needs a Name and values ≥ 0")
}
total += it.Value
targets += it.Target
}
if len(items) > 0 && math.Abs(targets-100) > 0.01 && targets != 0 {
panic(fmt.Sprintf("mujica: AllocationItem targets sum to %v, not 100", targets))
}
memo.total = total
memo.id, memo.slices = id, make([]ChartSlice, len(items))
for i, it := range items {
memo.slices[i] = ChartSlice{Label: it.Name, Value: it.Value}
}
memo.parts = pieMerge(memo.slices, 1, msg(c, "pie_chart.other"))
memo.markers = make([]int, len(items))
for i, it := range items {
at := slices.IndexFunc(memo.parts, func(part ChartSlice) bool { return part.Label == it.Name })
if at < 0 {
at = len(memo.parts) - 1
}
memo.markers[i] = at
}
}
parts := memo.parts
total := memo.total
frame := opts.Frame
if frame.Height == 0 {
frame.Height = 220
}
ui.Column(c).Width(frame.Height + 40).Children(func() {
res = PieChart(c, slices, PieChartOptions{Frame: frame, Donut: true, Format: opts.Format})
ui.Column(c).Width(max(frame.Height+40, 300)).Children(func() {
res = PieChart(c, parts, PieChartOptions{Frame: frame, Donut: true, Format: opts.Format})
})
if total == 0 {
return
}
size := FontSize(c, theme.CaptionSize)
g := ui.Grid(c).ColumnTracks(ui.FitContent(), ui.FitContent(), ui.FitContent(), ui.FitContent()).GapX(u * 4).GapY(u * 2).
Role(ui.RoleTable).Label(frame.Label)
g.Children(func() {
for _, h := range []string{"asset_allocation_chart.asset", "asset_allocation_chart.weight", "asset_allocation_chart.target", "asset_allocation_chart.drift"} {
ui.Text(c, msg(c, h)).FontSize(size).TextColor(k.TextMuted).Role(ui.RoleColumnHeader)
}
for i, it := range items {
w := it.Value / total * 100
drift := w - it.Target
ui.Row(c).Key(it.Name).Gap(u * 2).AlignItems(ui.Center).Children(func() {
ui.Box(c).Size(12, 12).Draw(func(p *ui.Painter, r ui.Rect) { internal.Marker(p, i, r.X+6, r.Y+6, 10, pal[i%len(pal)]) })
ui.Text(c, it.Name).SingleLine()
ui.Column(c).MinWidth(340).Grow(1).Children(func() {
g := ui.Grid(c).ColumnTracks(ui.Fr(2), ui.Fr(1), ui.Fr(1), ui.Fr(2)).GapX(u * 2).GapY(u * 2).
Role(ui.RoleTable).Label(frame.Label)
g.Children(func() {
for _, h := range []string{"asset_allocation_chart.asset", "asset_allocation_chart.weight", "asset_allocation_chart.target", "asset_allocation_chart.drift"} {
ui.Text(c, msg(c, h)).FontSize(size).TextColor(k.TextMuted).Role(ui.RoleColumnHeader)
}
})
state := ui.Local(row, "mujica.allocation.rows", func() ui.ListState { return ui.ListState{} })
ui.List(c, state, len(items), func(i int) {
it := items[i]
ui.Grid(c).ColumnTracks(ui.Fr(2), ui.Fr(1), ui.Fr(1), ui.Fr(2)).GapX(u * 2).MinHeight(Density(c).ControlHeight()).Children(func() {
w := it.Value / total * 100
drift := w - it.Target
ui.Row(c).Key(it.Name).Gap(u * 2).AlignItems(ui.Center).Children(func() {
ui.Box(c).Size(12, 12).Draw(func(p *ui.Painter, r ui.Rect) {
internal.Marker(p, memo.markers[i], r.X+6, r.Y+6, 10, pal[memo.markers[i]%len(pal)])
})
ui.Text(c, it.Name).SingleLine()
})
ui.Text(c, fmt.Sprintf("%.1f%%", w)).FontFeatures("tnum").JustifySelf(ui.End)
ui.Text(c, fmt.Sprintf("%.1f%%", it.Target)).FontFeatures("tnum").TextColor(k.TextMuted).JustifySelf(ui.End)
allocationDrift(c, drift, tol)
})
ui.Text(c, fmt.Sprintf("%.1f%%", w)).FontFeatures("tnum").JustifySelf(ui.End)
ui.Text(c, fmt.Sprintf("%.1f%%", it.Target)).FontFeatures("tnum").TextColor(k.TextMuted).JustifySelf(ui.End)
allocationDrift(c, drift, tol)
}
}).Height(min(float32(len(items))*Density(c).ControlHeight(), 240))
})
})
res.Element = row
return res
}
// allocationDrift shows the drift in points; beyond tolerance it warns with an icon and a word.
type allocationSlices struct {
id string
slices []ChartSlice
total float64
parts []ChartSlice
markers []int
}
func allocationDrift(c *ui.Context, drift, tol float64) {
k := Tokens(c)
text := statisticSigned(drift, 1) + " pt"