cat / 012
clone

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

main
012 / internal / sheet / condbars.go
condbars.go 6.2K
1package sheet23import (4 "errors"5 "math"6 "slices"7 "strings"8)910// Data bars and icon sets, as Excel's conditional formatting draws them:11// a bar across each number's cell as long as the number is far along12// from the rule's shortest point to its longest, or an icon by which of13// its thresholds the number reaches. Both keep their points in the14// rule's Scale, as a color scale does: a bar its shortest and longest,15// an icon set the threshold of each icon after the first.1617// IconSet is a set of icons, drawn in a terminal's own glyphs so they18// read without color: arrows, circles filling up, symbols and bars19// rising. Each set comes in the sizes Excel has it in.20type IconSet uint82122const (23 IconsNone IconSet = iota24 IconsArrows25 IconsCircles26 IconsSymbols27 IconsRating28 numIconSets29)3031var iconSetNames = [numIconSets]string{"", "arrows", "circles", "symbols", "rating"}32var iconSetTitles = [numIconSets]string{"None", "Arrows", "Circles", "Symbols", "Rating"}3334// iconGlyphs are each set's icons by size, lowest first.35var iconGlyphs = [numIconSets]map[int][]string{36 IconsArrows: {3: {"↓", "→", "↑"}, 4: {"↓", "↘", "↗", "↑"}, 5: {"↓", "↘", "→", "↗", "↑"}},37 IconsCircles: {3: {"○", "◑", "●"}, 4: {"○", "◔", "◕", "●"}, 5: {"○", "◔", "◑", "◕", "●"}},38 IconsSymbols: {3: {"✗", "!", "✓"}},39 IconsRating: {4: {"▂", "▄", "▆", "█"}, 5: {"▁", "▂", "▄", "▆", "█"}},40}4142func (i IconSet) String() string {43 if i >= numIconSets {44 return ""45 }46 return iconSetNames[i]47}4849// Title names the set for people, e.g. "Arrows".50func (i IconSet) Title() string {51 if i >= numIconSets {52 return ""53 }54 return iconSetTitles[i]55}5657// ParseIconSet is the inverse of IconSet.String.58func ParseIconSet(s string) (IconSet, bool) {59 i := slices.Index(iconSetNames[:], s)60 return IconSet(max(i, 0)), i >= 061}6263// IconSets lists the sets in the order the rules editor offers them.64func IconSets() []IconSet { return []IconSet{IconsArrows, IconsCircles, IconsSymbols, IconsRating} }6566// Sizes are how many icons the set comes with.67func (i IconSet) Sizes() []int {68 if i == IconsNone || i >= numIconSets {69 return nil70 }71 var out []int72 for n := 3; n <= 5; n++ {73 if _, ok := iconGlyphs[i][n]; ok {74 out = append(out, n)75 }76 }77 return out78}7980// Glyphs are the set's n icons, lowest first.81func (i IconSet) Glyphs(n int) []string {82 if i >= numIconSets {83 return nil84 }85 return iconGlyphs[i][n]86}8788// IconColor is the color icon k of n draws in: the lowest red, the89// highest green and those between yellow, as Excel colors arrows and90// symbols, or blue for every bar of a rating.91func (i IconSet) IconColor(k, n int) Color {92 switch {93 case i == IconsRating:94 return ColorBlue95 case k == 0:96 return ColorRed97 case k == n-1:98 return ColorGreen99 }100 return ColorYellow101}102103// IsBar reports whether the rule is a data bar.104func (f CondFormat) IsBar() bool { return f.Bar != ColorNone }105106// IsIcons reports whether the rule is an icon set.107func (f CondFormat) IsIcons() bool { return f.Icons != IconsNone }108109// checkBar checks a data bar: its two points and color.110func checkBar(f CondFormat) error {111 if len(f.Scale) != 2 {112 return errors.New("A data bar has a shortest and a longest point")113 }114 if f.Bar >= numColors {115 return errors.New("Choose the bar's color")116 }117 return checkPoints(f.Scale)118}119120// checkIcons checks an icon set: a size it comes in, and a threshold121// for each icon after the first, rising.122func checkIcons(f CondFormat) error {123 n := len(f.Scale) + 1124 if !slices.Contains(f.Icons.Sizes(), n) {125 return errors.New("The " + strings.ToLower(f.Icons.Title()) + " come in " + sizesText(f.Icons.Sizes()))126 }127 return checkPoints(f.Scale)128}129130func sizesText(ns []int) string {131 parts := make([]string, len(ns))132 for i, n := range ns {133 parts[i] = string(rune('0' + n))134 }135 return strings.Join(parts, " or ") + " icons"136}137138// checkPoints checks points that take values: a number, or a percent or139// percentile from 0 to 100.140func checkPoints(ps []ScalePoint) error {141 for _, p := range ps {142 if p.Kind >= numPointKinds {143 return errors.New("Unknown kind of point")144 }145 if !p.Kind.TakesValue() {146 continue147 }148 n, _, ok := ParseValue(strings.TrimSpace(p.Value))149 switch {150 case !ok:151 return errors.New("Enter a number for the " + strings.ToLower(p.Kind.Title()))152 case p.Kind != PointNumber && (n < 0 || n > 100):153 return errors.New(p.Kind.Title() + " goes from 0 to 100")154 }155 }156 return nil157}158159// barStats is a data bar's or icon set's points among the values of its160// ranges.161type barStats struct {162 ok bool163 at []float64164}165166func (c *looksCache) bar(s *Sheet, i int) *barStats {167 if st, ok := c.bars[i]; ok {168 return st169 }170 f := s.rules.formats[i]171 keep := slices.ContainsFunc(f.Scale, func(p ScalePoint) bool { return p.Kind == PointPercentile })172 lo, hi, n := math.Inf(1), math.Inf(-1), 0173 var vals []float64174 for _, r := range f.Ranges {175 for _, v := range s.cells.valuesIn(r) {176 if v.Kind != Number {177 continue178 }179 lo, hi, n = min(lo, v.Num), max(hi, v.Num), n+1180 if keep {181 vals = append(vals, v.Num)182 }183 }184 }185 st := &barStats{ok: n > 0}186 if st.ok {187 for k, p := range f.Scale {188 at := p.at(vals, lo, hi)189 // A bar's automatic shortest starts at zero for positive190 // numbers, as Excel's does, so a bar's length reads as the191 // number's size.192 if f.IsBar() && k == 0 && p.Kind == PointMin {193 at = min(lo, 0)194 }195 if f.IsBar() && k == 1 && p.Kind == PointMax {196 at = max(hi, 0)197 }198 st.at = append(st.at, at)199 }200 }201 c.bars[i] = st202 return st203}204205// barLook is how far along a data bar's cell of value v is drawn, from206// 0 to 1, and whether it has a bar.207func (st *barStats) barLook(v Value) (float64, bool) {208 if !st.ok || v.Kind != Number || len(st.at) != 2 {209 return 0, false210 }211 lo, hi := st.at[0], st.at[1]212 if hi <= lo {213 return 1, true214 }215 return min(max((v.Num-lo)/(hi-lo), 0), 1), true216}217218// icon is which of n icons the value v gets: the last threshold it219// reaches, reversed when the set is.220func (st *barStats) icon(v Value, reverse bool) (int, bool) {221 if !st.ok || v.Kind != Number {222 return 0, false223 }224 k := 0225 for i, at := range st.at {226 if v.Num >= at {227 k = i + 1228 }229 }230 if reverse {231 k = len(st.at) - k232 }233 return k, true234}