@walterra/pi-charts 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,13 +23,13 @@ Renders a Vega-Lite specification as a PNG image.
23
23
 
24
24
  ### Parameters
25
25
 
26
- | Parameter | Type | Required | Description |
27
- |-----------|------|----------|-------------|
28
- | `spec` | string | ✅ | Vega-Lite JSON specification |
29
- | `tsv_data` | string | | Optional TSV data to replace spec.data.values |
30
- | `width` | number | | Chart width in pixels (default: 600) |
31
- | `height` | number | | Chart height in pixels (default: 400) |
32
- | `save_path` | string | | Optional file path to save the PNG |
26
+ | Parameter | Type | Required | Description |
27
+ | ----------- | ------ | -------- | --------------------------------------------- |
28
+ | `spec` | string | ✅ | Vega-Lite JSON specification |
29
+ | `tsv_data` | string | | Optional TSV data to replace spec.data.values |
30
+ | `width` | number | | Chart width in pixels (default: 600) |
31
+ | `height` | number | | Chart height in pixels (default: 400) |
32
+ | `save_path` | string | | Optional file path to save the PNG |
33
33
 
34
34
  ### Example
35
35
 
@@ -38,22 +38,22 @@ Renders a Vega-Lite specification as a PNG image.
38
38
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
39
39
  "data": {
40
40
  "values": [
41
- {"category": "A", "value": 28},
42
- {"category": "B", "value": 55},
43
- {"category": "C", "value": 43}
41
+ { "category": "A", "value": 28 },
42
+ { "category": "B", "value": 55 },
43
+ { "category": "C", "value": 43 }
44
44
  ]
45
45
  },
46
46
  "mark": "bar",
47
47
  "encoding": {
48
- "x": {"field": "category", "type": "nominal"},
49
- "y": {"field": "value", "type": "quantitative"}
48
+ "x": { "field": "category", "type": "nominal" },
49
+ "y": { "field": "value", "type": "quantitative" }
50
50
  }
51
51
  }
52
52
  ```
53
53
 
54
54
  ## Reference Documentation
55
55
 
56
- See [vega-lite-reference.md](./extensions/vega-chart/vega-lite-reference.md) for comprehensive documentation on:
56
+ See [vega-lite-reference.md](./extensions/vega-chart/vega-lite-reference.md) for documentation on:
57
57
 
58
58
  - Data types and encoding channels
59
59
  - All mark types and properties
@@ -10,13 +10,13 @@
10
10
  * - Data-driven: Inline data or separate TSV input
11
11
  */
12
12
 
13
- import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
14
- import { Type } from '@sinclair/typebox';
15
13
  import { execSync } from 'node:child_process';
16
14
  import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
17
15
  import { tmpdir } from 'node:os';
18
16
  import { dirname, join } from 'node:path';
19
17
  import { fileURLToPath } from 'node:url';
18
+ import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
19
+ import { Type } from '@sinclair/typebox';
20
20
 
21
21
  // Compute reference path using ESM import.meta.url
22
22
  const VEGA_REFERENCE_PATH = join(dirname(fileURLToPath(import.meta.url)), 'vega-lite-reference.md');
@@ -72,14 +72,14 @@ Reference: https://vega.github.io/vega-lite/docs/`,
72
72
  tsv_data: Type.Optional(
73
73
  Type.String({
74
74
  description: 'Optional TSV data - if provided, replaces spec.data.values',
75
- }),
75
+ })
76
76
  ),
77
77
  width: Type.Optional(Type.Number({ description: 'Chart width in pixels (default: 600)' })),
78
78
  height: Type.Optional(Type.Number({ description: 'Chart height in pixels (default: 400)' })),
79
79
  save_path: Type.Optional(
80
80
  Type.String({
81
81
  description: 'Optional file path to save the PNG chart (in addition to displaying it)',
82
- }),
82
+ })
83
83
  ),
84
84
  }),
85
85
 
@@ -102,6 +102,23 @@ Reference: https://vega.github.io/vega-lite/docs/`,
102
102
  return { content: [{ type: 'text', text: 'Cancelled' }], details: {} };
103
103
  }
104
104
 
