@opendata-ai/openchart-engine 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.
@@ -215,6 +215,7 @@ function normalizeChartSpec(spec: ChartSpec, warnings: string[]): NormalizedChar
215
215
  darkMode: spec.darkMode ?? 'off',
216
216
  hiddenSeries: spec.hiddenSeries ?? [],
217
217
  seriesStyles: spec.seriesStyles ?? {},
218
+ watermark: spec.watermark ?? true,
218
219
  };
219
220
  }
220
221
 
@@ -233,6 +234,7 @@ function normalizeTableSpec(spec: TableSpec, _warnings: string[]): NormalizedTab
233
234
  compact: spec.compact ?? false,
234
235
  responsive: spec.responsive ?? true,
235
236
  animation: spec.animation,
237
+ watermark: spec.watermark ?? true,
236
238
  };
237
239
  }
238
240
 
@@ -254,6 +256,7 @@ function normalizeSankeySpec(spec: SankeySpec, _warnings: string[]): NormalizedS
254
256
  animation: spec.animation,
255
257
  valueFormat: spec.valueFormat,
256
258
  linkOpacity: spec.linkOpacity,
259
+ watermark: spec.watermark ?? true,
257
260
  };
258
261
  }
259
262
 
@@ -282,6 +285,7 @@ function normalizeGraphSpec(spec: GraphSpec, _warnings: string[]): NormalizedGra
282
285
  annotations: normalizeAnnotations(spec.annotations),
283
286
  theme: spec.theme ?? {},
284
287
  darkMode: spec.darkMode ?? 'off',
288
+ watermark: spec.watermark ?? true,
285
289
  };
286
290
  }
287
291
 
@@ -338,6 +342,7 @@ export function flattenLayers(
338
342
  parentData?: DataRow[],
339
343
  parentEncoding?: Encoding,
340
344
  parentTransforms?: import('@opendata-ai/openchart-core').Transform[],
345
+ parentWatermark?: boolean,
341
346
  ): ChartSpec[] {
342
347
  const resolvedData = spec.data ?? parentData;
343
348
  const resolvedEncoding: Encoding | undefined =
@@ -345,13 +350,23 @@ export function flattenLayers(
345
350
  ? { ...parentEncoding, ...spec.encoding }
346
351
  : (spec.encoding ?? parentEncoding);
347
352
  const resolvedTransforms = [...(parentTransforms ?? []), ...(spec.transform ?? [])];
353
+ // Layer-level watermark propagates to children (child can still override)
354
+ const resolvedWatermark = spec.watermark ?? parentWatermark;
348
355
 
349
356
  const leaves: ChartSpec[] = [];
350
357
 
351
358
  for (const child of spec.layer) {
352
359
  if (isLayerSpec(child)) {
353
360
  // Nested layer: recurse with merged context
354
- leaves.push(...flattenLayers(child, resolvedData, resolvedEncoding, resolvedTransforms));
361
+ leaves.push(
362
+ ...flattenLayers(
363
+ child,
364
+ resolvedData,
365
+ resolvedEncoding,
366
+ resolvedTransforms,
367
+ resolvedWatermark,
368
+ ),
369
+ );
355
370
  } else {
356
371
  // Leaf ChartSpec: merge inherited properties
357
372
  const mergedData = child.data ?? resolvedData ?? [];
@@ -365,6 +380,10 @@ export function flattenLayers(
365
380
  data: mergedData,
366
381
  encoding: mergedEncoding,
367
382
  transform: mergedTransforms.length > 0 ? mergedTransforms : undefined,
383
+ // Inherit parent watermark if child doesn't explicitly set one
384
+ ...(child.watermark === undefined && resolvedWatermark !== undefined
385
+ ? { watermark: resolvedWatermark }
386
+ : {}),
368
387
  });
369
388
  }
370
389
  }
@@ -78,6 +78,8 @@ export interface NormalizedChartSpec {
78
78
  responsive: boolean;
79
79
  theme: ThemeConfig;
80
80
  darkMode: DarkMode;
81
+ /** Whether the tryOpenData.ai watermark is enabled. */
82
+ watermark: boolean;
81
83
  /** Series names to hide from rendering. */
82
84
  hiddenSeries: string[];
83
85
  /** Per-series visual style overrides. */
@@ -93,6 +95,7 @@ export interface NormalizedTableSpec {
93
95
  chrome: NormalizedChrome;
94
96
  theme: ThemeConfig;
95
97
  darkMode: DarkMode;
98
+ watermark: boolean;
96
99
  search: boolean;
97
100
  pagination: boolean | { pageSize: number };
98
101
  stickyFirstColumn: boolean;
@@ -113,6 +116,7 @@ export interface NormalizedGraphSpec {
113
116
  annotations: Annotation[];
114
117
  theme: ThemeConfig;
115
118
  darkMode: DarkMode;
119
+ watermark: boolean;
116
120
  }
117
121
 
118
122
  /** Discriminated union of all normalized spec types. */
@@ -194,6 +194,10 @@ export function compileGraph(spec: unknown, options: CompileOptions): GraphCompi
194
194
 
195
195
  const graphSpec = normalized as NormalizedGraphSpec;
196
196
 
197
+ // Resolve watermark: explicit spec value wins, then options fallback, then default true.
198
+ const rawWatermark = (spec as Record<string, unknown>).watermark;
199
+ const watermark = rawWatermark !== undefined ? graphSpec.watermark : (options.watermark ?? true);
200
+
197
201
  // 2. Resolve theme
198
202
  const mergedThemeConfig = options.theme
199
203
  ? { ...graphSpec.theme, ...options.theme }
@@ -288,6 +292,9 @@ export function compileGraph(spec: unknown, options: CompileOptions): GraphCompi
288
292
  theme,
289
293
  options.width,
290
294
  options.measureText,
295
+ 'full',
296
+ undefined,
297
+ watermark,
291
298
  );
292
299
 
293
300
  // 12. Return compilation
@@ -304,6 +311,7 @@ export function compileGraph(spec: unknown, options: CompileOptions): GraphCompi
304
311
  height: options.height,
305
312
  },
306
313
  simulationConfig,
314
+ watermark,
307
315
  };
