@bendyline/squisq 2.3.3 → 2.4.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.
Files changed (33) hide show
  1. package/dist/{Doc-BrgZC7SE.d.ts → Doc-DLpyOAXJ.d.ts} +87 -2
  2. package/dist/{ImageEditDoc-rum0Xb9P.d.ts → ImageEditDoc-Cu30xb9b.d.ts} +1 -1
  3. package/dist/{chunk-2ZWIXGAC.js → chunk-2S74DPJH.js} +4 -0
  4. package/dist/{chunk-RS5AP3J4.js → chunk-AE3IUKMM.js} +1 -1
  5. package/dist/{chunk-QWCFK5FN.js → chunk-CUYHFOFL.js} +5 -1
  6. package/dist/{chunk-G3JOS25T.js → chunk-D4LPP3P5.js} +43 -14
  7. package/dist/{chunk-GQNH7PLN.js → chunk-F2IEPSMA.js} +1320 -8
  8. package/dist/{chunk-IZLKI3IL.js → chunk-KPBZZVGY.js} +29 -5
  9. package/dist/{chunk-BCCXTMN5.js → chunk-O7JILDEF.js} +7 -0
  10. package/dist/{chunk-SPTY4C6F.js → chunk-REDUXXDJ.js} +1 -1
  11. package/dist/{chunk-ABP4LAJS.js → chunk-ZHLNKB2I.js} +1 -1
  12. package/dist/chunk-ZYO3DBTA.js +983 -0
  13. package/dist/doc/index.d.ts +103 -6
  14. package/dist/doc/index.js +8 -6
  15. package/dist/generate/index.d.ts +1 -1
  16. package/dist/imageEdit/index.d.ts +3 -3
  17. package/dist/index.d.ts +6 -6
  18. package/dist/index.js +29 -11
  19. package/dist/jsonForm/index.d.ts +1 -1
  20. package/dist/jsonForm/index.js +2 -2
  21. package/dist/markdown/index.d.ts +118 -1
  22. package/dist/markdown/index.js +21 -5
  23. package/dist/{materializePageSection-CrZWYnXM.d.ts → materializePageSection-CgNs5Qmw.d.ts} +2 -2
  24. package/dist/narration/index.d.ts +1 -1
  25. package/dist/narration/index.js +4 -4
  26. package/dist/recommend/index.js +2 -2
  27. package/dist/schemas/index.d.ts +4 -4
  28. package/dist/schemas/index.js +2 -2
  29. package/dist/{themeLibrary-DJt89gyP.d.ts → themeLibrary-RWsNtlOu.d.ts} +1 -1
  30. package/dist/transform/index.d.ts +2 -2
  31. package/dist/transform/index.js +2 -2
  32. package/package.json +1 -1
  33. package/dist/chunk-UA5DTYAY.js +0 -400
@@ -21,7 +21,7 @@ import {
21
21
  parseAsciiTimeline,
22
22
  pickAutoTemplate,
23
23
  profileBlockContents
24
- } from "./chunk-QWCFK5FN.js";
24
+ } from "./chunk-CUYHFOFL.js";
25
25
  import {
26
26
  DEFAULT_THEME,
27
27
  FRONTMATTER_CUSTOM_TEMPLATES_KEY,
@@ -57,7 +57,7 @@ import {
57
57
  extractPlainText,
58
58
  readFrontmatterThemeId,
59
59
  walkMarkdownTree
60
- } from "./chunk-BCCXTMN5.js";
60
+ } from "./chunk-O7JILDEF.js";
61
61
  import {
62
62
  resolveBlockTransition
63
63
  } from "./chunk-4VOD55SX.js";
