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 / axis.go
axis.go 4.0K
1package chart23import (4 "math"56 "src.rest/cat/012/internal/sheet"7)89// newAxis fits dlo..dhi onto a value axis of length cells as newScale10// does, with the chart's axis options: a fixed minimum or maximum, and a11// log scale.12func newAxis(dlo, dhi float64, length, minK, maxN int, f sheet.Format, o sheet.ChartOptions) scale {13 if o.HasMin {14 dlo = o.Min15 }16 if o.HasMax {17 dhi = o.Max18 }19 if o.HasMin && !o.HasMax && dhi <= dlo {20 dhi = dlo + max(math.Abs(dlo), 1)21 }22 if o.HasMax && !o.HasMin && dlo >= dhi {23 dlo = dhi - max(math.Abs(dhi), 1)24 }25 if dlo > dhi {26 dlo, dhi = dhi, dlo27 }28 switch {29 case o.Log:30 return newLogScale(dlo, dhi, length, minK, f)31 case (o.HasMin || o.HasMax) && dlo < dhi:32 return newFixedScale(dlo, dhi, o.HasMin, o.HasMax, length, minK, maxN, f)33 }34 return newScale(dlo, dhi, length, minK, maxN, f)35}3637// newFixedScale is newScale keeping the ends that are fixed where they38// are: the other end rounds out to a whole step, and with both fixed the39// step divides the range, a round one where it can.40func newFixedScale(dlo, dhi float64, fixLo, fixHi bool, length, minK, maxN int, f sheet.Format) scale {41 best := scale{lo: dlo, hi: dhi, step: dhi - dlo, n: 1, k: max(length, 1)}42 fallback := false43 for target := min(maxN, max(length/minK, 1)); target >= 1; target-- {44 s, ok := fixedTicks(dlo, dhi, fixLo, fixHi, target)45 if !ok {46 continue47 }48 s.k = length / s.n49 if s.k < minK && (target > 1 || s.k < 1) {50 continue51 }52 if fixLo && fixHi && niceStep(s.step) != s.step {53 if !fallback { // the finest division, if no round step fits54 best, fallback = s, true55 }56 continue57 }58 best = s59 break60 }61 best.label = tickFormat(best, f)62 return best63}6465// fixedTicks divides dlo..dhi into about target steps, moving only the66// ends that aren't fixed.67func fixedTicks(dlo, dhi float64, fixLo, fixHi bool, target int) (scale, bool) {68 if fixLo && fixHi {69 return scale{lo: dlo, hi: dhi, step: (dhi - dlo) / float64(target), n: target}, true70 }71 step := niceStep((dhi - dlo) / float64(target))72 n := int(math.Ceil((dhi-dlo)/step - 1e-9))73 if n < 1 {74 return scale{}, false75 }76 if fixLo {77 return scale{lo: dlo, hi: dlo + float64(n)*step, step: step, n: n}, true78 }79 return scale{lo: dhi - float64(n)*step, hi: dhi, step: step, n: n}, true80}8182// newLogScale is a log axis over positive dlo..dhi: its ends round out to83// powers of ten, and ticks are one decade apart, or more when decades84// would be closer than minK cells.85func newLogScale(dlo, dhi float64, length, minK int, f sheet.Format) scale {86 if dhi <= 0 {87 dlo, dhi = 1, 1088 }89 if dlo <= 0 {90 dlo = dhi / 1091 }92 lo, hi := math.Floor(math.Log10(dlo)+1e-9), math.Ceil(math.Log10(dhi)-1e-9)93 if hi <= lo {94 hi = lo + 195 }96 decades := int(hi - lo)97 s := scale{lo: lo, log: true}98 for _, step := range []int{1, 2, 3, 5, 10, 20, 50, 100} {99 n := (decades + step - 1) / step100 s.step, s.n, s.k = float64(step), n, length/n101 if s.k >= minK || n == 1 {102 break103 }104 }105 s.k = max(s.k, 1)106 s.hi = lo + float64(s.n)*s.step107 s.label = func(v float64) string {108 return tickFormat(scale{lo: v, hi: v, step: v}, f)(v)109 }110 return s111}112113// base is where bars start: zero, or the end of the axis nearest it.114func (s scale) base() float64 {115 if s.log {116 return s.tick(0)117 }118 return min(max(0, s.lo), s.hi)119}120121// at is pos kept on the axis, for values beyond a fixed end.122func (s scale) at(v float64) float64 {123 p := s.pos(v)124 if math.IsNaN(p) {125 return 0126 }127 return min(max(p, 0), float64(s.cells()))128}129130// crossesZero reports whether zero lies inside the axis, where images131// draw a line.132func (s scale) crossesZero() bool { return !s.log && s.lo < 0 && s.hi > 0 }133134// positive is d with every value that isn't positive left out, as a log135// scale can't show them.136func positive(d sheet.ChartData) sheet.ChartData {137 out := d138 out.Series = make([]sheet.ChartSeries, len(d.Series))139 for j, s := range d.Series {140 vals := make([]float64, len(s.Values))141 for i, v := range s.Values {142 if v <= 0 {143 v = math.NaN()144 }145 vals[i] = v146 }147 out.Series[j] = sheet.ChartSeries{Name: s.Name, Values: vals}148 }149 return out150}