Skip to content

Commit b9332fa

Browse files
fix: echo console results and draw plots as real SVG
The console showed the typed statement but not the value of a bare top-level expression, so `1 + 1` printed nothing. Echo any bare top-level expression's value (REPL-style); print() and mutating calls return None and stay silent. The Plot tab rendered a text card instead of the chart. Replace the placeholder with an SVG chart: amber dots for scatter, a connected teal line for line, with axes, a faint grid and tick labels.
1 parent 21027d5 commit b9332fa

3 files changed

Lines changed: 119 additions & 8 deletions

File tree

‎src/engine/evaluator.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,15 @@ export function run(source: string, state: MemoryState): MemoryState {
2525
if (traceEnabled) state.trace = [cloneState(state)]; // frame 0: state before running
2626
const ast = parse(source);
2727
for (const stmt of ast) {
28-
execStatement(stmt, state);
28+
if (stmt.kind === 'ExprStatement') {
29+
// REPL-style: a bare top-level expression shows its value. `print()`
30+
// and mutating calls like `a.append(..)` return None, so they stay
31+
// silent (they already spoke for themselves, or have no value).
32+
const result = evalExpr(stmt.expr, state);
33+
if (result !== null) state.stdout.push(displayValue(result, state));
34+
} else {
35+
execStatement(stmt, state);
36+
}
2937
recordStep(state); // after each top-level statement
3038
}
3139
// A nested child may have been mutated in place (e.g. a[1].append(3)),

‎src/style.css‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -599,16 +599,23 @@ button.ghost,
599599
.plot-cards {
600600
display: flex;
601601
flex-direction: column;
602-
gap: 10px;
602+
gap: 16px;
603603
width: 100%;
604+
max-width: 560px;
604605
}
605606

606607
.plot-card {
608+
margin: 0;
607609
border: 1px solid var(--line);
608-
background: var(--panel);
609-
border-radius: 10px;
610-
padding: 10px 12px;
611-
font-size: 13px;
610+
background: #10161a;
611+
border-radius: 12px;
612+
padding: 8px 10px 10px;
613+
}
614+
615+
.plot-card .plot-svg {
616+
display: block;
617+
width: 100%;
618+
height: auto;
612619
}
613620

614621
.plot-canvas-img {

‎src/ui/board.ts‎

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,101 @@
1010
import type { MemoryState, PlotSpec } from '../engine/types';
1111
import { escapeHtml } from './dialog';
1212

13+
// Plot palette mirrors the memory canvas (memory.ts) so the two lenses read as
14+
// one board: amber = data, teal = the fitted trend, muted ink for axes/labels.
15+
const PLOT = {
16+
ink: '#E7EEF2',
17+
muted: '#8FA0A8',
18+
amber: '#F0A050',
19+
teal: '#5BC4B0',
20+
line: '#2A353B',
21+
};
22+
const PLOT_MONO = 'IBM Plex Mono, monospace';
23+
const PLOT_SANS = 'IBM Plex Sans, sans-serif';
24+
25+
/** Shorten an axis value for a tick label without losing its meaning. */
26+
function fmtTick(n: number): string {
27+
if (!isFinite(n)) return '';
28+
const abs = Math.abs(n);
29+
if (abs >= 10000 || (abs > 0 && abs < 0.01)) return n.toExponential(1);
30+
return String(Math.round(n * 100) / 100);
31+
}
32+
33+
/**
34+
* Render one plot spec as a self-contained SVG: light grid, labelled axes, and
35+
* the series (amber dots for `scatter`, a teal polyline for `line`). Returns the
36+
* full `<svg>` markup — no live DOM, so it composes safely into innerHTML.
37+
*/
38+
function plotSvg(spec: PlotSpec): string {
39+
const W = 480;
40+
const H = 360;
41+
const m = { l: 52, r: 18, t: 32, b: 42 };
42+
const px0 = m.l;
43+
const px1 = W - m.r;
44+
const py0 = H - m.b;
45+
const py1 = m.t;
46+
const pts = spec.points;
47+
48+
// Data bounds, padded 8% so points never sit on the axis edges.
49+
let xmin = Infinity;
50+
let xmax = -Infinity;
51+
let ymin = Infinity;
52+
let ymax = -Infinity;
53+
for (const [x, y] of pts) {
54+
if (x < xmin) xmin = x;
55+
if (x > xmax) xmax = x;
56+
if (y < ymin) ymin = y;
57+
if (y > ymax) ymax = y;
58+
}
59+
if (pts.length === 0) {
60+
xmin = 0; xmax = 1; ymin = 0; ymax = 1;
61+
}
62+
if (xmax === xmin) { xmin -= 1; xmax += 1; }
63+
if (ymax === ymin) { ymin -= 1; ymax += 1; }
64+
const padX = (xmax - xmin) * 0.08;
65+
const padY = (ymax - ymin) * 0.08;
66+
xmin -= padX; xmax += padX;
67+
ymin -= padY; ymax += padY;
68+
69+
const sx = (x: number) => px0 + ((x - xmin) / (xmax - xmin)) * (px1 - px0);
70+
const sy = (y: number) => py0 - ((y - ymin) / (ymax - ymin)) * (py0 - py1);
71+
72+
// Faint gridlines + tick labels, evenly spaced across the padded range.
73+
const TICKS = 5;
74+
let grid = '';
75+
let ticks = '';
76+
for (let i = 0; i < TICKS; i++) {
77+
const fx = xmin + ((xmax - xmin) * i) / (TICKS - 1);
78+
const fy = ymin + ((ymax - ymin) * i) / (TICKS - 1);
79+
const X = sx(fx).toFixed(1);
80+
const Y = sy(fy).toFixed(1);
81+
grid += `<line x1="${X}" y1="${py1}" x2="${X}" y2="${py0}" stroke="${PLOT.line}" stroke-width="1"/>`;
82+
grid += `<line x1="${px0}" y1="${Y}" x2="${px1}" y2="${Y}" stroke="${PLOT.line}" stroke-width="1"/>`;
83+
ticks += `<text x="${X}" y="${py0 + 18}" fill="${PLOT.muted}" font-size="11" text-anchor="middle" font-family="${PLOT_MONO}">${fmtTick(fx)}</text>`;
84+
ticks += `<text x="${px0 - 8}" y="${Number(Y) + 4}" fill="${PLOT.muted}" font-size="11" text-anchor="end" font-family="${PLOT_MONO}">${fmtTick(fy)}</text>`;
85+
}
86+
87+
// The series: a connected teal line (with node markers) or amber dots.
88+
let series = '';
89+
if (spec.kind === 'line' && pts.length >= 2) {
90+
const d = pts.map(([x, y], i) => `${i === 0 ? 'M' : 'L'}${sx(x).toFixed(1)},${sy(y).toFixed(1)}`).join(' ');
91+
series += `<path d="${d}" fill="none" stroke="${PLOT.teal}" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>`;
92+
series += pts.map(([x, y]) => `<circle cx="${sx(x).toFixed(1)}" cy="${sy(y).toFixed(1)}" r="3.5" fill="${PLOT.teal}"/>`).join('');
93+
} else {
94+
series = pts.map(([x, y]) => `<circle cx="${sx(x).toFixed(1)}" cy="${sy(y).toFixed(1)}" r="4.5" fill="${PLOT.amber}" fill-opacity="0.9" stroke="#0b0f12" stroke-width="1"/>`).join('');
95+
}
96+
97+
const axes =
98+
`<line x1="${px0}" y1="${py0}" x2="${px1}" y2="${py0}" stroke="${PLOT.muted}" stroke-width="1.5"/>` +
99+
`<line x1="${px0}" y1="${py1}" x2="${px0}" y2="${py0}" stroke="${PLOT.muted}" stroke-width="1.5"/>`;
100+
101+
const title = spec.title
102+
? `<text x="${(px0 + px1) / 2}" y="20" fill="${PLOT.ink}" font-size="13" text-anchor="middle" font-family="${PLOT_SANS}" font-weight="600">${escapeHtml(spec.title)}</text>`
103+
: '';
104+
105+
return `<svg class="plot-svg" viewBox="0 0 ${W} ${H}" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="${escapeHtml(spec.title || spec.kind)}">${title}${grid}${axes}${ticks}${series}</svg>`;
106+
}
107+
13108
export type BoardTab = 'memory' | 'plot' | 'ram';
14109

15110
export class BoardView {
@@ -211,9 +306,10 @@ export class BoardView {
211306
'<div class="board-empty"><p>No plot yet. Call <code>scatter(x, y)</code> or <code>line(x, y)</code> to draw one.</p></div>';
212307
return;
213308
}
214-
// Phase C fills this with real SVG. For now render the spec as a list.
309+
// Draw every plot as a real SVG chart (scatter = dots, line = connected
310+
// trend). Each keeps its title so the student can see what they plotted.
215311
const items = this.plots
216-
.map((p) => `<div class="plot-card"><strong>${escapeHtml(p.title || p.kind)}</strong> · ${p.points.length} points</div>`)
312+
.map((p) => `<figure class="plot-card">${plotSvg(p)}</figure>`)
217313
.join('');
218314
this.plotBody.innerHTML = `<div class="plot-cards">${items}</div>`;
219315
}

0 commit comments

Comments
 (0)