308
316
  }
309
317
 
@@ -101,4 +101,6 @@ export interface GraphCompilation {
101
101
  dimensions: { width: number; height: number };
102
102
  /** Force simulation configuration. */
103
103
  simulationConfig: SimulationConfig;
104
+ /** Whether to show the brand watermark. */
105
+ watermark: boolean;
104
106
  }
@@ -96,6 +96,7 @@ export function computeDimensions(
96
96
  legendLayout: LegendLayout,
97
97
  theme: ResolvedTheme,
98
98
  strategy?: LayoutStrategy,
99
+ watermark: boolean = true,
99
100
  ): LayoutDimensions {
100
101
  const { width, height } = options;
101
102
 
@@ -111,6 +112,7 @@ export function computeDimensions(
111
112
  options.measureText,
112
113
  chromeMode,
113
114
  padding,
115
+ watermark,
114
116
  );
115
117
 
116
118
  // Start with the total rect
@@ -334,6 +336,7 @@ export function computeDimensions(
334
336
  options.measureText,
335
337
  fallbackMode as 'compact' | 'hidden',
336
338
  padding,
339
+ watermark,
337
340
  );
338
341
 
339
342
  // Recalculate top/bottom margins with stripped chrome
@@ -99,8 +99,8 @@ export interface ResolvedScales {
99
99
  y?: ResolvedScale;
100
100
  color?: ResolvedScale;
101
101
  size?: ResolvedScale;
102
- /** Default color for single-series charts (first categorical palette color). */
103
- defaultColor?: string;
102
+ /** Default color for single-series charts (first categorical palette color or markDef.fill gradient). */
103
+ defaultColor?: string | import('@opendata-ai/openchart-core').GradientDef;
104
104
  }
105
105
 
106
106
  // ---------------------------------------------------------------------------
@@ -150,6 +150,7 @@ export function computeLegend(
150
150
  strategy: LayoutStrategy,
151
151
  theme: ResolvedTheme,
152
152
  chartArea: Rect,
153
+ watermark: boolean = true,
153
154
  ): LegendLayout {
154
155
  // Legend explicitly hidden via show: false, or height strategy says no legend
155
156
  if (spec.legend?.show === false || strategy.legendMaxHeight === 0) {
@@ -259,7 +260,8 @@ export function computeLegend(
259
260
 
260
261
  // Top/bottom-positioned legend: horizontal flow with overflow protection.
261
262
  // Reserve space on the right so legend entries don't overlap the brand watermark.
262
- const availableWidth = chartArea.width - LEGEND_PADDING * 2 - BRAND_RESERVE_WIDTH;
263
+ const availableWidth =
264
+ chartArea.width - LEGEND_PADDING * 2 - (watermark ? BRAND_RESERVE_WIDTH : 0);
263
265
 
264
266
  // Apply symbolLimit first if set (minimum 1), then fit remaining entries to available rows.
265
267
  if (spec.legend?.symbolLimit != null) {
@@ -211,6 +211,10 @@ export function compileSankey(spec: unknown, options: CompileOptions): SankeyLay
211
211
 
212
212
  const sankeySpec = normalized as NormalizedSankeySpec;
213
213
 
214
+ // Resolve watermark: explicit spec value wins, then options fallback, then default true.
215
+ const rawWatermark = (spec as Record<string, unknown>).watermark;
216
+ const watermark = rawWatermark !== undefined ? sankeySpec.watermark : (options.watermark ?? true);
217
+
214
218
  // 2. Resolve theme
215
219
  const mergedThemeConfig = options.theme
216
220
  ? { ...sankeySpec.theme, ...options.theme }
@@ -241,6 +245,9 @@ export function compileSankey(spec: unknown, options: CompileOptions): SankeyLay
241
245
  theme,
242
246
  options.width,
243
247
  options.measureText,
248
+ 'full',
249
+ undefined,
250
+ watermark,
244
251
  );
245
252
 
246
253
  // 4. Compute drawing area (total space minus chrome)
@@ -254,7 +261,7 @@ export function compileSankey(spec: unknown, options: CompileOptions): SankeyLay
254
261
 
255
262
  // Guard against negative dimensions
256
263
  if (fullArea.width <= 0 || fullArea.height <= 0) {
257
- return emptyLayout(fullArea, chrome, theme, options);
264
+ return emptyLayout(fullArea, chrome, theme, options, watermark);
258
265
  }
259
266
 
260
267
  // 5. Extract encoding fields
@@ -298,7 +305,7 @@ export function compileSankey(spec: unknown, options: CompileOptions): SankeyLay
298
305
  };
299
306
 
300
307
  if (area.height <= 0) {
301
- return emptyLayout(area, chrome, theme, options);
308
+ return emptyLayout(area, chrome, theme, options, watermark);
302
309
  }
303
310
 
304
311
  // 6. Run d3-sankey layout (may re-run once if labels overflow)
@@ -473,6 +480,7 @@ export function compileSankey(spec: unknown, options: CompileOptions): SankeyLay
473
480
  height: options.height,
474
481
  },
475
482
  animation: resolvedAnimation,
483
+ watermark,
476
484
  };
477
485
  }
