Add chart components 216-245; step-aware ticks, font-aware labels
Bars, lines, areas, pies, donuts, radar, histogram, scatter, bubbles, heatmaps, gauge, bullet, funnel, waterfall, treemap, candles, box plots, steps, rings, legend, axis, crosshair, polar area, pyramid, spectrum bars and win-loss strips, each drawn dot by dot. Ticks follow 1-2-5 steps and print as precisely as their step needs; a label row uses micro letters or the pixel font as a whole. Descriptions carry the data in numbers. Bad data panics.
This commit is contained in:
43 files changed
+3336
-1
No files matched your search
+127
@@ -0,0 +1,127 @@
|
||||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
// plot is a chart's value range and box, in dots.
|
||||
type plot struct {
|
||||
x, y, w, h int
|
||||
from, step float64
|
||||
n int
|
||||
lowest, upper float64
|
||||
// pad keeps marks this many dots inside the box.
|
||||
pad int
|
||||
}
|
||||
|
||||
// labelH is the room x labels need under a plot.
|
||||
func labelH(labels []string) int { return inkH(small(labels...)) + 3 }
|
||||
|
||||
// fit lays lo–hi in a w×h canvas, leaving label room.
|
||||
func fit(lo, hi float64, w, h int, labels []string) plot {
|
||||
from, step, n := ticks(lo, hi, 5)
|
||||
gutter := 0
|
||||
for i := range n {
|
||||
gutter = max(gutter, tinyW(tick(from+float64(i)*step, step)))
|
||||
}
|
||||
p := plot{x: gutter + 4, y: 3, from: from, step: step, n: n}
|
||||
p.w, p.h = w-p.x-1, h-p.y-labelH(labels)-2
|
||||
if p.w < 4 || p.h < 4 {
|
||||
panic(fmt.Sprintf("chart: a %d×%d chart leaves no room for its plot", w, h))
|
||||
}
|
||||
p.lowest, p.upper = from, from+float64(n-1)*step
|
||||
return p
|
||||
}
|
||||
|
||||
// at is the y of v.
|
||||
func (p plot) at(v float64) int {
|
||||
run := float64(p.h - 1 - 2*p.pad)
|
||||
return p.y + p.h - 1 - p.pad - int(math.Round((v-p.lowest)/(p.upper-p.lowest)*run))
|
||||
}
|
||||
|
||||
// slot is the middle x of category i of n.
|
||||
func (p plot) slot(i, n int) int { return p.x + (2*i+1)*p.w/(2*n) }
|
||||
|
||||
// grid draws value ticks, the frame and x labels.
|
||||
func (p plot) grid(g pixel.Canvas, labels []string) {
|
||||
for i := range p.n {
|
||||
v := p.from + float64(i)*p.step
|
||||
y := p.at(v)
|
||||
g.Checker(p.x, y, p.w, 1, pixel.Shade)
|
||||
s := tick(v, p.step)
|
||||
tiny(g, p.x-3-tinyW(s), y-2, s, pixel.Faint)
|
||||
}
|
||||
g.VLine(p.x-1, p.y, p.h, pixel.Edge)
|
||||
g.HLine(p.x-1, p.y+p.h, p.w+1, pixel.Edge)
|
||||
sm := small(labels...)
|
||||
for i, l := range labels {
|
||||
x := p.slot(i, len(labels))
|
||||
g.Dot(x, p.y+p.h+1, pixel.Edge)
|
||||
mark(g, max(0, min(g.W-markW(l, sm), x-markW(l, sm)/2)), p.y+p.h+3, l, sm, pixel.Faint)
|
||||
}
|
||||
}
|
||||
|
||||
type AxisOptions struct {
|
||||
// Min and Max bound ticks, zeros 0–100; Labels replace them.
|
||||
Min, Max float64
|
||||
Labels []string
|
||||
// Length is in dots; zero means 120.
|
||||
Length int
|
||||
Vertical bool
|
||||
}
|
||||
|
||||
// Axis draws ticks and micro labels, for any chart.
|
||||
func Axis(c *ui.Context, o AxisOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
n := pixel.Or("Axis Length", o.Length, 120)
|
||||
marks := o.Labels
|
||||
if marks == nil {
|
||||
lo, hi := o.Min, o.Max
|
||||
if lo == 0 && hi == 0 {
|
||||
hi = 100
|
||||
}
|
||||
if span([]float64{lo, hi}); !(lo < hi) {
|
||||
panic(fmt.Sprintf("chart: Axis needs Min below Max, or Labels; got %v–%v", lo, hi))
|
||||
}
|
||||
from, step, count := ticks(lo, hi, 5)
|
||||
for i := range count {
|
||||
marks = append(marks, tick(from+float64(i)*step, step))
|
||||
}
|
||||
}
|
||||
if len(marks) < 2 {
|
||||
panic("chart: Axis needs two marks or more")
|
||||
}
|
||||
sm, thick := small(marks...), 0
|
||||
for _, m := range marks {
|
||||
thick = max(thick, markW(m, sm))
|
||||
}
|
||||
w, h := n, labelH(marks)+3
|
||||
if o.Vertical {
|
||||
w, h = thick+5, n
|
||||
}
|
||||
return ui.Box(c).Size(k.D(w), k.D(h)).Role(ui.RoleImage).Label(marks[0] + "–" + marks[len(marks)-1]).Description(strings.Join(marks, ", ")).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
for i, m := range marks {
|
||||
at := 1 + i*(n-3)/(len(marks)-1)
|
||||
if o.Vertical {
|
||||
y := n - 1 - at
|
||||
g.HLine(w-3, y, 2, pixel.Edge)
|
||||
mark(g, w-4-markW(m, sm), max(0, min(n-inkH(sm), y-inkH(sm)/2)), m, sm, pixel.Faint)
|
||||
continue
|
||||
}
|
||||
g.VLine(at, 1, 2, pixel.Edge)
|
||||
mark(g, max(0, min(n-markW(m, sm), at-markW(m, sm)/2)), 4, m, sm, pixel.Faint)
|
||||
}
|
||||
if o.Vertical {
|
||||
g.VLine(w-1, 0, n, pixel.Edge)
|
||||
} else {
|
||||
g.HLine(0, 0, n, pixel.Edge)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user