Skip to content

Commit 2ffe2a8

Browse files
feat: extend the Plot board to the full data-visualization set
Add bar, countplot, histogram, pie, area, boxplot, heatmap, and regression alongside scatter/line: a PlotKind union and per-kind payload fields on PlotSpec, builtin call handling in the evaluator, and SVG renderers for each chart on the Plot tab. New levels and a deep lesson map each data question to the chart that answers it, with EN/DE/FA copy.
1 parent d222b36 commit 2ffe2a8

8 files changed

Lines changed: 964 additions & 17 deletions

File tree

‎src/engine/checks.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Goal checking for levels (analogue of LGB TreeCompare).
33
*/
44

5-
import type { GoalResult, GoalSpec, MemoryState, Snapshot, SnapshotHeapObject } from './types';
5+
import type { GoalResult, GoalSpec, MemoryState, PlotKind, Snapshot, SnapshotHeapObject } from './types';
66

77
/**
88
* Deep structural equality of two snapshot fragments.
@@ -95,7 +95,7 @@ export function checkGoal(goal: GoalSpec | null, snapshot: Snapshot, runtime: Me
9595

9696
if (goal.type === 'plot') {
9797
if (runtime.plots.length === 0) {
98-
return { ok: false, message: 'No plot yet — call scatter() or line().' };
98+
return { ok: false, message: 'No plot yet — draw one (scatter, line, bar, pie, hist, …).' };
9999
}
100100
if (goal.kind && !runtime.plots.some((p) => p.kind === goal.kind)) {
101101
return { ok: false, message: `Expected a ${goal.kind} plot.` };
@@ -320,9 +320,9 @@ export const preds = {
320320
/**
321321
* A plot has been drawn (optionally of a specific kind).
322322
*/
323-
hasPlot(kind?: 'scatter' | 'line') {
323+
hasPlot(kind?: PlotKind) {
324324
return (_snapshot: Snapshot, runtime: MemoryState): boolean | string => {
325-
if (runtime.plots.length === 0) return 'Draw a plot with scatter() or line().'
325+
if (runtime.plots.length === 0) return 'Draw a plot (scatter, line, bar, pie, hist, …).'
326326
if (kind && !runtime.plots.some((p) => p.kind === kind)) return `Draw a ${kind} plot.`
327327
return true;
328328
};

‎src/engine/types.ts‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,42 @@ export interface Frame {
5353
locals: Record<string, RtValue>;
5454
}
5555

56-
/** A simple static plot spec (rendered on the Plot board tab). */
56+
/** The chart kinds the Plot board tab can render. */
57+
export type PlotKind =
58+
| 'scatter'
59+
| 'line'
60+
| 'bar'
61+
| 'hist'
62+
| 'pie'
63+
| 'area'
64+
| 'boxplot'
65+
| 'countplot'
66+
| 'heatmap'
67+
| 'reg';
68+
69+
/**
70+
* A static plot spec (rendered on the Plot board tab). Most payload fields are
71+
* optional and selected by `kind`: `points` for the 2-D x/y charts, `labels` +
72+
* `values` for categorical charts, `values` alone (raw) for hist/boxplot (the
73+
* renderer bins / computes quartiles), `cols` + `matrix` for a heatmap, and
74+
* `slope`/`intercept` for a regression fit.
75+
*/
5776
export interface PlotSpec {
58-
kind: 'scatter' | 'line';
77+
kind: PlotKind;
5978
title: string;
60-
points: Array<[number, number]>;
79+
/** (x, y) pairs — scatter, line, area, reg. */
80+
points?: Array<[number, number]>;
81+
/** Category labels — bar, pie, countplot, heatmap rows. */
82+
labels?: string[];
83+
/** Values — bar, pie, and the raw data for hist / boxplot. */
84+
values?: number[];
85+
/** Heatmap column labels. */
86+
cols?: string[];
87+
/** Heatmap cell matrix (row-major, indexed by labels × cols). */
88+
matrix?: number[][];
89+
/** Least-squares fit — reg. */
90+
slope?: number;
91+
intercept?: number;
6192
}
6293

6394
export interface MemoryState {

‎src/i18n/de-g3.ts‎

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,86 @@ export const deG3: LevelGroup = {
77
'functional': 'Standardwerte & funktionale Werkzeuge',
88
'plotting': 'Diagramme & Capstone',
99
},
10-
deepLessons: {},
10+
deepLessons: {
11+
plotting: `## Beginner
12+
13+
Ein Diagramm ist eine **Entscheidung**, keine Deko. Deine Daten stellen schweigend eine Frage, und der Diagramm-*Typ* ist die Antwort. Bevor du ein Diagramm wählst, benenne die Frage. Die fünf Fragen, die du fast immer beantworten willst:
14+
15+
- **Zusammenhang** — „Wie hängen diese beiden Zahlen zusammen?" → scatter, line, area
16+
- **Verteilung** — „Wie sind die Werte verteilt?" → histogram, boxplot
17+
- **Zusammensetzung** — „Welchen Anteil hat jeder Teil am Ganzen?" → pie, area
18+
- **Vergleich** — „Welche Kategorie ist größer?" → bar
19+
- **Häufigkeit** — „Wie oft tritt etwas auf?" → countplot
20+
- **Korrelation** — „Wie stark hängen viele Spalten zusammen?" → heatmap
21+
22+
Wähle das Diagramm, das zur Frage passt — dann erklärt es sich selbst.
23+
24+
## Intermediate
25+
26+
Die Entscheidungslandkarte. Jeder Pfeil heißt „diese Frage wird am besten von diesem Diagramm beantwortet":
27+
28+
- Zusammenhang ← **scatter / line / area / reg**
29+
- Verteilung ← **hist / boxplot**
30+
- Zusammensetzung ← **pie / area**
31+
- Vergleich ← **bar**
32+
- Häufigkeit ← **countplot**
33+
- Korrelation ← **heatmap**
34+
35+
Einige schnelle Regeln, die die meisten Entscheidungen lösen:
36+
37+
- Zwei stetige Serien → **scatter** (sind sie korreliert?) oder **line** (ist eine geordnet, z. B. Zeit?).
38+
- Eine Spalte roher Zahlen → **hist** (die Form) oder **boxplot** (Spanne + Ausreißer).
39+
- Wenige Kategorien mit Werten → **bar**; wenn die Werte ein *Anteil eines Ganzen* sind → **pie**.
40+
- Viele Kategorien, nur zählen → **countplot**.
41+
- Ein Netz an Werten (Zeilen × Spalten) → **heatmap**.
42+
43+
## Advanced
44+
45+
**So baust du jedes ein.** Jedes Diagramm ist ein einziger Builtin, der ein Plot-Spec auf \`state.plots\` legt; der Plot-Tab rendert es. Hier die Argumentform jedes, mit einem ausführbaren Beispiel:
46+
47+
| Diagramm | Aufruf | Was jedes Argument ist |
48+
|----------|--------|------------------------|
49+
| scatter | \`scatter(xs, ys)\` | zwei ausgerichtete Zahlenlisten |
50+
| line | \`line(xs, ys)\` | zwei ausgerichtete Zahlenlisten, in Reihenfolge |
51+
| area | \`area(xs, ys)\` | zwei ausgerichtete Zahlenlisten, unter der Linie gefüllt |
52+
| reg | \`reg(xs, ys)\` | zwei ausgerichtete Zahlenlisten; passt eine Linie |
53+
| bar | \`bar(labels, values)\` | eine Label-Liste, eine Werte-Liste |
54+
| countplot | \`countplot(labels)\` | eine Liste roher Labels (darf sich wiederholen) |
55+
| hist | \`hist(values)\` | eine Liste roher Zahlen (automatisch gebinnt) |
56+
| pie | \`pie(labels, values)\` | eine Label-Liste, eine Werte-Liste |
57+
| boxplot | \`boxplot(values)\` | eine Liste roher Zahlen (Quartile berechnet) |
58+
| heatmap | \`heatmap(rows, cols, matrix)\` | Zeilen-Labels, Spalten-Labels, eine Liste von Listen |
59+
60+
Zwei Formen merken: **ausgerichtete Zahlenlisten** (\`scatter(xs, ys)\`) für die 2D-Diagramme, und **Labels + Werte** (\`bar(labels, values)\`) für die Kategorie-Diagramme. Die Ausnahme ist \`heatmap\`, das eine *Matrix* nimmt — eine Liste von Listen, eine innere Liste pro Zeile.
61+
62+
\`\`\`python
63+
xs = [1, 2, 3]
64+
ys = [1, 4, 9]
65+
scatter(xs, ys) # Zusammenhang
66+
reg(xs, ys) # dieselben Daten, passte Linie
67+
\`\`\`
68+
69+
## Expert
70+
71+
Die Datenform entscheidet, nicht dein Geschmack. Zwei Fallen, die in der Praxis auffallen:
72+
73+
- **bar braucht Labels und Werte; countplot nur rohe Labels.** Hast du sich wiederholende Kategorien, nutze \`countplot\` — es zählt selbst. Hast du schon je Kategorie einen Wert, nutze \`bar\`.
74+
- **hist und boxplot nehmen rohe Werte, nicht vorgebinnte.** \`hist(data)\` und \`boxplot(data)\` nehmen eine flache Zahlenliste und binnen / berechnen Quartile selbst. Nicht vorab berechnen.
75+
- **pie ist ein Anteil, bar ist eine Größe.** Summieren sich die Werte zu einem sinnvollen Ganzen, ist ein pie ehrlich; werden die Kategorien nur verglichen, ist es ein bar.
76+
- **reg liefert den Fit.** \`reg(xs, ys)\` berechnet die kleinste-Quadrate-Steigung und den y-Abschnitt im Engine und zeichnet die Linie durch die Punkte — Diagramm und Mathematik stimmen überein.
77+
78+
## Master
79+
80+
Produktionsnamen, damit du den echten Code liest. Jedes Diagramm hier hat einen matplotlib / seaborn-Ableger:
81+
82+
- scatter → \`plt.scatter\` · line → \`plt.plot\` · area → \`plt.fill_between\`
83+
- bar → \`plt.bar\` / \`sns.barplot\` · countplot → \`sns.countplot\`
84+
- hist → \`plt.hist\` / \`sns.histplot\` · boxplot → \`plt.boxplot\` / \`sns.boxplot\`
85+
- pie → \`plt.pie\` · heatmap → \`plt.imshow\` / \`sns.heatmap\`
86+
- reg → \`sns.regplot\` / \`sns.lmplot\`
87+
88+
Wo es kaputtgeht: eine line setzt Reihenfolge voraus und lügt bei ungeordneten Kategorien. Ein pie mit mehr als ~6 kleinen Scheiben ist unlesbar. Eine heatmap braucht eine feste, geordnete Achse (meist normalisierte Werte), sonst codieren die Farben nichts. Die Memory-Model-Auszahlung bleibt: jedes Diagramm liest *dieselben* Listen, die du auf dem Canvas hältst — das Diagramm ist nur diese Objekte, gezeichnet.`,
89+
},
1190
levels: {
1291
'shallow-copy-dict': {
1392
name: 'Shallow Copy',
@@ -125,6 +204,78 @@ Beobachte das Brett: ein kleiner Punkt erscheint am Badge des Plot-Tabs, und ein
125204
lesson: `Hier wird die Serie **berechnet**, nicht eingegeben. \`sq\` erzeugt jedes \`y\`, die \`ys\`-list wird aus diesen Ergebnissen gebaut, und \`line(xs, ys)\` verbindet die Punkte in der Reihenfolge.
126205
127206
Das ist die Form echten Plottings: Einige Daten transformieren, in abgestimmte Serien sammeln, dann zeichnen. Die Funktionsaufruf-Rahmen, die du in S5 gesehen hast, verrichten Arbeit, die das Diagramm füttert.`,
207+
},
208+
'plot-bar': {
209+
name: 'Balkendiagramm',
210+
brief: 'Baue `labels = ["A", "B", "C"]` und `vals = [3, 5, 2]`, dann `bar(labels, vals)`. Ein Balkendiagramm vergleicht einen Wert pro Kategorie mit den anderen.',
211+
learning: ['`bar(labels, values)` vergleicht einen Wert pro Kategorie mit den anderen', 'Ein Balkendiagramm braucht zwei gleich lange lists: Labels und Werte'],
212+
fieldNotes: ['matplotlib: `plt.bar(labels, vals)` / seaborn `sns.barplot`', 'Nutze bar für eine *Größe*; pie nur, wenn die Teile ein Ganzes ergeben'],
213+
lesson: `Ein **Balkendiagramm** beantwortet „welche Kategorie ist größer?". Du gibst zwei gleich lange lists: \`labels\` (die Kategorien) und \`vals\` (je ein Wert). \`bar\` paart sie und zeichnet pro Kategorie einen senkrechten Balken, dessen Höhe der Wert setzt.
214+
215+
Das Memory-Modell ist wie scatter — zwei lists auf dem heap — aber das Diagramm liest sie *kategorisch positionell*, nicht als (x, y)-Koordinaten. Der Plot-Tab zeigt das Label unter jedem Balken und den Wert darüber.`,
216+
},
217+
'plot-countplot': {
218+
name: 'Countplot',
219+
brief: 'Baue `cats = ["red", "blue", "red"]`, dann `countplot(cats)`. Ein countplot zählt, wie oft jedes Label auftritt — keine separate Werte-list nötig.',
220+
learning: ['`countplot(labels)` zählt eine rohe list sich wiederholender Labels', 'Zähl die Daten, tippe die Zählung nicht — das Diagramm macht es'],
221+
fieldNotes: ['seaborn: `sns.countplot(labels)`', 'countplot ist die „Häufigkeit"-Frage; bar ist die „Größen vergleichen"-Frage'],
222+
lesson: `Ein **countplot** beantwortet „wie oft tritt etwas auf?". Im Gegensatz zu \`bar\` nimmt er eine einzige list roher Labels, die **sich wiederholen** dürfen, und zählt sie selbst: \`red\` kommt zweimal vor, sein Balken ist 2.
223+
224+
Das ist die Häufigkeitsfrage. Du schreibst die Zählung nie — das Diagramm zählt die list. Schau den Plot-Tab: pro eindeutigem Wert ein Balken, die Höhe die Zählung.`,
225+
},
226+
'plot-hist': {
227+
name: 'Histogramm',
228+
brief: 'Baue eine list roher Zahlen, dann `hist(values)`. Ein Histogramm splittet die Zahlen in Klassen (Bins) und zeigt, wie viele in jede fallen — die Form einer Verteilung.',
229+
learning: ['`hist(values)` binnt eine rohe list von Zahlen automatisch', 'Ein Histogramm zeigt die *Form* einer Verteilung, nicht Einzelwerte'],
230+
fieldNotes: ['matplotlib `plt.hist` / seaborn `sns.histplot`', 'Die Binanzahl ändert das Bild — zu wenige verstecken Detail, zu viele werden Rauschen'],
231+
lesson: `Ein **Histogramm** beantwortet „wie sind die Werte verteilt?". Du gibst eine flache list roher Zahlen; es splittet den Bereich in Klassen und zeichnet pro Klasse einen Balken für die *Anzahl* der fallenden Werte. Diese Silhouette ist die **Form der Verteilung** — ein Gipfel, wo Werte haufen, ein Schwanz, wo sie dünner werden.
232+
233+
Du gibst rohe Werte, keine vorab gemachten Zählungen. Das Engine binnt sie, damit Diagramm und Daten immer stimmen.`,
234+
},
235+
'plot-pie': {
236+
name: 'Kuchendiagramm',
237+
brief: 'Baue `parts = ["x", "y"]` und `vals = [70, 30]`, dann `pie(parts, vals)`. Ein Kuchendiagramm zeigt den Anteil jedes Teils am Ganzen.',
238+
learning: ['`pie(labels, values)` zeigt den Anteil jedes Teils am Ganzen', 'Ein pie ist ein Anteilsdiagramm — nur für Teile-eines-Ganzen halten'],
239+
fieldNotes: ['matplotlib: `plt.pie(vals, labels=parts)`', 'Ein pie mit vielen kleinen Scheiben ist ein bar im Verkleidung — wechsle zu bar'],
240+
lesson: `Ein **Kuchendiagramm** beantwortet „welchen Anteil hat jeder Teil am Ganzen?". Es nutzt die gleiche Zwei-list-Form wie \`bar\` (Labels + Werte), zeichnet die Werte aber als **Scheiben eines Kreises**, damit das Auge Anteile vom Ganzen liest statt Größen.
241+
242+
Nutze einen pie, wenn die Werte zu etwas Sinnvollem summieren — Teile von 100 %, Stücke eines Budgets. Werden die Kategorien nur verglichen, ist bar das ehrlichere Diagramm.`,
243+
},
244+
'plot-area': {
245+
name: 'Flächendiagramm',
246+
brief: 'Baue `xs` und `ys`, dann `area(xs, ys)`. Ein Flächendiagramm ist eine Linie mit der darunter gefüllten Fläche — die kumulative Größe auf einer geordneten Achse.',
247+
learning: ['`area(xs, ys)` füllt den Bereich unter einer Linie', 'Ein Flächendiagramm betont die sich aufsummierende *Größe* auf einer geordneten Achse'],
248+
fieldNotes: ['matplotlib: `plt.fill_between(xs, ys)`', 'Gestapelte Flächendiagramme zeigen mehrere Serien, die zu einem Ganzen summieren'],
249+
lesson: `Ein **Flächendiagramm** ist eine Linie mit dem Bereich **darunter gefüllt**. Es hat die gleichen zwei abgestimmten Zahlenlisten wie \`line\`, aber das Füllen macht die *Größe* jedes Punktes auf einen Blick lesbar — gut, wenn die y-Werte kumulativ sind oder du betonen willst, wie viel Fläche sich angesammelt hat.
250+
251+
Die x-Achse muss geordnet sein (Zeit, ein Index); das Füllen ist nur auf einer geordneten Achse sinnvoll.`,
252+
},
253+
'plot-boxplot': {
254+
name: 'Boxplot',
255+
brief: 'Baue eine list von Zahlen, dann `boxplot(values)`. Ein Boxplot komprimiert eine ganze Verteilung in eine Box (Quartile) mit Whiskers und Ausreißer-Punkten.',
256+
learning: ['`boxplot(values)` fasst eine Verteilung in Quartile + Ausreißer zusammen', 'Die Box ist die mittlere Hälfte, die Linie der Median, die Punkte die Ausreißer'],
257+
fieldNotes: ['matplotlib `plt.boxplot` / seaborn `sns.boxplot`', 'Der Klassiker: mehrere Boxplots nebeneinander, um Verteilungen zu vergleichen'],
258+
lesson: `Ein **Boxplot** beantwortet „wie ist die Daten gestreut und wo sind die Ausreißer?" aus einer list roher Zahlen. Er berechnet die Quartile und zeichnet: eine **Box** vom 25. bis 75. Perzentil, eine Linie am **Median**, **Whiskers** bis zu den extremsten Werten im Bereich, und einen Punkt für jeden **Ausreißer** jenseits von 1,5× IQR.
259+
260+
Diese eine Box codiert die mittlere Hälfte der Daten, ihr Zentrum und die verdächtigen Werte an den Rändern — ein kompakter Ersatz für ein Histogramm, wenn Streuung und Extrema zählen.`,
261+
},
262+
'plot-heatmap': {
263+
name: 'Heatmap',
264+
brief: 'Baue `rows`, `cols` und eine `matrix` (list von lists), dann `heatmap(rows, cols, matrix)`. Eine heatmap färbt ein Netz an Werten, damit hohe/niedrige Werte auffallen.',
265+
learning: ['`heatmap(rows, cols, matrix)` färbt ein Netz nach Wert', 'Die matrix ist eine list von lists — eine innere list pro Zeile'],
266+
fieldNotes: ['matplotlib `plt.imshow` / seaborn `sns.heatmap`', 'Korrelations-Heatmaps nutzen dieselben Merkmale für Zeilen und Spalten'],
267+
lesson: `Eine **heatmap** beantwortet „wo in diesem Netz sind die Werte hoch oder niedrig?". Sie ist das einzige Diagramm hier, das eine **Matrix** nimmt — eine *list von lists*, eine innere list pro Zeile — plus die Zeilen- und Spaltenlabels. Jede Zelle wird nach ihrem Wert eingefärbt, damit Gipfel und Täler sofort lesbar sind.
268+
269+
So werden Korrelationstabellen gezeichnet: Zeilen und Spalten sind dasselbe Merkmal-Set, und jede Zelle ist die Korrelation eines Paares. Starke Farbe = starke Beziehung.`,
270+
},
271+
'plot-regression': {
272+
name: 'Regressionslinie',
273+
brief: 'Baue zwei abgestimmte Serien `xs` und `ys`, dann `reg(xs, ys)`. Eine Regression passt die kleinste-Quadrate-Linie durch das scatter — der Trend, dem die Punkte folgen.',
274+
learning: ['`reg(xs, ys)` passt die kleinste-Quadrate-Linie an ein scatter', 'Die Steigung ist die Änderungsrate des Trends; der y-Abschnitt sein Startwert'],
275+
fieldNotes: ['seaborn: `sns.regplot` / `sns.lmplot`', 'Ein Linienfit geht von einer linearen Beziehung aus — prüfe zuerst das scatter'],
276+
lesson: `Eine **Regression** beantwortet „gibt es einen geradlinigen Trend, und was ist er?". \`reg(xs, ys)\` berechnet die **kleinste-Quadrate**-Steigung und den y-Abschnitt im Engine und zeichnet die angepasste Linie durch deine scatter-Punkte — die Linie, die die Gesamtkarst zum Quadrat der Distanz zu den Punkten minimiert.
277+
278+
Das ist die Zusammenhangsfrage einen Schritt über \`scatter\`: Du siehst nicht nur, dass die Punkte einen Trend haben, du bekommst die *Gleichung* des Trends. Die Steigung ist die Änderungsrate, der y-Abschnitt der Punkt, wo die Linie die Achse kreuzt.`,
128279
},
129280
'capstone-shared-mutation': {
130281
name: 'Capstone: Alias + Mutation + Plot',

0 commit comments

Comments
 (0)