sourcelibs/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ᵗ⁼u̲se< "Strings"
14ᶠ⁼u̲se< "Format"
15ᵛ⁼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⍝## Chart drawing (private)
96
97⍝# The chart's frame: the picture's size and the plot area's edges.
98ʰW ← 480.0
99ʰH ← 280.0
100ʰL ← 64.0
101ʰR ← 456.0
102ʰT ← 44.0
103ʰB ← 232.0
104
105⍝# The line colors, in turn (blue, red, green, amber, violet, teal).
106ʰcolors ← "#2563eb" "#dc2626" "#16a34a" "#d97706" "#7c3aed" "#0d9488"
107
108⍝# Boxed numbers joined into one vector.
109ʰj̲oined ← { b → d̲isclose '{ x y → e̲nclose (d̲isclose x) c̲at d̲isclose y } r̲/ b }
110
111⍝# labels c_hart series: the picture's SVG text.
112ʰc̲hart ← { labels series →
113 all ← ʰj̲oined series
114 lo ← 'm̲in r̲/ all
115 hi ← 'm̲ax r̲/ all
116 pad ← lo ʰp̲ad hi
117 box ← (lo − pad) c̲at hi + pad
118 n ← 'm̲ax r̲/ '{ x → t̲ally d̲isclose x } e̲ach series
119 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
120 body ← (n ʰf̲rame box) c̲at lines c̲at labels ʰt̲itles series
121 (ʰW c̲at ʰH) ᵛp̲icture body
122}
123
124⍝# How far past its least and greatest values the vertical axis goes: 0,
125⍝# or for a flat series a tenth of its size (1 when it is 0).
126ʰp̲ad ← { lo hi →
127 hi > lo ? 0.0
128 lo = 0.0 ? 1.0
129 0.1 × a̲bs lo
130}
131
132⍝# Boxed texts joined into one text (none is the empty text).
133ʰj̲oinText ← { b →
134 0 = t̲ally b ? ""
135 d̲isclose '{ x y → e̲nclose (d̲isclose x) c̲at d̲isclose y } r̲/ b
136}
137
138⍝# The color of series i (1, 2, ...), the colors in turn.
139ʰc̲olor ← { i c → d̲isclose (1 + (i − 1) m̲od t̲ally c) s̲elect c }
140
141⍝# The x of positions p (0, 1, ...) of n: spread across the plot area,
142⍝# one position in its middle.
143ʰx̲at ← { n p →
144 n = 1 ? (s̲hape p) r̲eshape (ʰL + ʰR) ÷ 2.0
145 ʰr̲ound ʰL + (ʰR − ʰL) × (f̲loat p) ÷ f̲loat n − 1
146}
147
148⍝# x rounded to a whole number, still a Float.
149ʰr̲ound ← { x → f̲loat f̲loor 0.5 + x }
150
151⍝# The y of values v in the vertical range box (least, greatest).
152ʰy̲at ← { box v →
153 lo ← f̲irst box
154 hi ← f̲irst 1 d̲rop box
155 ʰr̲ound ʰB − (ʰB − ʰT) × ((f̲loat v) − lo) ÷ hi − lo
156}
157
158⍝# One series as a polyline (and a point when it has one value), in its
159⍝# color: color s_eries (n; box; values), boxed (n a Float).
160ʰs̲eries ← { color rest →
161 n ← f̲loor f̲irst d̲isclose f̲irst rest
162 box ← d̲isclose f̲irst 1 d̲rop rest
163 v ← d̲isclose f̲irst 2 d̲rop rest
164 xs ← n ʰx̲at o̲ffsets t̲ally v
165 ys ← box ʰy̲at v
166 line ← ((ᵛf̲ill "none") c̲at color ᵛs̲troke 2) ᵛp̲olyline (2 c̲at t̲ally v) r̲eshape xs c̲at ys
167 1 < t̲ally v ? line
168 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 "/>"
169}
170
171⍝# The text attributes of tick labels and names.
172ʰsmall ← " font-family=\"sans-serif\" font-size=\"11\" fill=\"#475569\""
173
174⍝# The axes, the grid lines and the tick labels: three values on the
175⍝# vertical axis (least, middle, greatest), the first and last
176⍝# positions on the horizontal one.
177ʰf̲rame ← { n box →
178 lo ← f̲irst box
179 hi ← f̲irst 1 d̲rop box
180 ticks ← lo c̲at ((lo + hi) ÷ 2.0) c̲at hi
181 ys ← box ʰy̲at ticks
182 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
183 texts ← d ᶠf̲ixed ticks
184 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
185 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
186 last ← 1 m̲ax n
187 at ← n ʰx̲at (1 + n > 1) r̲eshape 0 c̲at n − 1
188 marks ← (1 + n > 1) r̲eshape 1 c̲at last
189 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
190 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
191 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 "/>"
192 back c̲at grid c̲at axes c̲at left c̲at bottom
193}
194
195⍝# The title, the axes' names and the legend from labels (title,
196⍝# horizontal axis, vertical axis, then a name per series); empty ones
197⍝# are left out.
198ʰt̲itles ← { labels series →
199 n̲ame ← { k → 0 = t̲ally d̲isclose k s̲elect labels ? ""◆ d̲isclose k s̲elect labels }
200 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
201 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
202 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
203 names ← 3 d̲rop labels
204 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
205 legend ← ((ᵛa̲t ʰR c̲at 24.0) c̲at ʰsmall c̲at "text-anchor" ᵛa̲ttr "end") ᵛm̲arkup keys
206 ((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
207}
208
209⍝# An element, or nothing when the text it shows is empty.
210ʰk̲ept ← { t element →
211 0 = t̲ally t ? ""
212 element
213}