478
486
 
@@ -631,6 +639,7 @@ function emptyLayout(
631
639
  chrome: ReturnType<typeof computeChrome>,
632
640
  theme: ResolvedTheme,
633
641
  options: CompileOptions,
642
+ watermark: boolean,
634
643
  ): SankeyLayout {
635
644
  return {
636
645
  area,
@@ -664,5 +673,6 @@ function emptyLayout(
664
673
  width: options.width,
665
674
  height: options.height,
666
675
  },
676
+ watermark,
667
677
  };
668
678
  }
@@ -33,6 +33,7 @@ export interface NormalizedSankeySpec {
33
33
  legend?: LegendConfig;
34
34
  theme: ThemeConfig;
35
35
  darkMode: DarkMode;
36
+ watermark: boolean;
36
37
  animation?: AnimationSpec;
37
38
  valueFormat?: string;
38
39
  linkOpacity?: number;
@@ -382,6 +382,7 @@ export function compileTableLayout(
382
382
  });
383
383
 
384
384
  // 9. Compute chrome
385
+ const watermark = spec.watermark;
385
386
  const chrome = computeChrome(
386
387
  {
387
388
  title: spec.chrome.title,
@@ -393,6 +394,9 @@ export function compileTableLayout(
393
394
  theme,
394
395
  options.width,
395
396
  options.measureText,
397
+ 'full',
398
+ undefined,
399
+ watermark,
396
400
  );
397
401
 
398
402
  // 10. Build a11y
@@ -418,5 +422,6 @@ export function compileTableLayout(
418
422
  },
419
423
  theme,
420
424
  animation: resolveAnimation(spec.animation),
425
+ watermark,
421
426
  };
422
427
  }
