librarywork/xetal-libraries/libs/Plot/src/Plot.xtl

Plot: text charts -- bars, sparklines, histograms, scatter plots on a character grid -- and line charts as pictures, with axes, labels and several lines in one chart. Import it with an alias of your choice: "p:" u_se< "Plot". Put libs/Plot/src on XETAL_PATH ("just path"); the reference is libs/Plot/docs. Names with l: are exported; those under h: are private to this file.

A text chart is a character matrix: it prints as lines in a terminal, a notebook or the live demo. A picture (l_ine!) is an SVG document shown with []S_HOW: numbered files from the command line, a pane in the browser.

source · imports t: work/xetal-libraries/libs/Strings/src/Strings.xtl; f: work/xetal-libraries/libs/Format/src/Format.xtl; v: std/Svg.xtl

blocks : Char

value (private) · line 17
blocks ← "▁▂▃▄▅▆▇█"                          ⍝ eight heights, lowest first
Used in: ˡs̲park

width : Int

value (private) · line 18
width ← 40                                    ⍝ the longest bar
Used in: ˡb̲ars

Text charts

ʰs̲cale : (Num a, Num b) => a -> b -> Int

function (private) · line 23

v scaled to 0..m, rounded (all 0 when v is all 0).

ʰs̲cale ← { m v →
  top ← 'm̲ax r̲/ a̲bs f̲loat v
  top = 0.0 ? (s̲hape v) r̲eshape 0
  f̲loor 0.5 + (f̲loat m) × (f̲loat v) ÷ top
}
Used in: ˡb̲ars

ˡb̲ars : Num a => Box Char -> a -> Char

function · line 31

labels b_ars values: a horizontal bar per value, labeled, the longest 40 wide, each followed by its value (values >= 0).

ˡb̲ars ← { labels v →
  w ← 'm̲ax r̲/ '{ t̲ally d̲isclose ⍵ } e̲ach labels
  n ← width ʰs̲cale v
  ᵗm̲ix '{ i →
    label ← w ᵗp̲adRight d̲isclose i s̲elect labels
    bar ← (f̲irst i s̲elect n) r̲eshape "#"
    label c̲at " |" c̲at bar c̲at " " c̲at f̲ormat f̲irst i s̲elect v
  } m̲ap r̲ange t̲ally v
}
Used in: ˡh̲istogram

ˡs̲park : Num a => a -> Char

function · line 43

s_park v: v as a sparkline, one block character per value, its height from the least (lowest) to the greatest (highest).

ˡs̲park ← { v →
  lo ← 'm̲in r̲/ f̲loat v
  hi ← 'm̲ax r̲/ f̲loat v
  hi = lo ? (t̲ally v) r̲eshape 4 s̲elect blocks
  (1 + f̲loor 0.5 + 7.0 × ((f̲loat v) − lo) ÷ hi − lo) s̲elect blocks
}

ˡh̲istogram : Num a => Int -> a -> Char

function · line 52

n h_istogram v: v counted into n bins of equal width from its least to its greatest, drawn as bars labeled by each bin's range.

ˡh̲istogram ← { n v →
  lo ← 'm̲in r̲/ f̲loat v
  hi ← 'm̲ax r̲/ f̲loat v
  step ← (hi − lo) ÷ f̲loat n
  bin ← 1 + (n − 1) m̲in f̲loor ((f̲loat v) − lo) ÷ step m̲ax 0.000000000001
  counts ← '{ t̲ally w̲here bin = ⍵ } e̲ach r̲ange n
  edges ← lo + step × f̲loat o̲ffsets n + 1
  labels ← '{ i → (d̲isclose i s̲elect 2 ᶠf̲ixed edges) c̲at "-" c̲at d̲isclose (i + 1) s̲elect 2 ᶠf̲ixed edges } m̲ap r̲ange n
  labels ˡb̲ars counts
}

ˡs̲catter : Num a => Int -> a -> Char

function · line 65

(w c_at h) s_catter xy: the points xy (2 rows, x over y, as []P_ATH takes them) marked * on a character grid w wide and h high, y up.

