code wiki / _hdl_build / nx_chart.nx
nx_chart.nx source
↩ module page · 197 lines · 9325 B
1// nx_chart.nx -- IMS Thrust D R2: a SOVEREIGN, REUSABLE native-SVG bar-chart renderer.
2//
3// THE GAP IT FILLS: nx_viz_chart.nx is a whole-PAGE main() that reads a fixed TSV and writes an HTML
4// file -- not a reusable function. nx_wiki_graph emits SVG but only a circular link-graph. So the gap
5// is a clean LIBRARY function chart_svg(labels[], values[], n, title) -> <svg>...</svg> that any organ
6// can call to drop a bar chart inline into a page. Pure + deterministic (no I/O, no wall-clock).
7//
8// REUSE (zero geometry reinvented): imports nx_viz_axis -> which transitively gives the proven viz
9// primitives the d3-equivalent suite already KAT-grades:
10// vs_linear (nx_viz_scale) -- map a data value -> a pixel height (the bar scaling).
11// va_axis_left(nx_viz_axis) -- the y-axis: baseline + tick marks + numeric labels.
12// vsh_lit / vsh_n (nx_viz_shape) -- append a literal / an integer into the SVG buffer.
13// We add ONLY the bar-chart composition (the bars, the x-labels, the title) -- the part that did not
14// exist as a reusable call.
15//
16// SVG is static (no <script>) -> sovereign, renders identically everywhere. license_tier: ORIGINAL
17import "nx_viz_axis.nx"
18import "nx_syscalls.nx"
19const CH_MAGIC_8192: i64 = 8192
20
21// ===== sealed verdict surface (codes 2900-2909; distinct from VP_*/IP_*/NX_WGRAPH_*) =====
22const CH_OK: i64 = 0
23const CH_BAD_INPUT: i64 = 2900
24const CH_OVERFLOW: i64 = 2901
25
26// ===== named layout constants (M7 -- no magic numbers buried in render code) =====
27const CH_M_L: i64 = 56 // left margin (room for the y-axis labels)
28const CH_M_T: i64 = 40 // top margin (room for the title)
29const CH_PLOT_H: i64 = 300 // plot area height in px (data range maps into this)
30const CH_BW: i64 = 64 // bar width
31const CH_GAP: i64 = 34 // gap between bars
32const CH_M_B: i64 = 44 // bottom margin (room for the x-axis labels)
33const CH_TICKS: i64 = 6 // y-axis tick target
34const CH_LBLCAP: i64 = 64 // per-label byte cap (defensive)
35
36// max of an i64 array (>=0 domain top); 1 if all <= 0 so the scale never divides by zero.
37func ch_maxv(values: *i64, n: i64) -> i64 {
38 var mx: i64 = 1
39 var i: i64 = 0
40 while i < n {
41 if values[i] > mx { mx = values[i] }
42 i = i + 1
43 }
44 return mx
45}
46
47// round a domain top UP to a "nice" round number so ticks read cleanly (e.g. 21 -> 25, 468 -> 500).
48// purely deterministic: smallest of {1,2,5} * 10^k that is >= v.
49func ch_nice_top(v: i64) -> i64 {
50 if v < 1 { return 1 }
51 var pow: i64 = 1
52 var guard: i64 = 0
53 while guard < 30 {
54 if 1 * pow >= v { return 1 * pow }
55 if 2 * pow >= v { return 2 * pow }
56 if 5 * pow >= v { return 5 * pow }
57 pow = pow * 10
58 guard = guard + 1
59 }
60 return v
61}
62
63// =============================================================================
64// chart_svg: emit a complete <svg>...</svg> (NUL-terminated) bar chart into out.
65// labels_ptr[i]/labels_len[i] -- the i-th x-axis label (pointer+length; the codebase convention,
66// since a *u8[] cannot be passed cleanly).
67// values[i] -- the i-th bar's value (>=0 expected; negatives clamp to 0 height).
68// n -- number of bars.
69// title -- NUL-terminated chart title (drawn top-left).
70// out / cap -- caller buffer + capacity.
71// out_used -- receives bytes written (excluding the NUL).
72// Returns CH_OK or a negative CH_* verdict. PURE: no syscalls except sys_mmap scratch (via the
73// imported primitives). Deterministic for identical inputs.
74// =============================================================================
75func chart_svg(labels_ptr: *i64, labels_len: *i64, values: *i64, n: i64,
76 title: *u8, out: *u8, cap: i64, out_used: *i64) -> i64 {
77 if (out_used as i64) == 0 { return 0 - CH_BAD_INPUT }
78 out_used[0] = 0
79 if (out as i64) == 0 { return 0 - CH_BAD_INPUT }
80 if cap < 512 { return 0 - CH_BAD_INPUT }
81 if n < 0 { return 0 - CH_BAD_INPUT }
82
83 // domain top (nice-rounded) -> the bar/axis scale. baseline = bottom of the plot.
84 let raw_top: i64 = ch_maxv(values, n)
85 let dom_top: i64 = ch_nice_top(raw_top)
86 let baseline: i64 = CH_M_T + CH_PLOT_H
87 let plot_top: i64 = CH_M_T
88 let width: i64 = CH_M_L + n * (CH_BW + CH_GAP) + CH_GAP
89 let height: i64 = baseline + CH_M_B
90
91 // local cursor + a bounded literal/number appender that checks cap (the viz vsh_* do NOT bounds-
92 // check, so we guard the cap ourselves and only call them when there is room).
93 var o: i64 = 0
94
95 // ---- <svg> open ----
96 o = vsh_lit(out, o, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"nx-chart\" width=\"" as *u8)
97 o = vsh_n(out, o, width)
98 o = vsh_lit(out, o, "\" height=\"" as *u8)
99 o = vsh_n(out, o, height)
100 o = vsh_lit(out, o, "\" viewBox=\"0 0 " as *u8)
101 o = vsh_n(out, o, width); o = vsh_lit(out, o, " " as *u8); o = vsh_n(out, o, height)
102 o = vsh_lit(out, o, "\" role=\"img\" font-family=\"system-ui,Segoe UI,sans-serif\">" as *u8)
103 // sovereign inline CSS (no JS, no CDN)
104 o = vsh_lit(out, o, "<style>.nx-chart .bar{fill:#2a4d8f}.nx-chart .val{fill:#14141e;font-size:13px;font-weight:600}.nx-chart .xl{fill:#444;font-size:12px}.nx-chart .ttl{fill:#14141e;font-size:16px;font-weight:700}</style>" as *u8)
105
106 // ---- title (top-left) ----
107 o = vsh_lit(out, o, "<text class=\"ttl\" x=\"" as *u8); o = vsh_n(out, o, CH_M_L)
108 o = vsh_lit(out, o, "\" y=\"22\">" as *u8)
109 var ti: i64 = 0
110 while title[ti] != (0 as u8) {
111 if ti >= 200 { ti = 200 }
112 if ti < 200 {
113 if o + 8 >= cap { return 0 - CH_OVERFLOW }
114 out[o] = title[ti]; o = o + 1
115 }
116 ti = ti + 1
117 }
118 o = vsh_lit(out, o, "</text>" as *u8)
119
120 // ---- y-axis (baseline + ticks + numeric labels) via the proven va_axis_left ----
121 // va_axis_left(d0,d1,py0,py1,target,axis_x,out): py0 maps d0 (bottom), py1 maps d1 (top).
122 let axfrag: *u8 = sys_mmap(CH_MAGIC_8192)
123 va_axis_left(0, dom_top, baseline, plot_top, CH_TICKS, CH_M_L, axfrag)
124 o = vsh_lit(out, o, axfrag)
125
126 // ---- bars + value labels + x labels ----
127 var b: i64 = 0
128 while b < n {
129 if b >= 256 { b = n }
130 if b < n {
131 let bx: i64 = CH_M_L + CH_GAP + b * (CH_BW + CH_GAP)
132 // bar height = value scaled into [0, CH_PLOT_H] over domain [0, dom_top]; clamp negatives.
133 var v: i64 = values[b]
134 if v < 0 { v = 0 }
135 let bh: i64 = vs_linear(0, dom_top, 0, CH_PLOT_H, v)
136 let by: i64 = baseline - bh
137
138 // rough cap guard: a bar block is < 256 bytes; require room.
139 if o + 256 >= cap { return 0 - CH_OVERFLOW }
140
141 // <rect>
142 o = vsh_lit(out, o, "<rect class=\"bar\" x=\"" as *u8); o = vsh_n(out, o, bx)
143 o = vsh_lit(out, o, "\" y=\"" as *u8); o = vsh_n(out, o, by)
144 o = vsh_lit(out, o, "\" width=\"" as *u8); o = vsh_n(out, o, CH_BW)
145 o = vsh_lit(out, o, "\" height=\"" as *u8); o = vsh_n(out, o, bh)
146 o = vsh_lit(out, o, "\" rx=\"4\"/>" as *u8)
147
148 // value label above the bar (the original data value, not the scaled height)
149 o = vsh_lit(out, o, "<text class=\"val\" text-anchor=\"middle\" x=\"" as *u8)
150 o = vsh_n(out, o, bx + CH_BW / 2)
151 o = vsh_lit(out, o, "\" y=\"" as *u8); o = vsh_n(out, o, by - 6)
152 o = vsh_lit(out, o, "\">" as *u8); o = vsh_n(out, o, values[b]); o = vsh_lit(out, o, "</text>" as *u8)
153
154 // x-axis label centred under the bar (caller-supplied, bounded copy)
155 o = vsh_lit(out, o, "<text class=\"xl\" text-anchor=\"middle\" x=\"" as *u8)
156 o = vsh_n(out, o, bx + CH_BW / 2)
157 o = vsh_lit(out, o, "\" y=\"" as *u8); o = vsh_n(out, o, baseline + 20)
158 o = vsh_lit(out, o, "\">" as *u8)
159 let lp: *u8 = labels_ptr[b] as *u8
160 let ll: i64 = labels_len[b]
161 var k: i64 = 0
162 while k < ll {
163 if k >= CH_LBLCAP { k = ll }
164 if k < ll {
165 if o + 8 >= cap { return 0 - CH_OVERFLOW }
166 out[o] = lp[k]; o = o + 1
167 }
168 k = k + 1
169 }
170 o = vsh_lit(out, o, "</text>" as *u8)
171 }
172 b = b + 1
173 }
174
175 o = vsh_lit(out, o, "</svg>" as *u8)
176 if o + 1 >= cap { return 0 - CH_OVERFLOW }
177 out[o] = 0 as u8
178 out_used[0] = o
179 return CH_OK
180}
181
182// compile smoke; the real KAT lives in nx_figure_gate. (library organ; no main needed at link if
183// imported -- but a main keeps it independently buildable.)
184func main() -> i64 {
185 let lp: *i64 = sys_mmap(64) as *i64
186 let ll: *i64 = sys_mmap(64) as *i64
187 let vv: *i64 = sys_mmap(64) as *i64
188 lp[0] = "a" as *u8 as i64; ll[0] = 1; vv[0] = 8
189 lp[1] = "b" as *u8 as i64; ll[1] = 1; vv[1] = 21
190 let out: *u8 = sys_mmap(CH_MAGIC_8192)
191 let used: *i64 = sys_mmap(16) as *i64
192 let rc: i64 = chart_svg(lp, ll, vv, 2, "smoke" as *u8, out, CH_MAGIC_8192, used)
193 if rc != CH_OK { return 1 }
194 if used[0] <= 0 { return 2 }
195 if out[0] != 60 as u8 { return 3 } // '<'
196 return 0
197}