@opendata-ai/openchart-vanilla 8.4.0 → 8.4.1

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.
@@ -9,6 +9,14 @@ function setAttrs(el, attrs) {
9
9
  el.setAttribute(key, String(value));
10
10
  }
11
11
  }
12
+ function applyKnockoutHalo(el, color, fontSize) {
13
+ setAttrs(el, {
14
+ stroke: color,
15
+ "stroke-width": Math.round(fontSize * 0.3),
16
+ "stroke-linejoin": "round",
17
+ "paint-order": "stroke"
18
+ });
19
+ }
12
20
  function applyTextStyle(el, style) {
13
21
  const inline = el.style;
14
22
  inline.setProperty("fill", style.fill);
@@ -111,8 +119,9 @@ export {
111
119
  XLINK_NS,
112
120
  createSVGElement,
113
121
  setAttrs,
122
+ applyKnockoutHalo,
114
123
  applyTextStyle,
115
124
  buildThemeStyleBlock,
116
125
  injectThemeStyleBlock
117
126
  };
118
- //# sourceMappingURL=chunk-HGRNXLDF.js.map
127
+ //# sourceMappingURL=chunk-5RX6ZQGZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/renderers/svg-dom.ts","../src/theme-style-block.ts"],"sourcesContent":["/**\n * Shared SVG DOM helpers used across the per-concern renderers.\n *\n * Pure, stateless utilities. No layout/theme knowledge.\n */\n\nimport type { TextStyle } from '@opendata-ai/openchart-core';\n\nexport const SVG_NS = 'http://www.w3.org/2000/svg';\nexport const XLINK_NS = 'http://www.w3.org/1999/xlink';\n\nexport function createSVGElement(tag: string): SVGElement {\n return document.createElementNS(SVG_NS, tag);\n}\n\nexport function setAttrs(el: SVGElement, attrs: Record<string, string | number>): void {\n for (const [key, value] of Object.entries(attrs)) {\n el.setAttribute(key, String(value));\n }\n}\n\n/**\n * Cut a knockout halo behind text so it stays legible where it crosses\n * gridlines, marks, or a neighbouring series: a surface-colored stroke painted\n * under the glyphs.\n *\n * Presentation attributes, not inline styles, so a stylesheet can still\n * override the halo. The width scales with the font size -- a fixed width rings\n * small glyphs and disappears behind large ones.\n */\nexport function applyKnockoutHalo(el: SVGElement, color: string, fontSize: number): void {\n setAttrs(el, {\n stroke: color,\n 'stroke-width': Math.round(fontSize * 0.3),\n 'stroke-linejoin': 'round',\n 'paint-order': 'stroke',\n });\n}\n\nexport function applyTextStyle(el: SVGElement, style: TextStyle): void {\n // Use inline styles so engine-computed values take priority over CSS class\n // defaults (e.g. .oc-title { font-size: var(--oc-title-size) } would otherwise\n // override the responsive scaling applied by the chrome layout).\n const inline = (el as SVGElement & ElementCSSInlineStyle).style;\n inline.setProperty('fill', style.fill);\n inline.setProperty('font-size', `${style.fontSize}px`);\n inline.setProperty('font-weight', String(style.fontWeight));\n inline.setProperty('font-family', style.fontFamily);\n if (style.textAnchor) {\n el.setAttribute('text-anchor', style.textAnchor);\n }\n if (style.dominantBaseline) {\n el.setAttribute('dominant-baseline', style.dominantBaseline);\n }\n if (style.fontVariant) {\n el.setAttribute('font-variant', style.fontVariant);\n }\n}\n","/**\n * Theme `<style>` block for self-contained SVG export.\n *\n * Charts render most fills as inline SVG attributes, but a handful of chrome\n * elements (metric cells, the brand watermark dot, legend text, endpoint labels)\n * take their fill from CSS classes in `chrome.css` via `--oc-*` variables. That\n * works on-screen because the live SVG inherits the page stylesheet — but a\n * serialized/rasterized export (PNG/JPG/GIF) is detached from the page, so those\n * class-based fills vanish. This module builds a `<style>` block that resolves\n * every such rule against a concrete `ResolvedTheme`, so injecting it into an\n * export clone makes the SVG fully self-contained.\n *\n * Both the headless renderer (`static.ts`) and the browser export path use this,\n * so class-based styling round-trips identically no matter which path produced\n * the SVG. Browser-safe (no Node imports) so the browser exporters can import it.\n */\n\nimport type { ResolvedTheme } from '@opendata-ai/openchart-core';\nimport { adaptForLightLineStroke, cssTokenDefault } from '@opendata-ai/openchart-core';\nimport { SVG_NS } from './renderers/svg-dom';\n\n/**\n * Build the theme `<style>` block CSS text for a resolved theme. Sets the\n * `--oc-*` custom properties on `svg.oc-chart` and defines every class-based\n * chrome rule against them.\n */\nexport function buildThemeStyleBlock(theme: ResolvedTheme): string {\n const accent = theme.colors.categorical[0] ?? cssTokenDefault('--oc-accent', 'light');\n const bg = theme.colors.neutral.surface;\n\n const props = [\n `--oc-font-family: ${theme.fonts.family}`,\n `--oc-font-mono: ${theme.fonts.mono}`,\n `--oc-title-size: ${theme.chrome.title.fontSize}px`,\n `--oc-title-weight: ${theme.chrome.title.fontWeight}`,\n `--oc-title-tracking: ${cssTokenDefault('--oc-title-tracking', 'light')}`,\n `--oc-subtitle-size: ${theme.chrome.subtitle.fontSize}px`,\n `--oc-subtitle-weight: ${theme.chrome.subtitle.fontWeight}`,\n `--oc-source-size: ${theme.chrome.source.fontSize}px`,\n `--oc-source-weight: ${theme.chrome.source.fontWeight}`,\n `--oc-body-size: ${theme.fonts.sizes.body}px`,\n `--oc-eyebrow-size: ${theme.chrome.eyebrow.fontSize}px`,\n `--oc-eyebrow-weight: ${theme.chrome.eyebrow.fontWeight}`,\n `--oc-eyebrow-tracking: ${cssTokenDefault('--oc-eyebrow-tracking', 'light')}`,\n `--oc-bg: ${bg}`,\n `--oc-text: ${theme.colors.text}`,\n `--oc-text-muted: ${theme.colors.axis}`,\n `--oc-text-secondary: ${theme.colors.neutral.secondary}`,\n `--oc-text-faint: ${theme.colors.neutral.faint}`,\n `--oc-border: ${theme.colors.neutral.border}`,\n `--oc-gridline: ${theme.colors.gridline}`,\n // Hairline, not the tick-label ink.\n `--oc-axis: ${theme.colors.hairline}`,\n `--oc-border-radius: ${theme.borderRadius}px`,\n `--oc-accent: ${accent}`,\n `--oc-accent-strong: ${adaptForLightLineStroke(accent)}`,\n `--oc-positive: ${theme.colors.positive}`,\n `--oc-negative: ${theme.colors.negative}`,\n `--oc-legend-text: ${theme.isDark ? cssTokenDefault('--oc-legend-text', 'dark') : cssTokenDefault('--oc-legend-text', 'light')}`,\n `--oc-space-2: ${theme.spacing.chromeGap * 2}px`,\n `--oc-space-4: ${theme.spacing.padding}px`,\n ];\n\n const rules = [\n // Tabular figures everywhere in the chart's own text: axis ticks, value\n // labels and legend entries all line up column-wise.\n `svg.oc-chart { ${props.join('; ')}; font-variant-numeric: tabular-nums; }`,\n `.oc-chrome { font-family: var(--oc-font-family); }`,\n `.oc-eyebrow { font-size: var(--oc-eyebrow-size); font-weight: var(--oc-eyebrow-weight); letter-spacing: var(--oc-eyebrow-tracking); text-transform: uppercase; fill: var(--oc-accent); }`,\n `.oc-title { font-size: var(--oc-title-size); font-weight: var(--oc-title-weight); letter-spacing: var(--oc-title-tracking); fill: var(--oc-text); }`,\n `.oc-subtitle { font-size: var(--oc-subtitle-size); font-weight: var(--oc-subtitle-weight); fill: var(--oc-text-muted); }`,\n `.oc-source, .oc-byline, .oc-footer { font-size: var(--oc-source-size); font-weight: var(--oc-source-weight); fill: var(--oc-text-muted); }`,\n `.oc-brand { font-size: 11px; font-weight: 500; letter-spacing: 0.02em; fill: var(--oc-text-faint); }`,\n `.oc-brand-dot { fill: var(--oc-accent); }`,\n `.oc-eyebrow-dot { fill: var(--oc-accent); }`,\n `.oc-metrics { font-family: var(--oc-font-family); }`,\n `.oc-metric-label { font-size: 11px; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; fill: var(--oc-text-muted); }`,\n `.oc-metric-value { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; fill: var(--oc-text); font-variant-numeric: tabular-nums; }`,\n `.oc-metric-delta-up { fill: var(--oc-positive); font-size: 12px; font-weight: 500; }`,\n `.oc-metric-delta-down { fill: var(--oc-negative); font-size: 12px; font-weight: 500; }`,\n `.oc-axis-tick-inline { font-size: 11px; font-weight: 400; fill: var(--oc-text-muted); }`,\n `.oc-endpoint-labels { font-family: var(--oc-font-family); }`,\n `.oc-endpoint-label { fill: var(--oc-endpoint-label-color, var(--oc-text)); }`,\n `.oc-endpoint-value { fill: var(--oc-endpoint-value-color, var(--oc-text-muted)); }`,\n `.oc-endpoint-leader { stroke: var(--oc-endpoint-leader-color, currentColor); }`,\n `.oc-annotation-subtitle { fill: var(--oc-annotation-subtitle-color, var(--oc-text-muted)); }`,\n `.oc-metric-secondary { fill: var(--oc-text-muted); font-size: 12px; font-weight: 400; }`,\n `.oc-legend { font-family: var(--oc-font-family); font-size: var(--oc-body-size); }`,\n `.oc-legend-entry { cursor: default; }`,\n `.oc-legend text { fill: var(--oc-legend-text); }`,\n ];\n\n return rules.join('\\n');\n}\n\n/**\n * Inject the theme style block into an SVG clone's `<defs>` so class-based fills\n * survive serialization. Idempotent-ish: adds one `<style data-oc-theme>` at the\n * front of `<defs>`; call once per clone before serializing. No-op if `theme` is\n * undefined (nothing to resolve against).\n */\nexport function injectThemeStyleBlock(svg: SVGElement, theme: ResolvedTheme | undefined): void {\n if (!theme) return;\n let defs = svg.querySelector('defs');\n if (!defs) {\n defs = document.createElementNS(SVG_NS, 'defs');\n svg.insertBefore(defs, svg.firstChild);\n }\n const style = document.createElementNS(SVG_NS, 'style');\n style.setAttribute('data-oc-theme', '');\n style.textContent = buildThemeStyleBlock(theme);\n defs.insertBefore(style, defs.firstChild);\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  injectThemeStyleBlock
3
- } from "./chunk-HGRNXLDF.js";
3
+ } from "./chunk-5RX6ZQGZ.js";
4
4
 