ˡs̲catter ← { size xy →
  w ← f̲irst size
  h ← f̲irst 1 d̲rop size
  x ← f̲loat f̲irst xy
  y ← f̲loat f̲irst 1 d̲rop xy
  col ← 1 + f̲loor 0.5 + (f̲loat w − 1) × (x − 'm̲in r̲/ x) ÷ 1.0 m̲ax ('m̲ax r̲/ x) − 'm̲in r̲/ x
  row ← h − f̲loor 0.5 + (f̲loat h − 1) × (y − 'm̲in r̲/ y) ÷ 1.0 m̲ax ('m̲ax r̲/ y) − 'm̲in r̲/ y
  at ← (col − 1) + w × row − 1                   ⍝ offsets into the grid
  marks ← (o̲ffsets h × w) m̲ember? at
  (h c̲at w) r̲eshape (1 + marks) s̲elect " *"
}

Line charts

ˡl̲ine! : Num a => a -> Char

function · line 83

l_ine! v: v as a line chart with axes, a picture shown with []S_HOW; gives the picture's SVG text. The vertical axis spans the values (however small their range), the horizontal one the positions 1 to t_ally v; a single value is drawn as a point.

ˡl̲ine! ← { v → ⎕S̲HOW ("" "" "") ʰc̲hart 1 r̲eshape e̲nclose f̲loat r̲avel v }

ˡl̲ines! : Num a => Box Char -> Box a -> Char

function · line 88

names l_ines! series: several lines in one chart, one per boxed vector of series (each its own length), colored in turn and named in a legend by names (a boxed text per series).