105
+ const getErrorMessage = (err: unknown): string => {
106
+ if (err instanceof Error) return err.message;
107
+ if (typeof err === 'string') return err;
108
+ if (err && typeof err === 'object' && 'message' in err) {
109
+ return String((err as { message?: unknown }).message);
110
+ }
111
+ return 'Unknown error';
112
+ };
113
+
114
+ const getErrorStderr = (err: unknown): string | undefined => {
115
+ if (err && typeof err === 'object' && 'stderr' in err) {
116
+ const stderr = (err as { stderr?: unknown }).stderr;
117
+ return typeof stderr === 'string' ? stderr : undefined;
118
+ }
119
+ return undefined;
120
+ };
121
+
105
122
  try {
106
123
  // Check Python and dependencies, auto-install if needed using uv
107
124
  const ensureDependencies = (): { success: boolean; error?: string } => {
@@ -159,30 +176,44 @@ Reference: https://vega.github.io/vega-lite/docs/`,
159
176
  try {
160
177
  execSync(checkCmd, { encoding: 'utf-8', stdio: 'pipe' });
161
178
  return { success: true };
162
- } catch (err: any) {
179
+ } catch (err: unknown) {
180
+ const errorMsg = getErrorMessage(err);
163
181
  return {
164
182
  success: false,
165
- error: `Failed to setup Python environment with uv.\nPlease run manually: uv run --with altair --with pandas --with vl-convert-python python3\n\nError: ${err.message}`,
183
+ error: `Failed to setup Python environment with uv.\nPlease run manually: uv run --with altair --with pandas --with vl-convert-python python3\n\nError: ${errorMsg}`,
166
184
  };
167
185
  }
168
186
  };
169
187
 
170
188
  const deps = ensureDependencies();
171
189
  if (!deps.success) {
190
+ const errorText = deps.error ?? 'Dependencies not installed';
172
191
  return {
173
- content: [{ type: 'text', text: deps.error! }],
192
+ content: [{ type: 'text', text: errorText }],
174
193
  details: { error: 'Dependencies not installed' },
175
194
  isError: true,
176
195
  };
177
196
  }
178
197
 
179
198
  // Parse and validate the spec
180
- let vegaSpec: any;
199
+ type VegaSpec = {
200
+ $schema?: string;
201
+ width?: number;
202
+ height?: number;
203
+ data?: { values?: unknown[] };
204
+ [key: string]: unknown;
205
+ };
206
+
207
+ let vegaSpec: VegaSpec;
181
208
  try {
182
- vegaSpec = JSON.parse(spec);
209
+ const parsed = JSON.parse(spec);
210
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
211
+ throw new Error('Spec must be a JSON object');
212
+ }
213
+ vegaSpec = parsed as VegaSpec;
183
214
  } catch (e) {
184
215
  return {
185
- content: [{ type: 'text', text: `Invalid JSON in spec: ${e}` }],
216
+ content: [{ type: 'text', text: `Invalid JSON in spec: ${getErrorMessage(e)}` }],
186
217
  details: { error: 'Invalid JSON' },
187
218
  isError: true,
188
219
  };
@@ -239,7 +270,7 @@ print('OK')
239
270
  encoding: 'utf-8',
240
271
  timeout: 60000, // Longer timeout for first run when uv downloads packages
241
272
  maxBuffer: 10 * 1024 * 1024,
242
- },
273
+ }
243
274
  );
244
275
 
245
276
  if (!result.includes('OK')) {
@@ -260,9 +291,9 @@ print('OK')
260
291
  mkdirSync(dirname(save_path), { recursive: true });
261
292
  copyFileSync(tmpPng, save_path);
262
293
  savedPath = save_path;
263
- } catch (saveErr: any) {
294
+ } catch (saveErr: unknown) {
264
295
  // Don't fail the whole operation, just note the error
265
- console.error(`Failed to save to ${save_path}: ${saveErr.message}`);
296
+ console.error(`Failed to save to ${save_path}: ${getErrorMessage(saveErr)}`);
266
297
  }
267
298
  }
268
299
 
@@ -279,7 +310,9 @@ print('OK')
279
310
 
280
311
  const dataPoints = tsv_data
281
312
  ? tsv_data.trim().split('\n').length - 1
282
- : vegaSpec.data?.values?.length || 0;
313
+ : Array.isArray(vegaSpec.data?.values)
314
+ ? vegaSpec.data?.values.length
315
+ : 0;
283
316
 
284
317
  const textMsg = savedPath
285
318
  ? `Rendered Vega-Lite chart (${dataPoints} data points) - saved to ${savedPath}`
@@ -292,9 +325,9 @@ print('OK')
292
325
  ],
293
326
  details: { dataPoints, width: vegaSpec.width, height: vegaSpec.height, savedPath },
294
327
  };
