@opendata-ai/openchart-vanilla 2.0.0 → 2.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 +102 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/svg-renderer.test.ts +47 -0
- package/src/graph/canvas-renderer.ts +29 -0
- package/src/svg-renderer.ts +95 -0
- package/src/table-renderer.ts +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @opendata-ai/openchart-vanilla
|
|
2
|
+
|
|
3
|
+
DOM rendering adapter for OpenChart. Creates SVG charts, HTML tables, and canvas-based network graphs from compiled specs. Framework-agnostic.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @opendata-ai/openchart-vanilla
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
You typically don't install this directly unless you're working without a framework. The framework packages (`openchart-react`, `openchart-vue`, `openchart-svelte`) include it as a dependency and wrap it with lifecycle management.
|
|
12
|
+
|
|
13
|
+
## Charts
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createChart } from '@opendata-ai/openchart-vanilla';
|
|
17
|
+
|
|
18
|
+
const chart = createChart(container, spec, {
|
|
19
|
+
darkMode: 'auto',
|
|
20
|
+
responsive: true,
|
|
21
|
+
onMarkClick: (event) => console.log(event.datum),
|
|
22
|
+
onMarkHover: (event) => showTooltip(event),
|
|
23
|
+
onMarkLeave: () => hideTooltip(),
|
|
24
|
+
onLegendToggle: (series, visible) => console.log(series, visible),
|
|
25
|
+
onAnnotationClick: (annotation, event) => console.log(annotation),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
chart.update(newSpec); // Re-render with new spec
|
|
29
|
+
chart.resize(); // Manual resize trigger
|
|
30
|
+
chart.export('svg'); // Export as SVG string
|
|
31
|
+
await chart.export('png'); // Export as PNG Blob
|
|
32
|
+
chart.export('csv'); // Export data as CSV
|
|
33
|
+
chart.destroy(); // Clean up DOM and observers
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Responsive mode (default) uses a `ResizeObserver` on the container. Charts recompile at new dimensions automatically.
|
|
37
|
+
|
|
38
|
+
## Tables
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { createTable } from '@opendata-ai/openchart-vanilla';
|
|
42
|
+
|
|
43
|
+
const table = createTable(container, tableSpec, {
|
|
44
|
+
responsive: true,
|
|
45
|
+
onRowClick: (row) => console.log(row),
|
|
46
|
+
onStateChange: (state) => console.log(state.sort, state.search, state.page),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
table.update(newSpec);
|
|
50
|
+
table.getState(); // { sort, search, page }
|
|
51
|
+
table.setState({ page: 2 }); // Programmatic state control
|
|
52
|
+
table.export('csv'); // CSV export (respects sort/search, ignores pagination)
|
|
53
|
+
table.destroy();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Graphs
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { createGraph } from '@opendata-ai/openchart-vanilla';
|
|
60
|
+
|
|
61
|
+
const graph = createGraph(container, graphSpec, {
|
|
62
|
+
darkMode: 'auto',
|
|
63
|
+
responsive: true,
|
|
64
|
+
onNodeClick: (node) => console.log(node),
|
|
65
|
+
onNodeDoubleClick: (node) => console.log(node),
|
|
66
|
+
onSelectionChange: (nodeIds) => console.log(nodeIds),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
graph.search('query'); // Highlight matching nodes
|
|
70
|
+
graph.clearSearch();
|
|
71
|
+
graph.zoomToFit(); // Fit all nodes in viewport
|
|
72
|
+
graph.zoomToNode('node-id'); // Center on a specific node
|
|
73
|
+
graph.selectNode('node-id'); // Programmatic selection
|
|
74
|
+
graph.getSelectedNodes(); // Get selected node IDs
|
|
75
|
+
graph.update(newSpec);
|
|
76
|
+
graph.destroy();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Graphs render on canvas with a force simulation running in a web worker. Nodes support click, drag, and double-click. The simulation auto-fits nodes once it settles.
|
|
80
|
+
|
|
81
|
+
## Export utilities
|
|
82
|
+
|
|
83
|
+
Standalone export functions if you need them outside of an instance:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { exportSVG, exportPNG, exportCSV } from '@opendata-ai/openchart-vanilla';
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Other exports
|
|
90
|
+
|
|
91
|
+
- `observeResize()` - ResizeObserver wrapper for container tracking
|
|
92
|
+
- `attachKeyboardNav()` - Keyboard navigation for chart marks
|
|
93
|
+
- `createTooltipManager()` - Tooltip lifecycle management
|
|
94
|
+
- `renderChartSVG()` - Low-level SVG rendering from a ChartLayout
|
|
95
|
+
- `renderTable()` - Low-level table DOM rendering from a TableLayout
|
|
96
|
+
- `renderCell()` and cell-type renderers (`renderBarCell`, `renderSparklineCell`, `renderHeatmapCell`, etc.) - Individual table cell renderers
|
|
97
|
+
|
|
98
|
+
## Related docs
|
|
99
|
+
|
|
100
|
+
- [Getting started](../../docs/getting-started.md) for a hands-on tutorial
|
|
101
|
+
- [Integration guide](../../docs/integration-guide.md) for lifecycle management and events
|
|
102
|
+
- [Spec reference](../../docs/spec-reference.md) for field-by-field type details
|
package/dist/index.js
CHANGED
|
@@ -194,6 +194,26 @@ var GraphCanvasRenderer = class {
|
|
|
194
194
|
);
|
|
195
195
|
}
|
|
196
196
|
ctx.restore();
|
|
197
|
+
this.drawBrand(ctx, cssWidth, cssHeight, theme);
|
|
198
|
+
}
|
|
199
|
+
// -------------------------------------------------------------------------
|
|
200
|
+
// Brand rendering
|
|
201
|
+
// -------------------------------------------------------------------------
|
|
202
|
+
drawBrand(ctx, w, h, theme) {
|
|
203
|
+
if (w < 120) return;
|
|
204
|
+
const { dpr } = this;
|
|
205
|
+
ctx.save();
|
|
206
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
207
|
+
const padding = theme.spacing.padding;
|
|
208
|
+
const x = w - padding;
|
|
209
|
+
const y = h - 4;
|
|
210
|
+
ctx.font = `600 20px ${theme.fonts.family}`;
|
|
211
|
+
ctx.fillStyle = theme.colors.axis;
|
|
212
|
+
ctx.globalAlpha = 0.5;
|
|
213
|
+
ctx.textAlign = "right";
|
|
214
|
+
ctx.textBaseline = "alphabetic";
|
|
215
|
+
ctx.fillText("OpenData", x, y);
|
|
216
|
+
ctx.restore();
|
|
197
217
|
}
|
|
198
218
|
// -------------------------------------------------------------------------
|
|
199
219
|
// Batched edge drawing
|
|
@@ -2521,6 +2541,73 @@ function renderLegend(parent, legend) {
|
|
|
2521
2541
|
}
|
|
2522
2542
|
parent.appendChild(g);
|
|
2523
2543
|
}
|
|
2544
|
+
var BRAND_FONT_SIZE = 20;
|
|
2545
|
+
var BRAND_MIN_WIDTH = 120;
|
|
2546
|
+
var BRAND_URL = "https://tryopendata.ai";
|
|
2547
|
+
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
2548
|
+
function brandPosition(layout) {
|
|
2549
|
+
const { width } = layout.dimensions;
|
|
2550
|
+
const padding = layout.theme.spacing.padding;
|
|
2551
|
+
const rightEdge = width - padding;
|
|
2552
|
+
const { chrome } = layout;
|
|
2553
|
+
const xAxisExtent = layout.axes.x ? layout.axes.x.label ? 48 : 26 : 0;
|
|
2554
|
+
const bottomOffset = layout.area.y + layout.area.height + xAxisExtent;
|
|
2555
|
+
const firstBottom = chrome.source ?? chrome.byline ?? chrome.footer;
|
|
2556
|
+
const chromeY = firstBottom ? bottomOffset + firstBottom.y : bottomOffset + layout.theme.spacing.chartToFooter;
|
|
2557
|
+
const y = chromeY + BRAND_FONT_SIZE;
|
|
2558
|
+
const dataWidth = estimateTextWidth("Data", BRAND_FONT_SIZE, 600);
|
|
2559
|
+
const dataX = rightEdge - dataWidth;
|
|
2560
|
+
const openX = dataX;
|
|
2561
|
+
return { openX, dataX, y, fill: layout.theme.colors.axis };
|
|
2562
|
+
}
|
|
2563
|
+
function renderBrandOpen(parent, layout) {
|
|
2564
|
+
if (layout.dimensions.width < BRAND_MIN_WIDTH) return;
|
|
2565
|
+
const { openX, y, fill } = brandPosition(layout);
|
|
2566
|
+
const a = createSVGElement("a");
|
|
2567
|
+
a.setAttribute("href", BRAND_URL);
|
|
2568
|
+
a.setAttributeNS(XLINK_NS, "xlink:href", BRAND_URL);
|
|
2569
|
+
a.setAttribute("target", "_blank");
|
|
2570
|
+
a.setAttribute("rel", "noopener");
|
|
2571
|
+
a.setAttribute("class", "viz-axis-ref");
|
|
2572
|
+
const text = createSVGElement("text");
|
|
2573
|
+
setAttrs(text, {
|
|
2574
|
+
x: openX,
|
|
2575
|
+
y,
|
|
2576
|
+
"font-family": layout.theme.fonts.family,
|
|
2577
|
+
"font-size": BRAND_FONT_SIZE,
|
|
2578
|
+
"font-weight": 500,
|
|
2579
|
+
"text-anchor": "end",
|
|
2580
|
+
"fill-opacity": 0.55
|
|
2581
|
+
});
|
|
2582
|
+
text.style.setProperty("fill", fill);
|
|
2583
|
+
text.textContent = "Open";
|
|
2584
|
+
a.appendChild(text);
|
|
2585
|
+
parent.appendChild(a);
|
|
2586
|
+
}
|
|
2587
|
+
function renderBrandData(parent, layout) {
|
|
2588
|
+
if (layout.dimensions.width < BRAND_MIN_WIDTH) return;
|
|
2589
|
+
const { dataX, y, fill } = brandPosition(layout);
|
|
2590
|
+
const a = createSVGElement("a");
|
|
2591
|
+
a.setAttribute("href", BRAND_URL);
|
|
2592
|
+
a.setAttributeNS(XLINK_NS, "xlink:href", BRAND_URL);
|
|
2593
|
+
a.setAttribute("target", "_blank");
|
|
2594
|
+
a.setAttribute("rel", "noopener");
|
|
2595
|
+
a.setAttribute("class", "viz-chrome-ref");
|
|
2596
|
+
const text = createSVGElement("text");
|
|
2597
|
+
setAttrs(text, {
|
|
2598
|
+
x: dataX,
|
|
2599
|
+
y,
|
|
2600
|
+
"font-family": layout.theme.fonts.family,
|
|
2601
|
+
"font-size": BRAND_FONT_SIZE,
|
|
2602
|
+
"font-weight": 600,
|
|
2603
|
+
"text-anchor": "start",
|
|
2604
|
+
"fill-opacity": 0.55
|
|
2605
|
+
});
|
|
2606
|
+
text.style.setProperty("fill", fill);
|
|
2607
|
+
text.textContent = "Data";
|
|
2608
|
+
a.appendChild(text);
|
|
2609
|
+
parent.appendChild(a);
|
|
2610
|
+
}
|
|
2524
2611
|
function renderChartSVG(layout, container) {
|
|
2525
2612
|
const { width, height } = layout.dimensions;
|
|
2526
2613
|
const svg = createSVGElement("svg");
|
|
@@ -2561,7 +2648,9 @@ function renderChartSVG(layout, container) {
|
|
|
2561
2648
|
svg.appendChild(clippedGroup);
|
|
2562
2649
|
renderAnnotations(svg, layout);
|
|
2563
2650
|
renderLegend(svg, layout.legend);
|
|
2651
|
+
renderBrandOpen(svg, layout);
|
|
2564
2652
|
renderChrome(svg, layout);
|
|
2653
|
+
renderBrandData(svg, layout);
|
|
2565
2654
|
container.appendChild(svg);
|
|
2566
2655
|
return svg;
|
|
2567
2656
|
}
|
|
@@ -4371,6 +4460,18 @@ function renderTable(layout, container) {
|
|
|
4371
4460
|
liveRegion.setAttribute("aria-atomic", "true");
|
|
4372
4461
|
liveRegion.setAttribute("role", "status");
|
|
4373
4462
|
wrapper.appendChild(liveRegion);
|
|
4463
|
+
const brandColor = theme ? theme.colors.axis : "#999999";
|
|
4464
|
+
const brand = document.createElement("div");
|
|
4465
|
+
brand.className = "viz-table-ref";
|
|
4466
|
+
brand.style.cssText = "text-align: right; padding: 4px 8px;";
|
|
4467
|
+
const brandLink = document.createElement("a");
|
|
4468
|
+
brandLink.href = "https://tryopendata.ai";
|
|
4469
|
+
brandLink.target = "_blank";
|
|
4470
|
+
brandLink.rel = "noopener";
|
|
4471
|
+
brandLink.style.cssText = `font-size: 20px; color: ${brandColor}; opacity: 0.55; text-decoration: none; font-family: ${theme ? theme.fonts.family : "sans-serif"};`;
|
|
4472
|
+
brandLink.textContent = "OpenData";
|
|
4473
|
+
brand.appendChild(brandLink);
|
|
4474
|
+
wrapper.appendChild(brand);
|
|
4374
4475
|
container.appendChild(wrapper);
|
|
4375
4476
|
return wrapper;
|
|
4376
4477
|
}
|