@@ -19,7 +19,12 @@ import type {
19
19
  TooltipContent,
20
20
  TooltipField,
21
21
  } from '@opendata-ai/openchart-core';
22
- import { buildTemporalFormatter, formatDate, formatNumber } from '@opendata-ai/openchart-core';
22
+ import {
23
+ buildTemporalFormatter,
24
+ formatDate,
25
+ formatNumber,
26
+ getRepresentativeColor,
27
+ } from '@opendata-ai/openchart-core';
23
28
  import { format as d3Format } from 'd3-format';
24
29
 
25
30
  import type { NormalizedChartSpec } from '../compiler/types';
@@ -155,7 +160,7 @@ function tooltipsForPoint(
155
160
  markIndex: number,
156
161
  ): Array<[string, TooltipContent]> {
157
162
  const title = getTooltipTitle(mark.data, encoding);
158
- const fields = buildFields(mark.data, encoding, mark.fill);
163
+ const fields = buildFields(mark.data, encoding, getRepresentativeColor(mark.fill));
159
164
 
160
165
  return [[`point-${markIndex}`, { title, fields }]];
161
166
  }
@@ -166,7 +171,7 @@ function tooltipsForRect(
166
171
  markIndex: number,
167
172
  ): Array<[string, TooltipContent]> {
168
173
  const title = getTooltipTitle(mark.data, encoding);
169
- const fields = buildFields(mark.data, encoding, mark.fill);
174
+ const fields = buildFields(mark.data, encoding, getRepresentativeColor(mark.fill));
170
175
 
171
176
  return [[`rect-${markIndex}`, { title, fields }]];
172
177
  }
@@ -187,14 +192,14 @@ function tooltipsForArc(
187
192
  fields.push({
188
193
  label: categoryName,
189
194
  value: formatValue(row[encoding.y.field], encoding.y.type, encoding.y.axis?.format),
190
- color: mark.fill,
195
+ color: getRepresentativeColor(mark.fill),
191
196
  });
192
197
  }
193
198
  } else if (encoding.y) {
194
199
  fields.push({
195
200
  label: encoding.y.field,
196
201
  value: formatValue(row[encoding.y.field], encoding.y.type, encoding.y.axis?.format),
197
- color: mark.fill,
202
+ color: getRepresentativeColor(mark.fill),
198
203
  });
199
204
  }
200
205
 
@@ -214,7 +219,7 @@ function tooltipsForArea(
214
219
  for (const dp of mark.dataPoints) {
215
220
  dp.tooltip = {
216
221
  title: getTooltipTitle(dp.datum, encoding),
217
- fields: buildFields(dp.datum, encoding, mark.fill),
222
+ fields: buildFields(dp.datum, encoding, getRepresentativeColor(mark.fill)),
218
223
  };
219
224
  }
220
225
  }