Transaction
0B44C907F4DEBA…11998B5AFF40
Block 95,706 · index 0 · indexed
Summary
- Hash
- 0B44C907F4DEBA983990844D9F557B36EFBEDFD5B15BE71DD8C911998B5AFF40
- Block
- 95,706
- Size
- 6851 bytes
- Gas used
- 10,475,093 / 12,570,171
- Fee
- 1000000 ugnot
- Memo
- gnopublish
- Status
- success
Messages
Arguments · 9
- #1sparkline
- #2README.md
- #3# `gno.land/p/moul/x/daily/sparkline/v0` **Sparklines: a numeric series as one line of block characters** — `Ints`, `Scaled`, `Level`, `Bounds`, `Levels`, `Steps`. ```go import "gno.land/p/moul/x/daily/sparkline/v0" sparkline.Ints([]int{3, 5, 9, 14, 12, 18, 25, 21, 16, 11, 7, 4}) // ▁▁▂▄▃▅█▆▅▃▂▁ scaled between the series' own min and max sparkline.Scaled(pct, 0, 100) // fixed window; outliers clamp to the ends ``` A realm's `Render` returns markdown, so a trend normally means an image, a chart library, or client-side code — none of which a realm has. Eight block runes and one line of text do the job, and `gnoweb` needs nothing to display them. **Everything is integer math, deliberately.** Render output has to be byte-identical on every validating node, and floating point is the usual way that quietly stops being true. Scaling is integer division with floor rounding. Three behaviours worth knowing before you use it: - **A flat series renders mid-ramp** (`▅▅▅▅`), not along the floor. A series sitting unchanged at 1000 has no shape, but drawing it at the bottom would claim it was *zero* — a different thing, and a wrong one. - **`Scaled` clamps instead of failing.** A window is chosen for readability (`0..100` for a percentage), and one outlier should not be able to flatten the rest of the picture or break the `Render` that drew it. - **Rounding is floor**, so only a value at the maximum itself reaches `█`. The peak of a series is always visually distinct. `Level` is total: every input maps into `[0, Steps)`, including the extremes where the scaling arithmetic would otherwise wrap. That matters because `Scaled` indexes the ramp with it — an out-of-range result would panic the calling realm, so it is tested against `minInt`/`maxInt` windows directly. Rendering is `O(len(values))`. A realm should slice its series to a window rather than sparking an unbounded, user-grown slice. **Live demo:** [`r/moul/x/daily/sparklinedemo`](https://github.com/moul/gno-contracts/tree/main/r/moul/x/daily/sparklinedemo/v0) · render it at [`/r/moul/x/daily/sparklinedemo/v0`](https://gno.land/r/moul/x/daily/sparklinedemo/v0). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
- #4gnomod.toml
- #5module = "gno.land/p/moul/x/daily/sparkline/v0" gno = "0.9"
- #6sparkline.gno
- #7// Package sparkline renders a numeric series as one line of Unicode block // characters — ▁▂▃▄▅▆▇█ — so a realm's Render can show a trend inline, with no // image, no chart library and no client-side code. // // Everything is integer math, deliberately. A realm's Render must produce // byte-identical output on every validating node, and floating point is the // usual way that quietly stops being true. Scaling here is integer division // with the rounding rule stated below, so a given series always yields exactly // the same runes. // // Rendering is O(len(values)) and allocates one builder; a realm should bound // what it feeds in (slice to a window) rather than sparking an unbounded, // user-grown slice. // // A live demo of this package is at // [r/moul/x/daily/sparklinedemo](/r/moul/x/daily/sparklinedemo/v0). package sparkline import "strings" // Levels is the ramp, lowest to highest. A string constant rather than a // []rune so callers cannot mutate the ramp out from under other realms. const Levels = "▁▂▃▄▅▆▇█" // Steps is how many levels the ramp has. const Steps = 8 // maxInt is the largest int, used to keep the scaling multiply from wrapping. const maxInt = int(^uint(0) >> 1) // Ints renders values scaled between their own smallest and largest element. // An empty series renders as the empty string. func Ints(values []int) string { lo, hi, ok := Bounds(values) if !ok { return "" } return Scaled(values, lo, hi) } // Scaled renders values against an explicit [lo, hi] window. Values outside it // clamp to the ends rather than erroring: a window is chosen for readability // (0..100 for a percentage, say), and one outlier should not be able to break a // realm's Render. // // A window with hi <= lo is treated as flat; see Level. func Scaled(values []int, lo, hi int) string { if len(values) == 0 { return "" } ramp := []rune(Levels) var b strings.Builder for _, v := range values { b.WriteRune(ramp[Level(v, lo, hi)]) } return b.String() } // Level maps v within [lo, hi] to a ramp index in [0, Steps-1]. // // Rounding is floor, so only a value at hi itself reaches the top of the ramp; // everything below it rounds down. That makes the maximum visually distinct, // which is what a reader is looking for in a sparkline. // // A flat window (hi <= lo) maps everything to the MIDDLE of the ramp, not the // bottom: a series that sits unchanged at 1000 carries no shape, but drawing it // along the floor would read as "zero", which is a different and wrong claim. func Level(v, lo, hi int) int { if hi <= lo { return Steps / 2 } if v <= lo { return 0 } if v >= hi { return Steps - 1 } span, d, top := hi-lo, v-lo, Steps-1 if span <= 0 { // hi-lo wrapped: the window is wider than an int can express, so there // is no usable ramp. Split at the midpoint, computed without overflowing. if v < lo/2+hi/2 { return 0 } return Steps - 1 } // d*top would wrap on an extreme span; halve both until it cannot. The // chosen bucket is unchanged for any realistic series, and off by at most // one level for the pathological ones this protects against. for span > maxInt/top { span >>= 1 d >>= 1 } return d * top / span } // Bounds returns the smallest and largest value in values. ok is false when // values is empty, in which case lo and hi are zero. func Bounds(values []int) (lo, hi int, ok bool) { if len(values) == 0 { return 0, 0, false } lo, hi = values[0], values[0] for _, v := range values[1:] { if v < lo { lo = v } if v > hi { hi = v } } return lo, hi, true }
- #8/gno.MemPackageType
- #9 MPUserProd
Result log
msg:0,success:true,log:,events:[]