@openpresentation/opf-render 0.26.10 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/charts.js CHANGED
@@ -4,14 +4,15 @@
4
4
  // The table below is carried here so the preview stays synchronous and self-contained; test/chart-catalog.mjs checks it
5
5
  // against core's CHART_TYPES. Each type renders the construct PowerPoint shows for the exported chart; any other value
6
6
  // returns null so the caller keeps its legacy single-series preview.
7
- import { chartHighlightColors, chartHighlightMarks, chartNumber, chartPaletteForFill, chartSeriesPalette as coreChartSeriesPalette, chartTextSize, formatDataNumber, maxIntervalsFor, niceScale, stackCategoryValues, numberFormatError, resolveChartData, resolveTextStyle, textColorForFill, textWidthMeasurer } from "@openpresentation/opf/composition";
7
+ import { chartHighlightColors, chartHighlightMarks, chartLeadFromScheme, chartNumber, chartPaletteForFill, chartSeriesPalette as coreChartSeriesPalette, chartTextSize, CHART_MAX_INTERVALS, chartIntervalsFor, formatDataNumber, maxIntervalsFor, niceScale, stackCategoryValues, numberFormatError, resolveChartData, resolveTextStyle, textColorForFill, textWidthMeasurer } from "@openpresentation/opf/composition";
8
8
  import { drawAxisTitles, drawBarLabel, drawCenteredLabel, drawPointLabel, drawSliceLabel, labelString, outsideLabelReserve, reportOptionDiagnostics, reserveAxisTitles, resolveOptions } from "./chart-options.js";
9
9
 
10
10
  // The series colours for a chart panel: core's `chartSeriesPalette` (the deck colour scheme's accents, RR-89, with the chart's own
11
11
  // `colors` entries replacing their positions) adjusted by core's `chartPaletteForFill` (contrast against the panel without letting
12
12
  // two series merge). The PPTX export and the validator use the same pair, so the preview draws the colours they write.
13
+ // WI-5 (P18, D3): series 1 from the scheme reaches 4.5:1 on a dark panel (an authored `colors[0]` keeps its colour).
13
14
  export function chartSeriesPalette(surface, colorScheme, colors, context) {
14
- return chartPaletteForFill(surface, coreChartSeriesPalette(colorScheme, colors, context));
15
+ return chartPaletteForFill(surface, coreChartSeriesPalette(colorScheme, colors, context), { legibleLead: chartLeadFromScheme(colors, colorScheme, context) });
15
16
  }
16
17
 
17
18
  // FA-14: chart.highlight. Core resolves which series and categories are named (chartHighlightMarks) and the two colours
@@ -116,7 +117,8 @@ export function renderCatalogChart(item, box, bound, options, svg) {
116
117
  const data = render ? chartData(item, bound) : null;
117
118
  if (!data || !data.rows.length || data.columns.length < (CHARTEX_KINDS.has(spec.kind) ? 1 : 2)) return null;
118
119
  const c = chartContext(item, box, bound, options, svg, data);
119
- c.mark("rect", { x: box.x, y: box.y, width: box.width, height: box.height, fill: c.surface, stroke: bound.design.colors.border, "stroke-width": 1 }, item.path);
120
+ // WI-5 (P19): the panel is a fill only; the axes and gridlines draw the plot, so there is no outer frame (the PPTX chart area has no line either).
121
+ c.mark("rect", { x: box.x, y: box.y, width: box.width, height: box.height, fill: c.surface }, item.path);
120
122
  // RR-35: a chart with axis titles, a legend position or data labels reserves their space before the plot is laid out. A chart
121
123
  // with none of them skips this block entirely, so its output is unchanged.
122
124
  const resolved = resolveOptions(item.value, spec);
@@ -633,7 +635,7 @@ function categoryScale(c, dataMin, dataMax, length, labelExtent, percent) {
633
635
  const scale = scaleFrom(axis.min, axis.max, axis.majorUnit);
634
636
  if (scale.ticks.every(Number.isFinite)) return scale;
635
637
  }
636
- return niceScale(dataMin, dataMax, maxIntervalsFor(length, labelExtent), { percent });
638
+ return niceScale(dataMin, dataMax, chartIntervalsFor(length, labelExtent, c.u), { percent });
637
639
  }
