miller/pkg/bifs/sparkline.go
John Kerl a55327c7ef
Add sparkline DSL function and mlr sparkline verb (#166) (#2177)
Adds a sparkline(array|map) built-in that renders numeric values as a
Unicode block-character string (▁▂▃▄▅▆▇█), plus a new `mlr sparkline`
verb that summarizes each field's values in record order -- useful for
eyeballing trends without external plotting tools.

Also adds `-s` to `mlr histogram` to sparkline a field's binned counts
(its distribution shape) rather than emitting one record per bin. This
is a different chart from `mlr sparkline` (order-independent binning
vs. record-order values), and the docs for each cross-reference the
other to avoid conflating them.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 15:47:01 -04:00

72 lines
1.8 KiB
Go

package bifs
import (
"github.com/johnkerl/miller/v6/pkg/mlrval"
)
// sparklineTicks are the eighth-height Unicode block characters used to
// render array/map values as a compact one-line bar chart.
var sparklineTicks = []rune("▁▂▃▄▅▆▇█")
func BIF_sparkline(collection *mlrval.Mlrval) *mlrval.Mlrval {
ok, valueIfNot := check_collection(collection, "sparkline")
if !ok {
return valueIfNot
}
var floatValues []float64
if collection.IsArray() {
array := collection.AcquireArrayValue()
floatValues = make([]float64, 0, len(array))
for _, element := range array {
floatValue, isFloat := element.GetNumericToFloatValue()
if !isFloat {
return mlrval.FromNotNumericError("sparkline", element)
}
floatValues = append(floatValues, floatValue)
}
} else {
m := collection.AcquireMapValue()
floatValues = make([]float64, 0, m.FieldCount)
for pe := m.Head; pe != nil; pe = pe.Next {
floatValue, isFloat := pe.Value.GetNumericToFloatValue()
if !isFloat {
return mlrval.FromNotNumericError("sparkline", pe.Value)
}
floatValues = append(floatValues, floatValue)
}
}
if len(floatValues) == 0 {
return mlrval.VOID
}
lo := floatValues[0]
hi := floatValues[0]
for _, floatValue := range floatValues[1:] {
if floatValue < lo {
lo = floatValue
}
if floatValue > hi {
hi = floatValue
}
}
numTicks := len(sparklineTicks)
runes := make([]rune, len(floatValues))
for i, floatValue := range floatValues {
if hi == lo {
runes[i] = sparklineTicks[0]
continue
}
tickIndex := int(float64(numTicks-1)*(floatValue-lo)/(hi-lo) + 0.5)
if tickIndex < 0 {
tickIndex = 0
} else if tickIndex >= numTicks {
tickIndex = numTicks - 1
}
runes[i] = sparklineTicks[tickIndex]
}
return mlrval.FromString(string(runes))
}