code wiki / _hdl_build / nx_chart.nx
nx_chart.nx source
↩ module page · 202 lines · 9771 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_TITLE_MAX: i64 = 200 // title chars copied into the <text> element (the cap the title loop used to write into its own cursor)
29const CH_M_T: i64 = 40 // top margin (room for the title)
30const CH_PLOT_H: i64 = 300 // plot area height in px (data range maps into this)
31const CH_BW: i64 = 64 // bar width
32const CH_GAP: i64 = 34 // gap between bars
33const CH_M_B: i64 = 44 // bottom margin (room for the x-axis labels)
34const CH_TICKS: i64 = 6 // y-axis tick target
35const CH_LBLCAP: i64 = 64 // per-label byte cap (defensive)
36
37// max of an i64 array (>=0 domain top); 1 if all <= 0 so the scale never divides by zero.
38func ch_maxv(values: *i64, n: i64) -> i64 {
39 var mx: i64 = 1
40 var i: i64 = 0
41 while i < n {
42 if values[i] > mx { mx = values[i] }
43 i = i + 1
44 }
45 return mx
46}
47
48// round a domain top UP to a "nice" round number so ticks read cleanly (e.g. 21 -> 25, 468 -> 500).
49// purely deterministic: smallest of {1,2,5} * 10^k that is >= v.
50func ch_nice_top(v: i64) -> i64 {
51 if v < 1 { return 1 }
52 var pow: i64 = 1
53 var guard: i64 = 0
54 while guard < 30 {
55 if 1 * pow >= v { return 1 * pow }
56 if 2 * pow >= v { return 2 * pow }
57 if 5 * pow >= v { return 5 * pow }
58 pow = pow * 10
59 guard = guard + 1
60 }
61 return v
62}
63
64// =============================================================================
65// chart_svg: emit a complete <svg>...</svg> (NUL-terminated) bar chart into out.
66// labels_ptr[i]/labels_len[i] -- the i-th x-axis label (pointer+length; the codebase convention,
67// since a *u8[] cannot be passed cleanly).
68// values[i] -- the i-th bar's value (>=0 expected; negatives clamp to 0 height).
69// n -- number of bars.
70// title -- NUL-terminated chart title (drawn top-left).
71// out / cap -- caller buffer + capacity.
72// out_used -- receives bytes written (excluding the NUL).
73// Returns CH_OK or a negative CH_* verdict. PURE: no syscalls except sys_mmap scratch (via the
74// imported primitives). Deterministic for identical inputs.
75// =============================================================================
76func chart_svg(labels_ptr: *i64, labels_len: *i64, values: *i64, n: i64,
77 title: *u8, out: *u8, cap: i64, out_used: *i64) -> i64 {
78 if (out_used as i64) == 0 { return 0 - CH_BAD_INPUT }
79 out_used[0] = 0
80 if (out as i64) == 0 { return 0 - CH_BAD_INPUT }
81 if cap < 512 { return 0 - CH_BAD_INPUT }
82 if n < 0 { return 0 - CH_BAD_INPUT }
83
84 // domain top (nice-rounded) -> the bar/axis scale. baseline = bottom of the plot.
85 let raw_top: i64 = ch_maxv(values, n)
86 let dom_top: i64 = ch_nice_top(raw_top)
87 let baseline: i64 = CH_M_T + CH_PLOT_H
88 let plot_top: i64 = CH_M_T
89 let width: i64 = CH_M_L + n * (CH_BW + CH_GAP) + CH_GAP
90 let height: i64 = baseline + CH_M_B
91
92 // local cursor + a bounded literal/number appender that checks cap (the viz vsh_* do NOT bounds-
93 // check, so we guard the cap ourselves and only call them when there is room).
94 var o: i64 = 0
95
96 // ---- <svg> open ----
97 o = vsh_lit(out, o, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"nx-chart\" width=\"" as *u8)
98 o = vsh_n(out, o, width)
99 o = vsh_lit(out, o, "\" height=\"" as *u8)
100 o = vsh_n(out, o, height)
101 o = vsh_lit(out, o, "\" viewBox=\"0 0 " as *u8)
102 o = vsh_n(out, o, width); o = vsh_lit(out, o, " " as *u8); o = vsh_n(out, o, height)
103 o = vsh_lit(out, o, "\" role=\"img\" font-family=\"system-ui,Segoe UI,sans-serif\">" as *u8)
104 // sovereign inline CSS (no JS, no CDN)
105 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)
106
107 // ---- title (top-left) ----
108 o = vsh_lit(out, o, "<text class=\"ttl\" x=\"" as *u8); o = vsh_n(out, o, CH_M_L)
109 o = vsh_lit(out, o, "\" y=\"22\">" as *u8)
110 // title capped at CH_TITLE_MAX chars by a FLAG, not by writing the cap into the cursor (2026-09-02, nx_srclint rule 2:
111 // `ti = 200` inside `while title[ti]` kept reading past the cap until the NUL instead of stopping at it)
112 var ti: i64 = 0
113 var tf: i64 = 0
114 while tf == 0 {
115 if title[ti] == (0 as u8) { tf = 1 }
116 if tf == 0 { if ti >= CH_TITLE_MAX { tf = 1 } }
117 if tf == 0 {
118 if o + 8 >= cap { return 0 - CH_OVERFLOW }
119 out[o] = title[ti]; o = o + 1
120 ti = ti + 1
121 }
122 }
123 o = vsh_lit(out, o, "</text>" as *u8)
124
125 // ---- y-axis (baseline + ticks + numeric labels) via the proven va_axis_left ----
126 // va_axis_left(d0,d1,py0,py1,target,axis_x,out): py0 maps d0 (bottom), py1 maps d1 (top).
127 let axfrag: *u8 = sys_mmap(CH_MAGIC_8192)
128 va_axis_left(0, dom_top, baseline, plot_top, CH_TICKS, CH_M_L, axfrag)
129 o = vsh_lit(out, o, axfrag)
130
131 // ---- bars + value labels + x labels ----
132 var b: i64 = 0
133 while b < n {
134 if b >= 256 { b = n }
135 if b < n {
136 let bx: i64 = CH_M_L + CH_GAP + b * (CH_BW + CH_GAP)
137 // bar height = value scaled into [0, CH_PLOT_H] over domain [0, dom_top]; clamp negatives.
138 var v: i64 = values[b]
139 if v < 0 { v = 0 }
140 let bh: i64 = vs_linear(0, dom_top, 0, CH_PLOT_H, v)
141 let by: i64 = baseline - bh
142
143 // rough cap guard: a bar block is < 256 bytes; require room.
144 if o + 256 >= cap { return 0 - CH_OVERFLOW }
145
146 // <rect>
147 o = vsh_lit(out, o, "<rect class=\"bar\" x=\"" as *u8); o = vsh_n(out, o, bx)
148 o = vsh_lit(out, o, "\" y=\"" as *u8); o = vsh_n(out, o, by)
149 o = vsh_lit(out, o, "\" width=\"" as *u8); o = vsh_n(out, o, CH_BW)
150 o = vsh_lit(out, o, "\" height=\"" as *u8); o = vsh_n(out, o, bh)
151 o = vsh_lit(out, o, "\" rx=\"4\"/>" as *u8)
152
153 // value label above the bar (the original data value, not the scaled height)
154 o = vsh_lit(out, o, "<text class=\"val\" text-anchor=\"middle\" x=\"" as *u8)
155 o = vsh_n(out, o, bx + CH_BW / 2)
156 o = vsh_lit(out, o, "\" y=\"" as *u8); o = vsh_n(out, o, by - 6)
157 o = vsh_lit(out, o, "\">" as *u8); o = vsh_n(out, o, values[b]); o = vsh_lit(out, o, "</text>" as *u8)
158
159 // x-axis label centred under the bar (caller-supplied, bounded copy)
160 o = vsh_lit(out, o, "<text class=\"xl\" text-anchor=\"middle\" x=\"" as *u8)
161 o = vsh_n(out, o, bx + CH_BW / 2)
162 o = vsh_lit(out, o, "\" y=\"" as *u8); o = vsh_n(out, o, baseline + 20)
163 o = vsh_lit(out, o, "\">" as *u8)
164 let lp: *u8 = labels_ptr[b] as *u8
165 let ll: i64 = labels_len[b]
166 var k: i64 = 0
167 while k < ll {
168 if k >= CH_LBLCAP { k = ll }
169 if k < ll {
170 if o + 8 >= cap { return 0 - CH_OVERFLOW }
171 out[o] = lp[k]; o = o + 1
172 }
173 k = k + 1
174 }
175 o = vsh_lit(out, o, "</text>" as *u8)
176 }
177 b = b + 1
178 }
179
180 o = vsh_lit(out, o, "</svg>" as *u8)
181 if o + 1 >= cap { return 0 - CH_OVERFLOW }
182 out[o] = 0 as u8
183 out_used[0] = o
184 return CH_OK
185}
186
187// compile smoke; the real KAT lives in nx_figure_gate. (library organ; no main needed at link if
188// imported -- but a main keeps it independently buildable.)
189func main() -> i64 {
190 let lp: *i64 = sys_mmap(64) as *i64
191 let ll: *i64 = sys_mmap(64) as *i64
192 let vv: *i64 = sys_mmap(64) as *i64
193 lp[0] = "a" as *u8 as i64; ll[0] = 1; vv[0] = 8
194 lp[1] = "b" as *u8 as i64; ll[1] = 1; vv[1] = 21
195 let out: *u8 = sys_mmap(CH_MAGIC_8192)
196 let used: *i64 = sys_mmap(16) as *i64
197 let rc: i64 = chart_svg(lp, ll, vv, 2, "smoke" as *u8, out, CH_MAGIC_8192, used)
198 if rc != CH_OK { return 1 }
199 if used[0] <= 0 { return 2 }
200 if out[0] != 60 as u8 { return 3 } // '<'
201 return 0
202}