638
640
 
639
641
  function renderCategoryChart(c, spec) {
@@ -860,7 +862,7 @@ function renderComboChart(c, spec) {
860
862
  const tickWidthOf = (scale, format) => Math.max(...scale.ticks.map((tick) => c.width(formatTick(tick, false, format)))) + fontPx * 0.5;
861
863
  const axis = planCategoryAxis(c, entries, (reserve) => {
862
864
  const height = Math.max(1, box.y + box.height - pad - reserve - top);
863
- const intervals = maxIntervalsFor(height, c.lineHeight * 1.2);
865
+ const intervals = chartIntervalsFor(height, c.lineHeight * 1.2, c.u);
864
866
  const scale = niceScale(primaryMin, primaryMax, intervals);
865
867
  const secondary = secondaryLines.length ? niceScale(secondaryMin, secondaryMax, intervals) : null;
866
868
  const primaryWidth = tickWidthOf(scale, primaryFormat), secondaryWidth = secondary ? tickWidthOf(secondary, secondaryFormat) : 0;
@@ -1004,12 +1006,12 @@ function renderScatterChart(c) {
1004
1006
  const legendWidth = seriesLegend(c, series, "marker");
1005
1007
  const top = box.y + pad + c.lineHeight / 2, bottom = box.y + box.height - pad - c.lineHeight;
1006
1008
  const height = Math.max(1, bottom - top);
1007
- const yScale = niceScale(ys.length ? Math.min(...ys) : 0, ys.length ? Math.max(...ys) : 1, maxIntervalsFor(height, c.lineHeight * 1.2));
1009
+ const yScale = niceScale(ys.length ? Math.min(...ys) : 0, ys.length ? Math.max(...ys) : 1, chartIntervalsFor(height, c.lineHeight * 1.2, c.u));
1008
1010
  const tickWidth = Math.max(...yScale.ticks.map((tick) => c.width(formatTick(tick, false, yFormat)))) + fontPx * 0.5;
1009
1011
  const plot = { x: box.x + pad + tickWidth, y: top, height };
1010
1012
  plot.width = Math.max(1, box.x + box.width - pad - legendWidth - fontPx - plot.x);
1011
1013
  const xLabelWidth = Math.max(c.width(formatTick(xs.length ? Math.max(...xs) : 1, false, xFormat)), c.width(formatTick(xs.length ? Math.min(...xs) : 0, false, xFormat))) + fontPx;
1012
- const scaleForX = (w) => niceScale(xs.length ? Math.min(...xs) : 0, xs.length ? Math.max(...xs) : 1, maxIntervalsFor(w, xLabelWidth));
1014
+ const scaleForX = (w) => niceScale(xs.length ? Math.min(...xs) : 0, xs.length ? Math.max(...xs) : 1, chartIntervalsFor(w, xLabelWidth, c.u));
1013
1015
  let xScale = scaleForX(plot.width);
1014
1016
  // RR-54: a formatted X axis keeps its end labels inside the chart.
1015
1017
  if (xFormat !== undefined) ({ x: plot.x, width: plot.width, scale: xScale } = insetForEndLabels(c, plot.x, plot.width, xFormat, scaleForX));
@@ -1193,6 +1195,12 @@ export function histogramBins(values) {
1193
1195
  }
1194
1196
  }
1195
1197
 
1198
+ // WI-5: the plot is laid out before its axis is fitted, so the tick-label column is sized from the labels any of the axes the plot could end up with
1199
+ // (one to CHART_MAX_INTERVALS intervals) would draw, not from a ten-interval scale that the final, sparser axis may exceed ("100" after "90").
1200
+ function probeTicks(dataMin, dataMax) {
1201
+ return [...new Set(Array.from({ length: CHART_MAX_INTERVALS }, (_, index) => niceScale(dataMin, dataMax, index + 1).ticks).flat())];
1202
+ }
1203
+
1196
1204
  // Vertical layout shared by the axis-bearing constructs: category labels below,
1197
1205
  // value ticks on the left (and a percentage axis on the right for pareto).
1198
1206
  function chartexPlot(c, { categories, legendWidth = 0, tickLabels = [], percentAxis = false }) {
@@ -1238,9 +1246,9 @@ function renderHistogramChart(c, spec) {
1238
1246
  const total = bars.reduce((sum, bar) => sum + Math.max(0, bar.value), 0);
1239
1247
  if (!Number.isFinite(total)) throw chartAggregateError(c.path, "cumulative total");
1240
1248
  const dataMax = bars.length ? Math.max(...bars.map((bar) => bar.value)) : 1, dataMin = bars.length ? Math.min(0, ...bars.map((bar) => bar.value)) : 0;
1241
- const probe = niceScale(dataMin, dataMax, 10);
1249
+ const probe = { ticks: probeTicks(dataMin, dataMax) };
1242
1250
  const { plot, band } = chartexPlot(c, { categories: bars.map((bar) => ({ name: bar.name, path: bar.labelPath })), tickLabels: probe.ticks.map((tick) => formatTick(tick, false, format)), percentAxis: pareto });
1243
- const scale = niceScale(dataMin, dataMax, maxIntervalsFor(plot.height, c.lineHeight * 1.2));
1251
+ const scale = niceScale(dataMin, dataMax, chartIntervalsFor(plot.height, c.lineHeight * 1.2, c.u));
1244
1252
  const { at, crossing } = valueAxis(c, plot, scale, format);
1245
1253
  const width = band / 1.06, offset = (band - width) / 2;
1246
1254
  bars.forEach((bar, i) => {
@@ -1254,7 +1262,7 @@ function renderHistogramChart(c, spec) {
1254
1262
  });
1255
1263
  if (pareto && bars.length) {
1256
1264
  // Percentage axis on the right, 0-100%, and the cumulative line through the bar centres.
1257
- const percent = niceScale(0, 1, maxIntervalsFor(plot.height, c.lineHeight * 1.2), { percent: true });
1265
+ const percent = niceScale(0, 1, chartIntervalsFor(plot.height, c.lineHeight * 1.2, c.u), { percent: true });
1258
1266
  const right = plot.x + plot.width;
1259
1267
  axisLine(c, right, plot.y, right, plot.y + plot.height);
1260
1268
  for (const tick of percent.ticks) {
@@ -1285,9 +1293,9 @@ function renderWaterfallChart(c) {
1285
1293
  }).filter(Boolean);
1286
1294
  const extents = bars.flatMap((bar) => [bar.from, bar.to]);
1287
1295
  const dataMin = extents.length ? Math.min(0, ...extents) : 0, dataMax = extents.length ? Math.max(0, ...extents) : 1;
1288
- const probe = niceScale(dataMin, dataMax, 10);
1296
+ const probe = { ticks: probeTicks(dataMin, dataMax) };
1289
1297
  const { plot, band } = chartexPlot(c, { categories: categories.map((name, i) => ({ name, path: `${c.path}.data.rows.${i}.0` })), tickLabels: probe.ticks.map((tick) => formatTick(tick, false, format)) });
1290
- const scale = niceScale(dataMin, dataMax, maxIntervalsFor(plot.height, c.lineHeight * 1.2));
1298
+ const scale = niceScale(dataMin, dataMax, chartIntervalsFor(plot.height, c.lineHeight * 1.2, c.u));
1291
1299
  const { at } = valueAxis(c, plot, scale, format);
1292
1300
  const width = band / 1.5, offset = (band - width) / 2;
1293
1301
  bars.forEach((bar, k) => {
@@ -1443,9 +1451,9 @@ function renderBoxWhiskerChart(c) {
1443
1451
  const all = boxes.flat().flatMap((entry) => entry.stats ? [entry.stats.low, entry.stats.high, ...entry.stats.outliers] : []);
1444
1452
  const dataMin = all.length ? Math.min(...all) : 0, dataMax = all.length ? Math.max(...all) : 1;
1445
1453
  const legendWidth = seriesLegend(c, series);
1446
- const probe = niceScale(dataMin, dataMax, 10);
1454
+ const probe = { ticks: probeTicks(dataMin, dataMax) };
1447
1455
  const { plot, band } = chartexPlot(c, { categories: groups.map((group) => ({ name: group.name, path: lone ? c.path : `${c.path}.data.rows.${group.first}.0` })), legendWidth, tickLabels: probe.ticks.map((tick) => formatTick(tick, false, format)) });
1448
- const scale = niceScale(dataMin, dataMax, maxIntervalsFor(plot.height, c.lineHeight * 1.2));
1456
+ const scale = niceScale(dataMin, dataMax, chartIntervalsFor(plot.height, c.lineHeight * 1.2, c.u));
1449
1457
  const { at } = valueAxis(c, plot, scale, format);
1450
1458
  const group = band / 2, width = group / series.length, offset = (band - group) / 2;
1451
1459
  series.forEach((s, j) => {
package/dist/svg.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  layoutTable, fitList, fitRichText, composeSlide, resolveTextStyle, textWidthMeasurer, fitText, textColorForFill, chartColorForFill,
5
5
  resolveColorRef as resolveCoreColorRef, resolveScriptFonts, paragraphDirection, physicalAlignment, patternRuns,
6
- metricTrendMark as coreMetricTrendMark, tokenizeCode, codeLineRuns, codeSyntaxPaletteForRoles, readableSchemeColor, resolveChartData,
6
+ metricTrendMark as coreMetricTrendMark, metricValueColor, tokenizeCode, codeLineRuns, codeSyntaxPaletteForRoles, readableSchemeColor, resolveChartData,
7
7
  timelineMarkerShapes, timelineTextColor, layoutWatermark,
8
8
  codeHighlightLines, codeLineNumbers, codeHighlightBands, codeHighlightColors, fitImage, intrinsicImageAspect, intrinsicImageSize,
9
9
  ENGINE_DEFAULT_THEME, ENGINE_DEFAULT_COLOR_SCHEME, ENGINE_DEFAULT_FONT_SCHEME, ENGINE_DEFAULT_CHART_TYPES
@@ -1390,7 +1390,7 @@ function renderMetric(item, box, bound, options) {
1390
1390
  const children=[];
1391
1391
  // RR-07: a trend draws an arrow beside its word and colours the trend and delta text, from core's accepted geometry.
1392
1392
  const trendMark=metricTrendMark(layout,bound,options);
1393
- const partFill=part=>trendMark&&(part.role==='trend'||part.role==='delta')?trendMark.color:part.role==='value'?bound.design.colors.primary:bound.design.colors.text;
1393
+ const partFill=part=>trendMark&&(part.role==='trend'||part.role==='delta')?trendMark.color:part.role==='value'?metricValueColor(bound.design.colors.primary,bound.design.backgroundColor??bound.design.colors.background):bound.design.colors.text;
1394
1394
  for (const part of layout.parts) {
1395
1395
  const invalid=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/u.exec(part.text);
1396
1396
  if (invalid) throw new OPFRenderError('invalid-metric-text',`Metric text contains U+${invalid[0].codePointAt(0).toString(16).toUpperCase().padStart(4,'0')} at UTF-16 offset ${invalid.index}, which XML cannot represent; edit that character before rendering.`,{path:part.path});
@@ -1494,7 +1494,8 @@ function renderTable(item, box, bound, options) {
1494
1494
  const children = [];
1495
1495
  // RR-54: a dataset table has no rows or columns of its own to point at; every cell reports the table's authored path.
1496
1496
  const own = isDatasetTable(item, bound) ? () => item.path : (path) => path;
1497
- const separateBorders = layout.rows.some(row => row.cells.some(cell => cell.style?.borders));
1497
+ // WI-5 (P20): a table draws horizontal rules only by default (the header rule and a rule under every row, as the PPTX export writes them).
1498
+ // A vertical edge is drawn only where a cell sets that border; an explicit border always wins over the default for its edge.
1498
1499
  const defaultEdges = [], explicitEdges = [];
1499
1500
  for (const row of layout.rows) for (const cell of row.cells) {
1500
1501
  const style = cell.style ?? {};
@@ -1511,15 +1512,16 @@ function renderTable(item, box, bound, options) {
1511
1512
  x: stableNumber(cell.box.x), y: stableNumber(cell.box.y),
1512
1513
  width: stableNumber(cell.box.width), height: stableNumber(cell.box.height),
1513
1514
  fill,
1514
- stroke: separateBorders ? undefined : bound.design.colors.border, "stroke-width":separateBorders ? undefined : 1,
1515
1515
  ...traceAttrs(options, own(cell.sourcePath ?? cell.path))
1516
1516
  }));
1517
- if (separateBorders) {
1517
+ {
1518
1518
  const {x, y, width, height} = cell.box;
1519
1519
  const edges = {top:[x,y,x+width,y],right:[x+width,y,x+width,y+height],bottom:[x,y+height,x+width,y+height],left:[x,y,x,y+height]};
1520
1520
  for (const [edge, coordinates] of Object.entries(edges)) {
1521
1521
  const border = style.borders?.[edge];
1522
- (border ? explicitEdges : defaultEdges).push({coordinates, border, path:own(`${cell.sourcePath ?? cell.path}.style.borders.${edge}`)});
1522
+ // Default rules: under every cell, and over the first row only (a lower row's top edge is the rule under the row above).
1523
+ if (!border && (edge === "left" || edge === "right" || (edge === "top" && cell.row !== 0))) continue;
1524
+ (border ? explicitEdges : defaultEdges).push({coordinates, border, path:own(border ? `${cell.sourcePath ?? cell.path}.style.borders.${edge}` : (cell.sourcePath ?? cell.path))});
1523
1525
  }
1524
1526
  }
1525
1527
  // Core layout has already applied vertical alignment to cell.textBox.y.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-render",
3
- "version": "0.26.10",
3
+ "version": "0.27.0",
4
4
  "description": "Deterministic local OPF renderer for SVG, PNG, and PDF output.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -94,7 +94,7 @@
94
94
  "fontkit": "^2.0.4",
95
95
  "harfbuzzjs": "1.6.3",
96
96
  "pako": "3.0.2",
97
- "@openpresentation/opf": "^0.26.10"
97
+ "@openpresentation/opf": "^0.27.0"
98
98
  },
99
99
  "peerDependencies": {
100
100
  "@expo-google-fonts/arimo": "0.4.3",
@@ -343,7 +343,7 @@
343
343
  "pdfjs-dist": "6.4.299",
344
344
  "playwright": "1.64.0",
345
345
  "sharp": "0.35.5",
346
- "@openpresentation/gallery": "0.26.10"
346
+ "@openpresentation/gallery": "0.27.0"
347
347
  },
348
348
  "bin": {
349
349
  "opf-preview-fonts": "./dist/preview-fonts-cli.js"
@@ -399,5 +399,5 @@
399
399
  "test:preview-a11y-browser": "node test/preview-a11y-browser.mjs",
400
400
  "test:forced-colors-browser": "node test/forced-colors-browser.mjs artifacts/forced-colors"
401
401
  },
402
- "gitHead": "da3f869f6f3a152a0d7d5cb42eeb2843d302a46a"
402
+ "gitHead": "f90536d0e9a7a16b25aa718e03ced4e00a7316e1"
403
403
  }