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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}