5
5
  // src/export.ts
6
6
  function getSVGDimensions(svg) {
@@ -279,4 +279,4 @@ export {
279
279
  exportJPG,
280
280
  exportCSV
281
281
  };
282
- //# sourceMappingURL=chunk-UOB422LY.js.map
282
+ //# sourceMappingURL=chunk-6B5OBGIO.js.map
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  SVG_NS,
3
3
  XLINK_NS,
4
+ applyKnockoutHalo,
4
5
  applyTextStyle,
5
6
  createSVGElement,
6
7
  setAttrs
7
- } from "./chunk-HGRNXLDF.js";
8
+ } from "./chunk-5RX6ZQGZ.js";
8
9
 
9
10
  // src/svg-ids.ts
10
11
  var counter = 0;
@@ -156,15 +157,22 @@ function resolvePatternFill(pattern, patternMap) {
156
157
  var currentAnimation;
157
158
  var currentGradientMap = /* @__PURE__ */ new Map();
158
159
  var currentPatternMap = /* @__PURE__ */ new Map();
160
+ var currentSurface = "";
159
161
  function setMarkRenderState(state) {
160
162
  currentAnimation = state.animation;
161
163
  currentGradientMap = state.gradientMap;
162
164
  currentPatternMap = state.patternMap ?? /* @__PURE__ */ new Map();
165
+ currentSurface = state.surface ?? "";
163
166
  }
164
167
  function resetMarkRenderState() {
165
168
  currentAnimation = void 0;
166
169
  currentGradientMap = /* @__PURE__ */ new Map();
167
170
  currentPatternMap = /* @__PURE__ */ new Map();
171
+ currentSurface = "";
172
+ }
173
+ function applyLabelHalo(text, label) {
174
+ if (!currentSurface || label.halo === false) return;
175
+ applyKnockoutHalo(text, currentSurface, label.style.fontSize);
168
176
  }
169
177
  function resolveFillOrPattern(fill, pattern) {
170
178
  if (pattern) return resolvePatternFill(pattern, currentPatternMap);
@@ -217,6 +225,7 @@ function renderLineMark(mark, index) {
217
225
  }
218
226
  setAttrs(label, { x: mark.label.x, y: mark.label.y });
219
227
  applyTextStyle(label, mark.label.style);
228
+ applyLabelHalo(label, mark.label);
220
229
  label.textContent = mark.label.text;
221
230
  g.appendChild(label);
222
231
  if (mark.label.connector) {
@@ -345,6 +354,7 @@ function renderArcMark(mark, index) {
345
354
  y: mark.label.y - mark.center.y
346
355
  });
347
356
  applyTextStyle(label, mark.label.style);
357
+ applyLabelHalo(label, mark.label);
348
358
  label.textContent = mark.label.text;
349
359
  g.appendChild(label);
350
360
  }
@@ -529,6 +539,7 @@ function renderMarks(parent, layout, opts) {
529
539
  label.setAttribute("class", "oc-mark-label");
530
540
  setAttrs(label, { x: rect.label.x, y: rect.label.y });
531
541
  applyTextStyle(label, rect.label.style);
542
+ applyLabelHalo(label, rect.label);
532
543
  label.textContent = rect.label.text;
533
544
  if (currentAnimation?.enter) {
534
545
  const idx = rect.animationIndex ?? i;
@@ -574,6 +585,61 @@ import {
574
585
  DEFAULT_LINE_HEIGHT,
575
586
  parseAnnotationSpans
576
587
  } from "@opendata-ai/openchart-engine";
588
+
589
+ // src/theme-tokens.ts
590
+ import { adaptForLightLineStroke, cssTokenDefault } from "@opendata-ai/openchart-core";
591
+ function resolvedSurface(theme) {
592
+ return theme.colors.neutral.surface;
593
+ }
594
+ function stampThemeProperties(el, theme) {
595
+ const accent = theme.colors.categorical[0] ?? cssTokenDefault("--oc-accent", "light");
596
+ const bg = resolvedSurface(theme);
597
+ const n = theme.colors.neutral;
598
+ const props = [
599
+ ["--oc-font-family", theme.fonts.family],
600
+ ["--oc-font-mono", theme.fonts.mono],
601
+ ["--oc-title-size", `${theme.chrome.title.fontSize}px`],
602
+ ["--oc-title-weight", `${theme.chrome.title.fontWeight}`],
603
+ ["--oc-subtitle-size", `${theme.chrome.subtitle.fontSize}px`],
604
+ ["--oc-subtitle-weight", `${theme.chrome.subtitle.fontWeight}`],
605
+ ["--oc-source-size", `${theme.chrome.source.fontSize}px`],
606
+ ["--oc-source-weight", `${theme.chrome.source.fontWeight}`],
607
+ ["--oc-body-size", `${theme.fonts.sizes.body}px`],
608
+ ["--oc-eyebrow-size", `${theme.chrome.eyebrow.fontSize}px`],
609
+ ["--oc-eyebrow-weight", `${theme.chrome.eyebrow.fontWeight}`],
610
+ ["--oc-bg", bg],
611
+ ["--oc-text", theme.colors.text],
612
+ ["--oc-text-muted", theme.colors.axis],
613
+ // Secondary grays are derived from the theme's own text/background pair
614
+ // (ResolvedTheme.colors.neutral), so a warm or cool theme gets warm or
615
+ // cool grays instead of zinc. One direction only: theme -> CSS.
616
+ ["--oc-text-secondary", n.secondary],
617
+ ["--oc-text-faint", n.faint],
618
+ ["--oc-border", n.border],
619
+ ["--oc-gray-100", n[100]],
620
+ ["--oc-gray-200", n[200]],
621
+ ["--oc-gray-300", n[300]],
622
+ ["--oc-gray-400", n[400]],
623
+ ["--oc-gray-600", n[600]],
624
+ ["--oc-gray-800", n[800]],
625
+ ["--oc-gridline", theme.colors.gridline],
626
+ // --oc-axis is the hairline the axis line is drawn with, not the ink its
627
+ // tick labels take (that is --oc-text-muted, above).
628
+ ["--oc-axis", theme.colors.hairline],
629
+ ["--oc-border-radius", `${theme.borderRadius}px`],
630
+ ["--oc-accent", accent],
631
+ ["--oc-accent-strong", adaptForLightLineStroke(accent)],
632
+ ["--oc-positive", theme.colors.positive],
633
+ ["--oc-negative", theme.colors.negative],
634
+ ["--oc-space-2", `${theme.spacing.chromeGap * 2}px`],
635
+ ["--oc-space-4", `${theme.spacing.padding}px`]
636
+ ];
637
+ for (const [name, value] of props) {
638
+ el.style.setProperty(name, value);
639
+ }
640
+ }
641
+
642
+ // src/renderers/annotations.ts
577
643
  var CONNECTOR_STROKE_WIDTH = 1.25;
578
644
  function renderArrowhead(parent, tipX, tipY, tangentX, tangentY, stroke) {
579
645
  const head = computeArrowheadPoints(tipX, tipY, tangentX, tangentY);
@@ -801,10 +867,7 @@ function renderAnnotation(parent, annotation, index, bgColor) {
801
867
  });
802
868
  g.appendChild(bgRect);
803
869
  } else if (bgColor && annotation.label.halo !== false) {
804
- text.style.paintOrder = "stroke";
805
- text.style.stroke = bgColor;
806
- text.style.strokeWidth = `${Math.round(fontSize * 0.3)}px`;
807
- text.style.strokeLinejoin = "round";
870
+ applyKnockoutHalo(text, bgColor, fontSize);
808
871
  }
809
872
  g.appendChild(text);
810
873
  if (annotation.subtitle) {
@@ -824,7 +887,7 @@ function renderAnnotations(parent, layout) {
824
887
  if (layout.annotations.length === 0) return;
825
888
  const g = createSVGElement("g");
826
889
  g.setAttribute("class", "oc-annotations");
827
- const bgColor = layout.theme.colors.background;
890
+ const bgColor = resolvedSurface(layout.theme);
828
891
  for (let i = 0; i < layout.annotations.length; i++) {
829
892
  renderAnnotation(g, layout.annotations[i], i, bgColor);
830
893
  }
@@ -1433,6 +1496,29 @@ var LEGEND_HIT_PAD = 4;
1433
1496
  function isCategorical(legend) {
1434
1497
  return !legend.type || legend.type === "categorical";
1435
1498
  }
1499
+ function renderNoDataSwatch(parent, noData, labelStyle, chip) {
1500
+ const swatch = createSVGElement("rect");
1501
+ swatch.setAttribute("class", "oc-legend-nodata");
1502
+ setAttrs(swatch, {
1503
+ x: noData.x,
1504
+ y: noData.y,
1505
+ width: noData.size,
1506
+ height: noData.size,
1507
+ fill: noData.fill,
1508
+ ...chip ? { rx: chip.rx, ry: chip.rx } : { "shape-rendering": "crispEdges" }
1509
+ });
1510
+ parent.appendChild(swatch);
1511
+ const label = createSVGElement("text");
1512
+ setAttrs(label, {
1513
+ x: noData.labelX,
1514
+ y: noData.labelY,
1515
+ "text-anchor": "start",
1516
+ ...chip ? { "dominant-baseline": chip.baseline } : {}
1517
+ });
1518
+ applyTextStyle(label, labelStyle);
1519
+ label.textContent = noData.label;
1520
+ parent.appendChild(label);
1521
+ }
1436
1522
  function renderContinuousLegend(parent, legend) {
1437
1523
  if (legend.bounds.width <= 0 || legend.bounds.height <= 0) return;
1438
1524
  const g = createSVGElement("g");
@@ -1511,26 +1597,7 @@ function renderContinuousLegend(parent, legend) {
1511
1597
  g.appendChild(label);
1512
1598
  }
1513
1599
  if (legend.noData) {
1514
- const swatch = createSVGElement("rect");
1515
- swatch.setAttribute("class", "oc-legend-nodata");
1516
- setAttrs(swatch, {
1517
- x: legend.noData.x,
1518
- y: legend.noData.y,
1519
- width: legend.noData.size,
1520
- height: legend.noData.size,
1521
- fill: legend.noData.fill,
1522
- "shape-rendering": "crispEdges"
1523
- });
1524
- g.appendChild(swatch);
1525
- const label = createSVGElement("text");
1526
- setAttrs(label, {
1527
- x: legend.noData.labelX,
1528
- y: legend.noData.labelY,
1529
- "text-anchor": "start"
1530
- });
1531
- applyTextStyle(label, legend.labelStyle);
1532
- label.textContent = legend.noData.label;
1533
- g.appendChild(label);
1600
+ renderNoDataSwatch(g, legend.noData, legend.labelStyle);
1534
1601
  }
1535
1602
  parent.appendChild(g);
1536
1603
  }
@@ -1720,6 +1787,9 @@ function renderLegend(parent, legend) {
1720
1787
  }
1721
1788
  }
1722
1789
  }
1790
+ if (legend.noData) {
1791
+ renderNoDataSwatch(g, legend.noData, legend.labelStyle, { rx: 2, baseline: "central" });
1792
+ }
1723
1793
  parent.appendChild(g);
1724
1794
  }
1725
1795
 
@@ -1854,7 +1924,12 @@ function renderChartSVG(layout, container, opts) {
1854
1924
  defs
1855
1925
  );
1856
1926
  svg.appendChild(defs);
1857
- setMarkRenderState({ animation, gradientMap, patternMap });
1927
+ setMarkRenderState({
1928
+ animation,
1929
+ gradientMap,
1930
+ patternMap,
1931
+ surface: resolvedSurface(layout.theme)
1932
+ });
1858
1933
  try {
1859
1934
  if (layout.facet) {
1860
1935
  renderFacetedPanels(svg, layout, layout.facet.panels, defs);
@@ -1928,10 +2003,10 @@ function renderChartSVG(layout, container, opts) {
1928
2003
  clippedGroup.appendChild(dotsGroup);
1929
2004
  }
1930
2005
  svg.appendChild(clipGroup);
2006
+ renderAnnotations(svg, layout);
1931
2007
  if (markLabelsOverlay) {
1932
2008
  svg.appendChild(markLabelsOverlay);
1933
2009
  }
1934
- renderAnnotations(svg, layout);
1935
2010
  renderEndpointLabels(svg, layout);
1936
2011
  const epEntries = layout.endpointLabels?.entries ?? [];
1937
2012
  if (epEntries.length > 0) {
@@ -2022,10 +2097,10 @@ function renderFacetedPanels(svg, layout, panels, defs) {
2022
2097
  clippedGroup.setAttribute("clip-path", `url(#${panelClipId})`);
2023
2098
  const panelLabelsOverlay = renderMarks(clippedGroup, panelLayout);
2024
2099
  g.appendChild(clippedGroup);
2100
+ renderAnnotations(g, panelLayout);
2025
2101
  if (panelLabelsOverlay) {
2026
2102
  g.appendChild(panelLabelsOverlay);
2027
2103
  }
2028
- renderAnnotations(g, panelLayout);
2029
2104
  svg.appendChild(g);
2030
2105
  }
2031
2106
  }
@@ -2036,6 +2111,10 @@ export {
2036
2111
  resetSvgIdCounter,
2037
2112
  buildGradientDefs,
2038
2113
  resolveMarkFill,
2114
+ buildPatternDefs,
2115
+ resolvePatternFill,
2116
+ resolvedSurface,
2117
+ stampThemeProperties,
2039
2118
  renderChromeElement,
2040
2119
  renderLegend,
2041
2120
  registerMarkRenderer,
@@ -2043,4 +2122,4 @@ export {
2043
2122
  renderSingleMark,
2044
2123
  renderChartSVG
2045
2124
  };
2046
- //# sourceMappingURL=chunk-YGYMB7KB.js.map
2125
+ //# sourceMappingURL=chunk-KANNWAEA.js.map