Split into category packages; module github.com/ZacharyZhang-NY/MujicaUI
This commit is contained in:
1822 files changed
+24568
-21314
No files matched your search
@@ -0,0 +1,265 @@
|
||||
package chart
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/core"
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/internal"
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/theme"
|
||||
)
|
||||
|
||||
// ChartAxisKind is how an axis maps values.
|
||||
type ChartAxisKind int
|
||||
|
||||
const (
|
||||
AxisLinear ChartAxisKind = iota
|
||||
// AxisLog maps powers of ten; Min must be positive.
|
||||
AxisLog
|
||||
// AxisTime maps From..To; values are Unix seconds.
|
||||
AxisTime
|
||||
// AxisCategory gives each category a band; values are indices.
|
||||
AxisCategory
|
||||
)
|
||||
|
||||
// ChartSide is where an axis runs along the plot.
|
||||
type ChartSide int
|
||||
|
||||
const (
|
||||
SideBottom ChartSide = iota
|
||||
SideLeft
|
||||
SideRight
|
||||
SideTop
|
||||
)
|
||||
|
||||
// ChartAxisOptions configure a ChartAxis.
|
||||
type ChartAxisOptions struct {
|
||||
Kind ChartAxisKind
|
||||
Side ChartSide
|
||||
// Min and Max span a linear or log axis.
|
||||
Min, Max float64
|
||||
// From and To span a time axis.
|
||||
From, To time.Time
|
||||
Categories []string
|
||||
// Ticks fixes the tick values; nil picks them.
|
||||
Ticks []float64
|
||||
// Format overrides tick and readout labels.
|
||||
Format func(v float64) string
|
||||
}
|
||||
|
||||
// ChartAxisPart maps values and draws an axis.
|
||||
type ChartAxisPart struct {
|
||||
opts ChartAxisOptions
|
||||
lo, hi float64
|
||||
ticks []float64
|
||||
step float64
|
||||
layout string
|
||||
k theme.Tokens
|
||||
size float32
|
||||
}
|
||||
|
||||
// ChartAxis resolves an axis: its domain, ticks and labels.
|
||||
func ChartAxis(c *ui.Context, opts ChartAxisOptions) ChartAxisPart {
|
||||
a := ChartAxisPart{opts: opts, k: core.Tokens(c), size: core.FontSize(c, theme.CaptionSize)}
|
||||
switch opts.Kind {
|
||||
case AxisLinear, AxisLog:
|
||||
a.lo, a.hi = opts.Min, opts.Max
|
||||
if !(a.hi > a.lo) || math.IsInf(a.lo, 0) || math.IsInf(a.hi, 0) || opts.Kind == AxisLog && a.lo <= 0 {
|
||||
panic("mujica: ChartAxis needs Min < Max, and Min > 0 for a log axis")
|
||||
}
|
||||
case AxisTime:
|
||||
if !opts.To.After(opts.From) {
|
||||
panic("mujica: ChartAxis time axis needs From before To")
|
||||
}
|
||||
a.lo, a.hi = chartSeconds(opts.From), chartSeconds(opts.To)
|
||||
case AxisCategory:
|
||||
if len(opts.Categories) == 0 {
|
||||
panic("mujica: ChartAxis category axis needs Categories")
|
||||
}
|
||||
a.lo, a.hi = -0.5, float64(len(opts.Categories))-0.5
|
||||
default:
|
||||
panic("mujica: unknown ChartAxisKind")
|
||||
}
|
||||
if opts.Side < SideBottom || opts.Side > SideTop {
|
||||
panic("mujica: unknown ChartSide")
|
||||
}
|
||||
a.ticks = opts.Ticks
|
||||
for i, v := range a.ticks {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) || v < a.lo || v > a.hi || i > 0 && v <= a.ticks[i-1] {
|
||||
panic("mujica: ChartAxis ticks must ascend inside the domain")
|
||||
}
|
||||
}
|
||||
if a.ticks == nil {
|
||||
a.ticks = a.autoTicks()
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func chartSeconds(t time.Time) float64 { return float64(t.UnixNano()) / 1e9 }
|
||||
|
||||
// autoTicks picks round ticks inside the domain.
|
||||
func (a *ChartAxisPart) autoTicks() []float64 {
|
||||
var out []float64
|
||||
switch a.opts.Kind {
|
||||
case AxisLog:
|
||||
// Short spans include powers of two and five.
|
||||
mants := []float64{1}
|
||||
if a.hi/a.lo < 1000 {
|
||||
mants = []float64{1, 2, 5}
|
||||
}
|
||||
for e := math.Floor(math.Log10(a.lo)); math.Pow(10, e) <= a.hi*(1+1e-9); e++ {
|
||||
for _, m := range mants {
|
||||
if v := m * math.Pow(10, e); v >= a.lo*(1-1e-9) && v <= a.hi*(1+1e-9) {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
case AxisTime:
|
||||
ts, layout := internal.TimeTicks(a.opts.From, a.opts.To, 5)
|
||||
a.layout = layout
|
||||
for _, t := range ts {
|
||||
out = append(out, chartSeconds(t))
|
||||
}
|
||||
case AxisCategory:
|
||||
n := len(a.opts.Categories)
|
||||
step := max(1, (n+63)/64)
|
||||
for i := 0; i < n; i += step {
|
||||
out = append(out, float64(i))
|
||||
}
|
||||
if out[len(out)-1] != float64(n-1) {
|
||||
out = append(out, float64(n-1))
|
||||
}
|
||||
default:
|
||||
lo, hi, step := internal.Nice(a.lo, a.hi, 5)
|
||||
a.step = step
|
||||
for _, t := range internal.Ticks(lo, hi, step) {
|
||||
if t >= a.lo-step*1e-9 && t <= a.hi+step*1e-9 {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// horizontal reports whether the axis runs across the plot.
|
||||
func (a ChartAxisPart) horizontal() bool {
|
||||
return a.opts.Side == SideBottom || a.opts.Side == SideTop
|
||||
}
|
||||
|
||||
// fraction returns the normalized position along the axis.
|
||||
func (a ChartAxisPart) fraction(v float64) float64 {
|
||||
if a.opts.Kind == AxisLog {
|
||||
return (math.Log10(v) - math.Log10(a.lo)) / (math.Log10(a.hi) - math.Log10(a.lo))
|
||||
}
|
||||
return (v - a.lo) / (a.hi - a.lo)
|
||||
}
|
||||
|
||||
// Map returns the pixel position of v.
|
||||
func (a ChartAxisPart) Map(plot ui.Rect, v float64) float32 {
|
||||
f := float32(a.fraction(v))
|
||||
if a.horizontal() {
|
||||
return plot.X + f*plot.W
|
||||
}
|
||||
return plot.Y + plot.H - f*plot.H
|
||||
}
|
||||
|
||||
// Invert returns the value at a pixel position.
|
||||
func (a ChartAxisPart) Invert(plot ui.Rect, px float32) float64 {
|
||||
f := float64((px - plot.X) / plot.W)
|
||||
if !a.horizontal() {
|
||||
f = float64((plot.Y + plot.H - px) / plot.H)
|
||||
}
|
||||
if a.opts.Kind == AxisLog {
|
||||
return math.Pow(10, math.Log10(a.lo)+f*(math.Log10(a.hi)-math.Log10(a.lo)))
|
||||
}
|
||||
return a.lo + f*(a.hi-a.lo)
|
||||
}
|
||||
|
||||
// Ticks returns the tick values.
|
||||
func (a ChartAxisPart) Ticks() []float64 { return a.ticks }
|
||||
|
||||
// Label writes v as the axis does.
|
||||
func (a ChartAxisPart) Label(v float64) string {
|
||||
switch {
|
||||
case a.opts.Format != nil:
|
||||
return a.opts.Format(v)
|
||||
case a.opts.Kind == AxisCategory:
|
||||
return a.opts.Categories[max(0, min(len(a.opts.Categories)-1, int(math.Round(v))))]
|
||||
case a.opts.Kind == AxisTime:
|
||||
layout := a.layout
|
||||
if layout == "" {
|
||||
layout = "2006-01-02 15:04"
|
||||
}
|
||||
return chartTime(v).Format(layout)
|
||||
case a.opts.Kind == AxisLog:
|
||||
return strconv.FormatFloat(v, 'f', -1, 64)
|
||||
case a.step > 0:
|
||||
return internal.FormatTick(v, a.step)
|
||||
}
|
||||
return strconv.FormatFloat(v, 'g', 6, 64)
|
||||
}
|
||||
|
||||
// readout uses finer precision than ticks.
|
||||
func (a ChartAxisPart) readout(v float64) string {
|
||||
switch {
|
||||
case a.opts.Format != nil, a.opts.Kind == AxisCategory, a.opts.Kind == AxisTime:
|
||||
return a.Label(v)
|
||||
case a.opts.Kind == AxisLog:
|
||||
return strconv.FormatFloat(v, 'g', 3, 64)
|
||||
case a.step > 0:
|
||||
return internal.FormatTick(v, a.step/10)
|
||||
}
|
||||
return strconv.FormatFloat(v, 'g', 6, 64)
|
||||
}
|
||||
|
||||
// Draw paints the axis line and tick labels.
|
||||
func (a ChartAxisPart) Draw(p *ui.Painter, plot ui.Rect) {
|
||||
spacing := a.size * 2
|
||||
if a.horizontal() {
|
||||
spacing = a.size * 4
|
||||
}
|
||||
ticks := a.drawTicks(plot, spacing)
|
||||
pos := make([]float32, len(ticks))
|
||||
labels := make([]string, len(ticks))
|
||||
for i, t := range ticks {
|
||||
pos[i], labels[i] = a.Map(plot, t), a.Label(t)
|
||||
}
|
||||
k := a.k
|
||||
switch a.opts.Side {
|
||||
case SideBottom:
|
||||
internal.Rule(p, plot.X, plot.X+plot.W, plot.Y+plot.H, k.ControlBorder)
|
||||
internal.LabelsX(p, plot, plot.Y+plot.H+6, pos, labels, false, a.size, k)
|
||||
case SideTop:
|
||||
internal.Rule(p, plot.X, plot.X+plot.W, plot.Y, k.ControlBorder)
|
||||
internal.LabelsX(p, plot, plot.Y-6, pos, labels, true, a.size, k)
|
||||
case SideLeft:
|
||||
internal.RuleV(p, plot.X, plot.Y, plot.Y+plot.H, k.ControlBorder)
|
||||
internal.LabelsY(p, plot.X-6, pos, labels, false, a.size, k)
|
||||
case SideRight:
|
||||
internal.RuleV(p, plot.X+plot.W, plot.Y, plot.Y+plot.H, k.ControlBorder)
|
||||
internal.LabelsY(p, plot.X+plot.W+6, pos, labels, true, a.size, k)
|
||||
}
|
||||
}
|
||||
|
||||
func (a ChartAxisPart) drawTicks(plot ui.Rect, spacing float32) []float64 {
|
||||
length := plot.H
|
||||
if a.horizontal() {
|
||||
length = plot.W
|
||||
}
|
||||
limit := max(2, int(length/max(spacing, 1)))
|
||||
if len(a.ticks) <= limit {
|
||||
return a.ticks
|
||||
}
|
||||
step := (len(a.ticks) + limit - 1) / limit
|
||||
out := make([]float64, 0, limit+1)
|
||||
for i := 0; i < len(a.ticks); i += step {
|
||||
out = append(out, a.ticks[i])
|
||||
}
|
||||
if out[len(out)-1] != a.ticks[len(a.ticks)-1] {
|
||||
out = append(out, a.ticks[len(a.ticks)-1])
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user