@opendata-ai/openchart-vanilla 6.10.0 → 6.12.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/dist/index.d.ts +8 -0
- package/dist/index.js +131 -31
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/gradient-utils.test.ts +125 -0
- package/src/__tests__/svg-renderer.test.ts +7 -0
- package/src/gradient-utils.ts +134 -0
- package/src/graph/canvas-renderer.ts +3 -1
- package/src/graph/types.ts +2 -0
- package/src/graph-mount.ts +4 -0
- package/src/mount.ts +5 -2
- package/src/sankey-mount.ts +3 -0
- package/src/sankey-renderer.ts +3 -1
- package/src/svg-renderer.ts +20 -6
- package/src/table-mount.ts +3 -0
- package/src/table-renderer.ts +14 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-vanilla",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.12.0",
|
|
4
4
|
"description": "Vanilla JS renderer for openchart: SVG charts, HTML tables, force-directed graphs",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Riley Hilliard",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@floating-ui/dom": "^1.7.6",
|
|
53
|
-
"@opendata-ai/openchart-core": "6.
|
|
54
|
-
"@opendata-ai/openchart-engine": "6.
|
|
53
|
+
"@opendata-ai/openchart-core": "6.12.0",
|
|
54
|
+
"@opendata-ai/openchart-engine": "6.12.0",
|
|
55
55
|
"d3-force": "^3.0.0",
|
|
56
56
|
"d3-quadtree": "^3.0.1"
|
|
57
57
|
},
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { GradientDef } from '@opendata-ai/openchart-core';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { buildGradientDefs, resolveMarkFill } from '../gradient-utils';
|
|
4
|
+
|
|
5
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
6
|
+
|
|
7
|
+
function createDefs(): SVGElement {
|
|
8
|
+
return document.createElementNS(SVG_NS, 'defs') as SVGElement;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const linearGrad: GradientDef = {
|
|
12
|
+
gradient: 'linear',
|
|
13
|
+
stops: [
|
|
14
|
+
{ offset: 0, color: '#f00' },
|
|
15
|
+
{ offset: 1, color: '#00f' },
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const radialGrad: GradientDef = {
|
|
20
|
+
gradient: 'radial',
|
|
21
|
+
stops: [
|
|
22
|
+
{ offset: 0, color: '#fff' },
|
|
23
|
+
{ offset: 1, color: '#000' },
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe('buildGradientDefs', () => {
|
|
28
|
+
it('creates a linearGradient element for a linear gradient fill', () => {
|
|
29
|
+
const defs = createDefs();
|
|
30
|
+
buildGradientDefs([{ fill: linearGrad }], defs);
|
|
31
|
+
const el = defs.querySelector('linearGradient');
|
|
32
|
+
expect(el).not.toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('creates a radialGradient element for a radial gradient fill', () => {
|
|
36
|
+
const defs = createDefs();
|
|
37
|
+
buildGradientDefs([{ fill: radialGrad }], defs);
|
|
38
|
+
const el = defs.querySelector('radialGradient');
|
|
39
|
+
expect(el).not.toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('deduplicates identical gradients', () => {
|
|
43
|
+
const defs = createDefs();
|
|
44
|
+
buildGradientDefs([{ fill: linearGrad }, { fill: linearGrad }], defs);
|
|
45
|
+
const els = defs.querySelectorAll('linearGradient');
|
|
46
|
+
expect(els.length).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('creates separate gradient elements for different gradients', () => {
|
|
50
|
+
const defs = createDefs();
|
|
51
|
+
buildGradientDefs([{ fill: linearGrad }, { fill: radialGrad }], defs);
|
|
52
|
+
expect(defs.querySelector('linearGradient')).not.toBeNull();
|
|
53
|
+
expect(defs.querySelector('radialGradient')).not.toBeNull();
|
|
54
|
+
expect(defs.children.length).toBe(2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('skips marks with string fills', () => {
|
|
58
|
+
const defs = createDefs();
|
|
59
|
+
buildGradientDefs([{ fill: '#ff0000' }, { fill: 'steelblue' }], defs);
|
|
60
|
+
expect(defs.children.length).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('handles empty marks array', () => {
|
|
64
|
+
const defs = createDefs();
|
|
65
|
+
const map = buildGradientDefs([], defs);
|
|
66
|
+
expect(map.size).toBe(0);
|
|
67
|
+
expect(defs.children.length).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('sets correct default attributes on linearGradient', () => {
|
|
71
|
+
const defs = createDefs();
|
|
72
|
+
buildGradientDefs([{ fill: linearGrad }], defs);
|
|
73
|
+
const el = defs.querySelector('linearGradient')!;
|
|
74
|
+
expect(el.getAttribute('x1')).toBe('0');
|
|
75
|
+
expect(el.getAttribute('y1')).toBe('0');
|
|
76
|
+
expect(el.getAttribute('x2')).toBe('0');
|
|
77
|
+
expect(el.getAttribute('y2')).toBe('1');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('sets correct stop attributes', () => {
|
|
81
|
+
const grad: GradientDef = {
|
|
82
|
+
gradient: 'linear',
|
|
83
|
+
stops: [
|
|
84
|
+
{ offset: 0, color: '#f00', opacity: 0.5 },
|
|
85
|
+
{ offset: 1, color: '#00f' },
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
const defs = createDefs();
|
|
89
|
+
buildGradientDefs([{ fill: grad }], defs);
|
|
90
|
+
const stops = defs.querySelectorAll('stop');
|
|
91
|
+
expect(stops.length).toBe(2);
|
|
92
|
+
expect(stops[0].getAttribute('offset')).toBe('0');
|
|
93
|
+
expect(stops[0].getAttribute('stop-color')).toBe('#f00');
|
|
94
|
+
expect(stops[0].getAttribute('stop-opacity')).toBe('0.5');
|
|
95
|
+
expect(stops[1].getAttribute('offset')).toBe('1');
|
|
96
|
+
expect(stops[1].getAttribute('stop-color')).toBe('#00f');
|
|
97
|
+
expect(stops[1].hasAttribute('stop-opacity')).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('sets gradientUnits="objectBoundingBox"', () => {
|
|
101
|
+
const defs = createDefs();
|
|
102
|
+
buildGradientDefs([{ fill: linearGrad }], defs);
|
|
103
|
+
const el = defs.querySelector('linearGradient')!;
|
|
104
|
+
expect(el.getAttribute('gradientUnits')).toBe('objectBoundingBox');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('resolveMarkFill', () => {
|
|
109
|
+
it('returns the string directly for string fills', () => {
|
|
110
|
+
const map = new Map<string, string>();
|
|
111
|
+
expect(resolveMarkFill('#ff0000', map)).toBe('#ff0000');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns url(#id) for gradient fills that are in the map', () => {
|
|
115
|
+
const defs = createDefs();
|
|
116
|
+
const map = buildGradientDefs([{ fill: linearGrad }], defs);
|
|
117
|
+
const result = resolveMarkFill(linearGrad, map);
|
|
118
|
+
expect(result).toMatch(/^url\(#oc-grad-\d+\)$/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('returns #000000 for gradient fills not in the map', () => {
|
|
122
|
+
const map = new Map<string, string>();
|
|
123
|
+
expect(resolveMarkFill(linearGrad, map)).toBe('#000000');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -771,4 +771,11 @@ describe('brand watermark', () => {
|
|
|
771
771
|
const brandLink = svg.querySelector('.oc-chrome-ref');
|
|
772
772
|
expect(brandLink).toBeNull();
|
|
773
773
|
});
|
|
774
|
+
|
|
775
|
+
it('does not render brand when layout.watermark is false', () => {
|
|
776
|
+
const spec: ChartSpec = { ...lineSpec, watermark: false };
|
|
777
|
+
const { svg } = renderSpec(spec);
|
|
778
|
+
const brandLink = svg.querySelector('.oc-chrome-ref');
|
|
779
|
+
expect(brandLink).toBeNull();
|
|
780
|
+
});
|
|
774
781
|
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG gradient utilities: creates <linearGradient> and <radialGradient>
|
|
3
|
+
* elements from GradientDef specs and resolves mark fills to url(#id) refs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GradientDef, LinearGradient, RadialGradient } from '@opendata-ai/openchart-core';
|
|
7
|
+
import { isGradientDef } from '@opendata-ai/openchart-core';
|
|
8
|
+
|
|
9
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Produce a stable, deterministic key for a GradientDef.
|
|
13
|
+
* Used for deduplication so identical gradients share one SVG element.
|
|
14
|
+
* Recursively sorts object keys for stability across property order variations.
|
|
15
|
+
*/
|
|
16
|
+
function gradientKey(def: GradientDef): string {
|
|
17
|
+
return sortedStringify(def);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sortedStringify(value: unknown): string {
|
|
21
|
+
if (value === null || value === undefined) return String(value);
|
|
22
|
+
if (Array.isArray(value)) return `[${value.map(sortedStringify).join(',')}]`;
|
|
23
|
+
if (typeof value === 'object') {
|
|
24
|
+
const sorted = Object.keys(value)
|
|
25
|
+
.sort()
|
|
26
|
+
.map((k) => `${JSON.stringify(k)}:${sortedStringify((value as Record<string, unknown>)[k])}`);
|
|
27
|
+
return `{${sorted.join(',')}}`;
|
|
28
|
+
}
|
|
29
|
+
return JSON.stringify(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a single SVG gradient element (<linearGradient> or <radialGradient>).
|
|
34
|
+
* Uses gradientUnits="objectBoundingBox" so coordinates are in [0,1] space.
|
|
35
|
+
*/
|
|
36
|
+
function createGradientElement(def: GradientDef, id: string): SVGElement {
|
|
37
|
+
if (def.gradient === 'linear') {
|
|
38
|
+
return createLinearGradient(def, id);
|
|
39
|
+
}
|
|
40
|
+
return createRadialGradient(def, id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createLinearGradient(def: LinearGradient, id: string): SVGElement {
|
|
44
|
+
const el = document.createElementNS(SVG_NS, 'linearGradient');
|
|
45
|
+
el.setAttribute('id', id);
|
|
46
|
+
el.setAttribute('gradientUnits', 'objectBoundingBox');
|
|
47
|
+
el.setAttribute('x1', String(def.x1 ?? 0));
|
|
48
|
+
el.setAttribute('y1', String(def.y1 ?? 0));
|
|
49
|
+
el.setAttribute('x2', String(def.x2 ?? 0));
|
|
50
|
+
el.setAttribute('y2', String(def.y2 ?? 1));
|
|
51
|
+
|
|
52
|
+
for (const stop of def.stops) {
|
|
53
|
+
appendStop(el, stop);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return el;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createRadialGradient(def: RadialGradient, id: string): SVGElement {
|
|
60
|
+
const el = document.createElementNS(SVG_NS, 'radialGradient');
|
|
61
|
+
el.setAttribute('id', id);
|
|
62
|
+
el.setAttribute('gradientUnits', 'objectBoundingBox');
|
|
63
|
+
// SVG radialGradient: cx/cy/r = outer circle, fx/fy/fr = focal (inner) circle
|
|
64
|
+
// Spec: x2/y2/r2 = outer, x1/y1/r1 = inner (focal)
|
|
65
|
+
el.setAttribute('cx', String(def.x2 ?? 0.5));
|
|
66
|
+
el.setAttribute('cy', String(def.y2 ?? 0.5));
|
|
67
|
+
el.setAttribute('r', String(def.r2 ?? 0.5));
|
|
68
|
+
el.setAttribute('fx', String(def.x1 ?? 0.5));
|
|
69
|
+
el.setAttribute('fy', String(def.y1 ?? 0.5));
|
|
70
|
+
el.setAttribute('fr', String(def.r1 ?? 0));
|
|
71
|
+
|
|
72
|
+
for (const stop of def.stops) {
|
|
73
|
+
appendStop(el, stop);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return el;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function appendStop(
|
|
80
|
+
parent: SVGElement,
|
|
81
|
+
stop: { offset: number; color: string; opacity?: number },
|
|
82
|
+
): void {
|
|
83
|
+
const stopEl = document.createElementNS(SVG_NS, 'stop');
|
|
84
|
+
stopEl.setAttribute('offset', String(stop.offset));
|
|
85
|
+
stopEl.setAttribute('stop-color', stop.color);
|
|
86
|
+
if (stop.opacity !== undefined) {
|
|
87
|
+
stopEl.setAttribute('stop-opacity', String(stop.opacity));
|
|
88
|
+
}
|
|
89
|
+
parent.appendChild(stopEl);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Scan all marks for GradientDef fill values, create SVG gradient elements
|
|
94
|
+
* in the provided <defs> node, and return a map from gradient key to element ID.
|
|
95
|
+
*
|
|
96
|
+
* Identical gradients (by key) share a single SVG element.
|
|
97
|
+
*/
|
|
98
|
+
export function buildGradientDefs(
|
|
99
|
+
marks: Array<{ fill?: unknown }>,
|
|
100
|
+
defs: SVGElement,
|
|
101
|
+
): Map<string, string> {
|
|
102
|
+
const map = new Map<string, string>();
|
|
103
|
+
let counter = 0;
|
|
104
|
+
|
|
105
|
+
for (const mark of marks) {
|
|
106
|
+
const fill = mark.fill;
|
|
107
|
+
if (fill && isGradientDef(fill)) {
|
|
108
|
+
const key = gradientKey(fill);
|
|
109
|
+
if (!map.has(key)) {
|
|
110
|
+
const id = `oc-grad-${counter++}`;
|
|
111
|
+
const el = createGradientElement(fill, id);
|
|
112
|
+
defs.appendChild(el);
|
|
113
|
+
map.set(key, id);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return map;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolve a mark's fill value to a CSS/SVG fill string.
|
|
123
|
+
* Returns the color string directly for plain fills,
|
|
124
|
+
* or "url(#gradientId)" for gradient fills.
|
|
125
|
+
*/
|
|
126
|
+
export function resolveMarkFill(
|
|
127
|
+
fill: string | GradientDef,
|
|
128
|
+
gradientMap: Map<string, string>,
|
|
129
|
+
): string {
|
|
130
|
+
if (typeof fill === 'string') return fill;
|
|
131
|
+
const key = gradientKey(fill);
|
|
132
|
+
const id = gradientMap.get(key);
|
|
133
|
+
return id ? `url(#${id})` : '#000000';
|
|
134
|
+
}
|
|
@@ -229,7 +229,9 @@ export class GraphCanvasRenderer {
|
|
|
229
229
|
ctx.restore();
|
|
230
230
|
|
|
231
231
|
// Brand watermark in screen coordinates (unaffected by pan/zoom)
|
|
232
|
-
|
|
232
|
+
if (state.watermark) {
|
|
233
|
+
this.drawBrand(ctx, cssWidth, cssHeight, theme);
|
|
234
|
+
}
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
// -------------------------------------------------------------------------
|
package/src/graph/types.ts
CHANGED
package/src/graph-mount.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface GraphMountOptions {
|
|
|
35
35
|
theme?: ThemeConfig;
|
|
36
36
|
darkMode?: DarkMode;
|
|
37
37
|
responsive?: boolean;
|
|
38
|
+
/** Show the tryOpenData.ai watermark. Defaults to true. */
|
|
39
|
+
watermark?: boolean;
|
|
38
40
|
/** Show the built-in tooltip on node/edge hover. Defaults to true. */
|
|
39
41
|
tooltip?: boolean;
|
|
40
42
|
/** Show the built-in legend. Defaults to true. */
|
|
@@ -157,6 +159,7 @@ export function createGraph(
|
|
|
157
159
|
height,
|
|
158
160
|
theme: options?.theme,
|
|
159
161
|
darkMode,
|
|
162
|
+
watermark: options?.watermark,
|
|
160
163
|
};
|
|
161
164
|
|
|
162
165
|
return compileGraph(currentSpec, compileOpts);
|
|
@@ -465,6 +468,7 @@ export function createGraph(
|
|
|
465
468
|
theme: compilation.theme,
|
|
466
469
|
searchMatches: searchManager.getMatches(),
|
|
467
470
|
isGesturing,
|
|
471
|
+
watermark: compilation.watermark,
|
|
468
472
|
};
|
|
469
473
|
|
|
470
474
|
renderer.render(state);
|
package/src/mount.ts
CHANGED
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
ThemeConfig,
|
|
28
28
|
TooltipContent,
|
|
29
29
|
} from '@opendata-ai/openchart-core';
|
|
30
|
-
import { elementRef, isLayerSpec } from '@opendata-ai/openchart-core';
|
|
30
|
+
import { elementRef, getRepresentativeColor, isLayerSpec } from '@opendata-ai/openchart-core';
|
|
31
31
|
import { compileChart, compileLayer } from '@opendata-ai/openchart-engine';
|
|
32
32
|
import { cancelAnimations, setupAnimationCleanup } from './animation';
|
|
33
33
|
import {
|
|
@@ -57,6 +57,8 @@ export interface MountOptions extends ChartEventHandlers {
|
|
|
57
57
|
onDataPointClick?: (data: Record<string, unknown>) => void;
|
|
58
58
|
/** Enable responsive resizing. Defaults to true. */
|
|
59
59
|
responsive?: boolean;
|
|
60
|
+
/** Show the tryOpenData.ai watermark. Defaults to true. */
|
|
61
|
+
watermark?: boolean;
|
|
60
62
|
/** Initial selected element. */
|
|
61
63
|
selectedElement?: ElementRef;
|
|
62
64
|
}
|
|
@@ -243,7 +245,7 @@ function collectVoronoiPoints(layout: ChartLayout): VoronoiPoint[] {
|
|
|
243
245
|
const points: VoronoiPoint[] = [];
|
|
244
246
|
for (const mark of layout.marks) {
|
|
245
247
|
if ((mark.type === 'line' || mark.type === 'area') && mark.dataPoints) {
|
|
246
|
-
const color = mark.type === 'line' ? mark.stroke : mark.fill;
|
|
248
|
+
const color = mark.type === 'line' ? mark.stroke : getRepresentativeColor(mark.fill);
|
|
247
249
|
for (const dp of mark.dataPoints) {
|
|
248
250
|
points.push({ ...dp, color });
|
|
249
251
|
}
|
|
@@ -1820,6 +1822,7 @@ export function createChart(
|
|
|
1820
1822
|
height,
|
|
1821
1823
|
theme: options?.theme,
|
|
1822
1824
|
darkMode,
|
|
1825
|
+
watermark: options?.watermark,
|
|
1823
1826
|
measureText,
|
|
1824
1827
|
};
|
|
1825
1828
|
|
package/src/sankey-mount.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface SankeyMountOptions {
|
|
|
38
38
|
darkMode?: DarkMode;
|
|
39
39
|
/** Enable responsive resizing. Defaults to true. */
|
|
40
40
|
responsive?: boolean;
|
|
41
|
+
/** Show the tryOpenData.ai watermark. Defaults to true. */
|
|
42
|
+
watermark?: boolean;
|
|
41
43
|
/** Show tooltips on hover. Defaults to true. */
|
|
42
44
|
tooltip?: boolean;
|
|
43
45
|
/** Callback when a node is clicked. */
|
|
@@ -140,6 +142,7 @@ export function createSankey(
|
|
|
140
142
|
height,
|
|
141
143
|
theme: options?.theme,
|
|
142
144
|
darkMode,
|
|
145
|
+
watermark: options?.watermark,
|
|
143
146
|
};
|
|
144
147
|
|
|
145
148
|
return compileSankey(currentSpec, compileOpts);
|
package/src/sankey-renderer.ts
CHANGED
package/src/svg-renderer.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
} from '@opendata-ai/openchart-core';
|
|
31
31
|
import { BRAND_FONT_SIZE, BRAND_MIN_WIDTH, estimateTextWidth } from '@opendata-ai/openchart-core';
|
|
32
32
|
import { clampStaggerDelay } from '@opendata-ai/openchart-engine';
|
|
33
|
+
import { buildGradientDefs, resolveMarkFill } from './gradient-utils';
|
|
33
34
|
|
|
34
35
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
35
36
|
|
|
@@ -39,6 +40,12 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
|
39
40
|
*/
|
|
40
41
|
let currentAnimation: ResolvedAnimation | undefined;
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Module-level gradient map. Set by renderChartSVG after building gradient defs
|
|
45
|
+
* so mark renderers can resolve gradient fills without signature changes.
|
|
46
|
+
*/
|
|
47
|
+
let currentGradientMap: Map<string, string> = new Map();
|
|
48
|
+
|
|
42
49
|
/**
|
|
43
50
|
* Stamp animation index attributes on a mark element when animation is enabled.
|
|
44
51
|
* Sets `data-animation-index` (for querySelector) and `--oc-mark-index`
|
|
@@ -508,7 +515,7 @@ function renderAreaMark(mark: AreaMark, index: number): SVGElement {
|
|
|
508
515
|
const fill = createSVGElement('path');
|
|
509
516
|
setAttrs(fill, {
|
|
510
517
|
d: mark.path,
|
|
511
|
-
fill: mark.fill,
|
|
518
|
+
fill: resolveMarkFill(mark.fill, currentGradientMap),
|
|
512
519
|
'fill-opacity': mark.fillOpacity,
|
|
513
520
|
stroke: 'none',
|
|
514
521
|
});
|
|
@@ -549,7 +556,7 @@ function renderRectMark(mark: RectMark, index: number): SVGElement {
|
|
|
549
556
|
y: mark.y,
|
|
550
557
|
width: mark.width,
|
|
551
558
|
height: mark.height,
|
|
552
|
-
fill: mark.fill,
|
|
559
|
+
fill: resolveMarkFill(mark.fill, currentGradientMap),
|
|
553
560
|
});
|
|
554
561
|
if (mark.stroke) {
|
|
555
562
|
rect.setAttribute('stroke', mark.stroke);
|
|
@@ -585,7 +592,7 @@ function renderArcMark(mark: ArcMark, index: number): SVGElement {
|
|
|
585
592
|
const path = createSVGElement('path');
|
|
586
593
|
setAttrs(path, {
|
|
587
594
|
d: mark.path,
|
|
588
|
-
fill: mark.fill,
|
|
595
|
+
fill: resolveMarkFill(mark.fill, currentGradientMap),
|
|
589
596
|
stroke: mark.stroke,
|
|
590
597
|
'stroke-width': mark.strokeWidth,
|
|
591
598
|
});
|
|
@@ -619,7 +626,7 @@ function renderPointMark(mark: PointMark, index: number): SVGElement {
|
|
|
619
626
|
cx: mark.cx,
|
|
620
627
|
cy: mark.cy,
|
|
621
628
|
r: mark.r,
|
|
622
|
-
fill: mark.fill,
|
|
629
|
+
fill: resolveMarkFill(mark.fill, currentGradientMap),
|
|
623
630
|
stroke: mark.stroke,
|
|
624
631
|
'stroke-width': mark.strokeWidth,
|
|
625
632
|
});
|
|
@@ -1279,6 +1286,10 @@ export function renderChartSVG(
|
|
|
1279
1286
|
});
|
|
1280
1287
|
clipPath.appendChild(clipRect);
|
|
1281
1288
|
defs.appendChild(clipPath);
|
|
1289
|
+
|
|
1290
|
+
// Build gradient defs for marks with gradient fills
|
|
1291
|
+
currentGradientMap = buildGradientDefs(layout.marks as Array<{ fill?: unknown }>, defs);
|
|
1292
|
+
|
|
1282
1293
|
svg.appendChild(defs);
|
|
1283
1294
|
|
|
1284
1295
|
// Render layers in order (back to front)
|
|
@@ -1320,10 +1331,13 @@ export function renderChartSVG(
|
|
|
1320
1331
|
renderChrome(svg, layout);
|
|
1321
1332
|
|
|
1322
1333
|
// Brand renders as a footer item, right-aligned on the source/footer row
|
|
1323
|
-
|
|
1334
|
+
if (layout.watermark) {
|
|
1335
|
+
renderBrand(svg, layout);
|
|
1336
|
+
}
|
|
1324
1337
|
|
|
1325
|
-
// Reset module-level
|
|
1338
|
+
// Reset module-level state after rendering
|
|
1326
1339
|
currentAnimation = undefined;
|
|
1340
|
+
currentGradientMap = new Map();
|
|
1327
1341
|
|
|
1328
1342
|
container.appendChild(svg);
|
|
1329
1343
|
return svg;
|
package/src/table-mount.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface TableMountOptions {
|
|
|
39
39
|
theme?: ThemeConfig;
|
|
40
40
|
darkMode?: DarkMode;
|
|
41
41
|
responsive?: boolean;
|
|
42
|
+
/** Show the tryOpenData.ai watermark. Defaults to true. */
|
|
43
|
+
watermark?: boolean;
|
|
42
44
|
onRowClick?: (row: Record<string, unknown>) => void;
|
|
43
45
|
onStateChange?: (state: TableState) => void;
|
|
44
46
|
externalState?: { sort?: SortState | null; search?: string; page?: number };
|
|
@@ -175,6 +177,7 @@ export function createTable(
|
|
|
175
177
|
height: 600,
|
|
176
178
|
theme: options?.theme,
|
|
177
179
|
darkMode,
|
|
180
|
+
watermark: options?.watermark,
|
|
178
181
|
sort: state.sort ?? undefined,
|
|
179
182
|
search: state.search || undefined,
|
|
180
183
|
page: state.page,
|
package/src/table-renderer.ts
CHANGED
|
@@ -390,18 +390,20 @@ export function renderTable(
|
|
|
390
390
|
wrapper.appendChild(liveRegion);
|
|
391
391
|
|
|
392
392
|
// Brand watermark
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
393
|
+
if (layout.watermark) {
|
|
394
|
+
const brandColor = theme ? theme.colors.axis : '#999999';
|
|
395
|
+
const brand = document.createElement('div');
|
|
396
|
+
brand.className = 'oc-table-ref';
|
|
397
|
+
brand.style.cssText = 'text-align: right; padding: 4px 8px;';
|
|
398
|
+
const brandLink = document.createElement('a');
|
|
399
|
+
brandLink.href = BRAND_URL;
|
|
400
|
+
brandLink.target = '_blank';
|
|
401
|
+
brandLink.rel = 'noopener';
|
|
402
|
+
brandLink.style.cssText = `font-size: ${BRAND_FONT_SIZE}px; font-weight: 600; color: ${brandColor}; opacity: 0.55; text-decoration: none; font-family: ${theme ? theme.fonts.family : 'sans-serif'};`;
|
|
403
|
+
brandLink.textContent = 'tryOpenData.ai';
|
|
404
|
+
brand.appendChild(brandLink);
|
|
405
|
+
wrapper.appendChild(brand);
|
|
406
|
+
}
|
|
405
407
|
|
|
406
408
|
// Animation: stamp CSS custom properties and add oc-animate class BEFORE
|
|
407
409
|
// DOM insertion to avoid a flash of final state.
|