@openpresentation/opf-render 0.13.1 → 0.15.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.
@@ -3,17 +3,16 @@
3
3
  // Core owns the normalisation and the per-type support table (`resolveChartOptions`); this module owns the drawing.
4
4
  // A chart that carries none of the three fields resolves to `active: false` and never reaches any code here, so
5
5
  // its SVG is exactly what it was before.
6
- import * as core from "@openpresentation/opf/composition";
6
+ import { chartLabelText, formatChartLabelNumber, formatChartLabelPercent, formatDataNumber, resolveChartOptions, textColorForFill } from "@openpresentation/opf/composition";
7
7
 
8
8
  const INACTIVE = Object.freeze({ active: false, axisTitles: {}, diagnostics: [] });
9
9
 
10
10
  /**
11
- * The resolved options of a chart. An older core without `resolveChartOptions` ignores the fields, like any other
12
- * field it does not know. `spec` is the catalog spec this module's charts.js resolved (`kind`, `grouping`).
11
+ * The resolved options of a chart. `spec` is the catalog spec this module's charts.js resolved (`kind`, `grouping`).
13
12
  */
14
13
  export function resolveOptions(chart, spec) {
15
- if (typeof core.resolveChartOptions !== "function" || !chart || typeof chart !== "object") return INACTIVE;
16
- return core.resolveChartOptions(chart, { kind: spec.kind, stacked: spec.grouping === "stacked" || spec.grouping === "percentStacked" });
14
+ if (!chart || typeof chart !== "object") return INACTIVE;
15
+ return resolveChartOptions(chart, { kind: spec.kind, stacked: spec.grouping === "stacked" || spec.grouping === "percentStacked" });
17
16
  }
18
17
 
19
18
  export function reportOptionDiagnostics(c, resolved) {
@@ -22,9 +21,13 @@ export function reportOptionDiagnostics(c, resolved) {
22
21
  }
23
22
  }
24
23
 