@@ -423,6 +423,16 @@ var TEMPLATE_ALIASES = Object.freeze({
423
423
  function resolveTemplateName(name) {
424
424
  return TEMPLATE_ALIASES[name] ?? name;
425
425
  }
426
+ var TABLE_FED_TEMPLATES = /* @__PURE__ */ new Set([
427
+ "dataTable",
428
+ "barChart",
429
+ "columnChart",
430
+ "pieChart",
431
+ "donutChart",
432
+ "lineChart",
433
+ "areaChart",
434
+ "scatterChart"
435
+ ]);
426
436
  var CONTAINER_TEMPLATES = /* @__PURE__ */ new Set(["diagram", "drawing", "layout"]);
427
437
  function isContainerTemplate(name) {
428
438
  return !!name && CONTAINER_TEMPLATES.has(resolveTemplateName(name));
@@ -949,7 +959,7 @@ function sectionHeader(input, context) {
949
959
 
950
960
  // src/doc/templates/contentBlock.ts
951
961
  function completeBodyText(context) {
952
- return (context.block?.contents ?? []).map((node) => extractPlainText(node)).join("\n").trim();
962
+ return (context.block?.contents ?? []).filter((node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")).map((node) => extractPlainText(node)).join("\n").trim();
953
963
  }
954
964
  function bodyFontSize(body, context) {
955
965
  const base = body.length > 1800 ? 18 : body.length > 1100 ? 21 : body.length > 650 ? 24 : 28;
@@ -2739,6 +2749,32 @@ function deriveTemplateInputs(templateName, headingText, contents, options = {})
2739
2749
  if (tableData) return { ...tableData, ...headingText ? { title: headingText } : {} };
2740
2750
  return placeholders ? { headers: ["Column"], rows: [["Data"]] } : null;
2741
2751
  }
2752
+ case "barChart":
2753
+ case "columnChart":
2754
+ case "pieChart":
2755
+ case "donutChart":
2756
+ case "lineChart":
2757
+ case "areaChart":
2758
+ case "scatterChart": {
2759
+ const tableData = extractTableFromContents(contents);
2760
+ if (tableData) {
2761
+ return {
2762
+ headers: tableData.headers,
2763
+ rows: tableData.rows,
2764
+ ...headingText ? { title: headingText } : {}
2765
+ };
2766
+ }
2767
+ return placeholders && !bodyText3.trim() ? {
2768
+ headers: ["Quarter", "Revenue", "Costs"],
2769
+ rows: [
2770
+ ["Q1", "40", "28"],
2771
+ ["Q2", "55", "31"],
2772
+ ["Q3", "48", "30"],
2773
+ ["Q4", "62", "35"]
2774
+ ],
2775
+ ...headingText ? { title: headingText } : {}
2776
+ } : null;
2777
+ }
2742
2778
  case "imageWithCaption": {
2743
2779
  const img = extractFirstImage(contents);
2744
2780
  if (!img) return placeholders ? { caption: headingText } : null;
@@ -5585,6 +5621,1019 @@ function makeCustomTemplateFn(def) {
5585
5621
  };
5586
5622
  }
5587
5623
 
5624
+ // src/doc/templates/chart/parse.ts
5625
+ var NUMERIC_COLUMN_THRESHOLD = 0.6;
5626
+ function parseCellNumber(raw) {
5627
+ if (typeof raw !== "string") return null;
5628
+ let text = raw.trim();
5629
+ if (!text) return null;
5630
+ let negative = false;
5631
+ const accounting = /^\((.*)\)$/.exec(text);
5632
+ if (accounting) {
5633
+ negative = true;
5634
+ text = accounting[1].trim();
5635
+ }
5636
+ text = text.replace(/^([+-]?)\s*[$€£¥]\s*/, "$1");
5637
+ text = text.replace(/%$/, "").trim();
5638
+ text = text.replace(/,/g, "");
5639
+ if (/^[+-]?\d{1,3}(?: \d{3})+(?:\.\d+)?$/.test(text)) {
5640
+ text = text.replace(/ /g, "");
5641
+ }
5642
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(text)) return null;
5643
+ const value = Number(text);
5644
+ if (!Number.isFinite(value)) return null;
5645
+ return negative ? -value : value;
5646
+ }
5647
+ function isNumericColumn(rows, colIndex) {
5648
+ let nonEmpty = 0;
5649
+ let numeric = 0;
5650
+ for (const row of rows) {
5651
+ const cell = (row[colIndex] ?? "").trim();
5652
+ if (!cell) continue;
5653
+ nonEmpty += 1;
5654
+ if (parseCellNumber(cell) !== null) numeric += 1;
5655
+ }
5656
+ return numeric > 0 && numeric / Math.max(1, nonEmpty) >= NUMERIC_COLUMN_THRESHOLD;
5657
+ }
5658
+ function resolveColumnRef(ref, headers) {
5659
+ const text = String(ref).trim();
5660
+ if (!text) return null;
5661
+ const lower = text.toLowerCase();
5662
+ const byName = headers.findIndex((h) => h.trim().toLowerCase() === lower);
5663
+ if (byName >= 0) return byName;
5664
+ if (/^\d+$/.test(text)) {
5665
+ const index = Number(text);
5666
+ if (index < headers.length) return index;
5667
+ }
5668
+ return null;
5669
+ }
5670
+ function buildChartData(table, options = {}) {
5671
+ const headers = Array.isArray(table.headers) ? table.headers : [];
5672
+ const rows = Array.isArray(table.rows) ? table.rows : [];
5673
+ if (headers.length === 0 || rows.length === 0) return null;
5674
+ const warnings = [];
5675
+ let labelIndex = 0;
5676
+ if (options.labelColumn !== void 0 && String(options.labelColumn).trim() !== "") {
5677
+ const resolved = resolveColumnRef(String(options.labelColumn), headers);
5678
+ if (resolved === null) {
5679
+ warnings.push(
5680
+ `labelColumn "${options.labelColumn}" matches no column; using the first column`
5681
+ );
5682
+ } else {
5683
+ labelIndex = resolved;
5684
+ }
5685
+ }
5686
+ let valueIndexes = [];
5687
+ const requested = (options.valueColumns ?? []).map((ref) => String(ref)).filter((ref) => ref.trim() !== "");
5688
+ if (requested.length > 0) {
5689
+ for (const ref of requested) {
5690
+ const resolved = resolveColumnRef(ref, headers);
5691
+ if (resolved === null) {
5692
+ warnings.push(`valueColumns entry "${ref}" matches no column; skipped`);
5693
+ } else if (!valueIndexes.includes(resolved)) {
5694
+ valueIndexes.push(resolved);
5695
+ }
5696
+ }
5697
+ } else {
5698
+ valueIndexes = headers.map((_, index) => index).filter((index) => index !== labelIndex && isNumericColumn(rows, index));
5699
+ }
5700
+ if (valueIndexes.length === 0) return null;
5701
+ return {
5702
+ labels: rows.map((row) => (row[labelIndex] ?? "").trim()),
5703
+ labelHeader: headers[labelIndex] ?? "",
5704
+ labelNumbers: rows.map((row) => parseCellNumber(row[labelIndex] ?? "")),
5705
+ series: valueIndexes.map((index) => ({
5706
+ name: (headers[index] ?? `Column ${index + 1}`).trim() || `Column ${index + 1}`,
5707
+ values: rows.map((row) => parseCellNumber(row[index] ?? ""))
5708
+ })),
5709
+ warnings
5710
+ };
5711
+ }
5712
+
5713
+ // src/doc/templates/chart/scale.ts
5714
+ function niceNum(range, round4) {
5715
+ const exponent = Math.floor(Math.log10(range));
5716
+ const fraction = range / 10 ** exponent;
5717
+ let niceFraction;
5718
+ if (round4) {
5719
+ if (fraction < 1.5) niceFraction = 1;
5720
+ else if (fraction < 3) niceFraction = 2;
5721
+ else if (fraction < 7) niceFraction = 5;
5722
+ else niceFraction = 10;
5723
+ } else {
5724
+ if (fraction <= 1) niceFraction = 1;
5725
+ else if (fraction <= 2) niceFraction = 2;
5726
+ else if (fraction <= 5) niceFraction = 5;
5727
+ else niceFraction = 10;
5728
+ }
5729
+ return niceFraction * 10 ** exponent;
5730
+ }
5731
+ function niceTicks(min, max, targetCount = 5) {
5732
+ let lo = Number.isFinite(min) ? min : 0;
5733
+ let hi = Number.isFinite(max) ? max : 0;
5734
+ if (lo > hi) [lo, hi] = [hi, lo];
5735
+ if (lo === hi) {
5736
+ const pad = lo === 0 ? 1 : niceNum(Math.abs(lo), true);
5737
+ lo -= pad;
5738
+ hi += pad;
5739
+ if (min >= 0 && lo < 0) lo = 0;
5740
+ }
5741
+ const count = Math.max(2, targetCount);
5742
+ const range = niceNum(hi - lo, false);
5743
+ const step = niceNum(range / (count - 1), true);
5744
+ const niceMin = Math.floor(lo / step) * step;
5745
+ const niceMax = Math.ceil(hi / step) * step;
5746
+ const ticks = [];
5747
+ for (let tick = niceMin; tick <= niceMax + step * 1e-6; tick += step) {
5748
+ ticks.push(Math.abs(tick) < step * 1e-6 ? 0 : Number(tick.toPrecision(12)));
5749
+ }
5750
+ return { ticks, niceMin, niceMax, step };
5751
+ }
5752
+ function formatChartValue(value, unit) {
5753
+ if (!Number.isFinite(value)) return "\u2014";
5754
+ const sign = value < 0 ? "-" : "";
5755
+ const abs = Math.abs(value);
5756
+ let text;
5757
+ if (abs >= 1e6) {
5758
+ text = `${sign}${trimTrailingZero((abs / 1e6).toFixed(1))}M`;
5759
+ } else if (abs >= 1e3) {
5760
+ text = `${sign}${trimTrailingZero((abs / 1e3).toFixed(abs >= 1e4 ? 0 : 1))}K`;
5761
+ } else {
5762
+ text = `${sign}${abs.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
5763
+ }
5764
+ return unit ? `${text} ${unit}` : text;
5765
+ }
5766
+ function trimTrailingZero(text) {
5767
+ return text.replace(/\.0$/, "");
5768
+ }
5769
+
5770
+ // src/doc/templates/chart/geometry.ts
5771
+ function round3(value) {
5772
+ return Math.round(value * 100) / 100;
5773
+ }
5774
+ function pointOnCircle(cx, cy, r, deg) {
5775
+ const rad = (deg - 90) * Math.PI / 180;
5776
+ return { x: round3(cx + r * Math.cos(rad)), y: round3(cy + r * Math.sin(rad)) };
5777
+ }
5778
+ function arcPath(cx, cy, rOuter, rInner, startDeg, endDeg) {
5779
+ let sweep = endDeg - startDeg;
5780
+ if (sweep <= 0) return "";
5781
+ if (sweep >= 359.9) sweep = 359.9;
5782
+ const end = startDeg + sweep;
5783
+ const largeArc = sweep > 180 ? 1 : 0;
5784
+ const o1 = pointOnCircle(cx, cy, rOuter, startDeg);
5785
+ const o2 = pointOnCircle(cx, cy, rOuter, end);
5786
+ if (rInner <= 0) {
5787
+ return [
5788
+ `M ${round3(cx)} ${round3(cy)}`,
5789
+ `L ${o1.x} ${o1.y}`,
5790
+ `A ${round3(rOuter)} ${round3(rOuter)} 0 ${largeArc} 1 ${o2.x} ${o2.y}`,
5791
+ "Z"
5792
+ ].join(" ");
5793
+ }
5794
+ const i1 = pointOnCircle(cx, cy, rInner, startDeg);
5795
+ const i2 = pointOnCircle(cx, cy, rInner, end);
5796
+ return [
5797
+ `M ${o1.x} ${o1.y}`,
5798
+ `A ${round3(rOuter)} ${round3(rOuter)} 0 ${largeArc} 1 ${o2.x} ${o2.y}`,
5799
+ `L ${i2.x} ${i2.y}`,
5800
+ `A ${round3(rInner)} ${round3(rInner)} 0 ${largeArc} 0 ${i1.x} ${i1.y}`,
5801
+ "Z"
5802
+ ].join(" ");
5803
+ }
5804
+ function polylinePath(points) {
5805
+ if (points.length === 0) return "";
5806
+ return points.map((p, i) => `${i === 0 ? "M" : "L"} ${round3(p.x)} ${round3(p.y)}`).join(" ");
5807
+ }
5808
+ function areaPath(points, baselineY) {
5809
+ if (points.length === 0) return "";
5810
+ const open = polylinePath(points);
5811
+ const first = points[0];
5812
+ const last = points[points.length - 1];
5813
+ return `${open} L ${round3(last.x)} ${round3(baselineY)} L ${round3(first.x)} ${round3(baselineY)} Z`;
5814
+ }
5815
+
5816
+ // src/doc/templates/chart/layout.ts
5817
+ function computeChartFrame(options) {
5818
+ const W = options.viewportWidth;
5819
+ const H = options.viewportHeight;
5820
+ const marginX = W * 0.06;
5821
+ const marginY = H * 0.06;
5822
+ const innerWidth = W - marginX * 2;
5823
+ let cursor = marginY;
5824
+ let titleBand;
5825
+ if (options.hasTitle) {
5826
+ titleBand = { x: marginX, y: cursor, width: innerWidth, height: options.titleFontSize * 2.2 };
5827
+ cursor = titleBand.y + titleBand.height;
5828
+ }
5829
+ let tableBand;
5830
+ let tableHeight = 0;
5831
+ if (options.tableRowCount > 0) {
5832
+ tableHeight = Math.min(H * 0.4, (options.tableRowCount + 1) * options.tableFontSize * 2.4);
5833
+ }
5834
+ const legendHeight = options.legendRows > 0 ? options.legendRows * options.legendRowHeight : 0;
5835
+ const chartArea = {
5836
+ x: marginX,
5837
+ y: cursor,
5838
+ width: innerWidth,
5839
+ height: Math.max(40, H - marginY - cursor - legendHeight - tableHeight)
5840
+ };
5841
+ cursor = chartArea.y + chartArea.height;
5842
+ let legendBand;
5843
+ if (legendHeight > 0) {
5844
+ legendBand = { x: marginX, y: cursor, width: innerWidth, height: legendHeight };
5845
+ cursor += legendHeight;
5846
+ }
5847
+ if (tableHeight > 0) {
5848
+ tableBand = { x: marginX, y: cursor, width: innerWidth, height: tableHeight };
5849
+ }
5850
+ const plot = {
5851
+ x: chartArea.x + options.leftGutter,
5852
+ y: chartArea.y,
5853
+ width: Math.max(20, chartArea.width - options.leftGutter),
5854
+ height: Math.max(20, chartArea.height - options.bottomGutter)
5855
+ };
5856
+ return { titleBand, chartArea, plot, legendBand, tableBand };
5857
+ }
5858
+ function stackSeries(series) {
5859
+ const rowCount = series.reduce((max, s) => Math.max(max, s.values.length), 0);
5860
+ const positive = new Array(rowCount).fill(0);
5861
+ const negative = new Array(rowCount).fill(0);
5862
+ return series.map(
5863
+ (s) => Array.from({ length: rowCount }, (_, row) => {
5864
+ const value = s.values[row];
5865
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
5866
+ return { y0: positive[row], y1: positive[row] };
5867
+ }
5868
+ if (value >= 0) {
5869
+ const y0 = positive[row];
5870
+ positive[row] += value;
5871
+ return { y0, y1: positive[row] };
5872
+ }
5873
+ const y1 = negative[row];
5874
+ negative[row] += value;
5875
+ return { y0: negative[row], y1 };
5876
+ })
5877
+ );
5878
+ }
5879
+ function layoutLegend(labels, fontSize, maxWidth) {
5880
+ const swatchSize = fontSize * 0.9;
5881
+ const gap = fontSize * 1.6;
5882
+ const rowHeight = fontSize * 2;
5883
+ const widths = labels.map(
5884
+ (label) => swatchSize + fontSize * 0.5 + label.length * fontSize * 0.58
5885
+ );
5886
+ const rows = [];
5887
+ let current = [];
5888
+ let currentWidth = 0;
5889
+ widths.forEach((width, index) => {
5890
+ const extra = current.length > 0 ? gap : 0;
5891
+ if (current.length > 0 && currentWidth + extra + width > maxWidth) {
5892
+ rows.push(current);
5893
+ current = [];
5894
+ currentWidth = 0;
5895
+ }
5896
+ current.push(index);
5897
+ currentWidth += (current.length > 1 ? gap : 0) + width;
5898
+ });
5899
+ if (current.length > 0) rows.push(current);
5900
+ const items = [];
5901
+ rows.forEach((rowIndexes, rowIndex) => {
5902
+ const rowWidth = rowIndexes.reduce(
5903
+ (sum, index, i) => sum + widths[index] + (i > 0 ? gap : 0),
5904
+ 0
5905
+ );
5906
+ let x = Math.max(0, (maxWidth - rowWidth) / 2);
5907
+ rowIndexes.forEach((index) => {
5908
+ items.push({ label: labels[index], index, x, y: rowIndex * rowHeight, swatchSize });
5909
+ x += widths[index] + gap;
5910
+ });
5911
+ });
5912
+ return { items, rows: rows.length, rowHeight };
5913
+ }
5914
+ function categoryLabelStep(categoryCount, axisLength, fontSize) {
5915
+ if (categoryCount <= 1) return 1;
5916
+ const minPitch = fontSize * 6;
5917
+ const pitch = axisLength / categoryCount;
5918
+ return Math.max(1, Math.ceil(minPitch / Math.max(1, pitch)));
5919
+ }
5920
+
5921
+ // src/doc/templates/chart/render.ts
5922
+ var TEMPLATE_ID = {
5923
+ bar: "barChart",
5924
+ column: "columnChart",
5925
+ pie: "pieChart",
5926
+ donut: "donutChart",
5927
+ line: "lineChart",
5928
+ area: "areaChart",
5929
+ scatter: "scatterChart"
5930
+ };
5931
+ function renderChart(kind, input, context) {
5932
+ const table = resolveTable(input, context);
5933
+ const data = table ? buildChartData(table, input) : null;
5934
+ if (!table || !data) return contentFallback(input, context);
5935
+ const { theme, viewport } = context;
5936
+ const templateId = TEMPLATE_ID[kind];
5937
+ const title = (input.title ?? "").trim();
5938
+ const titleFontSize = themedFontSize(44, context, true);
5939
+ const axisFontSize = themedFontSize(20, context, false);
5940
+ const labelFontSize = themedFontSize(22, context, false);
5941
+ const legendFontSize = themedFontSize(22, context, false);
5942
+ const tableFontSize = themedFontSize(20, context, false);
5943
+ const bodyFont = getThemeFont(context, "body");
5944
+ const isRadial = kind === "pie" || kind === "donut";
5945
+ const legendLabels = resolveLegendLabels(kind, input, data);
5946
+ const colors = seriesColors(
5947
+ context,
5948
+ input.colorScheme,
5949
+ isRadial ? data.labels.length : data.series.length
5950
+ );
5951
+ if (isRadial && !data.series[0].values.some((v) => v !== null && v > 0)) {
5952
+ return contentFallback(input, context);
5953
+ }
5954
+ const innerWidth = viewport.width * 0.88;
5955
+ const legend = legendLabels.length > 0 ? layoutLegend(legendLabels, legendFontSize, innerWidth) : null;
5956
+ const gutters = computeGutters(kind, data, input, context, axisFontSize, labelFontSize);
5957
+ const frame = computeChartFrame({
5958
+ viewportWidth: viewport.width,
5959
+ viewportHeight: viewport.height,
5960
+ hasTitle: title.length > 0,
5961
+ titleFontSize,
5962
+ leftGutter: gutters.left,
5963
+ bottomGutter: gutters.bottom,
5964
+ legendRows: legend?.rows ?? 0,
5965
+ legendRowHeight: legendFontSize * 2,
5966
+ tableRowCount: input.showTable === true ? table.rows.length : 0,
5967
+ tableFontSize
5968
+ });
5969
+ const entranceHint = getTemplateHint(context, templateId, "entrance", "staggered");
5970
+ const entrance = (index) => ({
5971
+ type: "fadeIn",
5972
+ duration: 0.6,
5973
+ delay: entranceHint === "subtle" ? 0.1 : Math.min(1.2, 0.2 + index * 0.08)
5974
+ });
5975
+ const paint = {
5976
+ kind,
5977
+ input,
5978
+ context,
5979
+ data,
5980
+ frame,
5981
+ colors,
5982
+ axisFontSize,
5983
+ labelFontSize,
5984
+ bodyFont,
5985
+ gridColor: withAlpha(theme.colors.text, 0.12),
5986
+ axisTextColor: theme.colors.textMuted,
5987
+ entrance
5988
+ };
5989
+ const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
5990
+ if (title && frame.titleBand) {
5991
+ layers.push({
5992
+ type: "text",
5993
+ id: "title",
5994
+ content: {
5995
+ text: title,
5996
+ style: {
5997
+ fontSize: titleFontSize,
5998
+ fontFamily: getThemeFont(context, "title"),
5999
+ fontWeight: "bold",
6000
+ color: theme.colors.text,
6001
+ textAlign: "center",
6002
+ shadow: shouldUseShadow(context)
6003
+ }
6004
+ },
6005
+ position: {
6006
+ x: frame.titleBand.x + frame.titleBand.width / 2,
6007
+ y: frame.titleBand.y + frame.titleBand.height / 2,
6008
+ width: frame.titleBand.width,
6009
+ anchor: "center"
6010
+ },
6011
+ animation: { type: "fadeIn", duration: 0.8 }
6012
+ });
6013
+ }
6014
+ switch (kind) {
6015
+ case "bar":
6016
+ layers.push(...paintBars(paint));
6017
+ break;
6018
+ case "column":
6019
+ layers.push(...paintColumns(paint));
6020
+ break;
6021
+ case "pie":
6022
+ case "donut":
6023
+ layers.push(...paintRadial(paint));
6024
+ break;
6025
+ case "line":
6026
+ case "area":
6027
+ layers.push(...paintLines(paint));
6028
+ break;
6029
+ case "scatter":
6030
+ layers.push(...paintScatter(paint));
6031
+ break;
6032
+ }
6033
+ if (legend && frame.legendBand) {
6034
+ layers.push(...paintLegend(paint, legend, frame.legendBand, legendFontSize));
6035
+ }
6036
+ if (input.showTable === true && frame.tableBand) {
6037
+ layers.push(tableLayer(table, frame.tableBand, tableFontSize, input, context));
6038
+ }
6039
+ return layers;
6040
+ }
6041
+ function resolveTable(input, context) {
6042
+ if (Array.isArray(input.headers) && input.headers.length > 0 && Array.isArray(input.rows) && input.rows.length > 0) {
6043
+ return { headers: input.headers, rows: input.rows };
6044
+ }
6045
+ const contents = input.contents ?? context.block?.contents;
6046
+ return extractTableFromContents(contents);
6047
+ }
6048
+ function contentFallback(input, context) {
6049
+ const fallbackInput = {
6050
+ ...input,
6051
+ template: "content",
6052
+ title: input.title ?? context.block?.title ?? ""
6053
+ };
6054
+ return contentBlock(fallbackInput, context);
6055
+ }
6056
+ function resolveLegendLabels(kind, input, data) {
6057
+ if (input.showLegend === false) return [];
6058
+ if (kind === "pie" || kind === "donut") return data.labels;
6059
+ if (data.series.length > 1 || input.showLegend === true) {
6060
+ return data.series.map((s) => s.name);
6061
+ }
6062
+ return [];
6063
+ }
6064
+ function seriesColors(context, schemeName, count) {
6065
+ const schemes = context.theme.colorSchemes;
6066
+ const names = Object.keys(schemes);
6067
+ const primary = resolveColorScheme(context, schemeName).accent;
6068
+ const palette = [primary];
6069
+ const start = schemeName && names.includes(schemeName) ? names.indexOf(schemeName) : 0;
6070
+ for (let i = 1; i < names.length && palette.length < count; i++) {
6071
+ const accent = schemes[names[(start + i) % names.length]].accent;
6072
+ if (!palette.some((c) => c.toLowerCase() === accent.toLowerCase())) palette.push(accent);
6073
+ }
6074
+ const distinct = palette.length;
6075
+ for (let i = 0; palette.length < count; i++) {
6076
+ palette.push(withAlpha(palette[i % distinct], 0.55));
6077
+ }
6078
+ return palette.slice(0, Math.max(1, count));
6079
+ }
6080
+ function computeGutters(kind, data, input, context, axisFontSize, labelFontSize) {
6081
+ if (kind === "pie" || kind === "donut") return { left: 0, bottom: 0 };
6082
+ if (kind === "bar") {
6083
+ const longest = data.labels.reduce((max, label) => Math.max(max, label.length), 0);
6084
+ const left = Math.min(
6085
+ context.viewport.width * 0.24,
6086
+ Math.max(labelFontSize * 3, longest * labelFontSize * 0.58 + 16)
6087
+ );
6088
+ return { left, bottom: axisFontSize * 2.4 };
6089
+ }
6090
+ const ticks = valueTicks(kind, data, input);
6091
+ const longestTick = ticks.ticks.reduce(
6092
+ (max, tick) => Math.max(max, formatChartValue(tick, input.unit).length),
6093
+ 0
6094
+ );
6095
+ return {
6096
+ left: Math.max(axisFontSize * 2.5, longestTick * axisFontSize * 0.6 + 16),
6097
+ bottom: labelFontSize * 2.6
6098
+ };
6099
+ }
6100
+ function valueTicks(kind, data, input) {
6101
+ let values;
6102
+ if ((kind === "bar" || kind === "column") && input.stacked === true) {
6103
+ const extents = stackSeries(data.series).flat();
6104
+ values = extents.flatMap((e) => [e.y0, e.y1]);
6105
+ } else {
6106
+ values = data.series.flatMap((s) => s.values.filter((v) => v !== null));
6107
+ }
6108
+ if (values.length === 0) values = [0, 1];
6109
+ let min = Math.min(...values);
6110
+ let max = Math.max(...values);
6111
+ if (kind !== "line" && kind !== "scatter") {
6112
+ min = Math.min(0, min);
6113
+ max = Math.max(0, max);
6114
+ }
6115
+ return niceTicks(min, max, 5);
6116
+ }
6117
+ function paintValueAxisY(paint, scale, yFor) {
6118
+ const { frame, gridColor, axisTextColor, axisFontSize, bodyFont, input } = paint;
6119
+ const layers = [];
6120
+ scale.ticks.forEach((tick, index) => {
6121
+ const y = yFor(tick);
6122
+ layers.push({
6123
+ type: "shape",
6124
+ id: `grid-${index}`,
6125
+ content: {
6126
+ shape: "rect",
6127
+ fill: tick === 0 ? withAlpha(paint.context.theme.colors.text, 0.35) : gridColor
6128
+ },
6129
+ position: {
6130
+ x: frame.plot.x,
6131
+ y: y - (tick === 0 ? 1 : 0.5),
6132
+ width: frame.plot.width,
6133
+ height: tick === 0 ? 2 : 1
6134
+ }
6135
+ });
6136
+ layers.push({
6137
+ type: "text",
6138
+ id: `tick-${index}`,
6139
+ content: {
6140
+ text: formatChartValue(tick, input.unit),
6141
+ style: {
6142
+ fontSize: axisFontSize,
6143
+ fontFamily: bodyFont,
6144
+ color: axisTextColor,
6145
+ textAlign: "center"
6146
+ }
6147
+ },
6148
+ position: {
6149
+ x: frame.chartArea.x + (frame.plot.x - frame.chartArea.x) / 2,
6150
+ y,
6151
+ anchor: "center"
6152
+ }
6153
+ });
6154
+ });
6155
+ return layers;
6156
+ }
6157
+ function paintCategoryAxisX(paint, xForCenter) {
6158
+ const { frame, data, labelFontSize, axisTextColor, bodyFont } = paint;
6159
+ const step = categoryLabelStep(data.labels.length, frame.plot.width, labelFontSize);
6160
+ const layers = [];
6161
+ data.labels.forEach((label, index) => {
6162
+ if (index % step !== 0 || !label) return;
6163
+ layers.push({
6164
+ type: "text",
6165
+ id: `cat-${index}`,
6166
+ content: {
6167
+ text: label,
6168
+ style: {
6169
+ fontSize: labelFontSize,
6170
+ fontFamily: bodyFont,
6171
+ color: axisTextColor,
6172
+ textAlign: "center"
6173
+ }
6174
+ },
6175
+ position: {
6176
+ x: xForCenter(index),
6177
+ y: frame.plot.y + frame.plot.height + labelFontSize * 1.2,
6178
+ anchor: "center"
6179
+ }
6180
+ });
6181
+ });
6182
+ return layers;
6183
+ }
6184
+ function paintColumns(paint) {
6185
+ const { frame, data, colors, input, entrance } = paint;
6186
+ const scale = valueTicks("column", data, input);
6187
+ const yFor = (v) => frame.plot.y + frame.plot.height * (1 - (v - scale.niceMin) / (scale.niceMax - scale.niceMin));
6188
+ const layers = paintValueAxisY(paint, scale, yFor);
6189
+ const n = data.labels.length;
6190
+ const band = frame.plot.width / Math.max(1, n);
6191
+ const innerPad = band * 0.15;
6192
+ layers.push(...paintCategoryAxisX(paint, (i) => frame.plot.x + (i + 0.5) * band));
6193
+ const stacked = input.stacked === true && data.series.length > 1;
6194
+ const extents = stacked ? stackSeries(data.series) : null;
6195
+ const groupWidth = band - innerPad * 2;
6196
+ const barWidth = stacked ? groupWidth : Math.max(2, (groupWidth - 4 * (data.series.length - 1)) / data.series.length);
6197
+ data.series.forEach((series, s) => {
6198
+ series.values.forEach((value, i) => {
6199
+ if (i >= n) return;
6200
+ const extent = extents ? extents[s][i] : value === null ? null : { y0: Math.min(0, value), y1: Math.max(0, value) };
6201
+ if (!extent || extent.y0 === extent.y1) return;
6202
+ const top = yFor(extent.y1);
6203
+ const height = Math.max(1, yFor(extent.y0) - top);
6204
+ const x = stacked ? frame.plot.x + i * band + innerPad : frame.plot.x + i * band + innerPad + s * (barWidth + 4);
6205
+ layers.push({
6206
+ type: "shape",
6207
+ id: `mark-${s}-${i}`,
6208
+ content: { shape: "rect", fill: colors[s], borderRadius: stacked ? 0 : 3 },
6209
+ position: { x, y: top, width: barWidth, height },
6210
+ animation: entrance(i + s)
6211
+ });
6212
+ if (input.showValues === true && value !== null && !stacked) {
6213
+ layers.push(
6214
+ valueLabel(
6215
+ paint,
6216
+ `val-${s}-${i}`,
6217
+ value,
6218
+ x + barWidth / 2,
6219
+ value >= 0 ? top - paint.axisFontSize * 0.9 : top + height + paint.axisFontSize * 0.9
6220
+ )
6221
+ );
6222
+ }
6223
+ });
6224
+ });
6225
+ return layers;
6226
+ }
6227
+ function paintBars(paint) {
6228
+ const {
6229
+ frame,
6230
+ data,
6231
+ colors,
6232
+ input,
6233
+ entrance,
6234
+ labelFontSize,
6235
+ axisFontSize,
6236
+ bodyFont,
6237
+ axisTextColor,
6238
+ gridColor
6239
+ } = paint;
6240
+ const scale = valueTicks("bar", data, input);
6241
+ const labelGap = axisFontSize * 0.6;
6242
+ const labelWidthPx = (value) => formatChartValue(value, input.unit).length * axisFontSize * 0.58;
6243
+ const showValueLabels = input.showValues === true && !(input.stacked === true);
6244
+ const reservedForLabels = showValueLabels ? data.series.flatMap((series) => series.values).filter((v) => v !== null).reduce((max, v) => Math.max(max, labelWidthPx(v)), 0) + labelGap : 0;
6245
+ const axisWidth = Math.max(20, frame.plot.width - reservedForLabels);
6246
+ const xFor = (v) => frame.plot.x + axisWidth * ((v - scale.niceMin) / (scale.niceMax - scale.niceMin));
6247
+ const layers = [];
6248
+ scale.ticks.forEach((tick, index) => {
6249
+ const x = xFor(tick);
6250
+ layers.push({
6251
+ type: "shape",
6252
+ id: `grid-${index}`,
6253
+ content: {
6254
+ shape: "rect",
6255
+ fill: tick === 0 ? withAlpha(paint.context.theme.colors.text, 0.35) : gridColor
6256
+ },
6257
+ position: {
6258
+ x: x - (tick === 0 ? 1 : 0.5),
6259
+ y: frame.plot.y,
6260
+ width: tick === 0 ? 2 : 1,
6261
+ height: frame.plot.height
6262
+ }
6263
+ });
6264
+ layers.push({
6265
+ type: "text",
6266
+ id: `tick-${index}`,
6267
+ content: {
6268
+ text: formatChartValue(tick, input.unit),
6269
+ style: {
6270
+ fontSize: axisFontSize,
6271
+ fontFamily: bodyFont,
6272
+ color: axisTextColor,
6273
+ textAlign: "center"
6274
+ }
6275
+ },
6276
+ position: { x, y: frame.plot.y + frame.plot.height + axisFontSize * 1.2, anchor: "center" }
6277
+ });
6278
+ });
6279
+ const n = data.labels.length;
6280
+ const band = frame.plot.height / Math.max(1, n);
6281
+ const innerPad = band * 0.15;
6282
+ const stacked = input.stacked === true && data.series.length > 1;
6283
+ const extents = stacked ? stackSeries(data.series) : null;
6284
+ const groupHeight = band - innerPad * 2;
6285
+ const barHeight = stacked ? groupHeight : Math.max(2, (groupHeight - 4 * (data.series.length - 1)) / data.series.length);
6286
+ data.labels.forEach((label, i) => {
6287
+ if (!label) return;
6288
+ layers.push({
6289
+ type: "text",
6290
+ id: `cat-${i}`,
6291
+ content: {
6292
+ text: label,
6293
+ style: {
6294
+ fontSize: labelFontSize,
6295
+ fontFamily: bodyFont,
6296
+ color: axisTextColor,
6297
+ textAlign: "center"
6298
+ }
6299
+ },
6300
+ position: {
6301
+ x: frame.chartArea.x + (frame.plot.x - frame.chartArea.x) / 2,
6302
+ y: frame.plot.y + (i + 0.5) * band,
6303
+ anchor: "center"
6304
+ }
6305
+ });
6306
+ });
6307
+ data.series.forEach((series, s) => {
6308
+ series.values.forEach((value, i) => {
6309
+ if (i >= n) return;
6310
+ const extent = extents ? extents[s][i] : value === null ? null : { y0: Math.min(0, value), y1: Math.max(0, value) };
6311
+ if (!extent || extent.y0 === extent.y1) return;
6312
+ const left = xFor(extent.y0);
6313
+ const width = Math.max(1, xFor(extent.y1) - left);
6314
+ const y = stacked ? frame.plot.y + i * band + innerPad : frame.plot.y + i * band + innerPad + s * (barHeight + 4);
6315
+ layers.push({
6316
+ type: "shape",
6317
+ id: `mark-${s}-${i}`,
6318
+ content: { shape: "rect", fill: colors[s], borderRadius: stacked ? 0 : 3 },
6319
+ position: { x: left, y, width, height: barHeight },
6320
+ animation: entrance(i + s)
6321
+ });
6322
+ if (showValueLabels && value !== null) {
6323
+ const half = labelWidthPx(value) / 2;
6324
+ const labelX = value >= 0 ? left + width + labelGap + half : left - labelGap - half;
6325
+ layers.push(valueLabel(paint, `val-${s}-${i}`, value, labelX, y + barHeight / 2));
6326
+ }
6327
+ });
6328
+ });
6329
+ return layers;
6330
+ }
6331
+ function paintRadial(paint) {
6332
+ const { kind, frame, data, colors, input, entrance, context } = paint;
6333
+ const slices = data.labels.map((label, index) => ({ label, value: data.series[0].values[index], index })).filter(
6334
+ (s) => s.value !== null && Number.isFinite(s.value) && s.value > 0
6335
+ );
6336
+ const total = slices.reduce((sum, s) => sum + s.value, 0);
6337
+ const cx = frame.chartArea.x + frame.chartArea.width / 2;
6338
+ const cy = frame.chartArea.y + frame.chartArea.height / 2;
6339
+ const rOuter = Math.min(frame.chartArea.width, frame.chartArea.height) * 0.42;
6340
+ const rInner = kind === "donut" ? rOuter * 0.55 : 0;
6341
+ const layers = [];
6342
+ let startDeg = 0;
6343
+ slices.forEach((slice, order) => {
6344
+ const sweep = slice.value / total * 360;
6345
+ layers.push({
6346
+ type: "path",
6347
+ id: `mark-${slice.index}`,
6348
+ content: {
6349
+ d: arcPath(cx, cy, rOuter, rInner, startDeg, startDeg + sweep),
6350
+ fill: colors[slice.index % colors.length],
6351
+ stroke: context.theme.colors.background,
6352
+ strokeWidth: 2
6353
+ },
6354
+ position: { x: cx - rOuter, y: cy - rOuter, width: rOuter * 2, height: rOuter * 2 },
6355
+ animation: entrance(order)
6356
+ });
6357
+ if (input.showValues === true && sweep >= 14) {
6358
+ const mid = startDeg + sweep / 2;
6359
+ const rad = (mid - 90) * Math.PI / 180;
6360
+ const rLabel = rInner > 0 ? (rInner + rOuter) / 2 : rOuter * 0.68;
6361
+ const pctText = `${Math.round(slice.value / total * 100)}%`;
6362
+ layers.push({
6363
+ type: "text",
6364
+ id: `val-${slice.index}`,
6365
+ content: {
6366
+ text: pctText,
6367
+ style: {
6368
+ fontSize: paint.axisFontSize,
6369
+ fontFamily: paint.bodyFont,
6370
+ fontWeight: "bold",
6371
+ color: pickContrastingText(colors[slice.index % colors.length]),
6372
+ textAlign: "center"
6373
+ }
6374
+ },
6375
+ position: {
6376
+ x: cx + rLabel * Math.cos(rad),
6377
+ y: cy + rLabel * Math.sin(rad),
6378
+ anchor: "center"
6379
+ },
6380
+ animation: entrance(order)
6381
+ });
6382
+ }
6383
+ startDeg += sweep;
6384
+ });
6385
+ return layers;
6386
+ }
6387
+ function paintLines(paint) {
6388
+ const { kind, frame, data, colors, input, entrance } = paint;
6389
+ const scale = valueTicks(kind, data, input);
6390
+ const yFor = (v) => frame.plot.y + frame.plot.height * (1 - (v - scale.niceMin) / (scale.niceMax - scale.niceMin));
6391
+ const n = data.labels.length;
6392
+ const band = frame.plot.width / Math.max(1, n);
6393
+ const xForCenter = (i) => frame.plot.x + (i + 0.5) * band;
6394
+ const layers = paintValueAxisY(paint, scale, yFor);
6395
+ layers.push(...paintCategoryAxisX(paint, xForCenter));
6396
+ const baselineY = yFor(Math.max(scale.niceMin, Math.min(scale.niceMax, 0)));
6397
+ const showPoints = n <= 30;
6398
+ data.series.forEach((series, s) => {
6399
+ const segments = [];
6400
+ let current = [];
6401
+ series.values.forEach((value, i) => {
6402
+ if (i >= n) return;
6403
+ if (value === null) {
6404
+ if (current.length > 0) segments.push(current);
6405
+ current = [];
6406
+ return;
6407
+ }
6408
+ current.push({ x: xForCenter(i), y: yFor(value) });
6409
+ });
6410
+ if (current.length > 0) segments.push(current);
6411
+ segments.forEach((segment, seg) => {
6412
+ if (kind === "area" && segment.length > 1) {
6413
+ layers.push({
6414
+ type: "path",
6415
+ id: `area-${s}-${seg}`,
6416
+ content: {
6417
+ d: areaPath(segment, baselineY),
6418
+ fill: colors[s],
6419
+ fillOpacity: 0.25,
6420
+ stroke: "none",
6421
+ strokeWidth: 0
6422
+ },
6423
+ position: {
6424
+ x: frame.plot.x,
6425
+ y: frame.plot.y,
6426
+ width: frame.plot.width,
6427
+ height: frame.plot.height
6428
+ },
6429
+ animation: entrance(s)
6430
+ });
6431
+ }
6432
+ if (segment.length > 1) {
6433
+ layers.push({
6434
+ type: "path",
6435
+ id: `line-${s}-${seg}`,
6436
+ content: { d: polylinePath(segment), stroke: colors[s], strokeWidth: 4, fill: "none" },
6437
+ position: {
6438
+ x: frame.plot.x,
6439
+ y: frame.plot.y,
6440
+ width: frame.plot.width,
6441
+ height: frame.plot.height
6442
+ },
6443
+ animation: entrance(s)
6444
+ });
6445
+ }
6446
+ });
6447
+ if (showPoints) {
6448
+ series.values.forEach((value, i) => {
6449
+ if (i >= n || value === null) return;
6450
+ const r = 5;
6451
+ layers.push({
6452
+ type: "shape",
6453
+ id: `pt-${s}-${i}`,
6454
+ content: { shape: "circle", fill: colors[s] },
6455
+ position: { x: xForCenter(i) - r, y: yFor(value) - r, width: r * 2, height: r * 2 },
6456
+ animation: entrance(s)
6457
+ });
6458
+ if (input.showValues === true) {
6459
+ layers.push(
6460
+ valueLabel(
6461
+ paint,
6462
+ `val-${s}-${i}`,
6463
+ value,
6464
+ xForCenter(i),
6465
+ yFor(value) - paint.axisFontSize
6466
+ )
6467
+ );
6468
+ }
6469
+ });
6470
+ }
6471
+ });
6472
+ return layers;
6473
+ }
6474
+ function paintScatter(paint) {
6475
+ const { frame, data, colors, input, entrance } = paint;
6476
+ const scale = valueTicks("scatter", data, input);
6477
+ const yFor = (v) => frame.plot.y + frame.plot.height * (1 - (v - scale.niceMin) / (scale.niceMax - scale.niceMin));
6478
+ const layers = paintValueAxisY(paint, scale, yFor);
6479
+ const numericCount = data.labelNumbers.filter((v) => v !== null).length;
6480
+ const numericX = data.labels.length > 0 && numericCount / data.labels.length >= 0.6;
6481
+ let xFor;
6482
+ if (numericX) {
6483
+ const xs = data.labelNumbers.filter((v) => v !== null);
6484
+ const xScale = niceTicks(Math.min(...xs), Math.max(...xs), 5);
6485
+ const span = xScale.niceMax - xScale.niceMin;
6486
+ xFor = (i) => {
6487
+ const v = data.labelNumbers[i];
6488
+ return v === null ? null : frame.plot.x + frame.plot.width * ((v - xScale.niceMin) / span);
6489
+ };
6490
+ xScale.ticks.forEach((tick, index) => {
6491
+ const x = frame.plot.x + frame.plot.width * ((tick - xScale.niceMin) / span);
6492
+ layers.push({
6493
+ type: "shape",
6494
+ id: `xgrid-${index}`,
6495
+ content: { shape: "rect", fill: paint.gridColor },
6496
+ position: { x: x - 0.5, y: frame.plot.y, width: 1, height: frame.plot.height }
6497
+ });
6498
+ layers.push({
6499
+ type: "text",
6500
+ id: `xtick-${index}`,
6501
+ content: {
6502
+ text: formatChartValue(tick),
6503
+ style: {
6504
+ fontSize: paint.axisFontSize,
6505
+ fontFamily: paint.bodyFont,
6506
+ color: paint.axisTextColor,
6507
+ textAlign: "center"
6508
+ }
6509
+ },
6510
+ position: {
6511
+ x,
6512
+ y: frame.plot.y + frame.plot.height + paint.axisFontSize * 1.2,
6513
+ anchor: "center"
6514
+ }
6515
+ });
6516
+ });
6517
+ } else {
6518
+ const band = frame.plot.width / Math.max(1, data.labels.length);
6519
+ xFor = (i) => frame.plot.x + (i + 0.5) * band;
6520
+ layers.push(...paintCategoryAxisX(paint, (i) => frame.plot.x + (i + 0.5) * band));
6521
+ }
6522
+ const r = 7;
6523
+ data.series.forEach((series, s) => {
6524
+ series.values.forEach((value, i) => {
6525
+ if (value === null) return;
6526
+ const x = xFor(i);
6527
+ if (x === null) return;
6528
+ layers.push({
6529
+ type: "shape",
6530
+ id: `mark-${s}-${i}`,
6531
+ content: { shape: "circle", fill: colors[s], fillOpacity: 0.85 },
6532
+ position: { x: x - r, y: yFor(value) - r, width: r * 2, height: r * 2 },
6533
+ animation: entrance(Math.floor(i / 3) + s)
6534
+ });
6535
+ });
6536
+ });
6537
+ return layers;
6538
+ }
6539
+ function paintLegend(paint, legend, band, fontSize) {
6540
+ const layers = [];
6541
+ legend.items.forEach((item, index) => {
6542
+ const color = paint.colors[item.index % paint.colors.length];
6543
+ const rowCenterY = band.y + item.y + legend.rowHeight / 2;
6544
+ layers.push({
6545
+ type: "shape",
6546
+ id: `legend-swatch-${index}`,
6547
+ content: { shape: "rect", fill: color, borderRadius: 3 },
6548
+ position: {
6549
+ x: band.x + item.x,
6550
+ y: rowCenterY - item.swatchSize / 2,
6551
+ width: item.swatchSize,
6552
+ height: item.swatchSize
6553
+ }
6554
+ });
6555
+ layers.push({
6556
+ type: "text",
6557
+ id: `legend-label-${index}`,
6558
+ content: {
6559
+ text: item.label,
6560
+ style: { fontSize, fontFamily: paint.bodyFont, color: paint.context.theme.colors.text }
6561
+ },
6562
+ position: {
6563
+ x: band.x + item.x + item.swatchSize + fontSize * 0.5,
6564
+ y: rowCenterY - fontSize * 0.62
6565
+ }
6566
+ });
6567
+ });
6568
+ return layers;
6569
+ }
6570
+ function valueLabel(paint, id, value, x, y) {
6571
+ return {
6572
+ type: "text",
6573
+ id,
6574
+ content: {
6575
+ text: formatChartValue(value, paint.input.unit),
6576
+ style: {
6577
+ fontSize: paint.axisFontSize,
6578
+ fontFamily: paint.bodyFont,
6579
+ fontWeight: "bold",
6580
+ color: paint.context.theme.colors.text,
6581
+ textAlign: "center"
6582
+ }
6583
+ },
6584
+ position: { x, y, anchor: "center" }
6585
+ };
6586
+ }
6587
+ function tableLayer(table, band, fontSize, input, context) {
6588
+ const { theme } = context;
6589
+ const colors = resolveColorScheme(context, input.colorScheme);
6590
+ return {
6591
+ type: "table",
6592
+ id: "table",
6593
+ content: {
6594
+ headers: table.headers,
6595
+ rows: table.rows,
6596
+ align: table.align,
6597
+ style: {
6598
+ headerBackground: colors.accent,
6599
+ headerColor: pickContrastingText(colors.accent),
6600
+ cellBackground: withAlpha(theme.colors.text, 0.04),
6601
+ cellColor: theme.colors.text,
6602
+ borderColor: withAlpha(theme.colors.text, 0.15),
6603
+ fontSize,
6604
+ fontFamily: getThemeFont(context, "body"),
6605
+ headerFontFamily: getThemeFont(context, "title"),
6606
+ borderRadius: 8
6607
+ }
6608
+ },
6609
+ position: { x: band.x, y: band.y, width: band.width, height: band.height },
6610
+ animation: { type: "fadeIn", duration: 1, delay: 0.4 }
6611
+ };
6612
+ }
6613
+
6614
+ // src/doc/templates/chartBlock.ts
6615
+ function barChart(input, context) {
6616
+ return renderChart("bar", input, context);
6617
+ }
6618
+ function columnChart(input, context) {
6619
+ return renderChart("column", input, context);
6620
+ }
6621
+ function pieChart(input, context) {
6622
+ return renderChart("pie", input, context);
6623
+ }
6624
+ function donutChart(input, context) {
6625
+ return renderChart("donut", input, context);
6626
+ }
6627
+ function lineChart(input, context) {
6628
+ return renderChart("line", input, context);
6629
+ }
6630
+ function areaChart(input, context) {
6631
+ return renderChart("area", input, context);
6632
+ }
6633
+ function scatterChart(input, context) {
6634
+ return renderChart("scatter", input, context);
6635
+ }
6636
+
5588
6637
  // src/doc/templates/registry.ts
5589
6638
  var templateRegistry = {
5590
6639
  title: titleBlock,
@@ -5608,6 +6657,13 @@ var templateRegistry = {
5608
6657
  videoWithCaption,
5609
6658
  videoPullQuote,
5610
6659
  dataTable,
6660
+ barChart,
6661
+ columnChart,
6662
+ pieChart,
6663
+ donutChart,
6664
+ lineChart,
6665
+ areaChart,
6666
+ scatterChart,
5611
6667
  diagram: diagramBlock,
5612
6668
  tree: treeBlock,
5613
6669
  timeline: timelineBlock,
@@ -5723,6 +6779,49 @@ var CAPTION_POSITION_VALUES = ["bottom", "top", "center"];
5723
6779
  var MAP_STYLE_VALUES = ["terrain", "satellite", "road", "toner", "watercolor"];
5724
6780
  var DATE_EVENT_MOOD_VALUES = ["neutral", "somber", "celebratory"];
5725
6781
  var COLOR_SCHEME_HINT = "theme color-scheme name \u2014 e.g. 'blue', 'green'";
6782
+ function CHART_DESCRIPTORS(extra = []) {
6783
+ return [
6784
+ { key: "title", description: "Optional title above the chart" },
6785
+ {
6786
+ key: "labelColumn",
6787
+ description: "Category/label column",
6788
+ valueHint: "header name or 0-based index"
6789
+ },
6790
+ {
6791
+ key: "valueColumns",
6792
+ description: "Value columns to plot",
6793
+ coerce: "stringList",
6794
+ valueHint: "comma-separated header names or 0-based indexes"
6795
+ },
6796
+ {
6797
+ key: "showTable",
6798
+ description: "Render the source table beneath the chart",
6799
+ coerce: "boolean"
6800
+ },
6801
+ { key: "showLegend", description: "Show a series legend", coerce: "boolean" },
6802
+ { key: "showValues", description: "Print numeric value labels on marks", coerce: "boolean" },
6803
+ { key: "unit", description: 'Unit suffix for tick/value labels (e.g. "km")' },
6804
+ {
6805
+ key: "colorScheme",
6806
+ description: "Color scheme seeding the series color rotation",
6807
+ valueHint: COLOR_SCHEME_HINT
6808
+ },
6809
+ // `headers`/`rows` are normally derived from the block's markdown table;
6810
+ // declared so data-fence-supplied values aren't flagged as unknown.
6811
+ {
6812
+ key: "headers",
6813
+ description: "Header cells (normally derived from the table)",
6814
+ coerce: "stringList"
6815
+ },
6816
+ { key: "rows", description: "Table rows (supply via a data fence)" },
6817
+ ...extra
6818
+ ];
6819
+ }
6820
+ var STACKED_DESCRIPTOR = {
6821
+ key: "stacked",
6822
+ description: "Stack series instead of grouping them side by side",
6823
+ coerce: "boolean"
6824
+ };
5726
6825
  var BASE_INPUT_DESCRIPTORS = [
5727
6826
  {
5728
6827
  key: "imageTreatment",
@@ -5844,6 +6943,13 @@ var TEMPLATE_INPUT_DESCRIPTORS = {
5844
6943
  { key: "align", description: "Per-column alignment (supply via a data fence)" },
5845
6944
  { key: "colorScheme", description: "Header color scheme", valueHint: COLOR_SCHEME_HINT }
5846
6945
  ],
6946
+ barChart: CHART_DESCRIPTORS([STACKED_DESCRIPTOR]),
6947
+ columnChart: CHART_DESCRIPTORS([STACKED_DESCRIPTOR]),
6948
+ pieChart: CHART_DESCRIPTORS(),
6949
+ donutChart: CHART_DESCRIPTORS(),
6950
+ lineChart: CHART_DESCRIPTORS(),
6951
+ areaChart: CHART_DESCRIPTORS(),
6952
+ scatterChart: CHART_DESCRIPTORS(),
5847
6953
  tree: [
5848
6954
  { key: "title", description: "Optional title above the tree" },
5849
6955
  { key: "colorScheme", description: "Folder-row color scheme", valueHint: COLOR_SCHEME_HINT },
@@ -6315,6 +7421,55 @@ var BLOCK_MEDIA_LAYOUT_POLICIES = {
6315
7421
  unconsumedMedia: "reserved-slot",
6316
7422
  variants: DENSE_VARIANTS
6317
7423
  },
7424
+ barChart: {
7425
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7426
+ noMedia: "template-default",
7427
+ ownership: "supplemental",
7428
+ unconsumedMedia: "reserved-slot",
7429
+ variants: DENSE_VARIANTS
7430
+ },
7431
+ columnChart: {
7432
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7433
+ noMedia: "template-default",
7434
+ ownership: "supplemental",
7435
+ unconsumedMedia: "reserved-slot",
7436
+ variants: DENSE_VARIANTS
7437
+ },
7438
+ pieChart: {
7439
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7440
+ noMedia: "template-default",
7441
+ ownership: "supplemental",
7442
+ unconsumedMedia: "reserved-slot",
7443
+ variants: DENSE_VARIANTS
7444
+ },
7445
+ donutChart: {
7446
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7447
+ noMedia: "template-default",
7448
+ ownership: "supplemental",
7449
+ unconsumedMedia: "reserved-slot",
7450
+ variants: DENSE_VARIANTS
7451
+ },
7452
+ lineChart: {
7453
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7454
+ noMedia: "template-default",
7455
+ ownership: "supplemental",
7456
+ unconsumedMedia: "reserved-slot",
7457
+ variants: DENSE_VARIANTS
7458
+ },
7459
+ areaChart: {
7460
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7461
+ noMedia: "template-default",
7462
+ ownership: "supplemental",
7463
+ unconsumedMedia: "reserved-slot",
7464
+ variants: DENSE_VARIANTS
7465
+ },
7466
+ scatterChart: {
7467
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7468
+ noMedia: "template-default",
7469
+ ownership: "supplemental",
7470
+ unconsumedMedia: "reserved-slot",
7471
+ variants: DENSE_VARIANTS
7472
+ },
6318
7473
  diagram: {
6319
7474
  summary: "The node-and-edge canvas is intrinsic media and retains its spatial coordinate system.",
6320
7475
  noMedia: "intrinsic-visual",
@@ -7300,6 +8455,34 @@ var TEMPLATE_METADATA = {
7300
8455
  label: "Data Table",
7301
8456
  description: "Renders tabular data in a clean, styled table with a header row."
7302
8457
  },
8458
+ barChart: {
8459
+ label: "Bar Chart",
8460
+ description: "Horizontal bars from the block's markdown table \u2014 one bar per row."
8461
+ },
8462
+ columnChart: {
8463
+ label: "Column Chart",
8464
+ description: "Vertical columns from the block's markdown table \u2014 one column per row."
8465
+ },
8466
+ pieChart: {
8467
+ label: "Pie Chart",
8468
+ description: "A pie of the block's markdown table \u2014 each row becomes a labeled slice."
8469
+ },
8470
+ donutChart: {
8471
+ label: "Donut Chart",
8472
+ description: "A donut of the block's markdown table \u2014 each row becomes a labeled slice."
8473
+ },
8474
+ lineChart: {
8475
+ label: "Line Chart",
8476
+ description: "Lines from the block's markdown table \u2014 one line per value column."
8477
+ },
8478
+ areaChart: {
8479
+ label: "Area Chart",
8480
+ description: "Filled area lines from the block's markdown table \u2014 one area per value column."
8481
+ },
8482
+ scatterChart: {
8483
+ label: "Scatter Chart",
8484
+ description: "An x/y scatter plot from the block's markdown table's numeric columns."
8485
+ },
7303
8486
  diagram: {
7304
8487
  label: "Diagram",
7305
8488
  description: "Renders sub-headings as connected nodes \u2014 edit visually as a node-and-edge diagram."
@@ -7445,6 +8628,48 @@ var TEMPLATE_AUTHORING_METADATA = {
7445
8628
  safeForContentFirst: false,
7446
8629
  placement: "heading"
7447
8630
  },
8631
+ barChart: {
8632
+ role: "data",
8633
+ bodyPolicy: "structured",
8634
+ safeForContentFirst: false,
8635
+ placement: "heading"
8636
+ },
8637
+ columnChart: {
8638
+ role: "data",
8639
+ bodyPolicy: "structured",
8640
+ safeForContentFirst: false,
8641
+ placement: "heading"
8642
+ },
8643
+ pieChart: {
8644
+ role: "data",
8645
+ bodyPolicy: "structured",
8646
+ safeForContentFirst: false,
8647
+ placement: "heading"
8648
+ },
8649
+ donutChart: {
8650
+ role: "data",
8651
+ bodyPolicy: "structured",
8652
+ safeForContentFirst: false,
8653
+ placement: "heading"
8654
+ },
8655
+ lineChart: {
8656
+ role: "data",
8657
+ bodyPolicy: "structured",
8658
+ safeForContentFirst: false,
8659
+ placement: "heading"
8660
+ },
8661
+ areaChart: {
8662
+ role: "data",
8663
+ bodyPolicy: "structured",
8664
+ safeForContentFirst: false,
8665
+ placement: "heading"
8666
+ },
8667
+ scatterChart: {
8668
+ role: "data",
8669
+ bodyPolicy: "structured",
8670
+ safeForContentFirst: false,
8671
+ placement: "heading"
8672
+ },
7448
8673
  diagram: {
7449
8674
  role: "spatial",
7450
8675
  bodyPolicy: "structured",
@@ -8309,6 +9534,18 @@ function time(raw) {
8309
9534
  if (raw == null) return null;
8310
9535
  return parseTimeSeconds(raw);
8311
9536
  }
9537
+ function videoPlacement(params) {
9538
+ const explicit = params.placement ?? params.presentation;
9539
+ if (explicit === "content") return "content";
9540
+ if (explicit === "pip" || explicit === "picture-in-picture") return "picture-in-picture";
9541
+ if (explicit === "overlay" || explicit === "full-frame") return "overlay";
9542
+ if (params.pip === "" || params.pip === "true") return "picture-in-picture";
9543
+ if (params.overlay === "" || params.overlay === "true") return "overlay";
9544
+ return void 0;
9545
+ }
9546
+ function lockToBlock(raw) {
9547
+ return raw !== "false" && raw !== "0";
9548
+ }
8312
9549
  function toMediaClip(parsed, id) {
8313
9550
  const { template, params } = parsed;
8314
9551
  if (!template || !MEDIA_TEMPLATES.has(template)) return null;
@@ -8323,13 +9560,78 @@ function toMediaClip(parsed, id) {
8323
9560
  startAt: time(params.startAt ?? params.startTime) ?? 0,
8324
9561
  anchor
8325
9562
  };
9563
+ const placement = kind === "video" ? videoPlacement(params) : void 0;
9564
+ if (placement && placement !== "content") clip.placement = placement;
9565
+ if (kind === "video" && placement && placement !== "content" && params.lockToBlock != null) {
9566
+ const locked = lockToBlock(params.lockToBlock);
9567
+ clip.lockToBlock = locked;
9568
+ clip.anchor = locked ? "block" : "document";
9569
+ if (locked) clip.startAt = 0;
9570
+ }
8326
9571
  const clipStart = time(params.clipStart);
8327
9572
  const clipEnd = time(params.clipEnd);
8328
9573
  if (clipStart != null) clip.clipStart = clipStart;
8329
9574
  if (clipEnd != null) clip.clipEnd = clipEnd;
9575
+ if (clip.lockToBlock === true) delete clip.clipEnd;
8330
9576
  if (params.spillover === "true" || params.spillover === "") clip.spillover = true;
8331
9577
  return clip;
8332
9578
  }
9579
+ function firstVideoElement(children) {
9580
+ const visit = (nodes) => {
9581
+ for (const child of nodes) {
9582
+ if (child.type !== "htmlElement") continue;
9583
+ if (child.tagName === "video") return child;
9584
+ const nested = visit(child.children);
9585
+ if (nested) return nested;
9586
+ }
9587
+ return null;
9588
+ };
9589
+ return visit(children);
9590
+ }
9591
+ function standaloneHtmlVideo(node) {
9592
+ if (node.type === "htmlBlock") {
9593
+ if (!/^\s*<video\b/i.test(node.rawHtml)) return null;
9594
+ const element = firstVideoElement(node.htmlChildren);
9595
+ return element ? { element, raw: node.rawHtml.trim() } : null;
9596
+ }
9597
+ if (node.type !== "paragraph" || !node.children.every((child) => child.type === "htmlInline")) {
9598
+ return null;
9599
+ }
9600
+ const raw = node.children.map((child) => child.type === "htmlInline" ? child.rawHtml : "").join("");
9601
+ if (!/^\s*<video\b[\s\S]*<\/video>\s*$/i.test(raw)) return null;
9602
+ for (const child of node.children) {
9603
+ if (child.type !== "htmlInline") continue;
9604
+ const element = firstVideoElement(child.htmlChildren);
9605
+ if (element) return { element, raw: raw.trim() };
9606
+ }
9607
+ return null;
9608
+ }
9609
+ function htmlVideoClip(node, id) {
9610
+ const authored = standaloneHtmlVideo(node);
9611
+ if (!authored) return null;
9612
+ const { element } = authored;
9613
+ const placement = videoPlacement({
9614
+ placement: element.attributes["data-squisq-video-placement"] ?? ""
9615
+ });
9616
+ if (!placement || placement === "content") return null;
9617
+ const src = element.attributes.src;
9618
+ if (!src) return null;
9619
+ const locked = lockToBlock(element.attributes["data-squisq-video-lock-to-block"]);
9620
+ const clip = {
9621
+ id,
9622
+ src,
9623
+ kind: "video",
9624
+ placement,
9625
+ lockToBlock: locked,
9626
+ startAt: locked ? 0 : time(element.attributes["data-squisq-video-start-at"]) ?? 0,
9627
+ anchor: locked ? "block" : "document"
9628
+ };
9629
+ const clipStart = time(element.attributes["data-squisq-video-clip-start"]);
9630
+ const clipEnd = time(element.attributes["data-squisq-video-clip-end"]);
9631
+ if (clipStart != null) clip.clipStart = clipStart;
9632
+ if (!locked && clipEnd != null) clip.clipEnd = clipEnd;
9633
+ return clip;
9634
+ }
8333
9635
  function extractMediaFromContents(contents, makeId, blockId) {
8334
9636
  const media = [];
8335
9637
  const documentMedia = [];
@@ -8338,17 +9640,19 @@ function extractMediaFromContents(contents, makeId, blockId) {
8338
9640
  let originalIndex = 0;
8339
9641
  for (const node of contents ?? []) {
8340
9642
  const parsed = parseStandaloneAnnotation(node);
8341
- const clip = parsed ? toMediaClip(parsed, "") : null;
8342
- if (parsed && clip) {
9643
+ const clip = parsed ? toMediaClip(parsed, "") : htmlVideoClip(node, "");
9644
+ if (clip) {
8343
9645
  clip.id = makeId(clip.src, index++);
8344
9646
  const line = node.position?.start.line;
8345
9647
  if (line != null) clip.sourceLine = line;
8346
9648
  if (blockId !== void 0) {
8347
- const rawText = node.type === "paragraph" && node.children?.[0]?.type === "text" ? node.children[0].value.trim() : void 0;
9649
+ const authoredHtml = standaloneHtmlVideo(node);
9650
+ const rawText = node.type === "paragraph" && node.children?.[0]?.type === "text" ? node.children[0].value.trim() : authoredHtml?.raw;
8348
9651
  clip.origin = {
8349
9652
  blockId,
8350
9653
  index: originalIndex,
8351
- ...rawText !== void 0 ? { raw: rawText } : {}
9654
+ ...rawText !== void 0 ? { raw: rawText } : {},
9655
+ ...authoredHtml ? { format: "html" } : {}
8352
9656
  };
8353
9657
  }
8354
9658
  (clip.anchor === "document" ? documentMedia : media).push(clip);
@@ -8685,6 +9989,12 @@ function markdownToDoc(markdownDoc, options) {
8685
9989
  }
8686
9990
  currentTime += block.duration;
8687
9991
  }
9992
+ const blockById = new Map(allBlocks.map((block) => [block.id, block]));
9993
+ for (const clip of documentMedia) {
9994
+ if (clip.lockToBlock === false && clip.origin?.format === "html" && !/\bdata-squisq-video-start-at\s*=/i.test(clip.origin.raw ?? "")) {
9995
+ clip.startAt = blockById.get(clip.origin.blockId)?.startTime ?? 0;
9996
+ }
9997
+ }
8688
9998
  const captions = phrases.length > 0 ? {
8689
9999
  phrases,
8690
10000
  ...options?.captionsGeneratedAt ? { generatedAt: options.captionsGeneratedAt } : {},
@@ -8770,7 +10080,7 @@ function applyStructuredData(block, diagnostics) {
8770
10080
  }
8771
10081
  data = { ...data, ...result.data };
8772
10082
  }
8773
- if (block.template === "dataTable") {
10083
+ if (TABLE_FED_TEMPLATES.has(resolveTemplateName(block.template ?? ""))) {
8774
10084
  const provided = (key) => data && key in data || block.templateOverrides && key in block.templateOverrides;
8775
10085
  if (!provided("headers") && !provided("rows")) {
8776
10086
  const table = findFirstTable(block.contents);
@@ -9296,6 +10606,7 @@ export {
9296
10606
  wrapWithPersistentLayers,
9297
10607
  TEMPLATE_ALIASES,
9298
10608
  resolveTemplateName,
10609
+ TABLE_FED_TEMPLATES,
9299
10610
  CONTAINER_TEMPLATES,
9300
10611
  isContainerTemplate,
9301
10612
  resolveColorScheme,
@@ -9359,6 +10670,7 @@ export {
9359
10670
  videoWithCaption,
9360
10671
  videoPullQuote,
9361
10672
  dataTable,
10673
+ buildChartData,
9362
10674
  DIAGRAM_LABEL_LINE_HEIGHT,
9363
10675
  DIAGRAM_LABEL_HORIZONTAL_PADDING,
9364
10676
  DIAGRAM_LABEL_VERTICAL_PADDING,