cat / 012
clone

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

Register chart types in a table of layouts instead of switching on them

Dami committed 2daa815
Draw and Image look the type up in one table; each layout returns a
plan that draws its chart as text and as an image, and Draw shows a
plan's note in its place. The bar glyph walk that bar and column charts
each had is shared as barCells, and the bar chart's axes are drawn by
their own method.
commit
2daa81534f84bb1da002a4658ea5fb0c14a7ed76
parent
3945e7f2ef42ca3e7f094e63bf28101c28c14ee1
author
Dami <dami.etoile@gmail.com>
7 files changed, +156 -125
Minternal/chart/bar.go92+++++++++++++++++-------------
Minternal/chart/chart.go15++---
Minternal/chart/chart_test.go12++++
Minternal/chart/column.go27+--------
Minternal/chart/image.go78++++++++++---------------
Minternal/chart/pie.go5--
Ainternal/chart/types.go52+++++++++++++++++
M internal/chart/bar.go +51 -41
@@ -82,12 +82,36 @@ func newBarPlan(d sheet.ChartData, w, h int) *barPlan {
8282 func (p *barPlan) barRow(i, j int) int { return i*p.rows + j }8383 8484 func (p *barPlan) draw(g *Grid, o Options) {85- if p.msg != "" {86- g.message(p.msg)85+ p.drawAxis(g)86+ ns := len(p.d.Series)87+ for i := 0; i < p.shown; i++ {88+ label := ansi.Truncate(p.d.Categories[i], p.catW, "…")89+ g.text(p.catW-ansi.StringWidth(label), p.barRow(i, (ns-1)/2), label, Label)90+ }91+ if p.legend >= 0 {92+ g.legend(p.legend, seriesNames(p.d))93+ }94+ if o.Image {8795 return8896 }89- g.Plot = p.plot90- ns := len(p.d.Series)97+ zero := p.sc.pos(0)98+ for j, s := range p.d.Series {99+ for i := 0; i < p.shown && i < len(s.Values); i++ {100+ v := s.Values[i]101+ if math.IsNaN(v) {102+ continue103+ }104+ // Left of the axis, bars fill from the right.105+ barCells(zero, p.sc.pos(v), v >= 0, leftEighths, "▐", func(c int, glyph string) {106+ g.set(p.plot.Min.X+c, p.barRow(i, j), glyph, SeriesRole(j))107+ })108+ }109+ }110+}111+112+// drawAxis draws the category axis on the left and the value axis along113+// the bottom, labeling as many ticks as fit without touching.114+func (p *barPlan) drawAxis(g *Grid) {91115 for y := 0; y < p.axisRow; y++ {92116 g.set(p.axisX, y, "│", Axis)93117 }
@@ -95,7 +119,7 @@ func (p *barPlan) draw(g *Grid, o Options) {
95119 for x := p.axisX + 1; x < p.plot.Max.X; x++ {96120 g.set(x, p.axisRow, "─", Axis)97121 }98- next := 0122+ next := 0 // first free column99123 for j := 0; j <= p.sc.n; j++ {100124 x := p.axisX101125 if j > 0 {
@@ -110,42 +134,28 @@ func (p *barPlan) draw(g *Grid, o Options) {
110134 next = lx + lw + 1111135 }112136 }113- for i := 0; i < p.shown; i++ {114- label := ansi.Truncate(p.d.Categories[i], p.catW, "…")115- g.text(p.catW-ansi.StringWidth(label), p.barRow(i, (ns-1)/2), label, Label)116- }117- if p.legend >= 0 {118- g.legend(p.legend, seriesNames(p.d))119- }120- if o.Image {121- return122- }123- zero := p.sc.pos(0)124- for j, s := range p.d.Series {125- for i := 0; i < p.shown && i < len(s.Values); i++ {126- v := s.Values[i]127- if math.IsNaN(v) {128- continue129- }130- end := p.sc.pos(v)131- lo, hi := min(zero, end), max(zero, end)132- for c := int(math.Floor(lo)); float64(c) < hi; c++ {133- cover := min(hi, float64(c+1)) - max(lo, float64(c))134- glyph := ""135- if v >= 0 {136- glyph = leftEighths[int(math.Round(cover*8))]137- } else {138- switch {139- case cover >= 0.75:140- glyph = "█"141- case cover >= 0.25:142- glyph = "▐" // left of the axis, bars fill from the right143- }144- }145- if glyph != "" && glyph != " " {146- g.set(p.plot.Min.X+c, p.barRow(i, j), glyph, SeriesRole(j))147- }148- }137+}138+139+// barCells walks the cells a bar covers from zero to end, distances140+// along its axis in cells, calling fill with each cell and its glyph.141+// A positive bar grows away from the axis in eighths; a negative one142+// has no eighths growing the other way, so its partly covered cells are143+// a full block or the half block half.144+func barCells(zero, end float64, positive bool, eighths []string, half string, fill func(c int, glyph string)) {145+ lo, hi := min(zero, end), max(zero, end)146+ for c := int(math.Floor(lo)); float64(c) < hi; c++ {147+ cover := min(hi, float64(c+1)) - max(lo, float64(c))148+ glyph := ""149+ switch {150+ case positive:151+ glyph = eighths[int(math.Round(cover*8))]152+ case cover >= 0.75:153+ glyph = "█"154+ case cover >= 0.25:155+ glyph = half156+ }157+ if glyph != "" && glyph != " " {158+ fill(c, glyph)149159 }150160 }151161 }
M internal/chart/chart.go +6 -9
M internal/chart/chart_test.go +12 -0
M internal/chart/column.go +3 -24
M internal/chart/image.go +32 -46
@@ -28,36 +28,16 @@ func Image(t sheet.ChartType, d sheet.ChartData, w, h int, o Options, pal Palett
2828 if w < 8 || h < 3 {2929 return nil3030 }31- var plot image.Rectangle32- var draw func(c *canvas)33- switch t {34- case sheet.ChartBar:35- p := newBarPlan(d, w, h)36- plot, draw = p.plot, p.image37- if p.msg != "" {38- return nil39- }40- case sheet.ChartPie:41- p := newPiePlan(d, w, h, o)42- plot, draw = p.disc, p.image43- if p.msg != "" {44- return nil45- }46- default:47- p := newColumnPlan(d, w, h, t == sheet.ChartLine)48- plot, draw = p.plot, p.image49- if p.msg != "" {50- return nil51- }52- }53- if plot.Empty() {31+ p := planFor(t, d, w, h, o)32+ plot := p.plotArea()33+ if p.note() != "" || plot.Empty() {5434 return nil5535 }5636 c := &canvas{5737 RGBA: image.NewRGBA(image.Rect(0, 0, plot.Dx()*cw, plot.Dy()*ch)),5838 cw: float64(cw), ch: float64(ch), pal: pal,5939 }60- draw(c)40+ p.image(c)6141 return c.RGBA6242 }6343
@@ -159,31 +139,13 @@ func (p *columnPlan) image(cv *canvas) {
159139 for j := 1; j <= p.sc.n; j++ {160140 cv.hline(int(math.Round(y(p.sc.tick(j)))), cv.pal.Grid)161141 }162- if p.sc.lo < 0 && !p.line {163- cv.hline(int(math.Round(y(0))), cv.pal.Grid)164- }165142 if p.line {166- width := max(cv.ch/8, 2)167- for j, s := range p.d.Series {168- var run [][2]float64169- flush := func() {170- if len(run) > 0 {171- cv.polyline(run, width, cv.pal.Series[j%Colors])172- }173- run = run[:0]174- }175- for i := 0; i < p.shown && i < len(s.Values); i++ {176- if math.IsNaN(s.Values[i]) {177- flush()178- continue179- }180- x := (float64(p.dotX(i)) + 0.5) * cv.cw / 2181- run = append(run, [2]float64{x, y(s.Values[i])})182- }183- flush()184- }143+ p.lineImage(cv, y)185144 return186145 }146+ if p.sc.lo < 0 {147+ cv.hline(int(math.Round(y(0))), cv.pal.Grid)148+ }187149 inset := min(cv.cw/6, 2)188150 for j, s := range p.d.Series {189151 for i := 0; i < p.shown && i < len(s.Values); i++ {
@@ -199,6 +161,30 @@ func (p *columnPlan) image(cv *canvas) {
199161 }200162 }201163 164+// lineImage strokes each series as lines, broken where values are165+// missing; y maps a value to a pixel row.166+func (p *columnPlan) lineImage(cv *canvas, y func(float64) float64) {167+ width := max(cv.ch/8, 2)168+ for j, s := range p.d.Series {169+ var run [][2]float64170+ flush := func() {171+ if len(run) > 0 {172+ cv.polyline(run, width, cv.pal.Series[j%Colors])173+ }174+ run = run[:0]175+ }176+ for i := 0; i < p.shown && i < len(s.Values); i++ {177+ if math.IsNaN(s.Values[i]) {178+ flush()179+ continue180+ }181+ x := (float64(p.dotX(i)) + 0.5) * cv.cw / 2182+ run = append(run, [2]float64{x, y(s.Values[i])})183+ }184+ flush()185+ }186+}187+202188 // image draws horizontal bars.203189 func (p *barPlan) image(cv *canvas) {204190 x := func(v float64) float64 { return p.sc.pos(v) * cv.cw }
M internal/chart/pie.go +0 -5
A internal/chart/types.go +52 -0
@@ -0,0 +1,52 @@
1+package chart2+3+import (4+ "image"5+6+ "github.com/FelineStateMachine/012/internal/sheet"7+)8+9+// A plan is a chart laid out in w by h cells: the layout Draw and Image10+// share, so text and image line up.11+type plan interface {12+ // note is a message shown instead of the chart, such as "No numbers13+ // to chart", or "" when there's a chart to draw.14+ note() string15+ // plotArea is the rectangle, in cells, that an image covers.16+ plotArea() image.Rectangle17+ // draw draws the chart as text; with o.Image, all but the plot area.18+ draw(g *Grid, o Options)19+ // image draws the plot area, one cell per cv.cw by cv.ch pixels.20+ image(cv *canvas)21+}22+23+// A layout lays out data as one type of chart.24+type layout func(d sheet.ChartData, w, h int, o Options) plan25+26+// types are the chart types, each a layout whose plan draws it as text27+// and as an image. The types themselves, their names and the order the28+// chart editor offers them, are in the sheet package, since charts are29+// saved with sheets; adding one is a constant there and a row here.30+var types = map[sheet.ChartType]layout{31+ sheet.ChartColumn: func(d sheet.ChartData, w, h int, _ Options) plan { return newColumnPlan(d, w, h, false) },32+ sheet.ChartBar: func(d sheet.ChartData, w, h int, _ Options) plan { return newBarPlan(d, w, h) },33+ sheet.ChartLine: func(d sheet.ChartData, w, h int, _ Options) plan { return newColumnPlan(d, w, h, true) },34+ sheet.ChartPie: func(d sheet.ChartData, w, h int, o Options) plan { return newPiePlan(d, w, h, o) },35+}36+37+// planFor lays out d as a chart of type t; an unknown type is drawn as38+// columns.39+func planFor(t sheet.ChartType, d sheet.ChartData, w, h int, o Options) plan {40+ lay, ok := types[t]41+ if !ok {42+ lay = types[sheet.ChartColumn]43+ }44+ return lay(d, w, h, o)45+}46+47+func (p *columnPlan) note() string { return p.msg }48+func (p *columnPlan) plotArea() image.Rectangle { return p.plot }49+func (p *barPlan) note() string { return p.msg }50+func (p *barPlan) plotArea() image.Rectangle { return p.plot }51+func (p *piePlan) note() string { return p.msg }52+func (p *piePlan) plotArea() image.Rectangle { return p.disc }