25
- /** Which edge carries which axis title: the category axis of a bar or funnel chart is vertical (left). */
26
- function titleSides(spec) {
24
+ /**
25
+ * Which edge carries which axis title: the category axis of a bar or funnel chart is vertical (left). A combo chart titles its
26
+ * secondary value axis at the right, next to that axis; right to left, its two value axes trade sides (FA-15).
27
+ */
28
+ function titleSides(spec, rtl = false) {
27
29
  const verticalCategory = (spec.kind === "bar" && spec.dir === "bar") || spec.kind === "funnel";
30
+ if (spec.kind === "combo") return rtl ? { category: "bottom", value: "right", secondary: "left" } : { category: "bottom", value: "left", secondary: "right" };
28
31
  return verticalCategory ? { category: "left", value: "bottom" } : { category: "bottom", value: "left" };
29
32
  }
30
33
 
@@ -33,12 +36,13 @@ function titleSides(spec) {
33
36
  * The renderers lay the plot out inside `c.box`, so they need no knowledge of the bands.
34
37
  */
35
38
  export function reserveAxisTitles(c, spec, titles) {
36
- const sides = titleSides(spec);
37
- const bands = { left: 0, bottom: 0 };
39
+ const sides = titleSides(spec, spec.kind === "combo" && c.bound.geometry?.direction === "rtl");
40
+ const bands = { left: 0, bottom: 0, right: 0 };
38
41
  c.axisTitleBands = [];
39
- for (const axis of ["category", "value"]) {
42
+ for (const axis of ["category", "value", "secondary"]) {
40
43
  const text = titles[axis];
41
- if (!text) continue;
44
+ // A secondary title is drawn only when the combo chart has a secondary axis (core reports the title it drops).
45
+ if (!text || !sides[axis] || (axis === "secondary" && !c.combo?.some((entry) => entry.axis === "secondary"))) continue;
42
46
  const side = sides[axis];
43
47
  bands[side] = c.lineHeight;
44
48
  c.axisTitleBands.push({ axis, side, text });
@@ -46,7 +50,7 @@ export function reserveAxisTitles(c, spec, titles) {
46
50
  if (!c.axisTitleBands.length) return;
47
51
  const outer = c.box;
48
52
  c.axisTitleOuter = outer;
49
- c.box = { x: outer.x + bands.left, y: outer.y, width: Math.max(1, outer.width - bands.left), height: Math.max(1, outer.height - bands.bottom) };
53
+ c.box = { x: outer.x + bands.left, y: outer.y, width: Math.max(1, outer.width - bands.left - bands.right), height: Math.max(1, outer.height - bands.bottom) };
50
54
  }
51
55
 
52
56
  /**
@@ -65,8 +69,10 @@ export function drawAxisTitles(c) {
65
69
  c.text(text, rect, path, "center");
66
70
  } else {
67
71
  const span = plot ? { y: plot.y, height: plot.height } : { y: inner.y, height: inner.height };
68
- // The rotated text box is laid out horizontally, `length` wide and one line tall, then turned about its centre.
69
- const length = Math.max(1, span.height), centreX = outer.x + c.lineHeight / 2, centreY = span.y + span.height / 2;
72
+ // The rotated text box is laid out horizontally, `length` wide and one line tall, then turned about its centre. A right title
73
+ // (a combo chart's secondary axis) is rotated the same way, as PowerPoint draws every vertical axis title by default.
74
+ const length = Math.max(1, span.height), centreY = span.y + span.height / 2;
75
+ const centreX = side === "right" ? outer.x + outer.width - (c.axisTitleRightInset ?? 0) - c.lineHeight / 2 : outer.x + c.lineHeight / 2;
70
76
  const rect = { x: centreX - length / 2, y: centreY - c.lineHeight / 2, width: length, height: c.lineHeight };
71
77
  const element = c.textElement(text, rect, path, "center");
72
78
  if (element) c.children.push(c.svg.tag("g", { transform: `rotate(-90 ${c.num(centreX)} ${c.num(centreY)})` }, element));
@@ -84,9 +90,9 @@ export function labelString(c, { category, value, share, format }) {
84
90
  const labels = c.dataLabels;
85
91
  const parts = {};
86
92
  if (category !== undefined && category !== null) parts.category = String(category);
87
- if (typeof value === "number" && Number.isFinite(value)) parts.value = format !== undefined && typeof core.formatDataNumber === "function" ? core.formatDataNumber(value, format) : core.formatChartLabelNumber(value);
88
- if (typeof share === "number" && Number.isFinite(share)) parts.percent = core.formatChartLabelPercent(share);
89
- return core.chartLabelText(parts, labels.content, labels.separator);
93
+ if (typeof value === "number" && Number.isFinite(value)) parts.value = format !== undefined ? formatDataNumber(value, format) : formatChartLabelNumber(value);
94
+ if (typeof share === "number" && Number.isFinite(share)) parts.percent = formatChartLabelPercent(share);
95
+ return chartLabelText(parts, labels.content, labels.separator);
90
96
  }
91
97
 
92
98
  const labelSize = (c, text) => ({ width: c.width(text) + c.fontPx * 0.5, height: c.lineHeight });
@@ -99,7 +105,7 @@ function drawLabel(c, text, rect, path, fill) {
99
105
 
100
106
  /** The text colour for a label placed inside a mark of colour `fill`: the contrasting one; outside marks keep the chart text colour. */
101
107
  export function labelFill(c, position, fill) {
102
- return INSIDE.has(position) && fill ? core.textColorForFill?.(fill, c.labelColor) ?? c.labelColor : c.labelColor;
108
+ return INSIDE.has(position) && fill ? textColorForFill(fill, c.labelColor) : c.labelColor;
103
109
  }
104
110
 
105
111
  /**
@@ -133,9 +139,8 @@ export function drawBarLabel(c, text, rect, direction, path, fill) {
133
139
  drawLabel(c, text, { x, y, width, height }, path, labelFill(c, position, fill));
134
140
  }
135
141
 
136
- /** A label beside a point mark (line and scatter): above, below, left, right or centred on it. */
137
- export function drawPointLabel(c, text, point, path) {
138
- const position = c.dataLabels.position ?? "above";
142
+ /** A label beside a point mark (line and scatter): above, below, left, right or centred on it. `position` overrides the chart's (a combo chart's line series). */
143
+ export function drawPointLabel(c, text, point, path, position = c.dataLabels.position ?? "above") {
139
144
  const { width, height } = labelSize(c, text);
140
145
  const gap = 3 * c.pt + c.fontPx * 0.25;
141
146
  const [px, py] = point;