295
- } catch (error: any) {
328
+ } catch (error: unknown) {
296
329
  // Try to extract Python error details
297
- const errorMsg = error.stderr || error.message;
330
+ const errorMsg = getErrorStderr(error) ?? getErrorMessage(error);
298
331
  return {
299
332
  content: [{ type: 'text', text: `Error rendering chart: ${errorMsg}` }],
300
333
  details: { error: errorMsg },
@@ -1,6 +1,6 @@
1
1
  # Vega-Lite Visualization Reference
2
2
 
3
- A comprehensive reference for creating data visualizations using Vega-Lite specifications. This guide follows best practices from the [UW Interactive Data Lab Visualization Curriculum](https://idl.uw.edu/visualization-curriculum/intro.html) and established visualization research.
3
+ Reference for creating data visualizations using Vega-Lite specifications. This guide follows best practices from the [UW Interactive Data Lab Visualization Curriculum](https://idl.uw.edu/visualization-curriculum/intro.html) and established visualization research.
4
4
 
5
5
  ## Prerequisites
6
6
 
@@ -23,7 +23,7 @@ If dependencies cannot be installed, the tool returns an error with instructions
23
23
 
24
24
  ## Philosophy
25
25
 
26
- > "A visualization is a mapping from data to visual properties. The key insight is that this mapping should be **declarative** rather than imperative."
26
+ At its core, a visualization maps data to visual properties. Instead of specifying step-by-step drawing instructions (imperative), you describe what the visualization should represent (declarative).
27
27
 
28
28
  Vega-Lite embodies a **grammar of graphics**: you describe _what_ you want to visualize, not _how_ to draw it. This enables:
29
29
 
@@ -648,7 +648,12 @@ Control how scales/axes/legends are shared or independent.
648
648
  },
649
649
  "mark": { "type": "bar", "cornerRadius": 3 },
650
650
  "encoding": {
651
- "x": { "field": "month", "type": "ordinal", "title": null, "axis": { "labelAngle": 0 } },
651
+ "x": {
652
+ "field": "month",
653
+ "type": "ordinal",
654
+ "title": null,
655
+ "axis": { "labelAngle": 0 }
656
+ },
652
657
  "y": {
653
658
  "field": "revenue",
654
659
  "type": "quantitative",
@@ -664,7 +669,11 @@ Control how scales/axes/legends are shared or independent.
664
669
  ```json
665
670
  {
666
671
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
667
- "title": { "text": "Daily Temperature", "subtitle": "December 2025", "anchor": "start" },
672
+ "title": {
673
+ "text": "Daily Temperature",
674
+ "subtitle": "December 2025",
675
+ "anchor": "start"
676
+ },
668
677
  "width": 600,
669
678
  "height": 200,
670
679
  "data": {
@@ -679,14 +688,26 @@ Control how scales/axes/legends are shared or independent.
679
688
  "layer": [
680
689
  { "mark": { "type": "area", "opacity": 0.2, "color": "#e45756" } },
681
690
  { "mark": { "type": "line", "color": "#e45756", "strokeWidth": 2 } },
682
- { "mark": { "type": "point", "color": "#e45756", "filled": true, "size": 50 } },
691
+ {
692
+ "mark": {
693
+ "type": "point",
694
+ "color": "#e45756",
695
+ "filled": true,
696
+ "size": 50
697
+ }
698
+ },
683
699
  {
684
700
  "mark": { "type": "rule", "strokeDash": [4, 4], "color": "#999" },
685
701
  "encoding": { "y": { "datum": 20 } }
686
702
  }
687
703
  ],
688
704
  "encoding": {
689
- "x": { "field": "date", "type": "temporal", "title": null, "axis": { "format": "%b %d" } },
705
+ "x": {
706
+ "field": "date",
707
+ "type": "temporal",
708
+ "title": null,
709
+ "axis": { "format": "%b %d" }
710
+ },
690
711
  "y": {
691
712
  "field": "temp",
692
713
  "type": "quantitative",
@@ -718,11 +739,21 @@ Control how scales/axes/legends are shared or independent.
718
739
  },
719
740
  "layer": [
720
741
  {
721
- "mark": { "type": "line", "strokeWidth": 2.5, "point": { "filled": true, "size": 50 } }
742
+ "mark": {
743
+ "type": "line",
744
+ "strokeWidth": 2.5,
745
+ "point": { "filled": true, "size": 50 }
746
+ }
722
747
  },
723
748
  {
724
749
  "transform": [{ "filter": "datum.date == '2025-03'" }],
725
- "mark": { "type": "text", "align": "left", "dx": 8, "fontSize": 12, "fontWeight": "bold" },
750
+ "mark": {
751
+ "type": "text",
752
+ "align": "left",
753
+ "dx": 8,
754
+ "fontSize": 12,
755
+ "fontWeight": "bold"
756
+ },
726
757
  "encoding": { "text": { "field": "team" } }
727
758
  }
728
759
  ],
@@ -824,7 +855,12 @@ Control how scales/axes/legends are shared or independent.
824
855
  "mark": "bar",
825
856
  "encoding": {
826
857
  "x": { "field": "month", "type": "ordinal", "title": null },
827
- "y": { "field": "sales", "type": "quantitative", "stack": "zero", "title": "Sales" },
858
+ "y": {
859
+ "field": "sales",
860
+ "type": "quantitative",
861
+ "stack": "zero",
862
+ "title": "Sales"
863
+ },
828
864
  "color": { "field": "category", "type": "nominal", "title": null }
829
865
  }
830
866
  }
@@ -961,14 +997,22 @@ Use sorted bar chart instead - see "Never Use Pie or Donut Charts" in Best Pract
961
997
  "mark": "bar",
962
998
  "encoding": {
963
999
  "x": { "field": "date", "type": "temporal" },
964
- "y": { "field": "precipitation", "type": "quantitative", "title": "Precipitation (mm)" }
1000
+ "y": {
1001
+ "field": "precipitation",
1002
+ "type": "quantitative",
1003
+ "title": "Precipitation (mm)"
1004
+ }
965
1005
  }
966
1006
  },
967
1007
  {
968
1008
  "mark": { "type": "line", "color": "firebrick" },
969
1009
  "encoding": {
970
1010
  "x": { "field": "date", "type": "temporal" },
971
- "y": { "field": "temperature", "type": "quantitative", "title": "Temperature (°C)" }
1011
+ "y": {
1012
+ "field": "temperature",
1013
+ "type": "quantitative",
1014
+ "title": "Temperature (°C)"
1015
+ }
972
1016
  }
973
1017
  }
974
1018
  ],
@@ -1108,7 +1152,12 @@ Legends force eye movement. Label directly on the chart:
1108
1152
  "encoding": { "color": { "field": "series", "legend": null } }
1109
1153
  },
1110
1154
  {
1111
- "mark": { "type": "text", "align": "left", "dx": 8, "fontWeight": "bold" },
1155
+ "mark": {
1156
+ "type": "text",
1157
+ "align": "left",
1158
+ "dx": 8,
1159
+ "fontWeight": "bold"
1160
+ },
1112
1161
  "transform": [{ "filter": "datum.date == '2025-12-29'" }],
1113
1162
  "encoding": {
1114
1163
  "x": { "field": "date", "type": "temporal" },
@@ -1588,10 +1637,3 @@ Before finalizing any chart, verify:
1588
1637
  - [Vega-Lite Examples](https://vega.github.io/vega-lite/examples/)
1589
1638
  - [UW Visualization Curriculum](https://idl.uw.edu/visualization-curriculum/intro.html)
1590
1639
  - [Altair Documentation](https://altair-viz.github.io/)
1591
-
1592
- ### Visual Design References
1593
-
1594
- - _Sémiologie Graphique_ (1967) — semiotics of graphics, visual variables
1595
- - _The Visual Display of Quantitative Information_ (1983) — data-ink ratio, minimalist approach
1596
- - _The Functional Art_, _How Charts Lie_ — clarity over minimalism, communicative approach
1597
- - _Visualization Analysis and Design_ (2014) — systematic approach to visualization design
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walterra/pi-charts",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Vega-Lite chart extension for pi coding agent - render data visualizations as inline images",
5
5
  "type": "module",
6
6
  "files": [
@@ -10,7 +10,8 @@
10
10
  "pi": {
11
11
  "extensions": [
12
12
  "./extensions/vega-chart/index.ts"
13
- ]
13
+ ],
14
+ "video": "https://www.rafelsberger.at/assets/pi-charts-sm-0003.mp4"
14
15
  },
15
16
  "repository": {
16
17
  "type": "git",
@@ -18,6 +19,7 @@
18
19
  "directory": "packages/pi-charts"
19
20
  },
20
21
  "keywords": [
22
+ "pi-package",
21
23
  "pi",
22
24
  "pi-coding-agent",
23
25
  "extension",