@openpresentation/opf-render 0.26.9 → 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 +22 -14
- package/dist/svg.js +8 -6
- package/package.json +4 -4
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
|
-
|
|
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,
|
|
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 =
|
|
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,
|
|
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,
|
|
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 =
|
|
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,
|
|
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,
|
|
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 =
|
|
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,
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
402
|
+
"gitHead": "f90536d0e9a7a16b25aa718e03ced4e00a7316e1"
|
|
403
403
|
}
|