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

1⍝│ Plot: text charts -- bars, sparklines, histograms, scatter plots on 2⍝│ a character grid -- and line charts as pictures, with axes, labels 3⍝│ and several lines in one chart. 4⍝│ Import it with an alias of your choice: "p:" u_se< "Plot". 5⍝│ Put libs/Plot/src on XETAL_PATH ("just path"); the reference is libs/Plot/docs. 6⍝│ Names with l: are exported; those under h: are private to this file. 7⍝│ 8⍝│ A text chart is a character matrix: it prints as lines in a terminal, 9⍝│ a notebook or the live demo. A picture (l_ine!) is an SVG document 10⍝│ shown with []S_HOW: numbered files from the command line, a pane in 11⍝│ the browser. 12 13"t:" u̲se< "Strings" 14"f:" u̲se< "Format" 15"v:" u̲se< "Svg" 16 17blocks ← "▁▂▃▄▅▆▇█" ⍝ eight heights, lowest first 18width ← 40 ⍝ the longest bar 19 20⍝━━ Text charts 21 22⍝│ v scaled to 0..m, rounded (all 0 when v is all 0). 23ʰs̲cale ← { m v → 24 top ← 'm̲ax r̲/ a̲bs f̲loat v 25 top = 0.0 ? (s̲hape v) r̲eshape 0 26 f̲loor 0.5 + (f̲loat m) × (f̲loat v) ÷ top 27} 28 29⍝│ labels b_ars values: a horizontal bar per value, labeled, the 30⍝│ longest 40 wide, each followed by its value (values >= 0). 31ˡb̲ars ← { labels v → 32 w ← 'm̲ax r̲/ '{ t̲ally d̲isclose ⍵ } e̲ach labels 33 n ← width ʰs̲cale v 34 ᵗm̲ix '{ i → 35 label ← w ᵗp̲adRight d̲isclose i s̲elect labels 36 bar ← (f̲irst i s̲elect n) r̲eshape "#" 37 label c̲at " |" c̲at bar c̲at " " c̲at f̲ormat f̲irst i s̲elect v 38 } m̲ap r̲ange t̲ally v 39} 40 41⍝│ s_park v: v as a sparkline, one block character per value, its 42⍝│ height from the least (lowest) to the greatest (highest). 43ˡs̲park ← { v → 44 lo ← 'm̲in r̲/ f̲loat v 45 hi ← 'm̲ax r̲/ f̲loat v 46 hi = lo ? (t̲ally v) r̲eshape 4 s̲elect blocks 47 (1 + f̲loor 0.5 + 7.0 × ((f̲loat v) − lo) ÷ hi − lo) s̲elect blocks 48} 49 50⍝│ n h_istogram v: v counted into n bins of equal width from its least 51⍝│ to its greatest, drawn as bars labeled by each bin's range. 52ˡh̲istogram ← { n v → 53 lo ← 'm̲in r̲/ f̲loat v 54 hi ← 'm̲ax r̲/ f̲loat v 55 step ← (hi − lo) ÷ f̲loat n 56 bin ← 1 + (n − 1) m̲in f̲loor ((f̲loat v) − lo) ÷ step m̲ax 0.000000000001 57 counts ← '{ t̲ally w̲here bin = ⍵ } e̲ach r̲ange n 58 edges ← lo + step × f̲loat o̲ffsets n + 1 59 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 60 labels ˡb̲ars counts 61} 62 63⍝│ (w c_at h) s_catter xy: the points xy (2 rows, x over y, as []P_ATH 64⍝│ takes them) marked * on a character grid w wide and h high, y up. 65ˡs̲catter ← { size xy → 66 w ← f̲irst size 67 h ← f̲irst 1 d̲rop size 68 x ← f̲loat f̲irst xy 69 y ← f̲loat f̲irst 1 d̲rop xy 70 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 71 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 72 at ← (col − 1) + w × row − 1 ⍝ offsets into the grid 73 marks ← (o̲ffsets h × w) m̲ember? at 74 (h c̲at w) r̲eshape (1 + marks) s̲elect " *" 75} 76 77⍝━━ Line charts 78 79⍝│ l_ine! v: v as a line chart with axes, a picture shown with []S_HOW; 80⍝│ gives the picture's SVG text. The vertical axis spans the values 81⍝│ (however small their range), the horizontal one the positions 1 to 82⍝│ t_ally v; a single value is drawn as a point. 83ˡl̲ine! ← { v → ⎕S̲HOW ("" "" "") ʰc̲hart 1 r̲eshape e̲nclose f̲loat r̲avel v } 84 85⍝│ names l_ines! series: several lines in one chart, one per boxed 86⍝│ vector of series (each its own length), colored in turn and named in 87⍝│ a legend by names (a boxed text per series). 88ˡl̲ines! ← { names series → ⎕S̲HOW ("" "" "" c̲at names) ʰc̲hart '{ x → f̲loat r̲avel d̲isclose x } m̲ap series } 89 90⍝│ labels c_hart! series: as l_ines!, with a title and the names of the 91⍝│ axes: labels is the title, the horizontal axis's name, the vertical 92⍝│ axis's name, then a name per series ("" for none). 93ˡc̲hart! ← { labels series → ⎕S̲HOW labels ʰc̲hart '{ x → f̲loat r̲avel d̲isclose x } m̲ap series } 94 95⍝━━ Function plots (a real x-axis, not a position: l_ine! and its 96⍝━━ kin space a series across the plot area by count alone, fine for 97⍝━━ a sequence but not for y = f(x) over a range, where a reader 98⍝━━ expects the axis itself to read x's own values) 99 100⍝│ n l_inspace (lo, hi): n evenly spaced Floats from lo to hi 101⍝│ inclusive (n = 1 gives lo). Sample a function over it with e_ach or 102⍝│ m_ap: ys := 'f e_ach (n l_inspace (lo, hi)). 103ˡl̲inspace ← { n (lo, hi) → 104 1 = n ? (f̲loat lo) + (0.0 × (f̲loat o̲ffsets n)) 105 (f̲loat lo) + (((f̲loat hi) − (f̲loat lo)) × ((f̲loat o̲ffsets n) ÷ (f̲loat n − 1))) 106} 107 108⍝│ xy f_unction!: xy (2 rows, x over y, as []P_ATH and s_catter take 109⍝│ them) as a line chart with a real-valued horizontal axis, a picture 110⍝│ shown with []S_HOW; gives the picture's SVG text. The vertical axis 111⍝│ spans y's values; the horizontal one spans x's. 112ˡf̲unction! ← { xy → ⎕S̲HOW ("" "" "") ʰc̲hartXY (ʰx̲box xy, 1 r̲eshape e̲nclose f̲loat xy) } 113 114⍝│ names f_unctions! series: several curves in one chart, one per 115⍝│ boxed (x over y) pair of series (each its own length and its own x 116⍝│ values), colored in turn and named in a legend by names. 117ˡf̲unctions! ← { names series → ⎕S̲HOW ("" "" "" c̲at names) ʰc̲hartXY (ʰx̲boxAll series, '{ xy → f̲loat d̲isclose xy } m̲ap series) } 118 119⍝│ labels f_unctionChart! series: as f_unctions!, with a title and the 120⍝│ names of the axes (as c_hart!'s labels). 121ˡf̲unctionChart! ← { labels series → ⎕S̲HOW labels ʰc̲hartXY (ʰx̲boxAll series, '{ xy → f̲loat d̲isclose xy } m̲ap series) } 122 123⍝━━ Chart drawing (private) 124 125⍝│ The horizontal range of one (x over y) pair, padded as the 126⍝│ vertical axis is. 127ʰx̲box ← { xy → 128 x ← f̲irst xy 129 lo ← 'm̲in r̲/ x 130 hi ← 'm̲ax r̲/ x 131 pad ← lo ʰp̲ad hi 132 (lo − pad) c̲at hi + pad 133} 134 135⍝│ The horizontal range across every boxed (x over y) pair of series, 136⍝│ so curves of different x values still share one axis. 137ʰx̲boxAll ← { series → 138 xs ← ʰj̲oined '{ xy → f̲irst d̲isclose xy } m̲ap series 139 lo ← 'm̲in r̲/ xs 140 hi ← 'm̲ax r̲/ xs 141 pad ← lo ʰp̲ad hi 142 (lo − pad) c̲at hi + pad 143} 144 145⍝│ The chart's frame: the picture's size and the plot area's edges. 146ʰW ← 480.0 147ʰH ← 280.0 148ʰL ← 64.0 149ʰR ← 456.0 150ʰT ← 44.0 151ʰB ← 232.0 152 153⍝│ The line colors, in turn (blue, red, green, amber, violet, teal). 154ʰcolors ← "#2563eb" "#dc2626" "#16a34a" "#d97706" "#7c3aed" "#0d9488" 155 156⍝│ Boxed numbers joined into one vector. 157ʰj̲oined ← { b → d̲isclose '{ x y → e̲nclose (d̲isclose x) c̲at d̲isclose y } r̲/ b } 158 159⍝│ labels c_hart series: the picture's SVG text. 160ʰc̲hart ← { labels series → 161 all ← ʰj̲oined series 162 lo ← 'm̲in r̲/ all 163 hi ← 'm̲ax r̲/ all 164 pad ← lo ʰp̲ad hi 165 box ← (lo − pad) c̲at hi + pad 166 n ← 'm̲ax r̲/ '{ x → t̲ally d̲isclose x } e̲ach series 167 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 168 body ← (n ʰf̲rame box) c̲at lines c̲at ʰt̲itles labels 169 (ʰW c̲at ʰH) ᵛp̲icture body 170} 171 172⍝│ How far past its least and greatest values the vertical axis goes: 0, 173⍝│ or for a flat series a tenth of its size (1 when it is 0). 174ʰp̲ad ← { lo hi → 175 hi > lo ? 0.0 176 lo = 0.0 ? 1.0 177 0.1 × a̲bs lo 178} 179 180⍝│ Boxed texts joined into one text (none is the empty text). 181ʰj̲oinText ← { b → 182 0 = t̲ally b ? "" 183 d̲isclose '{ x y → e̲nclose (d̲isclose x) c̲at d̲isclose y } r̲/ b 184} 185 186⍝│ The color of series i (1, 2, ...), the colors in turn. 187ʰc̲olor ← { i c → d̲isclose (1 + (i − 1) m̲od t̲ally c) s̲elect c } 188 189⍝│ The x of positions p (0, 1, ...) of n: spread across the plot area, 190⍝│ one position in its middle. 191ʰx̲at ← { n p → 192 n = 1 ? (s̲hape p) r̲eshape (ʰL + ʰR) ÷ 2.0 193 ʰr̲ound ʰL + (ʰR − ʰL) × (f̲loat p) ÷ f̲loat n − 1 194} 195 196⍝│ x rounded to a whole number, still a Float. 197ʰr̲ound ← { x → f̲loat f̲loor 0.5 + x } 198 199⍝│ The y of values v in the vertical range box (least, greatest). 200ʰy̲at ← { box v → 201 lo ← f̲irst box 202 hi ← f̲irst 1 d̲rop box 203 ʰr̲ound ʰB − (ʰB − ʰT) × ((f̲loat v) − lo) ÷ hi − lo 204} 205 206⍝│ The x of values v in the horizontal range box (least, greatest): 207⍝│ h:y_at's mapping, left to right instead of inverted. 208ʰx̲atValue ← { box v → 209 lo ← f̲irst box 210 hi ← f̲irst 1 d̲rop box 211 ʰr̲ound ʰL + (ʰR − ʰL) × ((f̲loat v) − lo) ÷ hi − lo 212} 213 214⍝│ One (x over y) series as a polyline (and a point when it has one 215⍝│ value), in its color: color s_eriesXY (xbox; ybox; xy), boxed. 216ʰs̲eriesXY ← { color rest → 217 xbox ← d̲isclose f̲irst rest 218 ybox ← d̲isclose f̲irst 1 d̲rop rest 219 xy ← d̲isclose f̲irst 2 d̲rop rest 220 x ← f̲irst xy 221 y ← f̲irst 1 d̲rop xy 222 xs ← xbox ʰx̲atValue x 223 ys ← ybox ʰy̲at y 224 line ← ((ᵛf̲ill "none") c̲at color ᵛs̲troke 2) ᵛp̲olyline (2 c̲at t̲ally x) r̲eshape xs c̲at ys 225 1 < t̲ally x ? line 226 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 "/>" 227} 228 229⍝│ One series as a polyline (and a point when it has one value), in its 230⍝│ color: color s_eries (n; box; values), boxed (n a Float). 231ʰs̲eries ← { color rest → 232 n ← f̲loor f̲irst d̲isclose f̲irst rest 233 box ← d̲isclose f̲irst 1 d̲rop rest 234 v ← d̲isclose f̲irst 2 d̲rop rest 235 xs ← n ʰx̲at o̲ffsets t̲ally v 236 ys ← box ʰy̲at v 237 line ← ((ᵛf̲ill "none") c̲at color ᵛs̲troke 2) ᵛp̲olyline (2 c̲at t̲ally v) r̲eshape xs c̲at ys 238 1 < t̲ally v ? line 239 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 "/>" 240} 241 242⍝│ The text attributes of tick labels and names. 243ʰsmall ← " font-family=\"sans-serif\" font-size=\"11\" fill=\"#475569\"" 244 245⍝│ The axes, the grid lines and the tick labels: three values on the 246⍝│ vertical axis (least, middle, greatest), the first and last 247⍝│ positions on the horizontal one. 248ʰf̲rame ← { n box → 249 lo ← f̲irst box 250 hi ← f̲irst 1 d̲rop box 251 ticks ← lo c̲at ((lo + hi) ÷ 2.0) c̲at hi 252 ys ← box ʰy̲at ticks 253 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 254 texts ← d ᶠf̲ixed ticks 255 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 256 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 257 last ← 1 m̲ax n 258 at ← n ʰx̲at (1 + n > 1) r̲eshape 0 c̲at n − 1 259 marks ← (1 + n > 1) r̲eshape 1 c̲at last 260 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 261 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 262 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 "/>" 263 back c̲at grid c̲at axes c̲at left c̲at bottom 264} 265 266⍝│ The axes, the grid lines and the tick labels, both real-valued: 267⍝│ three ticks on each axis (least, middle, greatest), as h:f_rame's 268⍝│ vertical axis already reads, but the horizontal one too. 269ʰf̲rameXY ← { xbox ybox → 270 ylo ← f̲irst ybox 271 yhi ← f̲irst 1 d̲rop ybox 272 yticks ← ylo c̲at ((ylo + yhi) ÷ 2.0) c̲at yhi 273 ys ← ybox ʰy̲at yticks 274 yd ← 0 m̲ax 8 m̲in 1 − f̲loor (l̲og (yhi − ylo) ÷ 2.0) ÷ l̲og 10 275 ytexts ← yd ᶠf̲ixed yticks 276 xlo ← f̲irst xbox 277 xhi ← f̲irst 1 d̲rop xbox 278 xticks ← xlo c̲at ((xlo + xhi) ÷ 2.0) c̲at xhi 279 xs ← xbox ʰx̲atValue xticks 280 xd ← 0 m̲ax 8 m̲in 1 − f̲loor (l̲og (xhi − xlo) ÷ 2.0) ÷ l̲og 10 281 xtexts ← xd ᶠf̲ixed xticks 282 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 283 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 284 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 285 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 286 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 "/>" 287 back c̲at grid c̲at axes c̲at left c̲at bottom 288} 289 290⍝│ labels c_hartXY (xbox, series): as h:c_hart, but series is a boxed 291⍝│ (x over y) pair per curve and the horizontal axis spans xbox (its 292⍝│ own real values, not a count) instead of a position. 293ʰc̲hartXY ← { labels (xbox, series) → 294 allY ← ʰj̲oined '{ xy → f̲irst 1 d̲rop d̲isclose xy } m̲ap series 295 ylo ← 'm̲in r̲/ allY 296 yhi ← 'm̲ax r̲/ allY 297 ypad ← ylo ʰp̲ad yhi 298 ybox ← (ylo − ypad) c̲at yhi + ypad 299 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 300 body ← (xbox ʰf̲rameXY ybox) c̲at lines c̲at ʰt̲itles labels 301 (ʰW c̲at ʰH) ᵛp̲icture body 302} 303 304⍝│ The title, the axes' names and the legend from labels (title, 305⍝│ horizontal axis, vertical axis, then a name per series); empty ones 306⍝│ are left out. 307ʰt̲itles ← { labels → 308 n̲ame ← { k → 0 = t̲ally d̲isclose k s̲elect labels ? ""◆ d̲isclose k s̲elect labels } 309 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 310 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 311 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 312 names ← 3 d̲rop labels 313 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 314 legend ← ((ᵛa̲t ʰR c̲at 24.0) c̲at ʰsmall c̲at "text-anchor" ᵛa̲ttr "end") ᵛm̲arkup keys 315 ((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 316} 317 318⍝│ An element, or nothing when the text it shows is empty. 319ʰk̲ept ← { t element → 320 0 = t̲ally t ? "" 321 element 322}