ˡl̲ines! ← { names series → ⎕S̲HOW ("" "" "" c̲at names) ʰc̲hart '{ x → f̲loat r̲avel d̲isclose x } m̲ap series }

ˡc̲hart! : Num a => Box Char -> Box a -> Char

function · line 93

labels c_hart! series: as l_ines!, with a title and the names of the axes: labels is the title, the horizontal axis's name, the vertical axis's name, then a name per series ("" for none).

ˡc̲hart! ← { labels series → ⎕S̲HOW labels ʰc̲hart '{ x → f̲loat r̲avel d̲isclose x } m̲ap series }
Used in: _, _

expects the axis itself to read x's own values)

ˡl̲inspace : (Num a, Num b) => Int -> (a, b) -> Float

function · line 103

n l_inspace (lo, hi): n evenly spaced Floats from lo to hi inclusive (n = 1 gives lo). Sample a function over it with e_ach or m_ap: ys := 'f e_ach (n l_inspace (lo, hi)).

ˡl̲inspace ← { n (lo, hi) →
  1 = n ? (f̲loat lo) + (0.0 × (f̲loat o̲ffsets n))
  (f̲loat lo) + (((f̲loat hi) − (f̲loat lo)) × ((f̲loat o̲ffsets n) ÷ (f̲loat n − 1)))
}

ˡf̲unction! : Float -> Char

function · line 112

xy f_unction!: xy (2 rows, x over y, as []P_ATH and s_catter take them) as a line chart with a real-valued horizontal axis, a picture shown with []S_HOW; gives the picture's SVG text. The vertical axis spans y's values; the horizontal one spans x's.

ˡf̲unction! ← { xy → ⎕S̲HOW ("" "" "") ʰc̲hartXY (ʰx̲box xy, 1 r̲eshape e̲nclose f̲loat xy) }

ˡf̲unctions! : Box Char -> Box Float -> Char

function · line 117

names f_unctions! series: several curves in one chart, one per boxed (x over y) pair of series (each its own length and its own x values), colored in turn and named in a legend by names.

ˡf̲unctions! ← { names series → ⎕S̲HOW ("" "" "" c̲at names) ʰc̲hartXY (ʰx̲boxAll series, '{ xy → f̲loat d̲isclose xy } m̲ap series) }

ˡf̲unctionChart! : Box Char -> Box Float -> Char

function · line 121

labels f_unctionChart! series: as f_unctions!, with a title and the names of the axes (as c_hart!'s labels).

ˡf̲unctionChart! ← { labels series → ⎕S̲HOW labels ʰc̲hartXY (ʰx̲boxAll series, '{ xy → f̲loat d̲isclose xy } m̲ap series) }

Chart drawing (private)

ʰx̲box : Float -> Float

function (private) · line 127

The horizontal range of one (x over y) pair, padded as the vertical axis is.

ʰx̲box ← { xy →
  x ← f̲irst xy
  lo ← 'm̲in r̲/ x
  hi ← 'm̲ax r̲/ x
  pad ← lo ʰp̲ad hi
  (lo − pad) c̲at hi + pad
}
Used in: ˡf̲unction!

ʰx̲boxAll : Box Float -> Float

function (private) · line 137

The horizontal range across every boxed (x over y) pair of series, so curves of different x values still share one axis.

ʰx̲boxAll ← { series →
  xs ← ʰj̲oined '{ xy → f̲irst d̲isclose xy } m̲ap series
  lo ← 'm̲in r̲/ xs
  hi ← 'm̲ax r̲/ xs
  pad ← lo ʰp̲ad hi
  (lo − pad) c̲at hi + pad
}

ʰW : Float

value (private) · line 146

The chart's frame: the picture's size and the plot area's edges.

ʰW ← 480.0

ʰH : Float

value (private) · line 147
ʰH ← 280.0

ʰL : Float

value (private) · line 148
ʰL ← 64.0

ʰR : Float

value (private) · line 149
ʰR ← 456.0

ʰT : Float

value (private) · line 150
ʰT ← 44.0

ʰB : Float

value (private) · line 151
ʰB ← 232.0

ʰcolors : Box Char

value (private) · line 154

The line colors, in turn (blue, red, green, amber, violet, teal).

ʰcolors ← "#2563eb" "#dc2626" "#16a34a" "#d97706" "#7c3aed" "#0d9488"

ʰj̲oined : Box Float -> Float

function (private) · line 157

Boxed numbers joined into one vector.

ʰj̲oined ← { b → d̲isclose '{ x y → e̲nclose (d̲isclose x) c̲at d̲isclose y } r̲/ b }

ʰc̲hart : Box Char -> Box Float -> Char

function (private) · line 160

labels c_hart series: the picture's SVG text.

ʰc̲hart ← { labels series →
  all ← ʰj̲oined series
  lo ← 'm̲in r̲/ all
  hi ← 'm̲ax r̲/ all
  pad ← lo ʰp̲ad hi
  box ← (lo − pad) c̲at hi + pad
  n ← 'm̲ax r̲/ '{ x → t̲ally d̲isclose x } e̲ach series
  lines ← ʰj̲oinText '{ i → (i ʰc̲olor ʰcolors) ʰs̲eries (e̲nclose f̲loat n) c̲at (e̲nclose box) c̲at i s̲elect series } m̲ap r̲ange t̲ally series
  body ← (n ʰf̲rame box) c̲at lines c̲at ʰt̲itles labels
  (ʰW c̲at ʰH) ᵛp̲icture body
}

ʰp̲ad : Float -> Float -> Float

function (private) · line 174

How far past its least and greatest values the vertical axis goes: 0, or for a flat series a tenth of its size (1 when it is 0).

ʰp̲ad ← { lo hi →
  hi > lo ? 0.0
  lo = 0.0 ? 1.0
  0.1 × a̲bs lo
}

ʰj̲oinText : Box Char -> Char

function (private) · line 181

Boxed texts joined into one text (none is the empty text).

ʰj̲oinText ← { b →
  0 = t̲ally b ? ""
  d̲isclose '{ x y → e̲nclose (d̲isclose x) c̲at d̲isclose y } r̲/ b
}

ʰc̲olor : Int -> Box Char -> Char

function (private) · line 187

The color of series i (1, 2, ...), the colors in turn.

ʰc̲olor ← { i c → d̲isclose (1 + (i − 1) m̲od t̲ally c) s̲elect c }

ʰx̲at : Int -> Int -> Float

function (private) · line 191

The x of positions p (0, 1, ...) of n: spread across the plot area, one position in its middle.

ʰx̲at ← { n p →
  n = 1 ? (s̲hape p) r̲eshape (ʰL + ʰR) ÷ 2.0
  ʰr̲ound ʰL + (ʰR − ʰL) × (f̲loat p) ÷ f̲loat n − 1
}

ʰr̲ound : Float -> Float

function (private) · line 197

x rounded to a whole number, still a Float.

ʰr̲ound ← { x → f̲loat f̲loor 0.5 + x }

ʰy̲at : Float -> Float -> Float

function (private) · line 200

The y of values v in the vertical range box (least, greatest).

ʰy̲at ← { box v →
  lo ← f̲irst box
  hi ← f̲irst 1 d̲rop box
  ʰr̲ound ʰB − (ʰB − ʰT) × ((f̲loat v) − lo) ÷ hi − lo
}

ʰx̲atValue : Float -> Float -> Float

function (private) · line 208

The x of values v in the horizontal range box (least, greatest): h:y_at's mapping, left to right instead of inverted.

ʰx̲atValue ← { box v →
  lo ← f̲irst box
  hi ← f̲irst 1 d̲rop box
  ʰr̲ound ʰL + (ʰR − ʰL) × ((f̲loat v) − lo) ÷ hi − lo
}

ʰs̲eriesXY : Char -> Box Float -> Char

function (private) · line 216

One (x over y) series as a polyline (and a point when it has one value), in its color: color s_eriesXY (xbox; ybox; xy), boxed.

ʰs̲eriesXY ← { color rest →
  xbox ← d̲isclose f̲irst rest
  ybox ← d̲isclose f̲irst 1 d̲rop rest
  xy ← d̲isclose f̲irst 2 d̲rop rest
  x ← f̲irst xy
  y ← f̲irst 1 d̲rop xy
  xs ← xbox ʰx̲atValue x
  ys ← ybox ʰy̲at y
  line ← ((ᵛf̲ill "none") c̲at color ᵛs̲troke 2) ᵛp̲olyline (2 c̲at t̲ally x) r̲eshape xs c̲at ys
  1 < t̲ally x ? line
  line c̲at "<circle" c̲at ("cx" ᵛa̲ttr f̲ormat f̲irst xs) c̲at ("cy" ᵛa̲ttr f̲ormat f̲irst ys) c̲at " r=\"3\"" c̲at (ᵛf̲ill color) c̲at "/>"
}
Used in: ʰc̲hartXY

ʰs̲eries : Char -> Box Float -> Char

function (private) · line 231

One series as a polyline (and a point when it has one value), in its color: color s_eries (n; box; values), boxed (n a Float).

ʰs̲eries ← { color rest →
  n ← f̲loor f̲irst d̲isclose f̲irst rest
  box ← d̲isclose f̲irst 1 d̲rop rest
  v ← d̲isclose f̲irst 2 d̲rop rest
  xs ← n ʰx̲at o̲ffsets t̲ally v
  ys ← box ʰy̲at v
  line ← ((ᵛf̲ill "none") c̲at color ᵛs̲troke 2) ᵛp̲olyline (2 c̲at t̲ally v) r̲eshape xs c̲at ys
  1 < t̲ally v ? line
  line c̲at "<circle" c̲at ("cx" ᵛa̲ttr f̲ormat f̲irst xs) c̲at ("cy" ᵛa̲ttr f̲ormat f̲irst ys) c̲at " r=\"3\"" c̲at (ᵛf̲ill color) c̲at "/>"
}
Used in: ʰc̲hart

ʰsmall : Char

value (private) · line 243

The text attributes of tick labels and names.

ʰsmall ← " font-family=\"sans-serif\" font-size=\"11\" fill=\"#475569\""

ʰf̲rame : Int -> Float -> Char

function (private) · line 248

The axes, the grid lines and the tick labels: three values on the vertical axis (least, middle, greatest), the first and last positions on the horizontal one.

ʰf̲rame ← { n box →
  lo ← f̲irst box
  hi ← f̲irst 1 d̲rop box
  ticks ← lo c̲at ((lo + hi) ÷ 2.0) c̲at hi
  ys ← box ʰy̲at ticks
  d ← 0 m̲ax 8 m̲in 1 − f̲loor (l̲og (hi − lo) ÷ 2.0) ÷ l̲og 10           ⍝ decimals enough to tell the ticks apart
  texts ← d ᶠf̲ixed ticks
  grid ← ʰj̲oinText '{ y → ((ᵛf̲ill "none") c̲at "#e2e8f0" ᵛs̲troke 1) ᵛp̲olyline 2 2 r̲eshape ʰL c̲at ʰR c̲at y c̲at y } m̲ap ys
  left ← ʰj̲oinText '{ k → ((ᵛa̲t (ʰL − 8.0) c̲at 4.0 + k s̲elect ys) c̲at ʰsmall c̲at "text-anchor" ᵛa̲ttr "end") ᵛt̲ext d̲isclose k s̲elect texts } m̲ap r̲ange 3
  last ← 1 m̲ax n
  at ← n ʰx̲at (1 + n > 1) r̲eshape 0 c̲at n − 1
  marks ← (1 + n > 1) r̲eshape 1 c̲at last
  bottom ← ʰj̲oinText '{ k → ((ᵛa̲t (k s̲elect at) c̲at ʰB + 18.0) c̲at ʰsmall c̲at "text-anchor" ᵛa̲ttr "middle") ᵛt̲ext f̲ormat k s̲elect marks } m̲ap r̲ange t̲ally at
  axes ← ((ᵛf̲ill "none") c̲at "#64748b" ᵛs̲troke 1) ᵛp̲olyline 2 3 r̲eshape ʰL c̲at ʰL c̲at ʰR c̲at ʰT c̲at ʰB c̲at ʰB
  back ← "<rect" c̲at ("width" ᵛa̲ttr f̲ormat ʰW) c̲at ("height" ᵛa̲ttr f̲ormat ʰH) c̲at (ᵛf̲ill "#ffffff") c̲at "/>"
  back c̲at grid c̲at axes c̲at left c̲at bottom
}
Used in: ʰc̲hart

ʰf̲rameXY : Float -> Float -> Char

function (private) · line 269

The axes, the grid lines and the tick labels, both real-valued: three ticks on each axis (least, middle, greatest), as h:f_rame's vertical axis already reads, but the horizontal one too.

ʰf̲rameXY ← { xbox ybox →
  ylo ← f̲irst ybox
  yhi ← f̲irst 1 d̲rop ybox
  yticks ← ylo c̲at ((ylo + yhi) ÷ 2.0) c̲at yhi
  ys ← ybox ʰy̲at yticks
  yd ← 0 m̲ax 8 m̲in 1 − f̲loor (l̲og (yhi − ylo) ÷ 2.0) ÷ l̲og 10
  ytexts ← yd ᶠf̲ixed yticks
  xlo ← f̲irst xbox
  xhi ← f̲irst 1 d̲rop xbox
  xticks ← xlo c̲at ((xlo + xhi) ÷ 2.0) c̲at xhi
  xs ← xbox ʰx̲atValue xticks
  xd ← 0 m̲ax 8 m̲in 1 − f̲loor (l̲og (xhi − xlo) ÷ 2.0) ÷ l̲og 10
  xtexts ← xd ᶠf̲ixed xticks
  grid ← ʰj̲oinText '{ y → ((ᵛf̲ill "none") c̲at "#e2e8f0" ᵛs̲troke 1) ᵛp̲olyline 2 2 r̲eshape ʰL c̲at ʰR c̲at y c̲at y } m̲ap ys
  left ← ʰj̲oinText '{ k → ((ᵛa̲t (ʰL − 8.0) c̲at 4.0 + k s̲elect ys) c̲at ʰsmall c̲at "text-anchor" ᵛa̲ttr "end") ᵛt̲ext d̲isclose k s̲elect ytexts } m̲ap r̲ange 3
  bottom ← ʰj̲oinText '{ k → ((ᵛa̲t (k s̲elect xs) c̲at ʰB + 18.0) c̲at ʰsmall c̲at "text-anchor" ᵛa̲ttr "middle") ᵛt̲ext d̲isclose k s̲elect xtexts } m̲ap r̲ange 3
  axes ← ((ᵛf̲ill "none") c̲at "#64748b" ᵛs̲troke 1) ᵛp̲olyline 2 3 r̲eshape ʰL c̲at ʰL c̲at ʰR c̲at ʰT c̲at ʰB c̲at ʰB
  back ← "<rect" c̲at ("width" ᵛa̲ttr f̲ormat ʰW) c̲at ("height" ᵛa̲ttr f̲ormat ʰH) c̲at (ᵛf̲ill "#ffffff") c̲at "/>"
  back c̲at grid c̲at axes c̲at left c̲at bottom
}
Used in: ʰc̲hartXY

ʰc̲hartXY : Box Char -> (Float, Box Float) -> Char

function (private) · line 293

labels c_hartXY (xbox, series): as h:c_hart, but series is a boxed (x over y) pair per curve and the horizontal axis spans xbox (its own real values, not a count) instead of a position.

ʰc̲hartXY ← { labels (xbox, series) →
  allY ← ʰj̲oined '{ xy → f̲irst 1 d̲rop d̲isclose xy } m̲ap series
  ylo ← 'm̲in r̲/ allY
  yhi ← 'm̲ax r̲/ allY
  ypad ← ylo ʰp̲ad yhi
  ybox ← (ylo − ypad) c̲at yhi + ypad
  lines ← ʰj̲oinText '{ i → (i ʰc̲olor ʰcolors) ʰs̲eriesXY (e̲nclose xbox) c̲at (e̲nclose ybox) c̲at i s̲elect series } m̲ap r̲ange t̲ally series
  body ← (xbox ʰf̲rameXY ybox) c̲at lines c̲at ʰt̲itles labels
  (ʰW c̲at ʰH) ᵛp̲icture body
}

ʰt̲itles : Box Char -> Char

function (private) · line 307

The title, the axes' names and the legend from labels (title, horizontal axis, vertical axis, then a name per series); empty ones are left out.

ʰt̲itles ← { labels →
  n̲ame ← { k → 0 = t̲ally d̲isclose k s̲elect labels ? ""◆ d̲isclose k s̲elect labels }
  title ← ((ᵛa̲t ʰL c̲at 24.0) c̲at " font-family=\"sans-serif\" font-size=\"14\" font-weight=\"bold\" fill=\"#0f172a\"") ᵛt̲ext n̲ame 1
  across ← ((ᵛa̲t ((ʰL + ʰR) ÷ 2.0) c̲at ʰB + 38.0) c̲at ʰsmall c̲at "text-anchor" ᵛa̲ttr "middle") ᵛt̲ext n̲ame 2
  up ← (ʰsmall c̲at ("text-anchor" ᵛa̲ttr "middle") c̲at "transform" ᵛa̲ttr "translate(18 " c̲at (f̲ormat (ʰT + ʰB) ÷ 2.0) c̲at ") rotate(-90)") ᵛt̲ext n̲ame 3
  names ← 3 d̲rop labels
  keys ← ʰj̲oinText '{ i → ((i ʰc̲olor ʰcolors) ᵛs̲pan "■ ") c̲at ("#334155" ᵛs̲pan d̲isclose i s̲elect names) c̲at "   " } m̲ap r̲ange t̲ally names
  legend ← ((ᵛa̲t ʰR c̲at 24.0) c̲at ʰsmall c̲at "text-anchor" ᵛa̲ttr "end") ᵛm̲arkup keys
  ((n̲ame 1) ʰk̲ept title) c̲at ((n̲ame 2) ʰk̲ept across) c̲at ((n̲ame 3) ʰk̲ept up) c̲at keys ʰk̲ept legend
}

ʰk̲ept : Char -> Char -> Char

function (private) · line 319

An element, or nothing when the text it shows is empty.

ʰk̲ept ← { t element →
  0 = t̲ally t ? ""
  element
}
Used in: ʰt̲itles