@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 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
  }