Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

sparkline.gno

3.53 Kb · 114 lines
  1// Package sparkline renders a numeric series as one line of Unicode block
  2// characters — ▁▂▃▄▅▆▇█ — so a realm's Render can show a trend inline, with no
  3// image, no chart library and no client-side code.
  4//
  5// Everything is integer math, deliberately. A realm's Render must produce
  6// byte-identical output on every validating node, and floating point is the
  7// usual way that quietly stops being true. Scaling here is integer division
  8// with the rounding rule stated below, so a given series always yields exactly
  9// the same runes.
 10//
 11// Rendering is O(len(values)) and allocates one builder; a realm should bound
 12// what it feeds in (slice to a window) rather than sparking an unbounded,
 13// user-grown slice.
 14//
 15// A live demo of this package is at
 16// [r/moul/x/daily/sparklinedemo](/r/moul/x/daily/sparklinedemo/v0).
 17package sparkline
 18
 19import "strings"
 20
 21// Levels is the ramp, lowest to highest. A string constant rather than a
 22// []rune so callers cannot mutate the ramp out from under other realms.
 23const Levels = "▁▂▃▄▅▆▇█"
 24
 25// Steps is how many levels the ramp has.
 26const Steps = 8
 27
 28// maxInt is the largest int, used to keep the scaling multiply from wrapping.
 29const maxInt = int(^uint(0) >> 1)
 30
 31// Ints renders values scaled between their own smallest and largest element.
 32// An empty series renders as the empty string.
 33func Ints(values []int) string {
 34	lo, hi, ok := Bounds(values)
 35	if !ok {
 36		return ""
 37	}
 38	return Scaled(values, lo, hi)
 39}
 40
 41// Scaled renders values against an explicit [lo, hi] window. Values outside it
 42// clamp to the ends rather than erroring: a window is chosen for readability
 43// (0..100 for a percentage, say), and one outlier should not be able to break a
 44// realm's Render.
 45//
 46// A window with hi <= lo is treated as flat; see Level.
 47func Scaled(values []int, lo, hi int) string {
 48	if len(values) == 0 {
 49		return ""
 50	}
 51	ramp := []rune(Levels)
 52	var b strings.Builder
 53	for _, v := range values {
 54		b.WriteRune(ramp[Level(v, lo, hi)])
 55	}
 56	return b.String()
 57}
 58
 59// Level maps v within [lo, hi] to a ramp index in [0, Steps-1].
 60//
 61// Rounding is floor, so only a value at hi itself reaches the top of the ramp;
 62// everything below it rounds down. That makes the maximum visually distinct,
 63// which is what a reader is looking for in a sparkline.
 64//
 65// A flat window (hi <= lo) maps everything to the MIDDLE of the ramp, not the
 66// bottom: a series that sits unchanged at 1000 carries no shape, but drawing it
 67// along the floor would read as "zero", which is a different and wrong claim.
 68func Level(v, lo, hi int) int {
 69	if hi <= lo {
 70		return Steps / 2
 71	}
 72	if v <= lo {
 73		return 0
 74	}
 75	if v >= hi {
 76		return Steps - 1
 77	}
 78
 79	span, d, top := hi-lo, v-lo, Steps-1
 80	if span <= 0 {
 81		// hi-lo wrapped: the window is wider than an int can express, so there
 82		// is no usable ramp. Split at the midpoint, computed without overflowing.
 83		if v < lo/2+hi/2 {
 84			return 0
 85		}
 86		return Steps - 1
 87	}
 88	// d*top would wrap on an extreme span; halve both until it cannot. The
 89	// chosen bucket is unchanged for any realistic series, and off by at most
 90	// one level for the pathological ones this protects against.
 91	for span > maxInt/top {
 92		span >>= 1
 93		d >>= 1
 94	}
 95	return d * top / span
 96}
 97
 98// Bounds returns the smallest and largest value in values. ok is false when
 99// values is empty, in which case lo and hi are zero.
100func Bounds(values []int) (lo, hi int, ok bool) {
101	if len(values) == 0 {
102		return 0, 0, false
103	}
104	lo, hi = values[0], values[0]
105	for _, v := range values[1:] {
106		if v < lo {
107			lo = v
108		}
109		if v > hi {
110			hi = v
111		}
112	}
113	return lo, hi, true
114}