cat / 012
clone

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

main
012 / internal / chart / bar.go
bar.go 4.2K
1package chart23import (4 "image"5 "math"67 "github.com/charmbracelet/x/ansi"89 "src.rest/cat/012/internal/sheet"10)1112// barPlan lays out horizontal bar charts: category labels on the left,13// bars growing right, and the value axis along the bottom. Draw adds the14// legend.15//16// Jan ▐██████████▌17// Feb ▐███████▎18// └─────┬─────┬19// 0 1,000 2,00020type barPlan struct {21 d sheet.ChartData22 stacked bool23 grid bool24 msg string25 sc scale26 catW int // width of the category labels27 axisX int28 axisRow int29 tickRow int30 plot image.Rectangle31 shown int32 slots int // bars per category: the series, or one stack33 rows int // rows per category, including a gap row34}3536func newBarPlan(d sheet.ChartData, w, h int, o sheet.ChartOptions) *barPlan {37 d = prepare(d, o, true)38 p := &barPlan{d: d, stacked: o.Stack != sheet.StackNone, grid: !o.NoGrid, slots: len(d.Series)}39 lo, hi, ok := finite(d.Series)40 if p.stacked {41 lo, hi, ok = stackedRange(d, len(d.Categories))42 p.slots = 143 }44 if !ok || len(d.Categories) == 0 {45 p.msg = "No numbers to chart"46 return p47 }48 if !o.Log {49 lo, hi = min(lo, 0), max(hi, 0)50 }51 p.tickRow = h - 152 p.axisRow = p.tickRow - 153 if p.axisRow < 1 {54 p.msg = "Too small to chart"55 return p56 }57 for _, c := range d.Categories {58 p.catW = max(p.catW, ansi.StringWidth(c))59 }60 p.catW = max(min(p.catW, w/3), 1)61 p.axisX = p.catW + 162 x0 := p.axisX + 163 // Tick labels are centered on their ticks, so the last one needs half64 // its width to the right of the plot.65 labelW := 166 for range 2 {67 plotW := w - x0 - (labelW+1)/268 if plotW < 2 {69 p.msg = "Too small to chart"70 return p71 }72 p.sc = newAxis(lo, hi, plotW, labelW+2, 6, d.Format, o)73 for j := 0; j <= p.sc.n; j++ {74 labelW = max(labelW, ansi.StringWidth(p.sc.label(p.sc.tick(j))))75 }76 }77 p.plot = image.Rect(x0, 0, x0+p.sc.cells(), p.axisRow)78 p.rows = p.slots + 179 if len(d.Categories)*p.rows-1 > p.axisRow {80 p.rows = p.slots81 }82 p.shown = max(min(len(d.Categories), (p.axisRow+p.rows-p.slots)/p.rows), 1)83 return p84}8586// barRow returns the row of series j's bar in category i; stacked, all87// series share one.88func (p *barPlan) barRow(i, j int) int {89 if p.stacked {90 j = 091 }92 return i*p.rows + j93}9495// piles returns category i's bars as spans, as columnPlan.piles does.96func (p *barPlan) piles(i int, buf []span) []span {97 if p.stacked {98 return stack(p.d, i, buf)99 }100 buf = buf[:0]101 base := p.sc.base()102 for j, s := range p.d.Series {103 if v := value(s, i); !math.IsNaN(v) {104 buf = append(buf, span{min(v, base), max(v, base), j})105 }106 }107 return buf108}109110func (p *barPlan) draw(g *Grid, o Options) {111 p.drawAxis(g)112 for i := 0; i < p.shown; i++ {113 label := ansi.Truncate(p.d.Categories[i], p.catW, "…")114 g.text(p.catW-ansi.StringWidth(label), p.barRow(i, (p.slots-1)/2), label, Label)115 }116 if o.Image {117 return118 }119 if p.grid {120 for j := 1; j <= p.sc.n; j++ {121 for y := 0; y < p.axisRow; y++ {122 g.set(p.plot.Min.X+j*p.sc.k-1, y, "┊", Gridline)123 }124 }125 }126 drawPiles(p.sc, p.shown, p.stacked, p.piles, leftEighths, "▐", func(i, j, c int, cell Cell) {127 if at := g.At(p.plot.Min.X+c, p.barRow(i, j)); at != nil {128 *at = cell129 }130 })131}132133// drawAxis draws the category axis on the left and the value axis along134// the bottom.135func (p *barPlan) drawAxis(g *Grid) {136 for y := 0; y < p.axisRow; y++ {137 g.set(p.axisX, y, "│", Axis)138 }139 g.set(p.axisX, p.axisRow, "└", Axis)140 for x := p.axisX + 1; x < p.plot.Max.X; x++ {141 g.set(x, p.axisRow, "─", Axis)142 }143 drawXTicks(g, p.sc, p.axisX, p.plot.Min.X, p.axisRow, p.tickRow)144}145146// drawXTicks marks the ticks of a horizontal axis on row axisRow, the147// first at column axisX and the rest ending intervals from x0, and labels148// as many on row tickRow as fit without touching.149func drawXTicks(g *Grid, sc scale, axisX, x0, axisRow, tickRow int) {150 next := 0 // first free column151 for j := 0; j <= sc.n; j++ {152 x := axisX153 if j > 0 {154 x = x0 + j*sc.k - 1155 g.set(x, axisRow, "┬", Axis)156 }157 label := sc.label(sc.tick(j))158 lw := ansi.StringWidth(label)159 lx := min(max(x-(lw-1)/2, 0), g.W-lw)160 if lx >= next {161 g.text(lx, tickRow, label, Label)162 next = lx + lw + 1163 }164 }165}