cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / chart / area.go
area.go 4.3K
1package chart23import (4 "math"5 "slices"6)78// Area charts are line charts filled down to the axis. Each column of9// the plot (each pixel column of the image) samples the series between10// the two categories around it, so drawing costs the plot's width times11// the series, whatever the data.1213// pointX is category i's point in cells from the plot's left edge: the14// center of its braille dot, so fills meet the lines.15func (p *columnPlan) pointX(i int) float64 { return (float64(p.dotX(i)) + 0.5) / 2 }1617// areaCursor finds the categories around points along the plot, left to18// right.19type areaCursor struct {20 p *columnPlan21 k int22}2324// at returns category i and how far x (cells into the plot) lies from its25// point toward the next one's, or false outside the first and last points.26func (c *areaCursor) at(x float64) (int, float64, bool) {27 p := c.p28 if p.shown == 1 {29 return 0, 0, math.Abs(x-p.pointX(0)) <= 0.530 }31 if x < p.pointX(0) || x > p.pointX(p.shown-1) {32 return 0, 0, false33 }34 for c.k < p.shown-2 && p.pointX(c.k+1) < x {35 c.k++36 }37 x0, x1 := p.pointX(c.k), p.pointX(c.k+1)38 if x1 <= x0 {39 return c.k, 0, true40 }41 return c.k, (x - x0) / (x1 - x0), true42}4344// valueAt is series j between categories i and i+1, t of the way, or NaN45// where either is missing.46func (p *columnPlan) valueAt(j, i int, t float64) float64 {47 s := p.d.Series[j]48 a := value(s, i)49 if t == 0 || i+1 >= p.shown {50 return a51 }52 b := value(s, i+1)53 return a + (b-a)*t54}5556// areaSpans returns the area's spans at one point: stacked, the series'57// piles; otherwise each series from the axis to its value, the smaller58// in front, so each shows the part no smaller one covers.59func (p *columnPlan) areaSpans(i int, t float64, vals []float64, out []span) []span {60 vals = vals[:0]61 for j := range p.d.Series {62 vals = append(vals, p.valueAt(j, i, t))63 }64 if p.stacked {65 return pileUp(len(vals), func(j int) float64 { return vals[j] }, out)66 }67 return overlap(vals, p.sc.base(), out)68}6970// overlap turns values drawn from base, each in front of the larger ones,71// into the visible spans: from base up through the positive values in72// order, and down through the negative ones.73func overlap(vals []float64, base float64, out []span) []span {74 out = out[:0]75 order := make([]int, 0, len(vals))76 for j, v := range vals {77 if !math.IsNaN(v) {78 order = append(order, j)79 }80 }81 slices.SortStableFunc(order, func(a, b int) int { return cmpDistance(vals[a]-base, vals[b]-base) })82 up, down := base, base83 for _, j := range order {84 switch v := vals[j]; {85 case v > up:86 out = append(out, span{up, v, j})87 up = v88 case v < down:89 out = append(out, span{v, down, j})90 down = v91 }92 }93 return out94}9596// cmpDistance orders offsets by their distance from zero.97func cmpDistance(a, b float64) int {98 switch a, b = math.Abs(a), math.Abs(b); {99 case a < b:100 return -1101 case a > b:102 return 1103 }104 return 0105}106107// drawArea fills each column of the plot under the series with eighths.108func (p *columnPlan) drawArea(g *Grid) {109 cur := areaCursor{p: p}110 vals := make([]float64, 0, len(p.d.Series))111 var spans, cells []span112 for x := range p.plot.Dx() {113 i, t, ok := cur.at(float64(x) + 0.5)114 if !ok {115 continue116 }117 spans = p.areaSpans(i, t, vals, spans)118 cells = inCells(p.sc, spans, cells)119 from, to := pileExtent(cells)120 for r := from; r <= to; r++ {121 if c, ok := pileCell(cells, r, lowerEighths, "▀"); ok {122 if at := g.At(p.plot.Min.X+x, p.axisRow-1-r); at != nil {123 *at = c124 }125 }126 }127 }128}129130// areaImage fills the area a pixel column at a time: stacked piles131// opaque, otherwise each series translucent over the ones before, then132// strokes the lines on top.133func (p *columnPlan) areaImage(cv *canvas, y func(float64) float64) {134 cur := areaCursor{p: p}135 vals := make([]float64, 0, len(p.d.Series))136 var spans []span137 for px := range cv.Rect.Dx() {138 i, t, ok := cur.at((float64(px) + 0.5) / cv.cw)139 if !ok {140 continue141 }142 if p.stacked {143 spans = p.areaSpans(i, t, vals, spans)144 for _, s := range spans {145 cv.rect(float64(px), y(s.hi), float64(px+1), y(s.lo), cv.pal.Series[s.j%Colors])146 }147 continue148 }149 base := y(p.sc.base())150 for j := range p.d.Series {151 if v := p.valueAt(j, i, t); !math.IsNaN(v) {152 c := cv.pal.Series[j%Colors]153 c.A = 150154 cv.rect(float64(px), min(y(v), base), float64(px+1), max(y(v), base), c)155 }156 }157 }158 if !p.stacked {159 p.lineImage(cv, y)160 }161}