plotters-skill 0.1.0
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 +212 -0
- package/SKILL.md +535 -0
- package/index.js +153 -0
- package/node/index.d.ts +372 -0
- package/node/index.js +586 -0
- package/node/plotters-skill-addon.darwin-arm64.node +0 -0
- package/node/plotters-skill-addon.darwin-x64.node +0 -0
- package/node/plotters-skill-addon.linux-arm64-gnu.node +0 -0
- package/node/plotters-skill-addon.linux-x64-gnu.node +0 -0
- package/node/plotters-skill-addon.win32-x64-msvc.node +0 -0
- package/package.json +39 -0
- package/wasm/plotters_skill_wasm.d.ts +422 -0
- package/wasm/plotters_skill_wasm.js +1483 -0
- package/wasm/plotters_skill_wasm_bg.wasm +0 -0
package/SKILL.md
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plotters-skill
|
|
3
|
+
description: 'Generate charts and plots with the plotters-skill API (matplotlib pyplot-like). Use when: creating histograms, pie charts, line charts, scatter plots, bar charts, area charts, box plots, candlestick charts, subplots, or animated GIFs from data in TypeScript or JavaScript.'
|
|
4
|
+
argument-hint: 'Describe the chart you want, including data and style preferences'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# PlottersSkill — Copilot Skill
|
|
8
|
+
|
|
9
|
+
PlottersSkill is a high-performance plotting library with a **matplotlib pyplot-like API**.
|
|
10
|
+
It works in both Node.js (native addon) and the browser (WASM/SVG).
|
|
11
|
+
|
|
12
|
+
## API Reference
|
|
13
|
+
|
|
14
|
+
### Top-level functions
|
|
15
|
+
|
|
16
|
+
| Function | pyplot equivalent | Description |
|
|
17
|
+
|----------|-------------------|-------------|
|
|
18
|
+
| `figure()` | `plt.figure()` | Create a single figure |
|
|
19
|
+
| `axes()` | `fig, ax = plt.subplots()` | Create flexible Cartesian axes (mix line, scatter, bar, step, area) |
|
|
20
|
+
| `subplots(nrows, ncols)` | `plt.subplots(nrows, ncols)` | Create a uniform NxM grid of figures |
|
|
21
|
+
| `gridFigure()` | `plt.subplot_mosaic()` | Create a flexible grid where each row can have different column counts |
|
|
22
|
+
|
|
23
|
+
### Creating a figure
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { figure } from 'plotters-skill';
|
|
27
|
+
|
|
28
|
+
const fig = figure();
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Axes — flexible mixed-series chart
|
|
32
|
+
|
|
33
|
+
`axes()` creates a single Cartesian chart on which you can freely mix `plot`,
|
|
34
|
+
`scatter`, `bar`, `step`, and `fillBetween` — like matplotlib's `Axes` object.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { axes } from 'plotters-skill';
|
|
38
|
+
|
|
39
|
+
const ax = axes();
|
|
40
|
+
ax.figsize(900, 540);
|
|
41
|
+
ax.title('Mixed Series');
|
|
42
|
+
ax.xlabel('x');
|
|
43
|
+
ax.ylabel('y');
|
|
44
|
+
|
|
45
|
+
// Line series
|
|
46
|
+
ax.plot([0, 1, 2, 3, 4], [10, 25, 18, 30, 22], {
|
|
47
|
+
color: 'steelblue', lineWidth: 2, label: 'Trend',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Scatter series
|
|
51
|
+
ax.scatter([0, 1, 2, 3, 4], [12, 20, 22, 28, 25], {
|
|
52
|
+
color: 'coral', markerSize: 6, label: 'Actual',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Vertical bars
|
|
56
|
+
ax.bar([0, 1, 2, 3, 4], [8, 18, 14, 24, 20], {
|
|
57
|
+
color: 'green', barWidth: 0.6, label: 'Sales',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Step function
|
|
61
|
+
ax.step([0, 1, 2, 3, 4], [9, 22, 16, 28, 21], {
|
|
62
|
+
color: 'orange', lineWidth: 2, label: 'Threshold',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Filled area
|
|
66
|
+
ax.fillBetween([0, 1, 2, 3, 4], [15, 30, 22, 35, 28], {
|
|
67
|
+
y2: 5, alpha: 0.2, color: 'cyan', label: 'Range',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
ax.savefig('mixed.png');
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### Axes methods
|
|
74
|
+
|
|
75
|
+
| Method | pyplot equivalent | Description |
|
|
76
|
+
|--------|-------------------|-------------|
|
|
77
|
+
| `ax.plot(x, y, opts?)` | `ax.plot()` | Line series |
|
|
78
|
+
| `ax.scatter(x, y, opts?)` | `ax.scatter()` | Scatter / point markers |
|
|
79
|
+
| `ax.bar(x, heights, opts?)` | `ax.bar()` | Vertical bar chart |
|
|
80
|
+
| `ax.step(x, y, opts?)` | `ax.step()` | Step function |
|
|
81
|
+
| `ax.fillBetween(x, y1, opts?)` | `ax.fill_between()` | Filled area |
|
|
82
|
+
| `ax.xlim(min, max)` | `ax.set_xlim()` | Fix x-axis range |
|
|
83
|
+
| `ax.ylim(min, max)` | `ax.set_ylim()` | Fix y-axis range |
|
|
84
|
+
| `ax.margin(px)` | `ChartBuilder.margin()` | Chart margin in pixels |
|
|
85
|
+
| `ax.xLabelAreaSize(px)` | — | X-axis label area size |
|
|
86
|
+
| `ax.yLabelAreaSize(px)` | — | Y-axis label area size |
|
|
87
|
+
| `ax.grid(show)` | `ax.grid()` | Show/hide mesh grid |
|
|
88
|
+
| `ax.legend(show)` | `ax.legend()` | Show/hide legend |
|
|
89
|
+
| `ax.clear()` | `ax.cla()` | Clear all series |
|
|
90
|
+
|
|
91
|
+
#### ChartBuilder configuration
|
|
92
|
+
|
|
93
|
+
These methods directly expose plotters' `ChartBuilder` API for maximum
|
|
94
|
+
flexibility over chart layout.
|
|
95
|
+
|
|
96
|
+
| Method | plotters equivalent | Description |
|
|
97
|
+
|--------|---------------------|-------------|
|
|
98
|
+
| `ax.buildCartesian2d(xMin, xMax, yMin, yMax)` | `ChartBuilder.build_cartesian_2d()` | Set axis ranges (alternative to xlim+ylim) |
|
|
99
|
+
| `ax.marginTop(px)` | `ChartBuilder.margin_top()` | Top margin |
|
|
100
|
+
| `ax.marginBottom(px)` | `ChartBuilder.margin_bottom()` | Bottom margin |
|
|
101
|
+
| `ax.marginLeft(px)` | `ChartBuilder.margin_left()` | Left margin |
|
|
102
|
+
| `ax.marginRight(px)` | `ChartBuilder.margin_right()` | Right margin |
|
|
103
|
+
| `ax.topXLabelAreaSize(px)` | `ChartBuilder.top_x_label_area_size()` | Top X-axis label area size |
|
|
104
|
+
| `ax.rightYLabelAreaSize(px)` | `ChartBuilder.right_y_label_area_size()` | Right Y-axis label area size |
|
|
105
|
+
| `ax.captionFontSize(size)` | `ChartBuilder.caption(..., font_size)` | Caption/title font size |
|
|
106
|
+
| `ax.captionFontFamily(family)` | `ChartBuilder.caption(..., family)` | Caption/title font family |
|
|
107
|
+
|
|
108
|
+
#### `configureMesh(options)` — grid & axis appearance
|
|
109
|
+
|
|
110
|
+
Maps directly to plotters' `chart.configure_mesh()`. Pass an options object
|
|
111
|
+
with any subset of fields:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
ax.configureMesh({
|
|
115
|
+
xLabels: 10, // number of x-axis labels
|
|
116
|
+
yLabels: 5, // number of y-axis labels
|
|
117
|
+
disableXMesh: false, // hide x grid lines
|
|
118
|
+
disableYMesh: true, // hide y grid lines
|
|
119
|
+
disableAxes: false, // hide all axes
|
|
120
|
+
disableXAxis: false, // hide x axis
|
|
121
|
+
disableYAxis: false, // hide y axis
|
|
122
|
+
boldLineColor: '#333', // bold grid line colour
|
|
123
|
+
boldLineAlpha: 0.4, // bold grid line opacity
|
|
124
|
+
lightLineColor: '#ccc', // light grid line colour
|
|
125
|
+
lightLineAlpha: 0.1, // light grid line opacity
|
|
126
|
+
axisColor: '#000', // axis line colour
|
|
127
|
+
labelFontSize: 14, // axis tick label size
|
|
128
|
+
axisDescFontSize: 16, // axis description font size
|
|
129
|
+
xLabelFontSize: 12, // x-axis label font size (overrides labelFontSize)
|
|
130
|
+
yLabelFontSize: 12, // y-axis label font size (overrides labelFontSize)
|
|
131
|
+
tickMarkSize: 5, // tick mark size in pixels
|
|
132
|
+
xMaxLightLines: 10, // max minor grid lines between x-axis labels
|
|
133
|
+
yMaxLightLines: 10, // max minor grid lines between y-axis labels
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### `configureSeriesLabels(options)` — legend style
|
|
138
|
+
|
|
139
|
+
Maps directly to plotters' `chart.configure_series_labels()`:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
ax.configureSeriesLabels({
|
|
143
|
+
position: 'upper-left', // UpperLeft, UpperRight, LowerLeft, LowerRight, etc.
|
|
144
|
+
backgroundColor: '#ffffff', // legend box background colour
|
|
145
|
+
backgroundAlpha: 0.9, // legend box opacity
|
|
146
|
+
borderColor: '#000000', // legend box border colour
|
|
147
|
+
fontSize: 14, // legend label font size
|
|
148
|
+
margin: 10, // margin around legend box
|
|
149
|
+
legendAreaSize: 30, // size of the legend marker area
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Valid positions: `upper-left`, `upper-right`, `lower-left`, `lower-right`,
|
|
154
|
+
`upper-middle`, `middle-left`, `middle-right`, `lower-middle`, `middle`.
|
|
155
|
+
|
|
156
|
+
### Histogram — `fig.hist(data, options?)`
|
|
157
|
+
|
|
158
|
+
Call `hist()` multiple times to overlay datasets on the same axes — just like
|
|
159
|
+
`ax.hist()` in matplotlib. Each call auto-cycles through the tab10 palette
|
|
160
|
+
(C0 blue, C1 orange, C2 green, …) unless you set `color` explicitly.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// Single histogram
|
|
164
|
+
fig.hist([1, 2, 2, 3, 3, 3, 4, 5], {
|
|
165
|
+
bins: 10, // number of bins (default: 10)
|
|
166
|
+
color: 'steelblue', // bar colour: name, CSS name, or hex '#RRGGBB'
|
|
167
|
+
alpha: 0.7, // opacity 0.0–1.0
|
|
168
|
+
label: 'values', // legend label
|
|
169
|
+
rangeMin: 0, // clip data range min
|
|
170
|
+
rangeMax: 100, // clip data range max
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// Overlaid histograms — colors auto-cycle (C0, C1, …)
|
|
176
|
+
const fig = figure();
|
|
177
|
+
fig.hist(datasetA, { bins: 20, alpha: 0.5, label: 'Dataset A' }); // C0 blue
|
|
178
|
+
fig.hist(datasetB, { bins: 20, alpha: 0.5, label: 'Dataset B' }); // C1 orange
|
|
179
|
+
fig.hist(datasetC, { bins: 20, alpha: 0.5, label: 'Dataset C' }); // C2 green
|
|
180
|
+
fig.title('Distribution Comparison');
|
|
181
|
+
fig.xlabel('Value');
|
|
182
|
+
fig.ylabel('Frequency');
|
|
183
|
+
fig.savefig('comparison.png');
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Pie / Donut — `fig.pie(values, options?)`
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
fig.pie([30, 25, 20, 15, 10], {
|
|
190
|
+
labels: ['A', 'B', 'C', 'D', 'E'],
|
|
191
|
+
colors: ['steelblue', 'coral', 'gold', 'teal', 'salmon'],
|
|
192
|
+
startAngle: 90, // starting angle in degrees
|
|
193
|
+
donutHole: 60, // inner radius for donut chart
|
|
194
|
+
labelOffset: 1.2, // label offset relative to radius
|
|
195
|
+
showPercentages: true, // draw percentage labels inside slices
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Box Plot — `fig.boxplot(datasets, options?)`
|
|
200
|
+
|
|
201
|
+
Similar to `plt.boxplot(x)` in matplotlib. Each inner array produces one box
|
|
202
|
+
showing median, quartiles, whiskers, and optional outlier points.
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
fig.boxplot(
|
|
206
|
+
[
|
|
207
|
+
[72, 75, 78, 80, 82, 85, 87, 90, 92, 95, 98],
|
|
208
|
+
[55, 60, 63, 65, 68, 70, 72, 74, 76, 78, 80],
|
|
209
|
+
[88, 90, 91, 93, 94, 95, 96, 97, 98, 99, 100],
|
|
210
|
+
],
|
|
211
|
+
{
|
|
212
|
+
tickLabels: ['Midterm', 'Quiz', 'Final'], // category axis labels
|
|
213
|
+
color: 'steelblue', // box colour (auto-cycles if omitted)
|
|
214
|
+
label: 'Scores', // legend label
|
|
215
|
+
showMeans: true, // show mean marker (cross), like showmeans=True
|
|
216
|
+
showFliers: true, // show outlier points, like showfliers=True
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Area Chart — `fig.fillBetween(x, y1, options?)`
|
|
222
|
+
|
|
223
|
+
Similar to `plt.fill_between(x, y1, y2=0)` in matplotlib. Fills the region
|
|
224
|
+
between a curve and a baseline. Call multiple times to overlay areas.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Single area
|
|
228
|
+
fig.fillBetween(
|
|
229
|
+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
230
|
+
[10, 15, 22, 35, 50, 72, 95, 130, 170, 220],
|
|
231
|
+
{
|
|
232
|
+
y2: 0, // baseline value (default: 0)
|
|
233
|
+
color: 'steelblue', // fill colour (auto-cycles if omitted)
|
|
234
|
+
alpha: 0.35, // fill opacity 0.0–1.0 (default: 0.3)
|
|
235
|
+
label: 'Revenue', // legend label
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
// Overlapping areas — colors auto-cycle (C0, C1, …)
|
|
242
|
+
const fig = figure();
|
|
243
|
+
fig.fillBetween(x, seriesA, { alpha: 0.3, label: 'Product A' });
|
|
244
|
+
fig.fillBetween(x, seriesB, { alpha: 0.3, label: 'Product B' });
|
|
245
|
+
fig.title('Sales Comparison');
|
|
246
|
+
fig.xlabel('Month');
|
|
247
|
+
fig.ylabel('Revenue');
|
|
248
|
+
fig.savefig('area-comparison.png');
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Candlestick — `fig.candlestick(data, options?)`
|
|
252
|
+
|
|
253
|
+
Similar to `mplfinance.plot(df, type='candle')`. Each inner array has 4 values:
|
|
254
|
+
`[open, high, low, close]`. Bullish candles (close >= open) are drawn in the up
|
|
255
|
+
colour, bearish candles in the down colour.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
fig.candlestick(
|
|
259
|
+
[
|
|
260
|
+
[115.34, 117.25, 114.59, 115.91],
|
|
261
|
+
[116.17, 117.61, 116.05, 117.57],
|
|
262
|
+
[118.09, 118.44, 116.99, 117.65],
|
|
263
|
+
[117.39, 118.75, 116.71, 117.52],
|
|
264
|
+
[117.14, 120.82, 117.09, 120.22],
|
|
265
|
+
],
|
|
266
|
+
{
|
|
267
|
+
labels: ['Mar 15', 'Mar 18', 'Mar 19', 'Mar 20', 'Mar 21'], // x-axis date labels
|
|
268
|
+
upColor: 'green', // bullish candle colour (default: teal)
|
|
269
|
+
downColor: 'red', // bearish candle colour (default: red)
|
|
270
|
+
width: 15, // candle body width in pixels
|
|
271
|
+
label: 'MSFT', // legend label
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Line Chart — `fig.plot(x, y, options?)`
|
|
277
|
+
|
|
278
|
+
Similar to `plt.plot(x, y)`. Call `plot()` multiple times to overlay lines on
|
|
279
|
+
the same axes — each call auto-cycles through the tab10 palette unless you set
|
|
280
|
+
`color` explicitly.
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// Single line
|
|
284
|
+
fig.plot([0, 1, 2, 3, 4], [10, 25, 18, 30, 22], {
|
|
285
|
+
color: 'steelblue', // line colour (auto-cycles if omitted)
|
|
286
|
+
lineWidth: 2, // line width in pixels (default: 2)
|
|
287
|
+
label: 'Revenue', // legend label
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// Multi-series overlay — colours auto-cycle (C0, C1, C2)
|
|
293
|
+
const fig = figure();
|
|
294
|
+
fig.plot(months, seriesA, { label: 'Product A' });
|
|
295
|
+
fig.plot(months, seriesB, { label: 'Product B' });
|
|
296
|
+
fig.plot(months, seriesC, { label: 'Product C' });
|
|
297
|
+
fig.title('Sales Trend');
|
|
298
|
+
fig.xlabel('Month');
|
|
299
|
+
fig.ylabel('Sales');
|
|
300
|
+
fig.savefig('multi-line.png');
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Clearing a figure — `fig.clear()`
|
|
304
|
+
|
|
305
|
+
Removes all plot elements (histograms, pies, lines, etc.) while keeping the
|
|
306
|
+
title, axis labels, and dimensions. Use this for real-time/animated charts.
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
fig.clear();
|
|
310
|
+
fig.plot(newX, newY, { color: 'blue' });
|
|
311
|
+
fig.savefig('updated.png');
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Animated GIF — `fig.savegif(path, options)` (Node.js only)
|
|
315
|
+
|
|
316
|
+
Similar to `matplotlib.animation.FuncAnimation` + `writer='pillow'`. Each frame
|
|
317
|
+
is an array of `{ x, y }` line series rendered as a GIF animation frame.
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
const allX = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
321
|
+
const allY = [10, 25, 18, 30, 22, 45, 35, 50, 40, 55];
|
|
322
|
+
|
|
323
|
+
// Progressive reveal: each frame adds one more data point
|
|
324
|
+
const frames = [];
|
|
325
|
+
for (let i = 2; i <= allX.length; i++) {
|
|
326
|
+
frames.push([{ x: allX.slice(0, i), y: allY.slice(0, i) }]);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const fig = figure();
|
|
330
|
+
fig.figsize(640, 400);
|
|
331
|
+
fig.title('Animated Line');
|
|
332
|
+
fig.xlabel('Time');
|
|
333
|
+
fig.ylabel('Value');
|
|
334
|
+
fig.savegif('animation.gif', { frames, delayMs: 200 });
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Canvas animation (WASM / browser)
|
|
338
|
+
|
|
339
|
+
In the browser, drive animation with `requestAnimationFrame`. Use `clear()`
|
|
340
|
+
and `plot()` each frame — no special animation API is needed.
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
const fig = figure();
|
|
344
|
+
fig.figsize(800, 400);
|
|
345
|
+
fig.title('Real-Time Monitor');
|
|
346
|
+
|
|
347
|
+
let i = 2;
|
|
348
|
+
function animate() {
|
|
349
|
+
fig.clear();
|
|
350
|
+
fig.plot(x.slice(0, i), y.slice(0, i), { color: 'blue', label: 'CPU' });
|
|
351
|
+
fig.draw(canvas);
|
|
352
|
+
i++;
|
|
353
|
+
if (i <= x.length) requestAnimationFrame(animate);
|
|
354
|
+
}
|
|
355
|
+
animate();
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Setting labels and title
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
fig.title('My Chart');
|
|
362
|
+
fig.xlabel('Value');
|
|
363
|
+
fig.ylabel('Frequency');
|
|
364
|
+
fig.figsize(1024, 768); // width × height in pixels
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Axis ranges, margin, grid — ChartBuilder wrapper
|
|
368
|
+
|
|
369
|
+
These methods expose plotters' `ChartBuilder` flexibility through a pyplot-like
|
|
370
|
+
API. They correspond to `ax.set_xlim()`, `ax.set_ylim()`, and `ax.grid()` in
|
|
371
|
+
matplotlib.
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
fig.xlim(-1, 1); // fix x-axis range (like ax.set_xlim)
|
|
375
|
+
fig.ylim(-0.1, 1); // fix y-axis range (like ax.set_ylim)
|
|
376
|
+
fig.margin(5); // chart margin in pixels (plotters ChartBuilder.margin)
|
|
377
|
+
fig.grid(false); // hide mesh grid lines (like ax.grid(False))
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Example — the classic y = x² chart from the plotters ChartBuilder docs:
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
const fig = figure();
|
|
384
|
+
fig.figsize(800, 600);
|
|
385
|
+
fig.title('y = x²');
|
|
386
|
+
fig.xlim(-1, 1);
|
|
387
|
+
fig.ylim(-0.1, 1);
|
|
388
|
+
fig.xlabel('x');
|
|
389
|
+
fig.ylabel('y');
|
|
390
|
+
|
|
391
|
+
const x = Array.from({ length: 101 }, (_, i) => -1 + i * 0.02);
|
|
392
|
+
const y = x.map(v => v * v);
|
|
393
|
+
fig.plot(x, y, { color: 'red', label: 'y = x²' });
|
|
394
|
+
fig.savefig('quadratic.png');
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Settings persist through `clear()` calls — ideal for fixed-axis animations:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
const fig = figure();
|
|
401
|
+
fig.ylim(0, 100); // set once, kept across frames
|
|
402
|
+
function animate() {
|
|
403
|
+
fig.clear();
|
|
404
|
+
fig.plot(newX, newY);
|
|
405
|
+
fig.draw(canvas);
|
|
406
|
+
requestAnimationFrame(animate);
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Saving output (Node.js)
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
fig.savefig('output.png');
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Subplots — uniform grid
|
|
417
|
+
|
|
418
|
+
Similar to `fig, axs = plt.subplots(nrows, ncols)` in matplotlib.
|
|
419
|
+
|
|
420
|
+
**Important:** `ax()` extracts the figure from the grid. Configure it, then
|
|
421
|
+
put it back with `setAx()`. This is required because of Rust ownership rules
|
|
422
|
+
across the FFI boundary.
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import { subplots } from 'plotters-skill';
|
|
426
|
+
|
|
427
|
+
const grid = subplots(2, 2);
|
|
428
|
+
grid.figsize(1200, 800);
|
|
429
|
+
grid.suptitle('2×2 Grid');
|
|
430
|
+
|
|
431
|
+
let ax = grid.ax(0, 0);
|
|
432
|
+
ax.title('Distribution');
|
|
433
|
+
ax.hist(data, { bins: 15, color: 'steelblue' });
|
|
434
|
+
grid.setAx(0, 0, ax);
|
|
435
|
+
|
|
436
|
+
ax = grid.ax(0, 1);
|
|
437
|
+
ax.pie([30, 25, 20], { labels: ['A', 'B', 'C'] });
|
|
438
|
+
grid.setAx(0, 1, ax);
|
|
439
|
+
|
|
440
|
+
ax = grid.ax(1, 0);
|
|
441
|
+
ax.hist(otherData, { bins: 10, color: 'coral' });
|
|
442
|
+
grid.setAx(1, 0, ax);
|
|
443
|
+
|
|
444
|
+
ax = grid.ax(1, 1);
|
|
445
|
+
ax.pie([50, 50], { labels: ['Yes', 'No'] });
|
|
446
|
+
grid.setAx(1, 1, ax);
|
|
447
|
+
|
|
448
|
+
grid.savefig('grid.png');
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### GridFigure — flexible row layout
|
|
452
|
+
|
|
453
|
+
Similar to `plt.subplot_mosaic()` — each row can have a different number of
|
|
454
|
+
columns.
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
import { gridFigure } from 'plotters-skill';
|
|
458
|
+
|
|
459
|
+
const grid = gridFigure();
|
|
460
|
+
grid.addRow(2); // row 0: two columns
|
|
461
|
+
grid.addRow(1); // row 1: one full-width column
|
|
462
|
+
grid.figsize(1200, 800);
|
|
463
|
+
grid.suptitle('Mixed Layout');
|
|
464
|
+
|
|
465
|
+
let ax = grid.ax(0, 0);
|
|
466
|
+
ax.hist(data, { bins: 10 });
|
|
467
|
+
grid.setAx(0, 0, ax);
|
|
468
|
+
|
|
469
|
+
ax = grid.ax(0, 1);
|
|
470
|
+
ax.pie(values, { labels: ['A', 'B', 'C'] });
|
|
471
|
+
grid.setAx(0, 1, ax);
|
|
472
|
+
|
|
473
|
+
ax = grid.ax(1, 0);
|
|
474
|
+
ax.hist(otherData, { bins: 20, color: 'coral' });
|
|
475
|
+
grid.setAx(1, 0, ax);
|
|
476
|
+
|
|
477
|
+
grid.savefig('grid.png');
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Named colours
|
|
481
|
+
|
|
482
|
+
Supports matplotlib tab10 palette (`c0`–`c9`, `blue`, `orange`, `green`, `red`,
|
|
483
|
+
`purple`, `brown`, `pink`, `gray`, `olive`, `cyan`), common CSS names
|
|
484
|
+
(`steelblue`, `coral`, `tomato`, `gold`, `navy`, `teal`, `salmon`, `black`,
|
|
485
|
+
`white`), and hex codes (`#4682B4`).
|
|
486
|
+
|
|
487
|
+
## Full example — histogram + pie in a grid
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
import { subplots } from 'plotters-skill';
|
|
491
|
+
|
|
492
|
+
const data: number[] = Array.from({ length: 1000 }, () => Math.random() * 100);
|
|
493
|
+
|
|
494
|
+
const grid = subplots(1, 2);
|
|
495
|
+
grid.figsize(1200, 500);
|
|
496
|
+
grid.suptitle('Dashboard');
|
|
497
|
+
|
|
498
|
+
let ax = grid.ax(0, 0);
|
|
499
|
+
ax.title('Random Distribution');
|
|
500
|
+
ax.xlabel('Value');
|
|
501
|
+
ax.ylabel('Count');
|
|
502
|
+
ax.hist(data, { bins: 25, color: 'steelblue', alpha: 0.8 });
|
|
503
|
+
grid.setAx(0, 0, ax);
|
|
504
|
+
|
|
505
|
+
ax = grid.ax(0, 1);
|
|
506
|
+
ax.title('Categories');
|
|
507
|
+
ax.pie([40, 30, 20, 10], {
|
|
508
|
+
labels: ['Web', 'Mobile', 'Desktop', 'Other'],
|
|
509
|
+
colors: ['steelblue', 'coral', 'gold', 'teal'],
|
|
510
|
+
});
|
|
511
|
+
grid.setAx(0, 1, ax);
|
|
512
|
+
|
|
513
|
+
grid.savefig('dashboard.png');
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## CLI Usage
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
# Install skill for all AI tools (Copilot, Claude, generic agents)
|
|
520
|
+
npx plotters-skill install-skill
|
|
521
|
+
|
|
522
|
+
# Install for a specific tool only
|
|
523
|
+
npx plotters-skill install-skill --target copilot # → .github/skills/plotters-skill/SKILL.md
|
|
524
|
+
npx plotters-skill install-skill --target claude # → .claude/skills/plotters-skill/SKILL.md
|
|
525
|
+
npx plotters-skill install-skill --target agents # → .agents/skills/plotters-skill/SKILL.md
|
|
526
|
+
|
|
527
|
+
# Install into a different project directory
|
|
528
|
+
npx plotters-skill install-skill /path/to/project
|
|
529
|
+
|
|
530
|
+
# Run a script with the addon pre-imported
|
|
531
|
+
npx plotters-skill eval my_chart.js
|
|
532
|
+
|
|
533
|
+
# Run inline code
|
|
534
|
+
npx plotters-skill eval-inline "const fig = figure(); fig.hist([1,2,3]); fig.savefig('out.png');"
|
|
535
|
+
```
|
package/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// plotters-skill CLI
|
|
3
|
+
// Commands:
|
|
4
|
+
// install-skill [--target <tool>] [dir] Install SKILL.md for AI coding tools
|
|
5
|
+
// eval <file.js> Run a JS file with the addon pre-imported
|
|
6
|
+
// eval-inline <code> Run inline JS code with the addon pre-imported
|
|
7
|
+
|
|
8
|
+
const { execFileSync } = require('node:child_process');
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
|
|
13
|
+
const PKG_ROOT = path.resolve(__dirname);
|
|
14
|
+
const SKILL_SRC = path.join(PKG_ROOT, 'SKILL.md');
|
|
15
|
+
|
|
16
|
+
// The preamble that gets prepended to inline scripts and eval'd files.
|
|
17
|
+
// It imports the native addon so user code can call figure(), etc. directly.
|
|
18
|
+
const PREAMBLE = `const { figure, JsFigure } = require(${JSON.stringify(
|
|
19
|
+
path.join(PKG_ROOT, 'node', 'index.js')
|
|
20
|
+
)});\n`;
|
|
21
|
+
|
|
22
|
+
// Where each AI tool looks for skill files (relative to project root).
|
|
23
|
+
const SKILL_TARGETS = {
|
|
24
|
+
copilot: '.github/skills/plotters-skill',
|
|
25
|
+
claude: '.claude/skills/plotters-skill',
|
|
26
|
+
agents: '.agents/skills/plotters-skill',
|
|
27
|
+
};
|
|
28
|
+
const ALL_TARGETS = Object.keys(SKILL_TARGETS);
|
|
29
|
+
|
|
30
|
+
function usage() {
|
|
31
|
+
console.log(`
|
|
32
|
+
plotters-skill CLI
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
plotters-skill install-skill [options] [dir]
|
|
36
|
+
Install SKILL.md so AI tools can discover the plotters-skill API.
|
|
37
|
+
|
|
38
|
+
dir Project root directory (default: current working directory)
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
--target <tool> Install for a specific tool: copilot, claude, agents, or all (default: all)
|
|
42
|
+
copilot → .github/skills/plotters-skill/SKILL.md
|
|
43
|
+
claude → .claude/skills/plotters-skill/SKILL.md
|
|
44
|
+
agents → .agents/skills/plotters-skill/SKILL.md
|
|
45
|
+
all → installs for all three
|
|
46
|
+
|
|
47
|
+
plotters-skill eval <file.js> Execute a JS file with the addon pre-imported
|
|
48
|
+
plotters-skill eval-inline <code> Execute inline JS code with the addon pre-imported
|
|
49
|
+
plotters-skill --help Show this help message
|
|
50
|
+
`.trim());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function installSkill(args) {
|
|
54
|
+
if (!fs.existsSync(SKILL_SRC)) {
|
|
55
|
+
console.error('Error: SKILL.md not found in package.');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let target = 'all';
|
|
60
|
+
let dir = process.cwd();
|
|
61
|
+
|
|
62
|
+
// Parse arguments
|
|
63
|
+
for (let i = 0; i < args.length; i++) {
|
|
64
|
+
if (args[i] === '--target' && args[i + 1]) {
|
|
65
|
+
target = args[++i];
|
|
66
|
+
} else if (!args[i].startsWith('-')) {
|
|
67
|
+
dir = path.resolve(args[i]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const targets = target === 'all' ? ALL_TARGETS : [target];
|
|
72
|
+
for (const t of targets) {
|
|
73
|
+
const relDir = SKILL_TARGETS[t];
|
|
74
|
+
if (!relDir) {
|
|
75
|
+
console.error(`Error: unknown target '${t}'. Choose from: ${ALL_TARGETS.join(', ')}, all`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
const destDir = path.join(dir, relDir);
|
|
79
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
80
|
+
const dest = path.join(destDir, 'SKILL.md');
|
|
81
|
+
fs.copyFileSync(SKILL_SRC, dest);
|
|
82
|
+
console.log(` ✅ ${t}: ${path.relative(dir, dest)}`);
|
|
83
|
+
}
|
|
84
|
+
console.log('\nDone! AI tools will discover plotters-skill automatically.');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findNode() {
|
|
88
|
+
return process.execPath; // re-use the current Node.js binary
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function evalFile(filePath) {
|
|
92
|
+
if (!filePath) {
|
|
93
|
+
console.error('Error: eval requires a file path.');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
const resolved = path.resolve(filePath);
|
|
97
|
+
if (!fs.existsSync(resolved)) {
|
|
98
|
+
console.error(`Error: file not found: ${resolved}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const userCode = fs.readFileSync(resolved, 'utf-8');
|
|
102
|
+
runCode(PREAMBLE + userCode, resolved);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function evalInline(code) {
|
|
106
|
+
if (!code) {
|
|
107
|
+
console.error('Error: eval-inline requires a code string.');
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
runCode(PREAMBLE + code, '<inline>');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function runCode(fullCode, label) {
|
|
114
|
+
const tmpFile = path.join(
|
|
115
|
+
os.tmpdir(),
|
|
116
|
+
`plotters-skill-${Date.now()}-${Math.random().toString(36).slice(2)}.cjs`
|
|
117
|
+
);
|
|
118
|
+
try {
|
|
119
|
+
fs.writeFileSync(tmpFile, fullCode, 'utf-8');
|
|
120
|
+
const node = findNode();
|
|
121
|
+
execFileSync(node, [tmpFile], { stdio: 'inherit' });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err.status) process.exit(err.status);
|
|
124
|
+
throw err;
|
|
125
|
+
} finally {
|
|
126
|
+
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- main ---
|
|
131
|
+
const args = process.argv.slice(2);
|
|
132
|
+
const cmd = args[0];
|
|
133
|
+
|
|
134
|
+
switch (cmd) {
|
|
135
|
+
case 'install-skill':
|
|
136
|
+
installSkill(args.slice(1));
|
|
137
|
+
break;
|
|
138
|
+
case 'eval':
|
|
139
|
+
evalFile(args[1]);
|
|
140
|
+
break;
|
|
141
|
+
case 'eval-inline':
|
|
142
|
+
evalInline(args.slice(1).join(' '));
|
|
143
|
+
break;
|
|
144
|
+
case '--help':
|
|
145
|
+
case '-h':
|
|
146
|
+
case undefined:
|
|
147
|
+
usage();
|
|
148
|
+
break;
|
|
149
|
+
default:
|
|
150
|
+
console.error(`Unknown command: ${cmd}`);
|
|
151
|
+
usage();
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|