pptx-angular-viewer 1.1.15 → 1.1.16
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.
|
@@ -11833,6 +11833,1251 @@ function applyResize(start, handle, dx, dy, min = MIN_RESIZE) {
|
|
|
11833
11833
|
return { x, y, width, height };
|
|
11834
11834
|
}
|
|
11835
11835
|
|
|
11836
|
+
/**
|
|
11837
|
+
* View-model builders for combo and stock chart kinds.
|
|
11838
|
+
*
|
|
11839
|
+
* Ported from:
|
|
11840
|
+
* packages/react/src/viewer/utils/chart-waterfall-combo.tsx (renderComboChart)
|
|
11841
|
+
* packages/react/src/viewer/utils/chart-stock.tsx (renderStockChart)
|
|
11842
|
+
*
|
|
11843
|
+
* All functions here are pure TypeScript with zero Angular dependencies.
|
|
11844
|
+
* The component consumes a single `ChartViewModel` (same contract as the
|
|
11845
|
+
* helpers in chart-renderer-helpers.ts) that is the projection of a
|
|
11846
|
+
* `ChartPptxElement` -> SVG primitives.
|
|
11847
|
+
*
|
|
11848
|
+
* Combo charts:
|
|
11849
|
+
* series[0] → bar/column rectangles (one per category)
|
|
11850
|
+
* series[1…N] → line + dots (one polyline + N circles per series)
|
|
11851
|
+
*
|
|
11852
|
+
* Stock charts (HLC / OHLC candlesticks):
|
|
11853
|
+
* 3-series HLC → series: High, Low, Close
|
|
11854
|
+
* 4-series OHLC → series: Open, High, Low, Close
|
|
11855
|
+
* Per candle: a vertical wick line (high–low) + a body rect (open–close).
|
|
11856
|
+
* Body is green when close ≥ open, red otherwise.
|
|
11857
|
+
*
|
|
11858
|
+
* @module chart-combo-stock
|
|
11859
|
+
*/
|
|
11860
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11861
|
+
// Candle colours (stock chart)
|
|
11862
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11863
|
+
const CANDLE_UP_FILL = '#22c55e';
|
|
11864
|
+
const CANDLE_DOWN_FILL = '#ef4444';
|
|
11865
|
+
const CANDLE_WICK_COLOR = '#334155';
|
|
11866
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11867
|
+
// Combo chart
|
|
11868
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11869
|
+
/**
|
|
11870
|
+
* Build a `ChartViewModel` for a combo chart (bar + line overlay).
|
|
11871
|
+
*
|
|
11872
|
+
* Convention (mirrors the React renderer):
|
|
11873
|
+
* - `chartData.series[0]` → rendered as clustered bar columns
|
|
11874
|
+
* - `chartData.series[1…N]` → rendered as line series with dot markers
|
|
11875
|
+
*
|
|
11876
|
+
* A single shared value-axis range is computed across ALL series so that the
|
|
11877
|
+
* bar and line series share the same Y scale. The category-axis tick style is
|
|
11878
|
+
* "bar" (evenly spaced groups with no extra edge padding).
|
|
11879
|
+
*
|
|
11880
|
+
* @param element - The chart element providing width/height.
|
|
11881
|
+
* @param chartData - Parsed chart data including series and style.
|
|
11882
|
+
* @param categoryLabels - Ordered category axis labels.
|
|
11883
|
+
* @returns A fully assembled `ChartViewModel` ready for the template.
|
|
11884
|
+
*/
|
|
11885
|
+
function buildComboViewModel(element, chartData, categoryLabels) {
|
|
11886
|
+
const layout = computePlotLayout(element.width, element.height, chartData, true);
|
|
11887
|
+
const catCount = Math.max(categoryLabels.length, 1);
|
|
11888
|
+
// Single shared range across all series so bar and line share Y scale.
|
|
11889
|
+
const range = computeValueRange(chartData.series);
|
|
11890
|
+
const { gridlines, axisLabels } = buildGridlinesAndLabels(range, layout);
|
|
11891
|
+
const zeroLine = buildZeroLine(range, layout);
|
|
11892
|
+
const catLabels = buildCategoryLabels(categoryLabels, layout, 'bar');
|
|
11893
|
+
const legendPos = chartData.style?.legendPosition ?? 'b';
|
|
11894
|
+
const { legend, legendX, legendY, legendAnchor } = buildLegend(chartData.series, chartData.colorPalette, layout.svgWidth, legendPos, layout.svgHeight, layout.plotTop);
|
|
11895
|
+
const primitives = [];
|
|
11896
|
+
const dataLabels = [];
|
|
11897
|
+
// ── Bar series (series[0]) ──────────────────────────────────────────────
|
|
11898
|
+
const barSeries = chartData.series.slice(0, 1);
|
|
11899
|
+
if (barSeries.length > 0) {
|
|
11900
|
+
const barRects = computeBarRects(barSeries, catCount, layout, range, chartData.colorPalette);
|
|
11901
|
+
for (const r of barRects) {
|
|
11902
|
+
primitives.push({
|
|
11903
|
+
kind: 'rect',
|
|
11904
|
+
x: r.x,
|
|
11905
|
+
y: r.y,
|
|
11906
|
+
w: r.w,
|
|
11907
|
+
h: r.h,
|
|
11908
|
+
fill: r.fill,
|
|
11909
|
+
rx: 1,
|
|
11910
|
+
});
|
|
11911
|
+
}
|
|
11912
|
+
if (chartData.style?.hasDataLabels && barSeries[0]) {
|
|
11913
|
+
const barGroupWidth = layout.plotWidth / catCount;
|
|
11914
|
+
const singleBarWidth = barGroupWidth * 0.7;
|
|
11915
|
+
const groupOffset = (barGroupWidth - singleBarWidth) / 2;
|
|
11916
|
+
barSeries[0].values.forEach((val, vi) => {
|
|
11917
|
+
const x = layout.plotLeft + barGroupWidth * vi + groupOffset + singleBarWidth / 2;
|
|
11918
|
+
const zeroY = valueToY(0, range, layout.plotTop, layout.plotBottom);
|
|
11919
|
+
const valY = valueToY(val, range, layout.plotTop, layout.plotBottom);
|
|
11920
|
+
const labelY = val >= 0 ? Math.min(zeroY, valY) - 4 : Math.max(zeroY, valY) + 10;
|
|
11921
|
+
dataLabels.push({
|
|
11922
|
+
kind: 'text',
|
|
11923
|
+
x,
|
|
11924
|
+
y: labelY,
|
|
11925
|
+
text: formatAxisValue(val),
|
|
11926
|
+
fontSize: 7,
|
|
11927
|
+
fill: '#334155',
|
|
11928
|
+
textAnchor: 'middle',
|
|
11929
|
+
});
|
|
11930
|
+
});
|
|
11931
|
+
}
|
|
11932
|
+
}
|
|
11933
|
+
// ── Line series (series[1…N]) ───────────────────────────────────────────
|
|
11934
|
+
// Line X positions are centred within each bar group for visual alignment.
|
|
11935
|
+
const barGroupWidth = layout.plotWidth / catCount;
|
|
11936
|
+
const lineSeries = chartData.series.slice(1);
|
|
11937
|
+
lineSeries.forEach((series, si) => {
|
|
11938
|
+
if (series.values.length === 0) {
|
|
11939
|
+
return;
|
|
11940
|
+
}
|
|
11941
|
+
const seriesIdx = si + 1; // offset past the bar series
|
|
11942
|
+
const c = seriesColor(series, seriesIdx, chartData.colorPalette);
|
|
11943
|
+
// Build points centred within each bar group to align with bar midpoints.
|
|
11944
|
+
const pts = series.values.map((val, vi) => ({
|
|
11945
|
+
x: layout.plotLeft + barGroupWidth * vi + barGroupWidth / 2,
|
|
11946
|
+
y: valueToY(val, range, layout.plotTop, layout.plotBottom),
|
|
11947
|
+
}));
|
|
11948
|
+
const pointsStr = pts.map((p) => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ');
|
|
11949
|
+
primitives.push({
|
|
11950
|
+
kind: 'polyline',
|
|
11951
|
+
points: pointsStr,
|
|
11952
|
+
stroke: c,
|
|
11953
|
+
strokeWidth: 2.4,
|
|
11954
|
+
fill: 'none',
|
|
11955
|
+
});
|
|
11956
|
+
for (const pt of pts) {
|
|
11957
|
+
primitives.push({
|
|
11958
|
+
kind: 'circle',
|
|
11959
|
+
cx: pt.x,
|
|
11960
|
+
cy: pt.y,
|
|
11961
|
+
r: 2.5,
|
|
11962
|
+
fill: c,
|
|
11963
|
+
});
|
|
11964
|
+
}
|
|
11965
|
+
if (chartData.style?.hasDataLabels) {
|
|
11966
|
+
series.values.forEach((val, vi) => {
|
|
11967
|
+
const pt = pts[vi];
|
|
11968
|
+
if (!pt) {
|
|
11969
|
+
return;
|
|
11970
|
+
}
|
|
11971
|
+
dataLabels.push({
|
|
11972
|
+
kind: 'text',
|
|
11973
|
+
x: pt.x,
|
|
11974
|
+
y: pt.y - 7,
|
|
11975
|
+
text: formatAxisValue(val),
|
|
11976
|
+
fontSize: 7,
|
|
11977
|
+
fill: '#334155',
|
|
11978
|
+
textAnchor: 'middle',
|
|
11979
|
+
});
|
|
11980
|
+
});
|
|
11981
|
+
}
|
|
11982
|
+
});
|
|
11983
|
+
const title = chartData.style?.hasTitle && chartData.title ? chartData.title : undefined;
|
|
11984
|
+
return {
|
|
11985
|
+
svgWidth: layout.svgWidth,
|
|
11986
|
+
svgHeight: layout.svgHeight,
|
|
11987
|
+
title,
|
|
11988
|
+
titleX: layout.svgWidth / 2,
|
|
11989
|
+
titleY: 12,
|
|
11990
|
+
gridlines,
|
|
11991
|
+
axisLabels,
|
|
11992
|
+
zeroLine,
|
|
11993
|
+
categoryLabels: catLabels,
|
|
11994
|
+
primitives,
|
|
11995
|
+
dataLabels,
|
|
11996
|
+
legend: chartData.style?.hasLegend ? legend : [],
|
|
11997
|
+
legendX,
|
|
11998
|
+
legendY,
|
|
11999
|
+
legendAnchor,
|
|
12000
|
+
};
|
|
12001
|
+
}
|
|
12002
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12003
|
+
// Stock chart
|
|
12004
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12005
|
+
/**
|
|
12006
|
+
* Build a `ChartViewModel` for a stock (HLC / OHLC) candlestick chart.
|
|
12007
|
+
*
|
|
12008
|
+
* Series layout convention (mirrors the React renderer):
|
|
12009
|
+
* 3-series: series[0] = High, series[1] = Low, series[2] = Close
|
|
12010
|
+
* 4-series: series[0] = Open, series[1] = High, series[2] = Low, series[3] = Close
|
|
12011
|
+
*
|
|
12012
|
+
* Per data point the builder emits:
|
|
12013
|
+
* - An `SvgLine` for the high-to-low wick (vertical).
|
|
12014
|
+
* - An `SvgRect` for the open-to-close candle body.
|
|
12015
|
+
*
|
|
12016
|
+
* Candle body is green (#22c55e) when close ≥ open, red (#ef4444) otherwise.
|
|
12017
|
+
* When no open series is present the close value is used as the open (HLC
|
|
12018
|
+
* mode), causing all candles to show as a coloured body between close and the
|
|
12019
|
+
* previously computed running close – but for simplicity, with no open we set
|
|
12020
|
+
* open = low value, matching PowerPoint's own HLC rendering heuristic.
|
|
12021
|
+
*
|
|
12022
|
+
* @param element - The chart element providing width/height.
|
|
12023
|
+
* @param chartData - Parsed chart data including series and style.
|
|
12024
|
+
* @param categoryLabels - Ordered category axis labels.
|
|
12025
|
+
* @returns A fully assembled `ChartViewModel` ready for the template.
|
|
12026
|
+
*/
|
|
12027
|
+
function buildStockViewModel(element, chartData, categoryLabels) {
|
|
12028
|
+
const layout = computePlotLayout(element.width, element.height, chartData, true);
|
|
12029
|
+
const catCount = Math.max(categoryLabels.length, 1);
|
|
12030
|
+
const range = computeValueRange(chartData.series);
|
|
12031
|
+
const { gridlines, axisLabels } = buildGridlinesAndLabels(range, layout);
|
|
12032
|
+
const zeroLine = buildZeroLine(range, layout);
|
|
12033
|
+
const catLabels = buildCategoryLabels(categoryLabels, layout, 'bar');
|
|
12034
|
+
const legendPos = chartData.style?.legendPosition ?? 'b';
|
|
12035
|
+
const { legend, legendX, legendY, legendAnchor } = buildLegend(chartData.series, chartData.colorPalette, layout.svgWidth, legendPos, layout.svgHeight, layout.plotTop);
|
|
12036
|
+
// ── Resolve OHLC series slots ──────────────────────────────────────────
|
|
12037
|
+
const hasFour = chartData.series.length >= 4;
|
|
12038
|
+
const openSeries = hasFour ? chartData.series[0] : undefined;
|
|
12039
|
+
const highSeries = chartData.series[hasFour ? 1 : 0];
|
|
12040
|
+
const lowSeries = chartData.series[hasFour ? 2 : 1];
|
|
12041
|
+
const closeSeries = chartData.series[hasFour ? 3 : 2];
|
|
12042
|
+
const primitives = [];
|
|
12043
|
+
const dataLabels = [];
|
|
12044
|
+
if (highSeries && lowSeries && closeSeries) {
|
|
12045
|
+
const barGroupWidth = layout.plotWidth / catCount;
|
|
12046
|
+
const candleWidth = barGroupWidth * 0.5;
|
|
12047
|
+
for (let ci = 0; ci < catCount; ci++) {
|
|
12048
|
+
const high = highSeries.values[ci] ?? 0;
|
|
12049
|
+
const low = lowSeries.values[ci] ?? 0;
|
|
12050
|
+
const open = openSeries ? (openSeries.values[ci] ?? low) : low;
|
|
12051
|
+
const close = closeSeries.values[ci] ?? high;
|
|
12052
|
+
const isUp = close >= open;
|
|
12053
|
+
const cx = layout.plotLeft + barGroupWidth * ci + barGroupWidth / 2;
|
|
12054
|
+
const highY = valueToY(high, range, layout.plotTop, layout.plotBottom);
|
|
12055
|
+
const lowY = valueToY(low, range, layout.plotTop, layout.plotBottom);
|
|
12056
|
+
const openY = valueToY(open, range, layout.plotTop, layout.plotBottom);
|
|
12057
|
+
const closeY = valueToY(close, range, layout.plotTop, layout.plotBottom);
|
|
12058
|
+
// Wick: vertical line from high to low.
|
|
12059
|
+
primitives.push({
|
|
12060
|
+
kind: 'line',
|
|
12061
|
+
x1: cx,
|
|
12062
|
+
y1: highY,
|
|
12063
|
+
x2: cx,
|
|
12064
|
+
y2: lowY,
|
|
12065
|
+
stroke: CANDLE_WICK_COLOR,
|
|
12066
|
+
strokeWidth: 1,
|
|
12067
|
+
});
|
|
12068
|
+
// Body: rect from open to close.
|
|
12069
|
+
const bodyTop = Math.min(openY, closeY);
|
|
12070
|
+
const bodyHeight = Math.max(Math.abs(openY - closeY), 1);
|
|
12071
|
+
primitives.push({
|
|
12072
|
+
kind: 'rect',
|
|
12073
|
+
x: cx - candleWidth / 2,
|
|
12074
|
+
y: bodyTop,
|
|
12075
|
+
w: candleWidth,
|
|
12076
|
+
h: bodyHeight,
|
|
12077
|
+
fill: isUp ? CANDLE_UP_FILL : CANDLE_DOWN_FILL,
|
|
12078
|
+
rx: 1,
|
|
12079
|
+
});
|
|
12080
|
+
if (chartData.style?.hasDataLabels) {
|
|
12081
|
+
dataLabels.push({
|
|
12082
|
+
kind: 'text',
|
|
12083
|
+
x: cx,
|
|
12084
|
+
y: highY - 4,
|
|
12085
|
+
text: formatAxisValue(close),
|
|
12086
|
+
fontSize: 7,
|
|
12087
|
+
fill: '#334155',
|
|
12088
|
+
textAnchor: 'middle',
|
|
12089
|
+
});
|
|
12090
|
+
}
|
|
12091
|
+
}
|
|
12092
|
+
}
|
|
12093
|
+
const title = chartData.style?.hasTitle && chartData.title ? chartData.title : undefined;
|
|
12094
|
+
return {
|
|
12095
|
+
svgWidth: layout.svgWidth,
|
|
12096
|
+
svgHeight: layout.svgHeight,
|
|
12097
|
+
title,
|
|
12098
|
+
titleX: layout.svgWidth / 2,
|
|
12099
|
+
titleY: 12,
|
|
12100
|
+
gridlines,
|
|
12101
|
+
axisLabels,
|
|
12102
|
+
zeroLine,
|
|
12103
|
+
categoryLabels: catLabels,
|
|
12104
|
+
primitives,
|
|
12105
|
+
dataLabels,
|
|
12106
|
+
legend: chartData.style?.hasLegend ? legend : [],
|
|
12107
|
+
legendX,
|
|
12108
|
+
legendY,
|
|
12109
|
+
legendAnchor,
|
|
12110
|
+
};
|
|
12111
|
+
}
|
|
12112
|
+
|
|
12113
|
+
/**
|
|
12114
|
+
* View-model builders for surface and treemap chart kinds.
|
|
12115
|
+
*
|
|
12116
|
+
* Ported from:
|
|
12117
|
+
* packages/react/src/viewer/utils/chart-surface-treemap.tsx (surface + treemap)
|
|
12118
|
+
*
|
|
12119
|
+
* Produces a `ChartViewModel` (SVG primitives only, zero Angular dependencies)
|
|
12120
|
+
* that the Angular ChartRendererComponent template iterates over.
|
|
12121
|
+
*
|
|
12122
|
+
* Surface – isometric projection when the grid has ≥2 series and ≥2 categories,
|
|
12123
|
+
* flat colour-mapped grid otherwise.
|
|
12124
|
+
* Treemap – slice-and-dice rectangles sorted largest-first with inline labels.
|
|
12125
|
+
*
|
|
12126
|
+
* @module chart-surface-treemap
|
|
12127
|
+
*/
|
|
12128
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12129
|
+
// Isometric projection constants (mirrors React's ISO_COS30 / ISO_SIN30)
|
|
12130
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12131
|
+
const ISO_COS30 = Math.cos(Math.PI / 6);
|
|
12132
|
+
const ISO_SIN30 = Math.sin(Math.PI / 6);
|
|
12133
|
+
/** Project a 3-D (x, y, z) grid coordinate to 2-D isometric screen space. */
|
|
12134
|
+
function isoProject(x, y, z) {
|
|
12135
|
+
return {
|
|
12136
|
+
screenX: (x - y) * ISO_COS30,
|
|
12137
|
+
screenY: (x + y) * ISO_SIN30 - z,
|
|
12138
|
+
};
|
|
12139
|
+
}
|
|
12140
|
+
/** Map a normalised value t in [0..1] to a surface colour ramp (blue→green→red). */
|
|
12141
|
+
function surfaceColor(t) {
|
|
12142
|
+
return {
|
|
12143
|
+
r: Math.round(30 + 200 * t),
|
|
12144
|
+
g: Math.round(80 + 100 * (1 - Math.abs(t - 0.5) * 2)),
|
|
12145
|
+
b: Math.round(200 * (1 - t) + 30),
|
|
12146
|
+
};
|
|
12147
|
+
}
|
|
12148
|
+
/** Darken an rgb triplet by a factor in [0..1]. */
|
|
12149
|
+
function darkenRgb(r, g, b, factor) {
|
|
12150
|
+
return `rgb(${Math.round(r * factor)},${Math.round(g * factor)},${Math.round(b * factor)})`;
|
|
12151
|
+
}
|
|
12152
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12153
|
+
// Shared empty-chrome helper
|
|
12154
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12155
|
+
function emptyChrome() {
|
|
12156
|
+
return {
|
|
12157
|
+
gridlines: [],
|
|
12158
|
+
axisLabels: [],
|
|
12159
|
+
zeroLine: undefined,
|
|
12160
|
+
categoryLabels: [],
|
|
12161
|
+
dataLabels: [],
|
|
12162
|
+
};
|
|
12163
|
+
}
|
|
12164
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12165
|
+
// Surface: isometric renderer (≥2 series, ≥2 categories)
|
|
12166
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12167
|
+
function buildIsometricSurfaceViewModel(element, chartData, categoryLabels) {
|
|
12168
|
+
const layout = computePlotLayout(element.width, element.height, chartData, false);
|
|
12169
|
+
const range = computeValueRange(chartData.series);
|
|
12170
|
+
const catCount = Math.max(categoryLabels.length, 1);
|
|
12171
|
+
const seriesCount = chartData.series.length;
|
|
12172
|
+
// Grid cell count (vertices = cells + 1 in each dimension).
|
|
12173
|
+
const cols = Math.max(catCount - 1, 1);
|
|
12174
|
+
const rows = Math.max(seriesCount - 1, 1);
|
|
12175
|
+
const gridSpan = cols + rows;
|
|
12176
|
+
const cellByWidth = (layout.plotWidth * 0.9) / (gridSpan * ISO_COS30);
|
|
12177
|
+
const cellByHeight = (layout.plotHeight * 0.65) / (gridSpan * ISO_SIN30);
|
|
12178
|
+
const cellSize = Math.max(Math.min(cellByWidth, cellByHeight), 0.5);
|
|
12179
|
+
const zHeadroom = layout.plotHeight * 0.3;
|
|
12180
|
+
const zScale = range.span > 0 ? zHeadroom : 0;
|
|
12181
|
+
const normValue = (r, c) => {
|
|
12182
|
+
const ri = Math.min(r, seriesCount - 1);
|
|
12183
|
+
const ci = Math.min(c, catCount - 1);
|
|
12184
|
+
const val = chartData.series[ri]?.values[ci] ?? 0;
|
|
12185
|
+
return range.span > 0 ? (val - range.min) / range.span : 0;
|
|
12186
|
+
};
|
|
12187
|
+
// Compute isometric bounding box to centre the projection.
|
|
12188
|
+
const projectedPoints = [];
|
|
12189
|
+
for (let r = 0; r <= rows; r++) {
|
|
12190
|
+
for (let c = 0; c <= cols; c++) {
|
|
12191
|
+
projectedPoints.push(isoProject(c * cellSize, r * cellSize, normValue(r, c) * zScale));
|
|
12192
|
+
}
|
|
12193
|
+
}
|
|
12194
|
+
const minSX = Math.min(...projectedPoints.map((p) => p.screenX));
|
|
12195
|
+
const maxSX = Math.max(...projectedPoints.map((p) => p.screenX));
|
|
12196
|
+
const minSY = Math.min(...projectedPoints.map((p) => p.screenY));
|
|
12197
|
+
const maxSY = Math.max(...projectedPoints.map((p) => p.screenY));
|
|
12198
|
+
const projW = maxSX - minSX;
|
|
12199
|
+
const projH = maxSY - minSY;
|
|
12200
|
+
const offsetX = layout.plotLeft + layout.plotWidth / 2 - (minSX + projW / 2);
|
|
12201
|
+
const offsetY = layout.plotTop + layout.plotHeight / 2 - (minSY + projH / 2);
|
|
12202
|
+
const cells = [];
|
|
12203
|
+
for (let r = 0; r < rows; r++) {
|
|
12204
|
+
for (let c = 0; c < cols; c++) {
|
|
12205
|
+
cells.push({ row: r, col: c, depth: r + c });
|
|
12206
|
+
}
|
|
12207
|
+
}
|
|
12208
|
+
cells.sort((a, b) => a.depth - b.depth);
|
|
12209
|
+
const primitives = [];
|
|
12210
|
+
for (const { row, col } of cells) {
|
|
12211
|
+
// Four corners of the isometric parallelogram.
|
|
12212
|
+
const corners = [
|
|
12213
|
+
[col, row],
|
|
12214
|
+
[col + 1, row],
|
|
12215
|
+
[col + 1, row + 1],
|
|
12216
|
+
[col, row + 1],
|
|
12217
|
+
];
|
|
12218
|
+
const verts = corners.map(([c, r]) => {
|
|
12219
|
+
const nv = normValue(r, c);
|
|
12220
|
+
return isoProject(c * cellSize, r * cellSize, nv * zScale);
|
|
12221
|
+
});
|
|
12222
|
+
const avgT = (normValue(row, col) +
|
|
12223
|
+
normValue(row, col + 1) +
|
|
12224
|
+
normValue(row + 1, col + 1) +
|
|
12225
|
+
normValue(row + 1, col)) /
|
|
12226
|
+
4;
|
|
12227
|
+
const { r, g, b } = surfaceColor(avgT);
|
|
12228
|
+
const points = verts
|
|
12229
|
+
.map((v) => `${(v.screenX + offsetX).toFixed(2)},${(v.screenY + offsetY).toFixed(2)}`)
|
|
12230
|
+
.join(' ');
|
|
12231
|
+
// Face fill polygon.
|
|
12232
|
+
primitives.push({
|
|
12233
|
+
kind: 'polygon',
|
|
12234
|
+
points,
|
|
12235
|
+
fill: `rgb(${r},${g},${b})`,
|
|
12236
|
+
stroke: 'none',
|
|
12237
|
+
strokeWidth: 0,
|
|
12238
|
+
opacity: 0.9,
|
|
12239
|
+
});
|
|
12240
|
+
// Subtle edge overlay for depth perception.
|
|
12241
|
+
primitives.push({
|
|
12242
|
+
kind: 'polygon',
|
|
12243
|
+
points,
|
|
12244
|
+
fill: 'none',
|
|
12245
|
+
stroke: darkenRgb(r, g, b, 0.6),
|
|
12246
|
+
strokeWidth: 0.5,
|
|
12247
|
+
opacity: 0.7,
|
|
12248
|
+
});
|
|
12249
|
+
}
|
|
12250
|
+
const legendPos = chartData.style?.legendPosition ?? 'b';
|
|
12251
|
+
const { legend, legendX, legendY, legendAnchor } = buildLegend(chartData.series, chartData.colorPalette, layout.svgWidth, legendPos, layout.svgHeight, layout.plotTop);
|
|
12252
|
+
const title = chartData.style?.hasTitle && chartData.title ? chartData.title : undefined;
|
|
12253
|
+
return {
|
|
12254
|
+
svgWidth: layout.svgWidth,
|
|
12255
|
+
svgHeight: layout.svgHeight,
|
|
12256
|
+
title,
|
|
12257
|
+
titleX: layout.svgWidth / 2,
|
|
12258
|
+
titleY: 14,
|
|
12259
|
+
...emptyChrome(),
|
|
12260
|
+
primitives,
|
|
12261
|
+
legend: chartData.style?.hasLegend ? legend : [],
|
|
12262
|
+
legendX,
|
|
12263
|
+
legendY,
|
|
12264
|
+
legendAnchor,
|
|
12265
|
+
};
|
|
12266
|
+
}
|
|
12267
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12268
|
+
// Surface: flat colour-mapped grid (fallback)
|
|
12269
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12270
|
+
function buildFlatSurfaceViewModel(element, chartData, categoryLabels) {
|
|
12271
|
+
const layout = computePlotLayout(element.width, element.height, chartData, false);
|
|
12272
|
+
const range = computeValueRange(chartData.series);
|
|
12273
|
+
const catCount = Math.max(categoryLabels.length, 1);
|
|
12274
|
+
const seriesCount = chartData.series.length;
|
|
12275
|
+
const cellW = layout.plotWidth / Math.max(catCount - 1, 1);
|
|
12276
|
+
const cellH = layout.plotHeight / Math.max(seriesCount - 1, 1);
|
|
12277
|
+
const primitives = [];
|
|
12278
|
+
for (let si = 0; si < seriesCount; si++) {
|
|
12279
|
+
for (let ci = 0; ci < catCount; ci++) {
|
|
12280
|
+
const val = chartData.series[si]?.values[ci] ?? 0;
|
|
12281
|
+
const t = range.span > 0 ? (val - range.min) / range.span : 0;
|
|
12282
|
+
const { r, g, b } = surfaceColor(t);
|
|
12283
|
+
primitives.push({
|
|
12284
|
+
kind: 'rect',
|
|
12285
|
+
x: layout.plotLeft + ci * cellW,
|
|
12286
|
+
y: layout.plotTop + si * cellH,
|
|
12287
|
+
w: cellW + 0.5,
|
|
12288
|
+
h: cellH + 0.5,
|
|
12289
|
+
fill: `rgb(${r},${g},${b})`,
|
|
12290
|
+
opacity: 0.85,
|
|
12291
|
+
});
|
|
12292
|
+
}
|
|
12293
|
+
}
|
|
12294
|
+
const legendPos = chartData.style?.legendPosition ?? 'b';
|
|
12295
|
+
const { legend, legendX, legendY, legendAnchor } = buildLegend(chartData.series, chartData.colorPalette, layout.svgWidth, legendPos, layout.svgHeight, layout.plotTop);
|
|
12296
|
+
const title = chartData.style?.hasTitle && chartData.title ? chartData.title : undefined;
|
|
12297
|
+
return {
|
|
12298
|
+
svgWidth: layout.svgWidth,
|
|
12299
|
+
svgHeight: layout.svgHeight,
|
|
12300
|
+
title,
|
|
12301
|
+
titleX: layout.svgWidth / 2,
|
|
12302
|
+
titleY: 14,
|
|
12303
|
+
...emptyChrome(),
|
|
12304
|
+
primitives,
|
|
12305
|
+
legend: chartData.style?.hasLegend ? legend : [],
|
|
12306
|
+
legendX,
|
|
12307
|
+
legendY,
|
|
12308
|
+
legendAnchor,
|
|
12309
|
+
};
|
|
12310
|
+
}
|
|
12311
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12312
|
+
// Public: buildSurfaceViewModel
|
|
12313
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12314
|
+
/**
|
|
12315
|
+
* Build the view-model for a surface chart.
|
|
12316
|
+
*
|
|
12317
|
+
* Renders an isometric 3-D-like projection when the grid has ≥2 series and
|
|
12318
|
+
* ≥2 categories; falls back to a flat colour-mapped grid otherwise.
|
|
12319
|
+
* Mirrors `renderSurfaceChart` / `renderIsometricSurfaceFallback` in React's
|
|
12320
|
+
* `chart-surface-treemap.tsx`.
|
|
12321
|
+
*/
|
|
12322
|
+
function buildSurfaceViewModel(element, chartData, categoryLabels) {
|
|
12323
|
+
const catCount = Math.max(categoryLabels.length, 1);
|
|
12324
|
+
const seriesCount = chartData.series.length;
|
|
12325
|
+
if (seriesCount >= 2 && catCount >= 2) {
|
|
12326
|
+
return buildIsometricSurfaceViewModel(element, chartData, categoryLabels);
|
|
12327
|
+
}
|
|
12328
|
+
return buildFlatSurfaceViewModel(element, chartData, categoryLabels);
|
|
12329
|
+
}
|
|
12330
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12331
|
+
// Public: buildTreemapViewModel
|
|
12332
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12333
|
+
/**
|
|
12334
|
+
* Build the view-model for a treemap chart.
|
|
12335
|
+
*
|
|
12336
|
+
* Uses a slice-and-dice layout (alternate horizontal/vertical splits) with
|
|
12337
|
+
* items sorted largest-first. Inline labels are added when the cell is wide
|
|
12338
|
+
* enough. Mirrors `renderTreemapChart` in React's `chart-surface-treemap.tsx`.
|
|
12339
|
+
*/
|
|
12340
|
+
function buildTreemapViewModel(element, chartData, categoryLabels) {
|
|
12341
|
+
const layout = computePlotLayout(element.width, element.height, chartData, false);
|
|
12342
|
+
const allValues = chartData.series.flatMap((s) => s.values);
|
|
12343
|
+
const totalAbs = allValues.reduce((sum, v) => sum + Math.abs(v), 0) || 1;
|
|
12344
|
+
const primitives = [];
|
|
12345
|
+
let curX = layout.plotLeft;
|
|
12346
|
+
let curY = layout.plotTop;
|
|
12347
|
+
let remainW = layout.plotWidth;
|
|
12348
|
+
let remainH = layout.plotHeight;
|
|
12349
|
+
let remainTotal = totalAbs;
|
|
12350
|
+
// Sort items largest-first then lay out in slice-and-dice order.
|
|
12351
|
+
const items = allValues
|
|
12352
|
+
.map((v, i) => ({ value: Math.abs(v), index: i }))
|
|
12353
|
+
.sort((a, b) => b.value - a.value);
|
|
12354
|
+
for (const item of items) {
|
|
12355
|
+
const fraction = remainTotal > 0 ? item.value / remainTotal : 0;
|
|
12356
|
+
const useWidth = remainW >= remainH;
|
|
12357
|
+
const w = useWidth ? remainW * fraction : remainW;
|
|
12358
|
+
const h = useWidth ? remainH : remainH * fraction;
|
|
12359
|
+
const rw = Math.max(w - 1, 1);
|
|
12360
|
+
const rh = Math.max(h - 1, 1);
|
|
12361
|
+
primitives.push({
|
|
12362
|
+
kind: 'rect',
|
|
12363
|
+
x: curX,
|
|
12364
|
+
y: curY,
|
|
12365
|
+
w: rw,
|
|
12366
|
+
h: rh,
|
|
12367
|
+
fill: paletteColor(item.index, chartData.colorPalette),
|
|
12368
|
+
rx: 2,
|
|
12369
|
+
opacity: 0.85,
|
|
12370
|
+
});
|
|
12371
|
+
const label = categoryLabels[item.index] ?? String(item.index + 1);
|
|
12372
|
+
if (rw > 30 && rh > 14) {
|
|
12373
|
+
primitives.push({
|
|
12374
|
+
kind: 'text',
|
|
12375
|
+
x: curX + rw / 2,
|
|
12376
|
+
y: curY + rh / 2,
|
|
12377
|
+
text: label,
|
|
12378
|
+
fontSize: Math.min(10, rh * 0.3),
|
|
12379
|
+
fill: '#ffffff',
|
|
12380
|
+
textAnchor: 'middle',
|
|
12381
|
+
fontWeight: 'bold',
|
|
12382
|
+
dominantBaseline: 'central',
|
|
12383
|
+
});
|
|
12384
|
+
}
|
|
12385
|
+
if (useWidth) {
|
|
12386
|
+
curX += w;
|
|
12387
|
+
remainW -= w;
|
|
12388
|
+
}
|
|
12389
|
+
else {
|
|
12390
|
+
curY += h;
|
|
12391
|
+
remainH -= h;
|
|
12392
|
+
}
|
|
12393
|
+
remainTotal -= item.value;
|
|
12394
|
+
}
|
|
12395
|
+
// Legend: one entry per series (matching React: no per-item legend).
|
|
12396
|
+
const legendPos = chartData.style?.legendPosition ?? 'b';
|
|
12397
|
+
const { legend, legendX, legendY, legendAnchor } = buildLegend(chartData.series, chartData.colorPalette, layout.svgWidth, legendPos, layout.svgHeight, layout.plotTop);
|
|
12398
|
+
// Build per-category legend entries mirroring the React treemap colour
|
|
12399
|
+
// assignments: one swatch per category/value index.
|
|
12400
|
+
const catLegend = categoryLabels.map((cat, i) => ({
|
|
12401
|
+
color: paletteColor(i, chartData.colorPalette),
|
|
12402
|
+
label: cat,
|
|
12403
|
+
}));
|
|
12404
|
+
const title = chartData.style?.hasTitle && chartData.title ? chartData.title : undefined;
|
|
12405
|
+
return {
|
|
12406
|
+
svgWidth: layout.svgWidth,
|
|
12407
|
+
svgHeight: layout.svgHeight,
|
|
12408
|
+
title,
|
|
12409
|
+
titleX: layout.svgWidth / 2,
|
|
12410
|
+
titleY: 14,
|
|
12411
|
+
...emptyChrome(),
|
|
12412
|
+
primitives,
|
|
12413
|
+
// Prefer per-category legend over per-series legend for treemap.
|
|
12414
|
+
legend: chartData.style?.hasLegend ? (catLegend.length > 0 ? catLegend : legend) : [],
|
|
12415
|
+
legendX,
|
|
12416
|
+
legendY,
|
|
12417
|
+
legendAnchor,
|
|
12418
|
+
};
|
|
12419
|
+
}
|
|
12420
|
+
|
|
12421
|
+
/**
|
|
12422
|
+
* View-model builders for waterfall and regionMap chart kinds.
|
|
12423
|
+
*
|
|
12424
|
+
* Ported from:
|
|
12425
|
+
* packages/react/src/viewer/utils/chart-waterfall-combo.tsx (waterfall only)
|
|
12426
|
+
* packages/react/src/viewer/utils/chart-map.tsx (regionMap)
|
|
12427
|
+
*
|
|
12428
|
+
* Produces a `ChartViewModel` (SVG primitives only, zero Angular dependencies)
|
|
12429
|
+
* that the Angular ChartRendererComponent template iterates over.
|
|
12430
|
+
*
|
|
12431
|
+
* Waterfall – running-total bars with positive/negative/total colouring and
|
|
12432
|
+
* dashed connector lines between bars.
|
|
12433
|
+
* RegionMap – choropleth SVG with simplified world region outlines coloured by
|
|
12434
|
+
* the first data series; unmatched regions fall back to a table.
|
|
12435
|
+
*
|
|
12436
|
+
* @module chart-waterfall-map
|
|
12437
|
+
*/
|
|
12438
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12439
|
+
// Waterfall colours (mirrors React renderWaterfallChart)
|
|
12440
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12441
|
+
const WF_COLOR_POSITIVE = '#22c55e';
|
|
12442
|
+
const WF_COLOR_NEGATIVE = '#ef4444';
|
|
12443
|
+
const WF_COLOR_TOTAL = '#6366f1';
|
|
12444
|
+
const WF_CONNECTOR_COLOR = '#94a3b8';
|
|
12445
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12446
|
+
// Public: buildWaterfallViewModel
|
|
12447
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12448
|
+
/**
|
|
12449
|
+
* Build the view-model for a waterfall chart.
|
|
12450
|
+
*
|
|
12451
|
+
* Each bar starts from the running total of all previous values; the last bar
|
|
12452
|
+
* shows the grand total (reset to 0 base). Positive values get a green fill,
|
|
12453
|
+
* negative values get a red fill, and the final total bar uses indigo.
|
|
12454
|
+
* Dashed connector lines join adjacent bar tops/bottoms.
|
|
12455
|
+
*
|
|
12456
|
+
* Mirrors `renderWaterfallChart` in React's `chart-waterfall-combo.tsx`.
|
|
12457
|
+
*/
|
|
12458
|
+
function buildWaterfallViewModel(element, chartData, categoryLabels) {
|
|
12459
|
+
const layout = computePlotLayout(element.width, element.height, chartData, true);
|
|
12460
|
+
const values = chartData.series[0]?.values ?? [];
|
|
12461
|
+
const range = computeValueRange(chartData.series);
|
|
12462
|
+
const catCount = Math.max(categoryLabels.length, values.length, 1);
|
|
12463
|
+
const barWidth = (layout.plotWidth / catCount) * 0.6;
|
|
12464
|
+
const gap = (layout.plotWidth / catCount) * 0.2;
|
|
12465
|
+
const primitives = [];
|
|
12466
|
+
const dataLabels = [];
|
|
12467
|
+
let runningTotal = 0;
|
|
12468
|
+
for (let i = 0; i < values.length; i++) {
|
|
12469
|
+
const val = values[i] ?? 0;
|
|
12470
|
+
const isLast = i === values.length - 1;
|
|
12471
|
+
const startVal = isLast ? 0 : runningTotal;
|
|
12472
|
+
const endVal = runningTotal + val;
|
|
12473
|
+
const barStartY = valueToY(startVal, range, layout.plotTop, layout.plotBottom);
|
|
12474
|
+
const barEndY = valueToY(endVal, range, layout.plotTop, layout.plotBottom);
|
|
12475
|
+
const x = layout.plotLeft + (layout.plotWidth / catCount) * i + gap;
|
|
12476
|
+
const y = Math.min(barStartY, barEndY);
|
|
12477
|
+
const h = Math.max(Math.abs(barEndY - barStartY), 1);
|
|
12478
|
+
const barColor = isLast ? WF_COLOR_TOTAL : val >= 0 ? WF_COLOR_POSITIVE : WF_COLOR_NEGATIVE;
|
|
12479
|
+
primitives.push({
|
|
12480
|
+
kind: 'rect',
|
|
12481
|
+
x,
|
|
12482
|
+
y,
|
|
12483
|
+
w: barWidth,
|
|
12484
|
+
h,
|
|
12485
|
+
fill: barColor,
|
|
12486
|
+
rx: 1,
|
|
12487
|
+
});
|
|
12488
|
+
if (chartData.style?.hasDataLabels) {
|
|
12489
|
+
dataLabels.push({
|
|
12490
|
+
kind: 'text',
|
|
12491
|
+
x: x + barWidth / 2,
|
|
12492
|
+
y: y - 4,
|
|
12493
|
+
text: formatAxisValue(isLast ? endVal : val),
|
|
12494
|
+
fontSize: 7,
|
|
12495
|
+
fill: '#334155',
|
|
12496
|
+
textAnchor: 'middle',
|
|
12497
|
+
});
|
|
12498
|
+
}
|
|
12499
|
+
// Connector line to the next bar (not drawn after the last bar).
|
|
12500
|
+
if (!isLast && i < values.length - 1) {
|
|
12501
|
+
const nextX = layout.plotLeft + (layout.plotWidth / catCount) * (i + 1) + gap;
|
|
12502
|
+
primitives.push({
|
|
12503
|
+
kind: 'line',
|
|
12504
|
+
x1: x + barWidth,
|
|
12505
|
+
y1: barEndY,
|
|
12506
|
+
x2: nextX,
|
|
12507
|
+
y2: barEndY,
|
|
12508
|
+
stroke: WF_CONNECTOR_COLOR,
|
|
12509
|
+
strokeWidth: 0.8,
|
|
12510
|
+
dashArray: '3 2',
|
|
12511
|
+
});
|
|
12512
|
+
}
|
|
12513
|
+
if (!isLast) {
|
|
12514
|
+
runningTotal += val;
|
|
12515
|
+
}
|
|
12516
|
+
}
|
|
12517
|
+
const { gridlines, axisLabels } = buildGridlinesAndLabels(range, layout);
|
|
12518
|
+
const zeroLine = buildZeroLine(range, layout);
|
|
12519
|
+
const catLabels = buildCategoryLabels(categoryLabels, layout, 'bar');
|
|
12520
|
+
const legendPos = chartData.style?.legendPosition ?? 'b';
|
|
12521
|
+
const { legend, legendX, legendY, legendAnchor } = buildLegend(chartData.series, chartData.colorPalette, layout.svgWidth, legendPos, layout.svgHeight, layout.plotTop);
|
|
12522
|
+
const title = chartData.style?.hasTitle && chartData.title ? chartData.title : undefined;
|
|
12523
|
+
return {
|
|
12524
|
+
svgWidth: layout.svgWidth,
|
|
12525
|
+
svgHeight: layout.svgHeight,
|
|
12526
|
+
title,
|
|
12527
|
+
titleX: layout.svgWidth / 2,
|
|
12528
|
+
titleY: 12,
|
|
12529
|
+
gridlines,
|
|
12530
|
+
axisLabels,
|
|
12531
|
+
zeroLine,
|
|
12532
|
+
categoryLabels: catLabels,
|
|
12533
|
+
primitives,
|
|
12534
|
+
dataLabels,
|
|
12535
|
+
legend: chartData.style?.hasLegend ? legend : [],
|
|
12536
|
+
legendX,
|
|
12537
|
+
legendY,
|
|
12538
|
+
legendAnchor,
|
|
12539
|
+
};
|
|
12540
|
+
}
|
|
12541
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12542
|
+
// RegionMap: region alias lookup
|
|
12543
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12544
|
+
/**
|
|
12545
|
+
* Mapping from common category label strings (country names, ISO codes) to
|
|
12546
|
+
* internal region keys. Case-insensitive lookup.
|
|
12547
|
+
* Mirrors `REGION_ALIAS_MAP` from React's `chart-map.tsx`.
|
|
12548
|
+
*/
|
|
12549
|
+
const REGION_ALIAS_MAP = {
|
|
12550
|
+
us: 'US',
|
|
12551
|
+
usa: 'US',
|
|
12552
|
+
'united states': 'US',
|
|
12553
|
+
'united states of america': 'US',
|
|
12554
|
+
ca: 'CA',
|
|
12555
|
+
can: 'CA',
|
|
12556
|
+
canada: 'CA',
|
|
12557
|
+
br: 'BR',
|
|
12558
|
+
bra: 'BR',
|
|
12559
|
+
brazil: 'BR',
|
|
12560
|
+
gb: 'GB',
|
|
12561
|
+
gbr: 'GB',
|
|
12562
|
+
uk: 'GB',
|
|
12563
|
+
'united kingdom': 'GB',
|
|
12564
|
+
fr: 'FR',
|
|
12565
|
+
fra: 'FR',
|
|
12566
|
+
france: 'FR',
|
|
12567
|
+
de: 'DE',
|
|
12568
|
+
deu: 'DE',
|
|
12569
|
+
germany: 'DE',
|
|
12570
|
+
it: 'IT',
|
|
12571
|
+
ita: 'IT',
|
|
12572
|
+
italy: 'IT',
|
|
12573
|
+
es: 'ES',
|
|
12574
|
+
esp: 'ES',
|
|
12575
|
+
spain: 'ES',
|
|
12576
|
+
ru: 'RU',
|
|
12577
|
+
rus: 'RU',
|
|
12578
|
+
russia: 'RU',
|
|
12579
|
+
cn: 'CN',
|
|
12580
|
+
chn: 'CN',
|
|
12581
|
+
china: 'CN',
|
|
12582
|
+
in: 'IN',
|
|
12583
|
+
ind: 'IN',
|
|
12584
|
+
india: 'IN',
|
|
12585
|
+
jp: 'JP',
|
|
12586
|
+
jpn: 'JP',
|
|
12587
|
+
japan: 'JP',
|
|
12588
|
+
kr: 'KR',
|
|
12589
|
+
kor: 'KR',
|
|
12590
|
+
'south korea': 'KR',
|
|
12591
|
+
korea: 'KR',
|
|
12592
|
+
au: 'AU',
|
|
12593
|
+
aus: 'AU',
|
|
12594
|
+
australia: 'AU',
|
|
12595
|
+
mx: 'MX',
|
|
12596
|
+
mex: 'MX',
|
|
12597
|
+
mexico: 'MX',
|
|
12598
|
+
id: 'ID',
|
|
12599
|
+
idn: 'ID',
|
|
12600
|
+
indonesia: 'ID',
|
|
12601
|
+
tr: 'TR',
|
|
12602
|
+
tur: 'TR',
|
|
12603
|
+
turkey: 'TR',
|
|
12604
|
+
sa: 'SA',
|
|
12605
|
+
sau: 'SA',
|
|
12606
|
+
'saudi arabia': 'SA',
|
|
12607
|
+
za: 'ZA',
|
|
12608
|
+
zaf: 'ZA',
|
|
12609
|
+
'south africa': 'ZA',
|
|
12610
|
+
ar: 'AR',
|
|
12611
|
+
arg: 'AR',
|
|
12612
|
+
argentina: 'AR',
|
|
12613
|
+
ng: 'NG',
|
|
12614
|
+
nga: 'NG',
|
|
12615
|
+
nigeria: 'NG',
|
|
12616
|
+
eg: 'EG',
|
|
12617
|
+
egy: 'EG',
|
|
12618
|
+
egypt: 'EG',
|
|
12619
|
+
};
|
|
12620
|
+
/** Resolve a category label to a region key (case-insensitive). */
|
|
12621
|
+
function resolveRegionCode(label) {
|
|
12622
|
+
const normalized = label.trim().toLowerCase();
|
|
12623
|
+
return REGION_ALIAS_MAP[normalized];
|
|
12624
|
+
}
|
|
12625
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12626
|
+
// RegionMap: colour scale helpers
|
|
12627
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12628
|
+
/** Interpolate between two hex colours by ratio t in [0..1]. */
|
|
12629
|
+
function lerpColor(a, b, t) {
|
|
12630
|
+
const clamp = (v) => Math.max(0, Math.min(255, Math.round(v)));
|
|
12631
|
+
const ha = a.replace('#', '');
|
|
12632
|
+
const hb = b.replace('#', '');
|
|
12633
|
+
const r1 = parseInt(ha.substring(0, 2), 16);
|
|
12634
|
+
const g1 = parseInt(ha.substring(2, 4), 16);
|
|
12635
|
+
const b1 = parseInt(ha.substring(4, 6), 16);
|
|
12636
|
+
const r2 = parseInt(hb.substring(0, 2), 16);
|
|
12637
|
+
const g2 = parseInt(hb.substring(2, 4), 16);
|
|
12638
|
+
const b2 = parseInt(hb.substring(4, 6), 16);
|
|
12639
|
+
const r = clamp(r1 + (r2 - r1) * t);
|
|
12640
|
+
const g = clamp(g1 + (g2 - g1) * t);
|
|
12641
|
+
const bl = clamp(b1 + (b2 - b1) * t);
|
|
12642
|
+
const toHex = (n) => n.toString(16).padStart(2, '0');
|
|
12643
|
+
return `#${toHex(r)}${toHex(g)}${toHex(bl)}`;
|
|
12644
|
+
}
|
|
12645
|
+
/**
|
|
12646
|
+
* 3-stop sequential colour scale: light (#dbeafe) → mid (#3b82f6) → dark (#1e3a5f).
|
|
12647
|
+
* Mirrors `sequentialColorScale` in React's `chart-map.tsx`.
|
|
12648
|
+
*/
|
|
12649
|
+
function sequentialColorScale(t) {
|
|
12650
|
+
const clamped = Math.max(0, Math.min(1, t));
|
|
12651
|
+
if (clamped <= 0.5) {
|
|
12652
|
+
return lerpColor('#dbeafe', '#3b82f6', clamped * 2);
|
|
12653
|
+
}
|
|
12654
|
+
return lerpColor('#3b82f6', '#1e3a5f', (clamped - 0.5) * 2);
|
|
12655
|
+
}
|
|
12656
|
+
/** Normalise a value to [0..1] within a min/max range. */
|
|
12657
|
+
function normalizeValue(value, min, max) {
|
|
12658
|
+
if (max === min) {
|
|
12659
|
+
return 0.5;
|
|
12660
|
+
}
|
|
12661
|
+
return (value - min) / (max - min);
|
|
12662
|
+
}
|
|
12663
|
+
/** Simplified world region outlines (mirrors `WORLD_REGIONS` in React's chart-map.tsx). */
|
|
12664
|
+
const WORLD_REGIONS = [
|
|
12665
|
+
{
|
|
12666
|
+
code: 'US',
|
|
12667
|
+
name: 'United States',
|
|
12668
|
+
path: 'M130,160 L250,155 265,170 270,190 260,210 230,215 200,220 170,215 145,205 130,195Z M280,175 L295,165 310,170 310,185 295,195 280,190Z',
|
|
12669
|
+
labelXY: [200, 190],
|
|
12670
|
+
},
|
|
12671
|
+
{
|
|
12672
|
+
code: 'CA',
|
|
12673
|
+
name: 'Canada',
|
|
12674
|
+
path: 'M120,90 L280,85 290,100 295,130 280,150 250,155 200,155 160,155 130,155 115,140 110,115Z',
|
|
12675
|
+
labelXY: [200, 125],
|
|
12676
|
+
},
|
|
12677
|
+
{
|
|
12678
|
+
code: 'MX',
|
|
12679
|
+
name: 'Mexico',
|
|
12680
|
+
path: 'M145,215 L200,220 210,235 200,255 185,265 165,260 150,245 140,230Z',
|
|
12681
|
+
labelXY: [175, 240],
|
|
12682
|
+
},
|
|
12683
|
+
{
|
|
12684
|
+
code: 'BR',
|
|
12685
|
+
name: 'Brazil',
|
|
12686
|
+
path: 'M270,300 L310,280 335,290 340,320 330,355 310,370 285,365 265,345 260,320Z',
|
|
12687
|
+
labelXY: [300, 330],
|
|
12688
|
+
},
|
|
12689
|
+
{
|
|
12690
|
+
code: 'AR',
|
|
12691
|
+
name: 'Argentina',
|
|
12692
|
+
path: 'M260,370 L280,365 290,380 285,410 275,435 260,445 250,425 248,395Z',
|
|
12693
|
+
labelXY: [268, 410],
|
|
12694
|
+
},
|
|
12695
|
+
{
|
|
12696
|
+
code: 'GB',
|
|
12697
|
+
name: 'United Kingdom',
|
|
12698
|
+
path: 'M440,120 L448,110 455,115 455,135 448,142 440,138Z',
|
|
12699
|
+
labelXY: [448, 128],
|
|
12700
|
+
},
|
|
12701
|
+
{
|
|
12702
|
+
code: 'FR',
|
|
12703
|
+
name: 'France',
|
|
12704
|
+
path: 'M450,145 L470,140 480,150 478,168 465,175 450,170 445,158Z',
|
|
12705
|
+
labelXY: [463, 158],
|
|
12706
|
+
},
|
|
12707
|
+
{
|
|
12708
|
+
code: 'DE',
|
|
12709
|
+
name: 'Germany',
|
|
12710
|
+
path: 'M478,125 L498,120 505,130 502,148 490,152 478,148 475,138Z',
|
|
12711
|
+
labelXY: [490, 138],
|
|
12712
|
+
},
|
|
12713
|
+
{
|
|
12714
|
+
code: 'IT',
|
|
12715
|
+
name: 'Italy',
|
|
12716
|
+
path: 'M490,155 L498,152 505,162 500,180 492,190 488,178 486,165Z',
|
|
12717
|
+
labelXY: [495, 172],
|
|
12718
|
+
},
|
|
12719
|
+
{
|
|
12720
|
+
code: 'ES',
|
|
12721
|
+
name: 'Spain',
|
|
12722
|
+
path: 'M432,168 L460,165 465,175 460,188 442,192 430,185 428,175Z',
|
|
12723
|
+
labelXY: [448, 180],
|
|
12724
|
+
},
|
|
12725
|
+
{
|
|
12726
|
+
code: 'RU',
|
|
12727
|
+
name: 'Russia',
|
|
12728
|
+
path: 'M510,60 L780,50 830,70 840,100 820,120 750,115 700,105 650,100 580,105 530,110 510,100 505,80Z',
|
|
12729
|
+
labelXY: [670, 85],
|
|
12730
|
+
},
|
|
12731
|
+
{
|
|
12732
|
+
code: 'TR',
|
|
12733
|
+
name: 'Turkey',
|
|
12734
|
+
path: 'M530,165 L570,160 585,170 580,182 555,185 530,180Z',
|
|
12735
|
+
labelXY: [558, 175],
|
|
12736
|
+
},
|
|
12737
|
+
{
|
|
12738
|
+
code: 'EG',
|
|
12739
|
+
name: 'Egypt',
|
|
12740
|
+
path: 'M530,200 L555,195 565,205 560,225 545,230 530,222Z',
|
|
12741
|
+
labelXY: [548, 215],
|
|
12742
|
+
},
|
|
12743
|
+
{
|
|
12744
|
+
code: 'NG',
|
|
12745
|
+
name: 'Nigeria',
|
|
12746
|
+
path: 'M475,275 L500,270 510,280 505,298 490,302 475,295Z',
|
|
12747
|
+
labelXY: [492, 288],
|
|
12748
|
+
},
|
|
12749
|
+
{
|
|
12750
|
+
code: 'ZA',
|
|
12751
|
+
name: 'South Africa',
|
|
12752
|
+
path: 'M520,380 L545,370 560,380 555,400 540,410 520,405 515,392Z',
|
|
12753
|
+
labelXY: [538, 392],
|
|
12754
|
+
},
|
|
12755
|
+
{
|
|
12756
|
+
code: 'SA',
|
|
12757
|
+
name: 'Saudi Arabia',
|
|
12758
|
+
path: 'M565,220 L600,210 615,225 610,250 590,258 570,250 560,238Z',
|
|
12759
|
+
labelXY: [590, 238],
|
|
12760
|
+
},
|
|
12761
|
+
{
|
|
12762
|
+
code: 'IN',
|
|
12763
|
+
name: 'India',
|
|
12764
|
+
path: 'M640,210 L665,195 685,210 688,240 678,268 660,278 645,265 635,240Z',
|
|
12765
|
+
labelXY: [662, 240],
|
|
12766
|
+
},
|
|
12767
|
+
{
|
|
12768
|
+
code: 'CN',
|
|
12769
|
+
name: 'China',
|
|
12770
|
+
path: 'M700,120 L775,115 800,130 805,160 790,180 760,185 730,180 710,168 695,150 690,135Z',
|
|
12771
|
+
labelXY: [750, 155],
|
|
12772
|
+
},
|
|
12773
|
+
{
|
|
12774
|
+
code: 'JP',
|
|
12775
|
+
name: 'Japan',
|
|
12776
|
+
path: 'M835,145 L845,135 852,140 850,158 842,165 835,160Z',
|
|
12777
|
+
labelXY: [843, 152],
|
|
12778
|
+
},
|
|
12779
|
+
{
|
|
12780
|
+
code: 'KR',
|
|
12781
|
+
name: 'South Korea',
|
|
12782
|
+
path: 'M815,158 L825,152 830,160 827,170 818,172 813,165Z',
|
|
12783
|
+
labelXY: [822, 163],
|
|
12784
|
+
},
|
|
12785
|
+
{
|
|
12786
|
+
code: 'ID',
|
|
12787
|
+
name: 'Indonesia',
|
|
12788
|
+
path: 'M740,295 L780,288 810,292 830,298 825,310 790,312 755,308 740,305Z',
|
|
12789
|
+
labelXY: [785, 302],
|
|
12790
|
+
},
|
|
12791
|
+
{
|
|
12792
|
+
code: 'AU',
|
|
12793
|
+
name: 'Australia',
|
|
12794
|
+
path: 'M790,350 L850,340 880,355 885,385 870,405 840,410 810,400 790,380Z',
|
|
12795
|
+
labelXY: [838, 378],
|
|
12796
|
+
},
|
|
12797
|
+
];
|
|
12798
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12799
|
+
// Public: buildRegionMapViewModel
|
|
12800
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12801
|
+
/**
|
|
12802
|
+
* Build the view-model for a regionMap (choropleth) chart.
|
|
12803
|
+
*
|
|
12804
|
+
* Matches category labels against known world regions, colours them by the
|
|
12805
|
+
* first series' values using a sequential blue colour scale, and renders a
|
|
12806
|
+
* simple colour-legend bar below the map. Unmatched regions are collected
|
|
12807
|
+
* into a small fallback table rendered as SVG text rows.
|
|
12808
|
+
*
|
|
12809
|
+
* Mirrors `renderMapChart` in React's `chart-map.tsx`.
|
|
12810
|
+
*/
|
|
12811
|
+
function buildRegionMapViewModel(element, chartData, categoryLabels) {
|
|
12812
|
+
const svgWidth = Math.max(element.width, 320);
|
|
12813
|
+
const svgHeight = Math.max(element.height, 200);
|
|
12814
|
+
const categories = categoryLabels.length > 0 ? categoryLabels : chartData.categories;
|
|
12815
|
+
const values = chartData.series.length > 0 ? (chartData.series[0]?.values ?? []) : [];
|
|
12816
|
+
const finiteVals = values.filter((v) => Number.isFinite(v));
|
|
12817
|
+
const minVal = finiteVals.length > 0 ? Math.min(...finiteVals) : 0;
|
|
12818
|
+
const maxVal = finiteVals.length > 0 ? Math.max(...finiteVals) : 1;
|
|
12819
|
+
// Build region → value lookup.
|
|
12820
|
+
const regionValueMap = new Map();
|
|
12821
|
+
const unmatchedRows = [];
|
|
12822
|
+
for (let i = 0; i < categories.length; i++) {
|
|
12823
|
+
const cat = categories[i] ?? '';
|
|
12824
|
+
const value = values[i] ?? 0;
|
|
12825
|
+
const code = resolveRegionCode(cat);
|
|
12826
|
+
if (code !== undefined) {
|
|
12827
|
+
regionValueMap.set(code, { value, label: cat });
|
|
12828
|
+
}
|
|
12829
|
+
else {
|
|
12830
|
+
unmatchedRows.push({ label: cat, value });
|
|
12831
|
+
}
|
|
12832
|
+
}
|
|
12833
|
+
// Layout measurements.
|
|
12834
|
+
const legendHeight = 30;
|
|
12835
|
+
const fallbackRowH = 14;
|
|
12836
|
+
const maxFallbackRows = Math.min(unmatchedRows.length, 5);
|
|
12837
|
+
const fallbackTableH = unmatchedRows.length > 0 ? (maxFallbackRows + 1) * fallbackRowH + 8 : 0;
|
|
12838
|
+
const titleH = chartData.title ? 22 : 0;
|
|
12839
|
+
const mapAreaH = Math.max(svgHeight - titleH - legendHeight - fallbackTableH - 8, 80);
|
|
12840
|
+
// Scale the 1000 x 500 region coordinate space into the available area.
|
|
12841
|
+
const mapScale = Math.min((svgWidth - 20) / 1000, mapAreaH / 500);
|
|
12842
|
+
const mapOffsetX = (svgWidth - 1000 * mapScale) / 2;
|
|
12843
|
+
const mapOffsetY = titleH + 4;
|
|
12844
|
+
const primitives = [];
|
|
12845
|
+
// Background.
|
|
12846
|
+
primitives.push({
|
|
12847
|
+
kind: 'rect',
|
|
12848
|
+
x: 0,
|
|
12849
|
+
y: 0,
|
|
12850
|
+
w: svgWidth,
|
|
12851
|
+
h: svgHeight,
|
|
12852
|
+
fill: '#f8fafc',
|
|
12853
|
+
rx: 4,
|
|
12854
|
+
});
|
|
12855
|
+
// Title text.
|
|
12856
|
+
const titlePrimitive = chartData.title
|
|
12857
|
+
? {
|
|
12858
|
+
kind: 'text',
|
|
12859
|
+
x: svgWidth / 2,
|
|
12860
|
+
y: 16,
|
|
12861
|
+
text: chartData.title,
|
|
12862
|
+
fontSize: 12,
|
|
12863
|
+
fill: '#334155',
|
|
12864
|
+
textAnchor: 'middle',
|
|
12865
|
+
fontWeight: 'bold',
|
|
12866
|
+
dominantBaseline: 'auto',
|
|
12867
|
+
}
|
|
12868
|
+
: undefined;
|
|
12869
|
+
// Region shape paths.
|
|
12870
|
+
for (const region of WORLD_REGIONS) {
|
|
12871
|
+
const entry = regionValueMap.get(region.code);
|
|
12872
|
+
let fill = '#e2e8f0';
|
|
12873
|
+
if (entry !== undefined) {
|
|
12874
|
+
const t = normalizeValue(entry.value, minVal, maxVal);
|
|
12875
|
+
fill = sequentialColorScale(t);
|
|
12876
|
+
}
|
|
12877
|
+
// Embed the transform in the path's d attribute via a manual coordinate
|
|
12878
|
+
// scale+translate since SvgPath has no transform field. We replicate
|
|
12879
|
+
// React's `transform="translate(mapOffsetX,mapOffsetY) scale(mapScale)"`
|
|
12880
|
+
// by pre-scaling every coordinate pair in the path string.
|
|
12881
|
+
const scaledPath = scalePathD(region.path, mapScale, mapOffsetX, mapOffsetY);
|
|
12882
|
+
primitives.push({
|
|
12883
|
+
kind: 'path',
|
|
12884
|
+
d: scaledPath,
|
|
12885
|
+
fill,
|
|
12886
|
+
stroke: '#94a3b8',
|
|
12887
|
+
strokeWidth: Math.max(0.5 / mapScale, 0.3),
|
|
12888
|
+
});
|
|
12889
|
+
// Inline data label for matched regions.
|
|
12890
|
+
if (entry !== undefined) {
|
|
12891
|
+
const lx = region.labelXY[0] * mapScale + mapOffsetX;
|
|
12892
|
+
const ly = region.labelXY[1] * mapScale + mapOffsetY + 4;
|
|
12893
|
+
primitives.push({
|
|
12894
|
+
kind: 'text',
|
|
12895
|
+
x: lx,
|
|
12896
|
+
y: ly,
|
|
12897
|
+
text: formatAxisValue(entry.value),
|
|
12898
|
+
fontSize: Math.max(6, 7 * mapScale),
|
|
12899
|
+
fill: '#1e293b',
|
|
12900
|
+
textAnchor: 'middle',
|
|
12901
|
+
fontWeight: 'bold',
|
|
12902
|
+
dominantBaseline: 'central',
|
|
12903
|
+
});
|
|
12904
|
+
}
|
|
12905
|
+
}
|
|
12906
|
+
// Colour legend bar (rendered as gradient approximation: three rect stops).
|
|
12907
|
+
const legendY = mapOffsetY + mapAreaH + 4;
|
|
12908
|
+
const barW = Math.min(svgWidth * 0.4, 160);
|
|
12909
|
+
const barX = (svgWidth - barW) / 2;
|
|
12910
|
+
// Approximate the gradient using three colour stops as adjacent rects.
|
|
12911
|
+
const gradStops = [
|
|
12912
|
+
{ offset: 0, color: '#dbeafe' },
|
|
12913
|
+
{ offset: 0.5, color: '#3b82f6' },
|
|
12914
|
+
{ offset: 1, color: '#1e3a5f' },
|
|
12915
|
+
];
|
|
12916
|
+
const stopCount = gradStops.length - 1;
|
|
12917
|
+
for (let si = 0; si < stopCount; si++) {
|
|
12918
|
+
const stopA = gradStops[si];
|
|
12919
|
+
const stopB = gradStops[si + 1];
|
|
12920
|
+
if (stopA === undefined || stopB === undefined) {
|
|
12921
|
+
continue;
|
|
12922
|
+
}
|
|
12923
|
+
// Use the midpoint colour of each segment.
|
|
12924
|
+
const midColor = lerpColor(stopA.color, stopB.color, 0.5);
|
|
12925
|
+
primitives.push({
|
|
12926
|
+
kind: 'rect',
|
|
12927
|
+
x: barX + stopA.offset * barW,
|
|
12928
|
+
y: legendY,
|
|
12929
|
+
w: (stopB.offset - stopA.offset) * barW,
|
|
12930
|
+
h: 8,
|
|
12931
|
+
fill: midColor,
|
|
12932
|
+
rx: si === 0 ? 4 : 0,
|
|
12933
|
+
});
|
|
12934
|
+
}
|
|
12935
|
+
// Legend min/max labels.
|
|
12936
|
+
primitives.push({
|
|
12937
|
+
kind: 'text',
|
|
12938
|
+
x: barX,
|
|
12939
|
+
y: legendY + 18,
|
|
12940
|
+
text: formatAxisValue(minVal),
|
|
12941
|
+
fontSize: 7,
|
|
12942
|
+
fill: '#64748b',
|
|
12943
|
+
textAnchor: 'middle',
|
|
12944
|
+
}, {
|
|
12945
|
+
kind: 'text',
|
|
12946
|
+
x: barX + barW,
|
|
12947
|
+
y: legendY + 18,
|
|
12948
|
+
text: formatAxisValue(maxVal),
|
|
12949
|
+
fontSize: 7,
|
|
12950
|
+
fill: '#64748b',
|
|
12951
|
+
textAnchor: 'middle',
|
|
12952
|
+
});
|
|
12953
|
+
// Fallback table for unmatched regions.
|
|
12954
|
+
if (unmatchedRows.length > 0) {
|
|
12955
|
+
const tableY = legendY + 26;
|
|
12956
|
+
const fontSize = Math.min(8, fallbackRowH * 0.7);
|
|
12957
|
+
const colW = Math.min((svgWidth - 20) / 2, 120);
|
|
12958
|
+
const tableX = (svgWidth - colW * 2) / 2;
|
|
12959
|
+
primitives.push({
|
|
12960
|
+
kind: 'text',
|
|
12961
|
+
x: svgWidth / 2,
|
|
12962
|
+
y: tableY,
|
|
12963
|
+
text: 'Additional regions (not shown on map)',
|
|
12964
|
+
fontSize: 7,
|
|
12965
|
+
fill: '#94a3b8',
|
|
12966
|
+
textAnchor: 'middle',
|
|
12967
|
+
});
|
|
12968
|
+
for (let i = 0; i < maxFallbackRows; i++) {
|
|
12969
|
+
const row = unmatchedRows[i];
|
|
12970
|
+
if (row === undefined) {
|
|
12971
|
+
continue;
|
|
12972
|
+
}
|
|
12973
|
+
const ry = tableY + fallbackRowH * (i + 1);
|
|
12974
|
+
if (ry + fallbackRowH > svgHeight) {
|
|
12975
|
+
break;
|
|
12976
|
+
}
|
|
12977
|
+
if (i % 2 === 0) {
|
|
12978
|
+
primitives.push({
|
|
12979
|
+
kind: 'rect',
|
|
12980
|
+
x: tableX,
|
|
12981
|
+
y: ry - fallbackRowH + 4,
|
|
12982
|
+
w: colW * 2,
|
|
12983
|
+
h: fallbackRowH,
|
|
12984
|
+
fill: '#f1f5f9',
|
|
12985
|
+
rx: 2,
|
|
12986
|
+
});
|
|
12987
|
+
}
|
|
12988
|
+
primitives.push({
|
|
12989
|
+
kind: 'text',
|
|
12990
|
+
x: tableX + 4,
|
|
12991
|
+
y: ry,
|
|
12992
|
+
text: row.label,
|
|
12993
|
+
fontSize,
|
|
12994
|
+
fill: '#334155',
|
|
12995
|
+
textAnchor: 'start',
|
|
12996
|
+
}, {
|
|
12997
|
+
kind: 'text',
|
|
12998
|
+
x: tableX + colW + 4,
|
|
12999
|
+
y: ry,
|
|
13000
|
+
text: formatAxisValue(row.value),
|
|
13001
|
+
fontSize,
|
|
13002
|
+
fill: '#475569',
|
|
13003
|
+
textAnchor: 'start',
|
|
13004
|
+
});
|
|
13005
|
+
}
|
|
13006
|
+
if (unmatchedRows.length > 5) {
|
|
13007
|
+
const moreY = tableY + fallbackRowH * 6;
|
|
13008
|
+
primitives.push({
|
|
13009
|
+
kind: 'text',
|
|
13010
|
+
x: svgWidth / 2,
|
|
13011
|
+
y: moreY,
|
|
13012
|
+
text: `+${unmatchedRows.length - 5} more regions`,
|
|
13013
|
+
fontSize: 6,
|
|
13014
|
+
fill: '#94a3b8',
|
|
13015
|
+
textAnchor: 'middle',
|
|
13016
|
+
});
|
|
13017
|
+
}
|
|
13018
|
+
}
|
|
13019
|
+
const dataLabels = titlePrimitive !== undefined ? [titlePrimitive] : [];
|
|
13020
|
+
return {
|
|
13021
|
+
svgWidth,
|
|
13022
|
+
svgHeight,
|
|
13023
|
+
title: undefined, // Rendered inline as a dataLabel text primitive above.
|
|
13024
|
+
titleX: svgWidth / 2,
|
|
13025
|
+
titleY: 14,
|
|
13026
|
+
gridlines: [],
|
|
13027
|
+
axisLabels: [],
|
|
13028
|
+
zeroLine: undefined,
|
|
13029
|
+
categoryLabels: [],
|
|
13030
|
+
primitives,
|
|
13031
|
+
dataLabels,
|
|
13032
|
+
legend: [],
|
|
13033
|
+
legendX: svgWidth / 2,
|
|
13034
|
+
legendY: svgHeight - 8,
|
|
13035
|
+
legendAnchor: 'middle',
|
|
13036
|
+
};
|
|
13037
|
+
}
|
|
13038
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13039
|
+
// Internal: scale a simplified SVG path d string
|
|
13040
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13041
|
+
/**
|
|
13042
|
+
* Pre-scale and translate each coordinate pair in a simple SVG path `d`
|
|
13043
|
+
* attribute (M/L/Z commands only, space/comma delimited).
|
|
13044
|
+
*
|
|
13045
|
+
* This avoids needing a `transform` attribute on `SvgPath` which doesn't exist
|
|
13046
|
+
* in the existing primitive schema.
|
|
13047
|
+
*/
|
|
13048
|
+
function scalePathD(d, scale, dx, dy) {
|
|
13049
|
+
// Tokenise: split on whitespace, commas, and command letters while keeping
|
|
13050
|
+
// command letters in the output.
|
|
13051
|
+
const tokens = d.trim().split(/[\s,]+/u);
|
|
13052
|
+
const out = [];
|
|
13053
|
+
let i = 0;
|
|
13054
|
+
while (i < tokens.length) {
|
|
13055
|
+
const tok = tokens[i];
|
|
13056
|
+
if (tok === undefined) {
|
|
13057
|
+
i++;
|
|
13058
|
+
continue;
|
|
13059
|
+
}
|
|
13060
|
+
// Command letter (M, L, Z, etc.)
|
|
13061
|
+
if (/^[A-Za-z]$/u.test(tok)) {
|
|
13062
|
+
out.push(tok);
|
|
13063
|
+
i++;
|
|
13064
|
+
continue;
|
|
13065
|
+
}
|
|
13066
|
+
// Coordinate pair: tok = x value, tokens[i+1] = y value.
|
|
13067
|
+
const xRaw = parseFloat(tok);
|
|
13068
|
+
const yRaw = parseFloat(tokens[i + 1] ?? '0');
|
|
13069
|
+
if (!Number.isNaN(xRaw) && !Number.isNaN(yRaw)) {
|
|
13070
|
+
out.push(`${(xRaw * scale + dx).toFixed(2)},${(yRaw * scale + dy).toFixed(2)}`);
|
|
13071
|
+
i += 2;
|
|
13072
|
+
}
|
|
13073
|
+
else {
|
|
13074
|
+
out.push(tok);
|
|
13075
|
+
i++;
|
|
13076
|
+
}
|
|
13077
|
+
}
|
|
13078
|
+
return out.join(' ');
|
|
13079
|
+
}
|
|
13080
|
+
|
|
11836
13081
|
/**
|
|
11837
13082
|
* Pure, framework-agnostic helpers for chart rendering.
|
|
11838
13083
|
*
|
|
@@ -11854,9 +13099,11 @@ function applyResize(start, handle, dx, dy, min = MIN_RESIZE) {
|
|
|
11854
13099
|
* area / area3D -> polygon fill + polyline
|
|
11855
13100
|
* pie / doughnut / pie3D / ofPie -> arc paths
|
|
11856
13101
|
* scatter -> circle dots
|
|
13102
|
+
* bubble -> circle dots sized by a 3rd series
|
|
13103
|
+
* radar / radar3D -> polar polygons + spokes
|
|
11857
13104
|
*
|
|
11858
13105
|
* Deferred (fallback box rendered instead):
|
|
11859
|
-
*
|
|
13106
|
+
* stock, waterfall, combo, surface, treemap, sunburst,
|
|
11860
13107
|
* funnel, boxWhisker, histogram, regionMap, bar3D (complex 3-D shading),
|
|
11861
13108
|
* error bars, trendlines, secondary axes, data tables.
|
|
11862
13109
|
*
|
|
@@ -12198,6 +13445,47 @@ function computeScatterDots(values, maxXIndex, layout, range) {
|
|
|
12198
13445
|
cy: valueToY(val, range, layout.plotTop, layout.plotBottom),
|
|
12199
13446
|
}));
|
|
12200
13447
|
}
|
|
13448
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13449
|
+
// Bubble
|
|
13450
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13451
|
+
/**
|
|
13452
|
+
* Radius of a bubble given its size value, the max size in the chart, and a
|
|
13453
|
+
* median radius derived from the plot area. Mirrors `renderBubbleChart` in
|
|
13454
|
+
* React's chart-scatter-bubble.tsx: when no size value is present the bubble
|
|
13455
|
+
* uses the median radius; otherwise it scales from 0.5x to 2x the median.
|
|
13456
|
+
*/
|
|
13457
|
+
function computeBubbleRadius(sizeVal, maxBubble, medianRadius) {
|
|
13458
|
+
if (sizeVal === undefined) {
|
|
13459
|
+
return medianRadius;
|
|
13460
|
+
}
|
|
13461
|
+
const denom = maxBubble > 0 ? maxBubble : 1;
|
|
13462
|
+
return medianRadius * 0.5 + (Math.abs(sizeVal) / denom) * medianRadius * 1.5;
|
|
13463
|
+
}
|
|
13464
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13465
|
+
// Radar
|
|
13466
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13467
|
+
/** Angle (radians) of the i-th radar spoke; 0 points up (-90°), clockwise. */
|
|
13468
|
+
function radarAngle(index, catCount) {
|
|
13469
|
+
const n = Math.max(catCount, 1);
|
|
13470
|
+
return (Math.PI * 2 * index) / n - Math.PI / 2;
|
|
13471
|
+
}
|
|
13472
|
+
/** Project a series' values onto radar (polar) coordinates around (cx, cy). */
|
|
13473
|
+
function computeRadarPoints(values, maxVal, radius, cx, cy, catCount) {
|
|
13474
|
+
const denom = maxVal > 0 ? maxVal : 1;
|
|
13475
|
+
return values.slice(0, Math.max(catCount, 1)).map((val, i) => {
|
|
13476
|
+
const angle = radarAngle(i, catCount);
|
|
13477
|
+
const r = (Math.abs(val) / denom) * radius;
|
|
13478
|
+
return { x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle) };
|
|
13479
|
+
});
|
|
13480
|
+
}
|
|
13481
|
+
/** Points string for a radar gridline ring at radius `rr`. */
|
|
13482
|
+
function radarRingPoints(cx, cy, rr, catCount) {
|
|
13483
|
+
const n = Math.max(catCount, 1);
|
|
13484
|
+
return Array.from({ length: n }, (_, i) => {
|
|
13485
|
+
const angle = radarAngle(i, n);
|
|
13486
|
+
return `${(cx + rr * Math.cos(angle)).toFixed(2)},${(cy + rr * Math.sin(angle)).toFixed(2)}`;
|
|
13487
|
+
}).join(' ');
|
|
13488
|
+
}
|
|
12201
13489
|
function resolveChartKind(chartType) {
|
|
12202
13490
|
switch (chartType) {
|
|
12203
13491
|
case 'bar':
|
|
@@ -12217,6 +13505,24 @@ function resolveChartKind(chartType) {
|
|
|
12217
13505
|
return 'doughnut';
|
|
12218
13506
|
case 'scatter':
|
|
12219
13507
|
return 'scatter';
|
|
13508
|
+
case 'bubble':
|
|
13509
|
+
return 'bubble';
|
|
13510
|
+
case 'radar':
|
|
13511
|
+
case 'radar3D':
|
|
13512
|
+
return 'radar';
|
|
13513
|
+
case 'combo':
|
|
13514
|
+
return 'combo';
|
|
13515
|
+
case 'stock':
|
|
13516
|
+
return 'stock';
|
|
13517
|
+
case 'surface':
|
|
13518
|
+
case 'surface3D':
|
|
13519
|
+
return 'surface';
|
|
13520
|
+
case 'treemap':
|
|
13521
|
+
return 'treemap';
|
|
13522
|
+
case 'waterfall':
|
|
13523
|
+
return 'waterfall';
|
|
13524
|
+
case 'regionMap':
|
|
13525
|
+
return 'regionMap';
|
|
12220
13526
|
default:
|
|
12221
13527
|
return 'unsupported';
|
|
12222
13528
|
}
|
|
@@ -12245,6 +13551,27 @@ function buildChartViewModel(element) {
|
|
|
12245
13551
|
if (kind === 'pie' || kind === 'doughnut') {
|
|
12246
13552
|
return buildPieViewModel(element, chartData, categoryLabels, kind === 'doughnut');
|
|
12247
13553
|
}
|
|
13554
|
+
if (kind === 'radar') {
|
|
13555
|
+
return buildRadarViewModel(element, chartData, categoryLabels);
|
|
13556
|
+
}
|
|
13557
|
+
if (kind === 'combo') {
|
|
13558
|
+
return buildComboViewModel(element, chartData, categoryLabels);
|
|
13559
|
+
}
|
|
13560
|
+
if (kind === 'stock') {
|
|
13561
|
+
return buildStockViewModel(element, chartData, categoryLabels);
|
|
13562
|
+
}
|
|
13563
|
+
if (kind === 'surface') {
|
|
13564
|
+
return buildSurfaceViewModel(element, chartData, categoryLabels);
|
|
13565
|
+
}
|
|
13566
|
+
if (kind === 'treemap') {
|
|
13567
|
+
return buildTreemapViewModel(element, chartData, categoryLabels);
|
|
13568
|
+
}
|
|
13569
|
+
if (kind === 'waterfall') {
|
|
13570
|
+
return buildWaterfallViewModel(element, chartData, categoryLabels);
|
|
13571
|
+
}
|
|
13572
|
+
if (kind === 'regionMap') {
|
|
13573
|
+
return buildRegionMapViewModel(element, chartData, categoryLabels);
|
|
13574
|
+
}
|
|
12248
13575
|
return buildCartesianViewModel(element, chartData, categoryLabels, kind);
|
|
12249
13576
|
}
|
|
12250
13577
|
function buildFallbackViewModel(width, height, label) {
|
|
@@ -12361,7 +13688,7 @@ function buildCartesianViewModel(element, chartData, categoryLabels, kind) {
|
|
|
12361
13688
|
: computeValueRange(chartData.series);
|
|
12362
13689
|
const { gridlines, axisLabels } = buildGridlinesAndLabels(range, layout);
|
|
12363
13690
|
const zeroLine = buildZeroLine(range, layout);
|
|
12364
|
-
const catAxisStyle = kind === 'line' || kind === 'area' || kind === 'scatter' ? 'line' : 'bar';
|
|
13691
|
+
const catAxisStyle = kind === 'line' || kind === 'area' || kind === 'scatter' || kind === 'bubble' ? 'line' : 'bar';
|
|
12365
13692
|
const catLabels = buildCategoryLabels(categoryLabels, layout, catAxisStyle);
|
|
12366
13693
|
const legendPos = chartData.style?.legendPosition ?? 'b';
|
|
12367
13694
|
const { legend, legendX, legendY, legendAnchor } = buildLegend(chartData.series, chartData.colorPalette, layout.svgWidth, legendPos, layout.svgHeight, layout.plotTop);
|
|
@@ -12536,9 +13863,165 @@ function buildCartesianViewModel(element, chartData, categoryLabels, kind) {
|
|
|
12536
13863
|
textAnchor: 'middle',
|
|
12537
13864
|
});
|
|
12538
13865
|
});
|
|
12539
|
-
}
|
|
13866
|
+
}
|
|
13867
|
+
}
|
|
13868
|
+
}
|
|
13869
|
+
else if (kind === 'bubble') {
|
|
13870
|
+
// X = category index, Y = first/second series value, size = third series.
|
|
13871
|
+
const allIndices = chartData.series.flatMap((s) => s.values.map((_, i) => i));
|
|
13872
|
+
const maxXIndex = Math.max(1, ...allIndices);
|
|
13873
|
+
const sizeSeries = chartData.series.length >= 3 ? chartData.series[2] : undefined;
|
|
13874
|
+
const maxBubble = sizeSeries ? Math.max(1, ...sizeSeries.values.map((v) => Math.abs(v))) : 1;
|
|
13875
|
+
const medianRadius = Math.min(layout.plotWidth, layout.plotHeight) * 0.04;
|
|
13876
|
+
const pointSeries = chartData.series.slice(0, 2);
|
|
13877
|
+
for (let si = 0; si < pointSeries.length; si++) {
|
|
13878
|
+
const series = pointSeries[si];
|
|
13879
|
+
const c = seriesColor(series, si, chartData.colorPalette);
|
|
13880
|
+
const dots = computeScatterDots(series.values, maxXIndex, layout, range);
|
|
13881
|
+
dots.forEach((dot, vi) => {
|
|
13882
|
+
const r = computeBubbleRadius(sizeSeries?.values[vi], maxBubble, medianRadius);
|
|
13883
|
+
primitives.push({
|
|
13884
|
+
kind: 'circle',
|
|
13885
|
+
cx: dot.cx,
|
|
13886
|
+
cy: dot.cy,
|
|
13887
|
+
r,
|
|
13888
|
+
fill: c,
|
|
13889
|
+
opacity: 0.6,
|
|
13890
|
+
});
|
|
13891
|
+
});
|
|
13892
|
+
if (chartData.style?.hasDataLabels) {
|
|
13893
|
+
series.values.forEach((val, vi) => {
|
|
13894
|
+
const dot = dots[vi];
|
|
13895
|
+
if (!dot) {
|
|
13896
|
+
return;
|
|
13897
|
+
}
|
|
13898
|
+
dataLabels.push({
|
|
13899
|
+
kind: 'text',
|
|
13900
|
+
x: dot.cx,
|
|
13901
|
+
y: dot.cy - 10,
|
|
13902
|
+
text: formatAxisValue(val),
|
|
13903
|
+
fontSize: 7,
|
|
13904
|
+
fill: '#334155',
|
|
13905
|
+
textAnchor: 'middle',
|
|
13906
|
+
});
|
|
13907
|
+
});
|
|
13908
|
+
}
|
|
13909
|
+
}
|
|
13910
|
+
}
|
|
13911
|
+
const title = chartData.style?.hasTitle && chartData.title ? chartData.title : undefined;
|
|
13912
|
+
return {
|
|
13913
|
+
svgWidth: layout.svgWidth,
|
|
13914
|
+
svgHeight: layout.svgHeight,
|
|
13915
|
+
title,
|
|
13916
|
+
titleX: layout.svgWidth / 2,
|
|
13917
|
+
titleY: 12,
|
|
13918
|
+
gridlines,
|
|
13919
|
+
axisLabels,
|
|
13920
|
+
zeroLine,
|
|
13921
|
+
categoryLabels: catLabels,
|
|
13922
|
+
primitives,
|
|
13923
|
+
dataLabels,
|
|
13924
|
+
legend: chartData.style?.hasLegend ? legend : [],
|
|
13925
|
+
legendX,
|
|
13926
|
+
legendY,
|
|
13927
|
+
legendAnchor,
|
|
13928
|
+
};
|
|
13929
|
+
}
|
|
13930
|
+
const RADAR_RINGS = 4;
|
|
13931
|
+
const RADAR_RING_COLOR = '#cbd5e1';
|
|
13932
|
+
const RADAR_SPOKE_COLOR = '#94a3b8';
|
|
13933
|
+
const RADAR_LABEL_COLOR = '#64748b';
|
|
13934
|
+
/**
|
|
13935
|
+
* Build the view-model for a radar / spider chart. Polar, so it has no
|
|
13936
|
+
* cartesian gridlines/axes; ring + spoke geometry and the data polygons all
|
|
13937
|
+
* live in `primitives`, perimeter category labels in `categoryLabels`.
|
|
13938
|
+
* Mirrors React's `renderRadarChart` (chart-radar.tsx).
|
|
13939
|
+
*/
|
|
13940
|
+
function buildRadarViewModel(element, chartData, categoryLabels) {
|
|
13941
|
+
const layout = computePlotLayout(element.width, element.height, chartData, false);
|
|
13942
|
+
const cx = layout.plotLeft + layout.plotWidth / 2;
|
|
13943
|
+
const cy = layout.plotTop + layout.plotHeight / 2;
|
|
13944
|
+
const radius = Math.max(Math.min(layout.plotWidth, layout.plotHeight) / 2 - 4, 1);
|
|
13945
|
+
const catCount = Math.max(categoryLabels.length, 1);
|
|
13946
|
+
const maxVal = Math.max(1, ...chartData.series.flatMap((s) => s.values.map((v) => Math.abs(v))));
|
|
13947
|
+
const primitives = [];
|
|
13948
|
+
const perimeterLabels = [];
|
|
13949
|
+
// Concentric gridline rings (dashed except the outermost).
|
|
13950
|
+
for (let r = 1; r <= RADAR_RINGS; r++) {
|
|
13951
|
+
const rr = (radius * r) / RADAR_RINGS;
|
|
13952
|
+
primitives.push({
|
|
13953
|
+
kind: 'polygon',
|
|
13954
|
+
points: radarRingPoints(cx, cy, rr, catCount),
|
|
13955
|
+
fill: 'none',
|
|
13956
|
+
stroke: RADAR_RING_COLOR,
|
|
13957
|
+
strokeWidth: 0.5,
|
|
13958
|
+
dashArray: r < RADAR_RINGS ? '3 2' : undefined,
|
|
13959
|
+
});
|
|
13960
|
+
}
|
|
13961
|
+
// Axis spokes + perimeter category labels.
|
|
13962
|
+
for (let i = 0; i < catCount; i++) {
|
|
13963
|
+
const angle = radarAngle(i, catCount);
|
|
13964
|
+
primitives.push({
|
|
13965
|
+
kind: 'line',
|
|
13966
|
+
x1: cx,
|
|
13967
|
+
y1: cy,
|
|
13968
|
+
x2: cx + radius * Math.cos(angle),
|
|
13969
|
+
y2: cy + radius * Math.sin(angle),
|
|
13970
|
+
stroke: RADAR_SPOKE_COLOR,
|
|
13971
|
+
strokeWidth: 0.5,
|
|
13972
|
+
});
|
|
13973
|
+
const labelR = radius + 10;
|
|
13974
|
+
perimeterLabels.push({
|
|
13975
|
+
kind: 'text',
|
|
13976
|
+
x: cx + labelR * Math.cos(angle),
|
|
13977
|
+
y: cy + labelR * Math.sin(angle),
|
|
13978
|
+
text: categoryLabels[i] ?? '',
|
|
13979
|
+
fontSize: 8,
|
|
13980
|
+
fill: RADAR_LABEL_COLOR,
|
|
13981
|
+
textAnchor: 'middle',
|
|
13982
|
+
dominantBaseline: 'central',
|
|
13983
|
+
});
|
|
13984
|
+
}
|
|
13985
|
+
// Per-series data polygons + vertex dots.
|
|
13986
|
+
const dataLabels = [];
|
|
13987
|
+
chartData.series.forEach((series, si) => {
|
|
13988
|
+
const c = seriesColor(series, si, chartData.colorPalette);
|
|
13989
|
+
const pts = computeRadarPoints(series.values, maxVal, radius, cx, cy, catCount);
|
|
13990
|
+
if (pts.length === 0) {
|
|
13991
|
+
return;
|
|
13992
|
+
}
|
|
13993
|
+
const pointsStr = pts.map((p) => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ');
|
|
13994
|
+
primitives.push({
|
|
13995
|
+
kind: 'polygon',
|
|
13996
|
+
points: pointsStr,
|
|
13997
|
+
fill: c,
|
|
13998
|
+
opacity: 0.2,
|
|
13999
|
+
stroke: c,
|
|
14000
|
+
strokeWidth: 1.5,
|
|
14001
|
+
});
|
|
14002
|
+
for (const p of pts) {
|
|
14003
|
+
primitives.push({ kind: 'circle', cx: p.x, cy: p.y, r: 3, fill: c });
|
|
14004
|
+
}
|
|
14005
|
+
if (chartData.style?.hasDataLabels) {
|
|
14006
|
+
pts.forEach((p, vi) => {
|
|
14007
|
+
const val = series.values[vi];
|
|
14008
|
+
if (val === undefined) {
|
|
14009
|
+
return;
|
|
14010
|
+
}
|
|
14011
|
+
dataLabels.push({
|
|
14012
|
+
kind: 'text',
|
|
14013
|
+
x: p.x,
|
|
14014
|
+
y: p.y - 8,
|
|
14015
|
+
text: formatAxisValue(val),
|
|
14016
|
+
fontSize: 7,
|
|
14017
|
+
fill: '#334155',
|
|
14018
|
+
textAnchor: 'middle',
|
|
14019
|
+
});
|
|
14020
|
+
});
|
|
12540
14021
|
}
|
|
12541
|
-
}
|
|
14022
|
+
});
|
|
14023
|
+
const legendPos = chartData.style?.legendPosition ?? 'b';
|
|
14024
|
+
const { legend, legendX, legendY, legendAnchor } = buildLegend(chartData.series, chartData.colorPalette, layout.svgWidth, legendPos, layout.svgHeight, layout.plotTop);
|
|
12542
14025
|
const title = chartData.style?.hasTitle && chartData.title ? chartData.title : undefined;
|
|
12543
14026
|
return {
|
|
12544
14027
|
svgWidth: layout.svgWidth,
|
|
@@ -12546,10 +14029,10 @@ function buildCartesianViewModel(element, chartData, categoryLabels, kind) {
|
|
|
12546
14029
|
title,
|
|
12547
14030
|
titleX: layout.svgWidth / 2,
|
|
12548
14031
|
titleY: 12,
|
|
12549
|
-
gridlines,
|
|
12550
|
-
axisLabels,
|
|
12551
|
-
zeroLine,
|
|
12552
|
-
categoryLabels:
|
|
14032
|
+
gridlines: [],
|
|
14033
|
+
axisLabels: [],
|
|
14034
|
+
zeroLine: undefined,
|
|
14035
|
+
categoryLabels: perimeterLabels,
|
|
12553
14036
|
primitives,
|
|
12554
14037
|
dataLabels,
|
|
12555
14038
|
legend: chartData.style?.hasLegend ? legend : [],
|
|
@@ -12569,9 +14052,11 @@ function buildCartesianViewModel(element, chartData, categoryLabels, kind) {
|
|
|
12569
14052
|
* area / area3D
|
|
12570
14053
|
* pie / doughnut / pie3D / ofPie
|
|
12571
14054
|
* scatter
|
|
14055
|
+
* bubble
|
|
14056
|
+
* radar / radar3D
|
|
12572
14057
|
*
|
|
12573
14058
|
* Deferred (labelled fallback box):
|
|
12574
|
-
*
|
|
14059
|
+
* stock, waterfall, combo, surface, treemap, sunburst,
|
|
12575
14060
|
* funnel, boxWhisker, histogram, regionMap, bar3D (complex 3-D shading),
|
|
12576
14061
|
* error bars, trendlines, secondary axes, data tables.
|
|
12577
14062
|
*
|
|
@@ -12616,6 +14101,9 @@ class ChartRendererComponent {
|
|
|
12616
14101
|
asLine(p) {
|
|
12617
14102
|
return p;
|
|
12618
14103
|
}
|
|
14104
|
+
asPolygon(p) {
|
|
14105
|
+
return p;
|
|
14106
|
+
}
|
|
12619
14107
|
asText(p) {
|
|
12620
14108
|
return p;
|
|
12621
14109
|
}
|
|
@@ -12697,6 +14185,7 @@ class ChartRendererComponent {
|
|
|
12697
14185
|
[attr.text-anchor]="lbl.textAnchor"
|
|
12698
14186
|
[attr.font-size]="lbl.fontSize"
|
|
12699
14187
|
[attr.fill]="lbl.fill"
|
|
14188
|
+
[attr.dominant-baseline]="lbl.dominantBaseline ?? 'auto'"
|
|
12700
14189
|
>
|
|
12701
14190
|
{{ lbl.text }}
|
|
12702
14191
|
</text>
|
|
@@ -12752,6 +14241,16 @@ class ChartRendererComponent {
|
|
|
12752
14241
|
[attr.stroke-width]="asLine(prim).strokeWidth"
|
|
12753
14242
|
/>
|
|
12754
14243
|
}
|
|
14244
|
+
@case ('polygon') {
|
|
14245
|
+
<polygon
|
|
14246
|
+
[attr.points]="asPolygon(prim).points"
|
|
14247
|
+
[attr.fill]="asPolygon(prim).fill"
|
|
14248
|
+
[attr.stroke]="asPolygon(prim).stroke"
|
|
14249
|
+
[attr.stroke-width]="asPolygon(prim).strokeWidth"
|
|
14250
|
+
[attr.opacity]="asPolygon(prim).opacity ?? 1"
|
|
14251
|
+
[attr.stroke-dasharray]="asPolygon(prim).dashArray ?? null"
|
|
14252
|
+
/>
|
|
14253
|
+
}
|
|
12755
14254
|
}
|
|
12756
14255
|
}
|
|
12757
14256
|
|
|
@@ -12875,6 +14374,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
12875
14374
|
[attr.text-anchor]="lbl.textAnchor"
|
|
12876
14375
|
[attr.font-size]="lbl.fontSize"
|
|
12877
14376
|
[attr.fill]="lbl.fill"
|
|
14377
|
+
[attr.dominant-baseline]="lbl.dominantBaseline ?? 'auto'"
|
|
12878
14378
|
>
|
|
12879
14379
|
{{ lbl.text }}
|
|
12880
14380
|
</text>
|
|
@@ -12930,6 +14430,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
12930
14430
|
[attr.stroke-width]="asLine(prim).strokeWidth"
|
|
12931
14431
|
/>
|
|
12932
14432
|
}
|
|
14433
|
+
@case ('polygon') {
|
|
14434
|
+
<polygon
|
|
14435
|
+
[attr.points]="asPolygon(prim).points"
|
|
14436
|
+
[attr.fill]="asPolygon(prim).fill"
|
|
14437
|
+
[attr.stroke]="asPolygon(prim).stroke"
|
|
14438
|
+
[attr.stroke-width]="asPolygon(prim).strokeWidth"
|
|
14439
|
+
[attr.opacity]="asPolygon(prim).opacity ?? 1"
|
|
14440
|
+
[attr.stroke-dasharray]="asPolygon(prim).dashArray ?? null"
|
|
14441
|
+
/>
|
|
14442
|
+
}
|
|
12933
14443
|
}
|
|
12934
14444
|
}
|
|
12935
14445
|
|
|
@@ -12971,6 +14481,435 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
12971
14481
|
}]
|
|
12972
14482
|
}], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }] } });
|
|
12973
14483
|
|
|
14484
|
+
/**
|
|
14485
|
+
* Pure, framework-agnostic A* orthogonal connector router.
|
|
14486
|
+
*
|
|
14487
|
+
* Angular port of the React connector-router suite:
|
|
14488
|
+
* packages/react/src/viewer/utils/connector-router.ts
|
|
14489
|
+
* packages/react/src/viewer/utils/connector-router-graph.ts
|
|
14490
|
+
* packages/react/src/viewer/utils/connector-router-astar.ts
|
|
14491
|
+
*
|
|
14492
|
+
* All logic is consolidated into this single file (no barrel split needed for
|
|
14493
|
+
* an Angular helper module). Compatible with `connector-path.ts` / the
|
|
14494
|
+
* `ConnectorRendererComponent`: pass the returned `Point[]` to
|
|
14495
|
+
* `waypointsToPathD()` to obtain the SVG `d` attribute string that the
|
|
14496
|
+
* component renders on a `<path>`.
|
|
14497
|
+
*
|
|
14498
|
+
* Constraints kept from the React source:
|
|
14499
|
+
* - No `any`.
|
|
14500
|
+
* - No `String.prototype.replaceAll` / named regex capture groups.
|
|
14501
|
+
* - No `Math.random` / `Date` — fully deterministic.
|
|
14502
|
+
* - No Angular / Vue / React imports.
|
|
14503
|
+
*/
|
|
14504
|
+
// ---------------------------------------------------------------------------
|
|
14505
|
+
// Constants
|
|
14506
|
+
// ---------------------------------------------------------------------------
|
|
14507
|
+
/** Default obstacle padding in pixels. */
|
|
14508
|
+
const ROUTING_PADDING_DEFAULT = 12;
|
|
14509
|
+
/** Safety cap on A* iterations to avoid O(n²) hangs on degenerate inputs. */
|
|
14510
|
+
const MAX_ASTAR_ITERATIONS = 2000;
|
|
14511
|
+
/** Sentinel canvas size when caller does not supply one. */
|
|
14512
|
+
const CANVAS_SENTINEL = 100_000;
|
|
14513
|
+
// ---------------------------------------------------------------------------
|
|
14514
|
+
// Geometry helpers (exported for tests)
|
|
14515
|
+
// ---------------------------------------------------------------------------
|
|
14516
|
+
/**
|
|
14517
|
+
* Expand a rect by `pad` pixels on every side. Returns a new Rect.
|
|
14518
|
+
*/
|
|
14519
|
+
function inflateRect(r, pad) {
|
|
14520
|
+
return {
|
|
14521
|
+
x: r.x - pad,
|
|
14522
|
+
y: r.y - pad,
|
|
14523
|
+
width: r.width + pad * 2,
|
|
14524
|
+
height: r.height + pad * 2,
|
|
14525
|
+
};
|
|
14526
|
+
}
|
|
14527
|
+
/**
|
|
14528
|
+
* Return true when point `p` lies strictly inside (or on the border of) `r`.
|
|
14529
|
+
*/
|
|
14530
|
+
function pointInRect(p, r) {
|
|
14531
|
+
return p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height;
|
|
14532
|
+
}
|
|
14533
|
+
/**
|
|
14534
|
+
* Return true when the axis-aligned segment `a→b` intersects rectangle `r`.
|
|
14535
|
+
*
|
|
14536
|
+
* Only horizontal and vertical segments are handled; diagonal segments are
|
|
14537
|
+
* treated as intersecting (safe / conservative fallback).
|
|
14538
|
+
*/
|
|
14539
|
+
function segmentIntersectsRect(a, b, r) {
|
|
14540
|
+
const minX = Math.min(a.x, b.x);
|
|
14541
|
+
const maxX = Math.max(a.x, b.x);
|
|
14542
|
+
const minY = Math.min(a.y, b.y);
|
|
14543
|
+
const maxY = Math.max(a.y, b.y);
|
|
14544
|
+
const rRight = r.x + r.width;
|
|
14545
|
+
const rBottom = r.y + r.height;
|
|
14546
|
+
// Quick reject: bounding boxes don't overlap.
|
|
14547
|
+
if (maxX < r.x || minX > rRight || maxY < r.y || minY > rBottom) {
|
|
14548
|
+
return false;
|
|
14549
|
+
}
|
|
14550
|
+
// Horizontal segment.
|
|
14551
|
+
if (Math.abs(a.y - b.y) < 0.5) {
|
|
14552
|
+
return a.y >= r.y && a.y <= rBottom && maxX >= r.x && minX <= rRight;
|
|
14553
|
+
}
|
|
14554
|
+
// Vertical segment.
|
|
14555
|
+
if (Math.abs(a.x - b.x) < 0.5) {
|
|
14556
|
+
return a.x >= r.x && a.x <= rRight && maxY >= r.y && minY <= rBottom;
|
|
14557
|
+
}
|
|
14558
|
+
// Diagonal — conservative: treat as intersecting.
|
|
14559
|
+
return true;
|
|
14560
|
+
}
|
|
14561
|
+
/**
|
|
14562
|
+
* Return true when the direct segment `start→end` is clear of all inflated
|
|
14563
|
+
* obstacle rectangles.
|
|
14564
|
+
*/
|
|
14565
|
+
function directPathClear(start, end, inflated) {
|
|
14566
|
+
for (const rect of inflated) {
|
|
14567
|
+
if (segmentIntersectsRect(start, end, rect)) {
|
|
14568
|
+
return false;
|
|
14569
|
+
}
|
|
14570
|
+
}
|
|
14571
|
+
return true;
|
|
14572
|
+
}
|
|
14573
|
+
/** Manhattan-distance heuristic for A*. */
|
|
14574
|
+
function heuristic(a, b) {
|
|
14575
|
+
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
14576
|
+
}
|
|
14577
|
+
/**
|
|
14578
|
+
* Stable string key for a point (rounded to nearest pixel).
|
|
14579
|
+
* Used as Map keys in A*.
|
|
14580
|
+
*/
|
|
14581
|
+
function pointKey(p) {
|
|
14582
|
+
return `${Math.round(p.x)},${Math.round(p.y)}`;
|
|
14583
|
+
}
|
|
14584
|
+
// ---------------------------------------------------------------------------
|
|
14585
|
+
// Navigation graph construction
|
|
14586
|
+
// ---------------------------------------------------------------------------
|
|
14587
|
+
/**
|
|
14588
|
+
* Build the set of candidate navigation nodes for A*:
|
|
14589
|
+
* - Start and end points.
|
|
14590
|
+
* - Corners of each inflated obstacle (with a small clearance margin).
|
|
14591
|
+
* - Orthogonal projections of every node onto every other node's row/column.
|
|
14592
|
+
*
|
|
14593
|
+
* Nodes that fall inside an obstacle or outside the canvas are discarded.
|
|
14594
|
+
*/
|
|
14595
|
+
function buildGraphNodes(start, end, inflated, canvasWidth, canvasHeight) {
|
|
14596
|
+
const cornerMargin = 4;
|
|
14597
|
+
const nodes = [start, end];
|
|
14598
|
+
for (const r of inflated) {
|
|
14599
|
+
const corners = [
|
|
14600
|
+
{ x: r.x - cornerMargin, y: r.y - cornerMargin },
|
|
14601
|
+
{ x: r.x + r.width + cornerMargin, y: r.y - cornerMargin },
|
|
14602
|
+
{ x: r.x - cornerMargin, y: r.y + r.height + cornerMargin },
|
|
14603
|
+
{ x: r.x + r.width + cornerMargin, y: r.y + r.height + cornerMargin },
|
|
14604
|
+
];
|
|
14605
|
+
for (const c of corners) {
|
|
14606
|
+
if (c.x >= 0 && c.x <= canvasWidth && c.y >= 0 && c.y <= canvasHeight) {
|
|
14607
|
+
let blocked = false;
|
|
14608
|
+
for (const rect of inflated) {
|
|
14609
|
+
if (pointInRect(c, rect)) {
|
|
14610
|
+
blocked = true;
|
|
14611
|
+
break;
|
|
14612
|
+
}
|
|
14613
|
+
}
|
|
14614
|
+
if (!blocked) {
|
|
14615
|
+
nodes.push(c);
|
|
14616
|
+
}
|
|
14617
|
+
}
|
|
14618
|
+
}
|
|
14619
|
+
}
|
|
14620
|
+
// Orthogonal projections: for every existing node, add axis-aligned
|
|
14621
|
+
// intersection points with start and end rows/columns.
|
|
14622
|
+
const projections = [];
|
|
14623
|
+
for (const node of nodes) {
|
|
14624
|
+
projections.push({ x: start.x, y: node.y });
|
|
14625
|
+
projections.push({ x: node.x, y: start.y });
|
|
14626
|
+
projections.push({ x: end.x, y: node.y });
|
|
14627
|
+
projections.push({ x: node.x, y: end.y });
|
|
14628
|
+
}
|
|
14629
|
+
for (const p of projections) {
|
|
14630
|
+
if (p.x >= 0 && p.x <= canvasWidth && p.y >= 0 && p.y <= canvasHeight) {
|
|
14631
|
+
let blocked = false;
|
|
14632
|
+
for (const rect of inflated) {
|
|
14633
|
+
if (pointInRect(p, rect)) {
|
|
14634
|
+
blocked = true;
|
|
14635
|
+
break;
|
|
14636
|
+
}
|
|
14637
|
+
}
|
|
14638
|
+
if (!blocked) {
|
|
14639
|
+
nodes.push(p);
|
|
14640
|
+
}
|
|
14641
|
+
}
|
|
14642
|
+
}
|
|
14643
|
+
return nodes;
|
|
14644
|
+
}
|
|
14645
|
+
// ---------------------------------------------------------------------------
|
|
14646
|
+
// A* search
|
|
14647
|
+
// ---------------------------------------------------------------------------
|
|
14648
|
+
/**
|
|
14649
|
+
* Run A* over the navigation graph to find the shortest orthogonal path
|
|
14650
|
+
* from `start` to `end` that avoids all `inflated` obstacle rectangles.
|
|
14651
|
+
*
|
|
14652
|
+
* Returns an array of waypoints (possibly including intermediate bend points
|
|
14653
|
+
* when L-shaped edges are taken). Falls back to `[start, end]` when no path
|
|
14654
|
+
* is found within {@link MAX_ASTAR_ITERATIONS}.
|
|
14655
|
+
*/
|
|
14656
|
+
function aStarOrthogonal(nodes, start, end, inflated) {
|
|
14657
|
+
const startKey = pointKey(start);
|
|
14658
|
+
const endKey = pointKey(end);
|
|
14659
|
+
/**
|
|
14660
|
+
* Check whether two nodes can be directly connected (axis-aligned or
|
|
14661
|
+
* L-shaped) without crossing an obstacle.
|
|
14662
|
+
*/
|
|
14663
|
+
const canConnect = (a, b) => {
|
|
14664
|
+
const isHoriz = Math.abs(a.y - b.y) < 1;
|
|
14665
|
+
const isVert = Math.abs(a.x - b.x) < 1;
|
|
14666
|
+
if (isHoriz || isVert) {
|
|
14667
|
+
// Straight segment.
|
|
14668
|
+
for (const rect of inflated) {
|
|
14669
|
+
if (segmentIntersectsRect(a, b, rect)) {
|
|
14670
|
+
return false;
|
|
14671
|
+
}
|
|
14672
|
+
}
|
|
14673
|
+
return true;
|
|
14674
|
+
}
|
|
14675
|
+
// L-shaped: try both bend orientations.
|
|
14676
|
+
const bend1 = { x: b.x, y: a.y };
|
|
14677
|
+
const bend2 = { x: a.x, y: b.y };
|
|
14678
|
+
let path1Clear = true;
|
|
14679
|
+
let path2Clear = true;
|
|
14680
|
+
for (const rect of inflated) {
|
|
14681
|
+
if (path1Clear &&
|
|
14682
|
+
(segmentIntersectsRect(a, bend1, rect) || segmentIntersectsRect(bend1, b, rect))) {
|
|
14683
|
+
path1Clear = false;
|
|
14684
|
+
}
|
|
14685
|
+
if (path2Clear &&
|
|
14686
|
+
(segmentIntersectsRect(a, bend2, rect) || segmentIntersectsRect(bend2, b, rect))) {
|
|
14687
|
+
path2Clear = false;
|
|
14688
|
+
}
|
|
14689
|
+
if (!path1Clear && !path2Clear) {
|
|
14690
|
+
break;
|
|
14691
|
+
}
|
|
14692
|
+
}
|
|
14693
|
+
return path1Clear || path2Clear;
|
|
14694
|
+
};
|
|
14695
|
+
const gScore = new Map();
|
|
14696
|
+
const fScore = new Map();
|
|
14697
|
+
const cameFrom = new Map();
|
|
14698
|
+
/** When an L-shaped edge was used to reach this node, the bend point. */
|
|
14699
|
+
const bendPoint = new Map();
|
|
14700
|
+
gScore.set(startKey, 0);
|
|
14701
|
+
fScore.set(startKey, heuristic(start, end));
|
|
14702
|
+
const openSet = new Set([startKey]);
|
|
14703
|
+
const nodeMap = new Map();
|
|
14704
|
+
for (const n of nodes) {
|
|
14705
|
+
nodeMap.set(pointKey(n), n);
|
|
14706
|
+
}
|
|
14707
|
+
/** Pop the node with the lowest fScore from the open set. */
|
|
14708
|
+
const getLowest = () => {
|
|
14709
|
+
let best;
|
|
14710
|
+
let bestScore = Infinity;
|
|
14711
|
+
for (const key of openSet) {
|
|
14712
|
+
const score = fScore.get(key) ?? Infinity;
|
|
14713
|
+
if (score < bestScore) {
|
|
14714
|
+
bestScore = score;
|
|
14715
|
+
best = key;
|
|
14716
|
+
}
|
|
14717
|
+
}
|
|
14718
|
+
return best;
|
|
14719
|
+
};
|
|
14720
|
+
let iterations = 0;
|
|
14721
|
+
while (openSet.size > 0 && iterations < MAX_ASTAR_ITERATIONS) {
|
|
14722
|
+
iterations++;
|
|
14723
|
+
const currentKey = getLowest();
|
|
14724
|
+
if (currentKey === undefined) {
|
|
14725
|
+
break;
|
|
14726
|
+
}
|
|
14727
|
+
if (currentKey === endKey) {
|
|
14728
|
+
// Reconstruct the path from cameFrom chain.
|
|
14729
|
+
const path = [];
|
|
14730
|
+
let key = endKey;
|
|
14731
|
+
while (key !== undefined) {
|
|
14732
|
+
const node = nodeMap.get(key);
|
|
14733
|
+
if (node !== undefined) {
|
|
14734
|
+
const bp = bendPoint.get(key);
|
|
14735
|
+
if (bp !== undefined && bp !== null) {
|
|
14736
|
+
path.unshift(node);
|
|
14737
|
+
path.unshift(bp);
|
|
14738
|
+
}
|
|
14739
|
+
else {
|
|
14740
|
+
path.unshift(node);
|
|
14741
|
+
}
|
|
14742
|
+
}
|
|
14743
|
+
key = cameFrom.get(key);
|
|
14744
|
+
}
|
|
14745
|
+
return path;
|
|
14746
|
+
}
|
|
14747
|
+
openSet.delete(currentKey);
|
|
14748
|
+
const current = nodeMap.get(currentKey);
|
|
14749
|
+
if (current === undefined) {
|
|
14750
|
+
continue;
|
|
14751
|
+
}
|
|
14752
|
+
for (const neighbor of nodes) {
|
|
14753
|
+
const neighborKey = pointKey(neighbor);
|
|
14754
|
+
if (neighborKey === currentKey) {
|
|
14755
|
+
continue;
|
|
14756
|
+
}
|
|
14757
|
+
if (!canConnect(current, neighbor)) {
|
|
14758
|
+
continue;
|
|
14759
|
+
}
|
|
14760
|
+
const isHoriz = Math.abs(current.y - neighbor.y) < 1;
|
|
14761
|
+
const isVert = Math.abs(current.x - neighbor.x) < 1;
|
|
14762
|
+
let dist;
|
|
14763
|
+
let bp = null;
|
|
14764
|
+
if (isHoriz || isVert) {
|
|
14765
|
+
dist = heuristic(current, neighbor);
|
|
14766
|
+
}
|
|
14767
|
+
else {
|
|
14768
|
+
// L-shaped: pick the shorter valid bend.
|
|
14769
|
+
const bend1 = { x: neighbor.x, y: current.y };
|
|
14770
|
+
const bend2 = { x: current.x, y: neighbor.y };
|
|
14771
|
+
let use1 = true;
|
|
14772
|
+
for (const rect of inflated) {
|
|
14773
|
+
if (segmentIntersectsRect(current, bend1, rect) ||
|
|
14774
|
+
segmentIntersectsRect(bend1, neighbor, rect)) {
|
|
14775
|
+
use1 = false;
|
|
14776
|
+
break;
|
|
14777
|
+
}
|
|
14778
|
+
}
|
|
14779
|
+
bp = use1 ? bend1 : bend2;
|
|
14780
|
+
dist =
|
|
14781
|
+
Math.abs(current.x - bp.x) +
|
|
14782
|
+
Math.abs(current.y - bp.y) +
|
|
14783
|
+
Math.abs(bp.x - neighbor.x) +
|
|
14784
|
+
Math.abs(bp.y - neighbor.y);
|
|
14785
|
+
}
|
|
14786
|
+
const tentativeG = (gScore.get(currentKey) ?? Infinity) + dist;
|
|
14787
|
+
if (tentativeG < (gScore.get(neighborKey) ?? Infinity)) {
|
|
14788
|
+
cameFrom.set(neighborKey, currentKey);
|
|
14789
|
+
bendPoint.set(neighborKey, bp);
|
|
14790
|
+
gScore.set(neighborKey, tentativeG);
|
|
14791
|
+
fScore.set(neighborKey, tentativeG + heuristic(neighbor, end));
|
|
14792
|
+
openSet.add(neighborKey);
|
|
14793
|
+
}
|
|
14794
|
+
}
|
|
14795
|
+
}
|
|
14796
|
+
// No path found — fall back to a direct two-point path.
|
|
14797
|
+
return [start, end];
|
|
14798
|
+
}
|
|
14799
|
+
// ---------------------------------------------------------------------------
|
|
14800
|
+
// Path simplification
|
|
14801
|
+
// ---------------------------------------------------------------------------
|
|
14802
|
+
/**
|
|
14803
|
+
* Remove collinear intermediate waypoints from a path.
|
|
14804
|
+
*
|
|
14805
|
+
* A waypoint is dropped when the previous and next waypoints share the same
|
|
14806
|
+
* axis as both flanking segments (i.e. three consecutive collinear points).
|
|
14807
|
+
* This keeps the output minimal while preserving every directional change.
|
|
14808
|
+
*/
|
|
14809
|
+
function simplifyPath(points) {
|
|
14810
|
+
if (points.length <= 2) {
|
|
14811
|
+
return [...points];
|
|
14812
|
+
}
|
|
14813
|
+
const result = [points[0]];
|
|
14814
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
14815
|
+
const prev = result[result.length - 1];
|
|
14816
|
+
const curr = points[i];
|
|
14817
|
+
const next = points[i + 1];
|
|
14818
|
+
const sameX = Math.abs(prev.x - curr.x) < 1 && Math.abs(curr.x - next.x) < 1;
|
|
14819
|
+
const sameY = Math.abs(prev.y - curr.y) < 1 && Math.abs(curr.y - next.y) < 1;
|
|
14820
|
+
// Drop only when strictly collinear on one axis.
|
|
14821
|
+
if (!sameX && !sameY) {
|
|
14822
|
+
result.push(curr);
|
|
14823
|
+
}
|
|
14824
|
+
else if (!sameX || !sameY) {
|
|
14825
|
+
// One axis matches — still a direction change if the other doesn't.
|
|
14826
|
+
if (!(sameX || sameY)) {
|
|
14827
|
+
result.push(curr);
|
|
14828
|
+
}
|
|
14829
|
+
}
|
|
14830
|
+
// else: fully collinear on both axes → drop.
|
|
14831
|
+
}
|
|
14832
|
+
result.push(points[points.length - 1]);
|
|
14833
|
+
return result;
|
|
14834
|
+
}
|
|
14835
|
+
// ---------------------------------------------------------------------------
|
|
14836
|
+
// Public routing API
|
|
14837
|
+
// ---------------------------------------------------------------------------
|
|
14838
|
+
/**
|
|
14839
|
+
* Route an orthogonal connector between `start` and `end`, avoiding all
|
|
14840
|
+
* `obstacles`. Returns a list of waypoints (including the start and end
|
|
14841
|
+
* points) that form an axis-aligned polyline.
|
|
14842
|
+
*
|
|
14843
|
+
* Strategy (fast-path first, A* as fallback):
|
|
14844
|
+
* 1. No obstacles → return `[start, end]` directly.
|
|
14845
|
+
* 2. Direct line clear → return `[start, end]`.
|
|
14846
|
+
* 3. Single horizontal elbow (`start → (end.x, start.y) → end`) clear → use it.
|
|
14847
|
+
* 4. Single vertical elbow (`start → (start.x, end.y) → end`) clear → use it.
|
|
14848
|
+
* 5. Full A* search on the navigation graph.
|
|
14849
|
+
*/
|
|
14850
|
+
function routeOrthogonalConnector(start, end, obstacles, opts) {
|
|
14851
|
+
const padding = opts?.padding ?? ROUTING_PADDING_DEFAULT;
|
|
14852
|
+
const canvasWidth = opts?.canvasWidth ?? CANVAS_SENTINEL;
|
|
14853
|
+
const canvasHeight = opts?.canvasHeight ?? CANVAS_SENTINEL;
|
|
14854
|
+
if (obstacles.length === 0) {
|
|
14855
|
+
return [start, end];
|
|
14856
|
+
}
|
|
14857
|
+
const inflated = obstacles.map((r) => inflateRect(r, padding));
|
|
14858
|
+
if (directPathClear(start, end, inflated)) {
|
|
14859
|
+
return [start, end];
|
|
14860
|
+
}
|
|
14861
|
+
// Try single horizontal elbow.
|
|
14862
|
+
const midH = { x: end.x, y: start.y };
|
|
14863
|
+
const midV = { x: start.x, y: end.y };
|
|
14864
|
+
let elbowHClear = true;
|
|
14865
|
+
let elbowVClear = true;
|
|
14866
|
+
for (const rect of inflated) {
|
|
14867
|
+
if (elbowHClear &&
|
|
14868
|
+
(segmentIntersectsRect(start, midH, rect) || segmentIntersectsRect(midH, end, rect))) {
|
|
14869
|
+
elbowHClear = false;
|
|
14870
|
+
}
|
|
14871
|
+
if (elbowVClear &&
|
|
14872
|
+
(segmentIntersectsRect(start, midV, rect) || segmentIntersectsRect(midV, end, rect))) {
|
|
14873
|
+
elbowVClear = false;
|
|
14874
|
+
}
|
|
14875
|
+
if (!elbowHClear && !elbowVClear) {
|
|
14876
|
+
break;
|
|
14877
|
+
}
|
|
14878
|
+
}
|
|
14879
|
+
if (elbowHClear) {
|
|
14880
|
+
return [start, midH, end];
|
|
14881
|
+
}
|
|
14882
|
+
if (elbowVClear) {
|
|
14883
|
+
return [start, midV, end];
|
|
14884
|
+
}
|
|
14885
|
+
// Full A* search.
|
|
14886
|
+
const nodes = buildGraphNodes(start, end, inflated, canvasWidth, canvasHeight);
|
|
14887
|
+
const path = aStarOrthogonal(nodes, start, end, inflated);
|
|
14888
|
+
return simplifyPath(path);
|
|
14889
|
+
}
|
|
14890
|
+
// ---------------------------------------------------------------------------
|
|
14891
|
+
// SVG path serialisation
|
|
14892
|
+
// ---------------------------------------------------------------------------
|
|
14893
|
+
/**
|
|
14894
|
+
* Convert an array of waypoints to an SVG `path` `d` attribute string.
|
|
14895
|
+
*
|
|
14896
|
+
* Returns an empty string for an empty waypoint array, `"M x y"` for a
|
|
14897
|
+
* single point, and `"M x y L x1 y1 …"` for a polyline.
|
|
14898
|
+
*
|
|
14899
|
+
* This output is compatible with the `pathD` field on `ConnectorGeometry`
|
|
14900
|
+
* from `connector-path.ts` and can be bound directly to a `<path [attr.d]>`.
|
|
14901
|
+
*/
|
|
14902
|
+
function waypointsToPathD(waypoints) {
|
|
14903
|
+
if (waypoints.length === 0) {
|
|
14904
|
+
return '';
|
|
14905
|
+
}
|
|
14906
|
+
const parts = [`M${waypoints[0].x},${waypoints[0].y}`];
|
|
14907
|
+
for (let i = 1; i < waypoints.length; i++) {
|
|
14908
|
+
parts.push(`L${waypoints[i].x},${waypoints[i].y}`);
|
|
14909
|
+
}
|
|
14910
|
+
return parts.join(' ');
|
|
14911
|
+
}
|
|
14912
|
+
|
|
12974
14913
|
/**
|
|
12975
14914
|
* Pure, framework-agnostic helpers for rendering connector elements.
|
|
12976
14915
|
*
|
|
@@ -12990,7 +14929,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
12990
14929
|
* This is a pure function: no side-effects, no Angular/Vue/React imports.
|
|
12991
14930
|
* The component calls this once per change-detection cycle inside a `computed()`.
|
|
12992
14931
|
*/
|
|
12993
|
-
function buildConnectorGeometry(element, zIndex) {
|
|
14932
|
+
function buildConnectorGeometry(element, zIndex, routing) {
|
|
12994
14933
|
const ss = hasShapeProperties(element) ? element.shapeStyle : undefined;
|
|
12995
14934
|
const strokeWidth = Math.max(0, ss?.strokeWidth ?? 2);
|
|
12996
14935
|
const strokeColor = ss?.strokeColor ?? DEFAULT_STROKE_COLOR;
|
|
@@ -13003,7 +14942,26 @@ function buildConnectorGeometry(element, zIndex) {
|
|
|
13003
14942
|
const x2 = element.flipHorizontal ? 0 : svgW;
|
|
13004
14943
|
const y2 = element.flipVertical ? 0 : svgH;
|
|
13005
14944
|
const shapeType = element.shapeType;
|
|
13006
|
-
|
|
14945
|
+
let pathD = buildConnectorPathD(shapeType, x1, y1, x2, y2, connectorBendFraction(element));
|
|
14946
|
+
// Obstacle-avoiding A* routing for bent connectors. Routes in absolute slide
|
|
14947
|
+
// coordinates (so it can detour outside the connector's own bounding box —
|
|
14948
|
+
// the SVG uses `overflow: visible`), then translates waypoints back to
|
|
14949
|
+
// element-local space for the path data.
|
|
14950
|
+
if (routing &&
|
|
14951
|
+
routing.obstacles.length > 0 &&
|
|
14952
|
+
connectorKind(shapeType) === 'bent' &&
|
|
14953
|
+
(element.width > 0 || element.height > 0)) {
|
|
14954
|
+
const start = { x: element.x + x1, y: element.y + y1 };
|
|
14955
|
+
const end = { x: element.x + x2, y: element.y + y2 };
|
|
14956
|
+
const waypoints = routeOrthogonalConnector(start, end, routing.obstacles, {
|
|
14957
|
+
canvasWidth: routing.canvasWidth,
|
|
14958
|
+
canvasHeight: routing.canvasHeight,
|
|
14959
|
+
});
|
|
14960
|
+
if (waypoints.length > 2) {
|
|
14961
|
+
const local = waypoints.map((p) => ({ x: p.x - element.x, y: p.y - element.y }));
|
|
14962
|
+
pathD = waypointsToPathD(local);
|
|
14963
|
+
}
|
|
14964
|
+
}
|
|
13007
14965
|
const markerSeed = element.id.replace(/[^a-zA-Z0-9_-]/gu, '_');
|
|
13008
14966
|
const startMarkerId = `${markerSeed}-start`;
|
|
13009
14967
|
const endMarkerId = `${markerSeed}-end`;
|
|
@@ -13162,6 +15120,200 @@ function buildWrapperStyle(element, zIndex) {
|
|
|
13162
15120
|
return parts.join(';');
|
|
13163
15121
|
}
|
|
13164
15122
|
|
|
15123
|
+
/**
|
|
15124
|
+
* Connector text overlay — Angular port of the Vue `ConnectorTextOverlay.vue`
|
|
15125
|
+
* (packages/vue/src/viewer/components/ConnectorTextOverlay.vue).
|
|
15126
|
+
*
|
|
15127
|
+
* Renders a connector's label text centred over the connector's bounding box.
|
|
15128
|
+
* PowerPoint allows authors to attach a text run to a connector element
|
|
15129
|
+
* (`<p:cxnSp>` with a non-empty `<p:txBody>`); the label is painted on top of
|
|
15130
|
+
* the connector path, centred both horizontally and vertically within the
|
|
15131
|
+
* element's bounding box.
|
|
15132
|
+
*
|
|
15133
|
+
* The overlay is an absolutely-positioned flex container rendered as a sibling
|
|
15134
|
+
* of the SVG connector — NOT part of the SVG — so per-segment rich text
|
|
15135
|
+
* renders with standard HTML text layout. It is `pointer-events: none` and
|
|
15136
|
+
* never intercepts selection or hit-testing on the connector beneath it.
|
|
15137
|
+
*
|
|
15138
|
+
* ### Pure label-geometry helper
|
|
15139
|
+
* All positioning / style math lives in the exported pure functions
|
|
15140
|
+
* `buildOverlayContainerStyle`, `buildOverlayBlockStyle`, and `buildSegmentStyle`
|
|
15141
|
+
* so they can be unit-tested without TestBed (see
|
|
15142
|
+
* `connector-text-overlay.component.test.ts`).
|
|
15143
|
+
*/
|
|
15144
|
+
// ---------------------------------------------------------------------------
|
|
15145
|
+
// Pure exported helpers (tested independently, no Angular dependency)
|
|
15146
|
+
// ---------------------------------------------------------------------------
|
|
15147
|
+
/**
|
|
15148
|
+
* Inline style string for the outer overlay container.
|
|
15149
|
+
*
|
|
15150
|
+
* The container fills the connector bounding box, uses flexbox to centre
|
|
15151
|
+
* content, and is `pointer-events: none`.
|
|
15152
|
+
*
|
|
15153
|
+
* @param align - OOXML paragraph alignment token. Variants `justLow`, `dist`,
|
|
15154
|
+
* and `thaiDist` collapse to `justify`; every other value passes through;
|
|
15155
|
+
* absent → `center` (connector-label convention).
|
|
15156
|
+
*/
|
|
15157
|
+
function buildOverlayContainerStyle(align) {
|
|
15158
|
+
let textAlign;
|
|
15159
|
+
if (align === 'justLow' || align === 'dist' || align === 'thaiDist') {
|
|
15160
|
+
textAlign = 'justify';
|
|
15161
|
+
}
|
|
15162
|
+
else if (align !== undefined) {
|
|
15163
|
+
textAlign = align;
|
|
15164
|
+
}
|
|
15165
|
+
else {
|
|
15166
|
+
textAlign = 'center';
|
|
15167
|
+
}
|
|
15168
|
+
return [
|
|
15169
|
+
'position:absolute',
|
|
15170
|
+
'inset:0',
|
|
15171
|
+
'display:flex',
|
|
15172
|
+
'align-items:center',
|
|
15173
|
+
'justify-content:center',
|
|
15174
|
+
'overflow:hidden',
|
|
15175
|
+
'pointer-events:none',
|
|
15176
|
+
`text-align:${textAlign}`,
|
|
15177
|
+
].join(';');
|
|
15178
|
+
}
|
|
15179
|
+
/**
|
|
15180
|
+
* Inline style string for the inner text-block `<div>`.
|
|
15181
|
+
*
|
|
15182
|
+
* Applies paragraph-level defaults: font family, size, colour, weight, style,
|
|
15183
|
+
* and decoration from `textStyle`, with sensible fall-backs for connector
|
|
15184
|
+
* labels (10pt, black, normal).
|
|
15185
|
+
*/
|
|
15186
|
+
function buildOverlayBlockStyle(textStyle) {
|
|
15187
|
+
const ts = textStyle;
|
|
15188
|
+
const parts = [
|
|
15189
|
+
`font-family:${ts?.fontFamily ?? 'inherit'}`,
|
|
15190
|
+
`font-size:${ts?.fontSize !== undefined ? `${ts.fontSize}pt` : '10pt'}`,
|
|
15191
|
+
`color:${ts?.color ?? '#000000'}`,
|
|
15192
|
+
`font-weight:${ts?.bold ? 'bold' : 'normal'}`,
|
|
15193
|
+
`font-style:${ts?.italic ? 'italic' : 'normal'}`,
|
|
15194
|
+
`text-decoration:${ts?.underline ? 'underline' : 'none'}`,
|
|
15195
|
+
'padding:0 4px',
|
|
15196
|
+
'white-space:pre-wrap',
|
|
15197
|
+
'line-height:1.2',
|
|
15198
|
+
'max-width:100%',
|
|
15199
|
+
];
|
|
15200
|
+
return parts.join(';');
|
|
15201
|
+
}
|
|
15202
|
+
/**
|
|
15203
|
+
* Inline style string for a single text-run `<span>`.
|
|
15204
|
+
*
|
|
15205
|
+
* Run-level properties override paragraph-level properties where both are
|
|
15206
|
+
* present. Falls back to `textStyle` (paragraph defaults) for each property.
|
|
15207
|
+
*/
|
|
15208
|
+
function buildSegmentStyle(segment, textStyle) {
|
|
15209
|
+
const s = segment.style;
|
|
15210
|
+
const ts = textStyle;
|
|
15211
|
+
const parts = [
|
|
15212
|
+
`font-family:${s?.fontFamily ?? ts?.fontFamily ?? 'inherit'}`,
|
|
15213
|
+
`color:${s?.color ?? ts?.color ?? '#000000'}`,
|
|
15214
|
+
`font-weight:${s?.bold ? 'bold' : ts?.bold ? 'bold' : 'normal'}`,
|
|
15215
|
+
`font-style:${s?.italic ? 'italic' : ts?.italic ? 'italic' : 'normal'}`,
|
|
15216
|
+
`text-decoration:${s?.underline ? 'underline' : 'none'}`,
|
|
15217
|
+
];
|
|
15218
|
+
if (s?.fontSize !== undefined) {
|
|
15219
|
+
parts.push(`font-size:${s.fontSize}pt`);
|
|
15220
|
+
}
|
|
15221
|
+
return parts.join(';');
|
|
15222
|
+
}
|
|
15223
|
+
// ---------------------------------------------------------------------------
|
|
15224
|
+
// Component
|
|
15225
|
+
// ---------------------------------------------------------------------------
|
|
15226
|
+
/**
|
|
15227
|
+
* `<pptx-connector-text-overlay>` — renders a connector's text label.
|
|
15228
|
+
*
|
|
15229
|
+
* **Usage** (inside `ConnectorRendererComponent`'s host wrapper `<div>`):
|
|
15230
|
+
* ```html
|
|
15231
|
+
* @if (hasLabel()) {
|
|
15232
|
+
* <pptx-connector-text-overlay
|
|
15233
|
+
* [text]="element().text"
|
|
15234
|
+
* [segments]="element().textSegments"
|
|
15235
|
+
* [textStyle]="element().textStyle"
|
|
15236
|
+
* />
|
|
15237
|
+
* }
|
|
15238
|
+
* ```
|
|
15239
|
+
*
|
|
15240
|
+
* All inputs are optional; the component renders nothing when `text` is falsy
|
|
15241
|
+
* or `segments` is empty.
|
|
15242
|
+
*/
|
|
15243
|
+
class ConnectorTextOverlayComponent {
|
|
15244
|
+
/**
|
|
15245
|
+
* Trimmed plain-text label. When falsy the overlay is not rendered.
|
|
15246
|
+
* This is the `text` property from `ConnectorPptxElement`.
|
|
15247
|
+
*/
|
|
15248
|
+
text = input(undefined, /* @ts-ignore */
|
|
15249
|
+
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
15250
|
+
/**
|
|
15251
|
+
* Per-run rich-text segments from `ConnectorPptxElement.textSegments`.
|
|
15252
|
+
* When absent or empty the overlay is not rendered.
|
|
15253
|
+
*/
|
|
15254
|
+
segments = input(undefined, /* @ts-ignore */
|
|
15255
|
+
...(ngDevMode ? [{ debugName: "segments" }] : /* istanbul ignore next */ []));
|
|
15256
|
+
/**
|
|
15257
|
+
* Paragraph-level text style from `ConnectorPptxElement.textStyle`.
|
|
15258
|
+
* Controls alignment, default font, colour, etc.
|
|
15259
|
+
*/
|
|
15260
|
+
textStyle = input(undefined, /* @ts-ignore */
|
|
15261
|
+
...(ngDevMode ? [{ debugName: "textStyle" }] : /* istanbul ignore next */ []));
|
|
15262
|
+
// -----------------------------------------------------------------------
|
|
15263
|
+
// Derived signals
|
|
15264
|
+
// -----------------------------------------------------------------------
|
|
15265
|
+
/** True when there is a non-empty label to display. */
|
|
15266
|
+
hasText = computed(() => Boolean(this.text()) && this.segments() !== undefined && (this.segments()?.length ?? 0) > 0, /* @ts-ignore */
|
|
15267
|
+
...(ngDevMode ? [{ debugName: "hasText" }] : /* istanbul ignore next */ []));
|
|
15268
|
+
/** Inline style for the outer flex container. */
|
|
15269
|
+
containerStyle = computed(() => buildOverlayContainerStyle(this.textStyle()?.align), /* @ts-ignore */
|
|
15270
|
+
...(ngDevMode ? [{ debugName: "containerStyle" }] : /* istanbul ignore next */ []));
|
|
15271
|
+
/** Inline style for the inner paragraph block. */
|
|
15272
|
+
blockStyle = computed(() => buildOverlayBlockStyle(this.textStyle()), /* @ts-ignore */
|
|
15273
|
+
...(ngDevMode ? [{ debugName: "blockStyle" }] : /* istanbul ignore next */ []));
|
|
15274
|
+
/** Pre-computed segments with their individual inline style strings. */
|
|
15275
|
+
styledSegments = computed(() => {
|
|
15276
|
+
const segs = this.segments() ?? [];
|
|
15277
|
+
const ts = this.textStyle();
|
|
15278
|
+
return segs
|
|
15279
|
+
.filter((s) => !s.isParagraphBreak)
|
|
15280
|
+
.map((s) => ({ text: s.text, style: buildSegmentStyle(s, ts) }));
|
|
15281
|
+
}, /* @ts-ignore */
|
|
15282
|
+
...(ngDevMode ? [{ debugName: "styledSegments" }] : /* istanbul ignore next */ []));
|
|
15283
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ConnectorTextOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
15284
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: ConnectorTextOverlayComponent, isStandalone: true, selector: "pptx-connector-text-overlay", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, segments: { classPropertyName: "segments", publicName: "segments", isSignal: true, isRequired: false, transformFunction: null }, textStyle: { classPropertyName: "textStyle", publicName: "textStyle", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
15285
|
+
@if (hasText()) {
|
|
15286
|
+
<div class="pptx-ng-connector-text" [style]="containerStyle()">
|
|
15287
|
+
<div class="pptx-ng-connector-text__block" [style]="blockStyle()">
|
|
15288
|
+
@for (seg of styledSegments(); track $index) {
|
|
15289
|
+
<span class="pptx-ng-connector-text__run" [style]="seg.style">{{ seg.text }}</span>
|
|
15290
|
+
}
|
|
15291
|
+
</div>
|
|
15292
|
+
</div>
|
|
15293
|
+
}
|
|
15294
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15295
|
+
}
|
|
15296
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ConnectorTextOverlayComponent, decorators: [{
|
|
15297
|
+
type: Component,
|
|
15298
|
+
args: [{
|
|
15299
|
+
selector: 'pptx-connector-text-overlay',
|
|
15300
|
+
standalone: true,
|
|
15301
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
15302
|
+
imports: [],
|
|
15303
|
+
template: `
|
|
15304
|
+
@if (hasText()) {
|
|
15305
|
+
<div class="pptx-ng-connector-text" [style]="containerStyle()">
|
|
15306
|
+
<div class="pptx-ng-connector-text__block" [style]="blockStyle()">
|
|
15307
|
+
@for (seg of styledSegments(); track $index) {
|
|
15308
|
+
<span class="pptx-ng-connector-text__run" [style]="seg.style">{{ seg.text }}</span>
|
|
15309
|
+
}
|
|
15310
|
+
</div>
|
|
15311
|
+
</div>
|
|
15312
|
+
}
|
|
15313
|
+
`,
|
|
15314
|
+
}]
|
|
15315
|
+
}], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], segments: [{ type: i0.Input, args: [{ isSignal: true, alias: "segments", required: false }] }], textStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "textStyle", required: false }] }] } });
|
|
15316
|
+
|
|
13165
15317
|
/**
|
|
13166
15318
|
* ConnectorRendererComponent — Angular port of the Vue `ConnectorRenderer.vue`
|
|
13167
15319
|
* (and the React `ConnectorElementRenderer`, basic subset).
|
|
@@ -13171,25 +15323,53 @@ function buildWrapperStyle(element, zIndex) {
|
|
|
13171
15323
|
* is baked into the endpoints (not a CSS transform) so arrowheads point the
|
|
13172
15324
|
* right way.
|
|
13173
15325
|
*
|
|
13174
|
-
*
|
|
13175
|
-
*
|
|
15326
|
+
* Renders straight (`<line>`), bent (elbow) and curved (Bézier) connectors via
|
|
15327
|
+
* `connector-path.ts`. When obstacle rects are supplied (`obstacles` input,
|
|
15328
|
+
* absolute slide coords), bent connectors are routed around them with an A*
|
|
15329
|
+
* orthogonal router. A connector's optional text label is painted on top via
|
|
15330
|
+
* `ConnectorTextOverlayComponent`.
|
|
15331
|
+
*
|
|
15332
|
+
* All path/style math lives in `connector-path.ts` / `connector-routing.ts`
|
|
15333
|
+
* (pure TS, no Angular dependency) so it can be unit-tested without TestBed.
|
|
13176
15334
|
*
|
|
13177
|
-
* Not yet ported (TODO, see PORTING.md):
|
|
13178
|
-
*
|
|
13179
|
-
* shadows/glow. Bent/curved connectors currently fall back to a straight line.
|
|
15335
|
+
* Not yet ported (TODO, see PORTING.md): compound (double/triple) lines, line
|
|
15336
|
+
* shadows/glow.
|
|
13180
15337
|
*/
|
|
13181
15338
|
class ConnectorRendererComponent {
|
|
13182
15339
|
element = input.required(/* @ts-ignore */
|
|
13183
15340
|
...(ngDevMode ? [{ debugName: "element" }] : /* istanbul ignore next */ []));
|
|
13184
15341
|
zIndex = input(0, /* @ts-ignore */
|
|
13185
15342
|
...(ngDevMode ? [{ debugName: "zIndex" }] : /* istanbul ignore next */ []));
|
|
15343
|
+
/** Obstacle rects (absolute slide coords) for A* routing of bent connectors. */
|
|
15344
|
+
obstacles = input([], /* @ts-ignore */
|
|
15345
|
+
...(ngDevMode ? [{ debugName: "obstacles" }] : /* istanbul ignore next */ []));
|
|
15346
|
+
canvasWidth = input(0, /* @ts-ignore */
|
|
15347
|
+
...(ngDevMode ? [{ debugName: "canvasWidth" }] : /* istanbul ignore next */ []));
|
|
15348
|
+
canvasHeight = input(0, /* @ts-ignore */
|
|
15349
|
+
...(ngDevMode ? [{ debugName: "canvasHeight" }] : /* istanbul ignore next */ []));
|
|
13186
15350
|
/** All derived geometry, recomputed on every input change. */
|
|
13187
|
-
geo = computed(() =>
|
|
15351
|
+
geo = computed(() => {
|
|
15352
|
+
const obstacles = this.obstacles();
|
|
15353
|
+
const routing = obstacles.length > 0
|
|
15354
|
+
? { obstacles, canvasWidth: this.canvasWidth(), canvasHeight: this.canvasHeight() }
|
|
15355
|
+
: undefined;
|
|
15356
|
+
return buildConnectorGeometry(this.element(), this.zIndex(), routing);
|
|
15357
|
+
}, /* @ts-ignore */
|
|
13188
15358
|
...(ngDevMode ? [{ debugName: "geo" }] : /* istanbul ignore next */ []));
|
|
13189
15359
|
viewBox = computed(() => `0 0 ${this.geo().svgW} ${this.geo().svgH}`, /* @ts-ignore */
|
|
13190
15360
|
...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
|
|
15361
|
+
// Connectors carry an optional text label (PptxTextProperties). Narrow the
|
|
15362
|
+
// union once here so the template can bind the overlay inputs.
|
|
15363
|
+
textProps = computed(() => this.element(), /* @ts-ignore */
|
|
15364
|
+
...(ngDevMode ? [{ debugName: "textProps" }] : /* istanbul ignore next */ []));
|
|
15365
|
+
connectorText = computed(() => this.textProps().text, /* @ts-ignore */
|
|
15366
|
+
...(ngDevMode ? [{ debugName: "connectorText" }] : /* istanbul ignore next */ []));
|
|
15367
|
+
connectorSegments = computed(() => this.textProps().textSegments, /* @ts-ignore */
|
|
15368
|
+
...(ngDevMode ? [{ debugName: "connectorSegments" }] : /* istanbul ignore next */ []));
|
|
15369
|
+
connectorTextStyle = computed(() => this.textProps().textStyle, /* @ts-ignore */
|
|
15370
|
+
...(ngDevMode ? [{ debugName: "connectorTextStyle" }] : /* istanbul ignore next */ []));
|
|
13191
15371
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ConnectorRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13192
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: ConnectorRendererComponent, isStandalone: true, selector: "pptx-connector-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
15372
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: ConnectorRendererComponent, isStandalone: true, selector: "pptx-connector-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, obstacles: { classPropertyName: "obstacles", publicName: "obstacles", isSignal: true, isRequired: false, transformFunction: null }, canvasWidth: { classPropertyName: "canvasWidth", publicName: "canvasWidth", isSignal: true, isRequired: false, transformFunction: null }, canvasHeight: { classPropertyName: "canvasHeight", publicName: "canvasHeight", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
13193
15373
|
<div
|
|
13194
15374
|
class="pptx-ng-element pptx-ng-connector"
|
|
13195
15375
|
[style]="geo().wrapperStyle"
|
|
@@ -13268,8 +15448,13 @@ class ConnectorRendererComponent {
|
|
|
13268
15448
|
/>
|
|
13269
15449
|
}
|
|
13270
15450
|
</svg>
|
|
15451
|
+
<pptx-connector-text-overlay
|
|
15452
|
+
[text]="connectorText()"
|
|
15453
|
+
[segments]="connectorSegments()"
|
|
15454
|
+
[textStyle]="connectorTextStyle()"
|
|
15455
|
+
/>
|
|
13271
15456
|
</div>
|
|
13272
|
-
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15457
|
+
`, isInline: true, dependencies: [{ kind: "component", type: ConnectorTextOverlayComponent, selector: "pptx-connector-text-overlay", inputs: ["text", "segments", "textStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13273
15458
|
}
|
|
13274
15459
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ConnectorRendererComponent, decorators: [{
|
|
13275
15460
|
type: Component,
|
|
@@ -13277,7 +15462,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
13277
15462
|
selector: 'pptx-connector-renderer',
|
|
13278
15463
|
standalone: true,
|
|
13279
15464
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
13280
|
-
imports: [],
|
|
15465
|
+
imports: [ConnectorTextOverlayComponent],
|
|
13281
15466
|
template: `
|
|
13282
15467
|
<div
|
|
13283
15468
|
class="pptx-ng-element pptx-ng-connector"
|
|
@@ -13357,10 +15542,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
13357
15542
|
/>
|
|
13358
15543
|
}
|
|
13359
15544
|
</svg>
|
|
15545
|
+
<pptx-connector-text-overlay
|
|
15546
|
+
[text]="connectorText()"
|
|
15547
|
+
[segments]="connectorSegments()"
|
|
15548
|
+
[textStyle]="connectorTextStyle()"
|
|
15549
|
+
/>
|
|
13360
15550
|
</div>
|
|
13361
15551
|
`,
|
|
13362
15552
|
}]
|
|
13363
|
-
}], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }] } });
|
|
15553
|
+
}], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }], obstacles: [{ type: i0.Input, args: [{ isSignal: true, alias: "obstacles", required: false }] }], canvasWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "canvasWidth", required: false }] }], canvasHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "canvasHeight", required: false }] }] } });
|
|
13364
15554
|
|
|
13365
15555
|
/**
|
|
13366
15556
|
* Scalar viewer defaults — shared across the React, Vue, and Angular bindings
|
|
@@ -18000,6 +20190,13 @@ class ElementRendererComponent {
|
|
|
18000
20190
|
...(ngDevMode ? [{ debugName: "mediaDataUrls" }] : /* istanbul ignore next */ []));
|
|
18001
20191
|
zIndex = input(0, /* @ts-ignore */
|
|
18002
20192
|
...(ngDevMode ? [{ debugName: "zIndex" }] : /* istanbul ignore next */ []));
|
|
20193
|
+
/** Obstacle rects (absolute slide coords) for connector A* routing. */
|
|
20194
|
+
obstacles = input([], /* @ts-ignore */
|
|
20195
|
+
...(ngDevMode ? [{ debugName: "obstacles" }] : /* istanbul ignore next */ []));
|
|
20196
|
+
canvasWidth = input(0, /* @ts-ignore */
|
|
20197
|
+
...(ngDevMode ? [{ debugName: "canvasWidth" }] : /* istanbul ignore next */ []));
|
|
20198
|
+
canvasHeight = input(0, /* @ts-ignore */
|
|
20199
|
+
...(ngDevMode ? [{ debugName: "canvasHeight" }] : /* istanbul ignore next */ []));
|
|
18003
20200
|
containerStyle = computed(() => getContainerStyle(this.element(), this.zIndex()), /* @ts-ignore */
|
|
18004
20201
|
...(ngDevMode ? [{ debugName: "containerStyle" }] : /* istanbul ignore next */ []));
|
|
18005
20202
|
shapeContainerStyle = computed(() => ({
|
|
@@ -18119,10 +20316,16 @@ class ElementRendererComponent {
|
|
|
18119
20316
|
return style;
|
|
18120
20317
|
}
|
|
18121
20318
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ElementRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
18122
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: ElementRendererComponent, isStandalone: true, selector: "pptx-element-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, mediaDataUrls: { classPropertyName: "mediaDataUrls", publicName: "mediaDataUrls", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
20319
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: ElementRendererComponent, isStandalone: true, selector: "pptx-element-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, mediaDataUrls: { classPropertyName: "mediaDataUrls", publicName: "mediaDataUrls", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, obstacles: { classPropertyName: "obstacles", publicName: "obstacles", isSignal: true, isRequired: false, transformFunction: null }, canvasWidth: { classPropertyName: "canvasWidth", publicName: "canvasWidth", isSignal: true, isRequired: false, transformFunction: null }, canvasHeight: { classPropertyName: "canvasHeight", publicName: "canvasHeight", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
18123
20320
|
@switch (true) {
|
|
18124
20321
|
@case (element().type === 'connector') {
|
|
18125
|
-
<pptx-connector-renderer
|
|
20322
|
+
<pptx-connector-renderer
|
|
20323
|
+
[element]="element()"
|
|
20324
|
+
[zIndex]="zIndex()"
|
|
20325
|
+
[obstacles]="obstacles()"
|
|
20326
|
+
[canvasWidth]="canvasWidth()"
|
|
20327
|
+
[canvasHeight]="canvasHeight()"
|
|
20328
|
+
/>
|
|
18126
20329
|
}
|
|
18127
20330
|
@case (element().type === 'ink') {
|
|
18128
20331
|
<pptx-ink-renderer
|
|
@@ -18277,7 +20480,7 @@ class ElementRendererComponent {
|
|
|
18277
20480
|
</div>
|
|
18278
20481
|
}
|
|
18279
20482
|
}
|
|
18280
|
-
`, isInline: true, dependencies: [{ kind: "component", type: ElementRendererComponent, selector: "pptx-element-renderer", inputs: ["element", "mediaDataUrls", "zIndex"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: ConnectorRendererComponent, selector: "pptx-connector-renderer", inputs: ["element", "zIndex"] }, { kind: "component", type: TableRendererComponent, selector: "pptx-table-renderer", inputs: ["element"] }, { kind: "component", type: ChartRendererComponent, selector: "pptx-chart-renderer", inputs: ["element"] }, { kind: "component", type: SmartArtRendererComponent, selector: "pptx-smart-art-renderer", inputs: ["element", "zIndex"] }, { kind: "component", type: InkRendererComponent, selector: "pptx-ink-renderer", inputs: ["element", "zIndex", "mediaDataUrls"] }, { kind: "component", type: OleRendererComponent, selector: "pptx-ole-renderer", inputs: ["element", "zIndex"] }, { kind: "component", type: Model3DRendererComponent, selector: "pptx-model3d-renderer", inputs: ["element", "zIndex", "mediaDataUrls"] }, { kind: "component", type: ZoomRendererComponent, selector: "pptx-zoom-renderer", inputs: ["element", "zIndex", "mediaDataUrls"] }, { kind: "component", type: EquationRendererComponent, selector: "pptx-equation-renderer", inputs: ["equationXml", "equationNumber"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
20483
|
+
`, isInline: true, dependencies: [{ kind: "component", type: ElementRendererComponent, selector: "pptx-element-renderer", inputs: ["element", "mediaDataUrls", "zIndex", "obstacles", "canvasWidth", "canvasHeight"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: ConnectorRendererComponent, selector: "pptx-connector-renderer", inputs: ["element", "zIndex", "obstacles", "canvasWidth", "canvasHeight"] }, { kind: "component", type: TableRendererComponent, selector: "pptx-table-renderer", inputs: ["element"] }, { kind: "component", type: ChartRendererComponent, selector: "pptx-chart-renderer", inputs: ["element"] }, { kind: "component", type: SmartArtRendererComponent, selector: "pptx-smart-art-renderer", inputs: ["element", "zIndex"] }, { kind: "component", type: InkRendererComponent, selector: "pptx-ink-renderer", inputs: ["element", "zIndex", "mediaDataUrls"] }, { kind: "component", type: OleRendererComponent, selector: "pptx-ole-renderer", inputs: ["element", "zIndex"] }, { kind: "component", type: Model3DRendererComponent, selector: "pptx-model3d-renderer", inputs: ["element", "zIndex", "mediaDataUrls"] }, { kind: "component", type: ZoomRendererComponent, selector: "pptx-zoom-renderer", inputs: ["element", "zIndex", "mediaDataUrls"] }, { kind: "component", type: EquationRendererComponent, selector: "pptx-equation-renderer", inputs: ["equationXml", "equationNumber"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
18281
20484
|
}
|
|
18282
20485
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ElementRendererComponent, decorators: [{
|
|
18283
20486
|
type: Component,
|
|
@@ -18300,7 +20503,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
18300
20503
|
template: `
|
|
18301
20504
|
@switch (true) {
|
|
18302
20505
|
@case (element().type === 'connector') {
|
|
18303
|
-
<pptx-connector-renderer
|
|
20506
|
+
<pptx-connector-renderer
|
|
20507
|
+
[element]="element()"
|
|
20508
|
+
[zIndex]="zIndex()"
|
|
20509
|
+
[obstacles]="obstacles()"
|
|
20510
|
+
[canvasWidth]="canvasWidth()"
|
|
20511
|
+
[canvasHeight]="canvasHeight()"
|
|
20512
|
+
/>
|
|
18304
20513
|
}
|
|
18305
20514
|
@case (element().type === 'ink') {
|
|
18306
20515
|
<pptx-ink-renderer
|
|
@@ -18457,7 +20666,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
18457
20666
|
}
|
|
18458
20667
|
`,
|
|
18459
20668
|
}]
|
|
18460
|
-
}], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], mediaDataUrls: [{ type: i0.Input, args: [{ isSignal: true, alias: "mediaDataUrls", required: false }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }] } });
|
|
20669
|
+
}], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], mediaDataUrls: [{ type: i0.Input, args: [{ isSignal: true, alias: "mediaDataUrls", required: false }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }], obstacles: [{ type: i0.Input, args: [{ isSignal: true, alias: "obstacles", required: false }] }], canvasWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "canvasWidth", required: false }] }], canvasHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "canvasHeight", required: false }] }] } });
|
|
18461
20670
|
|
|
18462
20671
|
/** Default slide stage colour when a slide carries no usable background. */
|
|
18463
20672
|
const DEFAULT_SLIDE_BACKGROUND = '#ffffff';
|
|
@@ -18794,6 +21003,15 @@ class SlideCanvasComponent {
|
|
|
18794
21003
|
}
|
|
18795
21004
|
elements = computed(() => this.slide()?.elements ?? [], /* @ts-ignore */
|
|
18796
21005
|
...(ngDevMode ? [{ debugName: "elements" }] : /* istanbul ignore next */ []));
|
|
21006
|
+
/**
|
|
21007
|
+
* Obstacle rects (absolute slide coords) for connector A* routing: every
|
|
21008
|
+
* non-connector element with a positive footprint. Bent connectors detour
|
|
21009
|
+
* around these instead of cutting straight through neighbouring shapes.
|
|
21010
|
+
*/
|
|
21011
|
+
connectorObstacles = computed(() => this.elements()
|
|
21012
|
+
.filter((e) => e.type !== 'connector' && e.width > 0 && e.height > 0)
|
|
21013
|
+
.map((e) => ({ x: e.x, y: e.y, width: e.width, height: e.height })), /* @ts-ignore */
|
|
21014
|
+
...(ngDevMode ? [{ debugName: "connectorObstacles" }] : /* istanbul ignore next */ []));
|
|
18797
21015
|
/** Bounding boxes (stage coords) for the selected elements. */
|
|
18798
21016
|
selectionBoxes = computed(() => {
|
|
18799
21017
|
const selected = new Set(this.selectedIds());
|
|
@@ -19158,6 +21376,9 @@ class SlideCanvasComponent {
|
|
|
19158
21376
|
[element]="element"
|
|
19159
21377
|
[mediaDataUrls]="mediaDataUrls()"
|
|
19160
21378
|
[zIndex]="i"
|
|
21379
|
+
[obstacles]="connectorObstacles()"
|
|
21380
|
+
[canvasWidth]="canvasSize().width"
|
|
21381
|
+
[canvasHeight]="canvasSize().height"
|
|
19161
21382
|
/>
|
|
19162
21383
|
}
|
|
19163
21384
|
@for (box of selectionBoxes(); track box.id) {
|
|
@@ -19224,7 +21445,7 @@ class SlideCanvasComponent {
|
|
|
19224
21445
|
</div>
|
|
19225
21446
|
</div>
|
|
19226
21447
|
</div>
|
|
19227
|
-
`, isInline: true, styles: [".pptx-ng-canvas-stage.is-editable{touch-action:none}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: ElementRendererComponent, selector: "pptx-element-renderer", inputs: ["element", "mediaDataUrls", "zIndex"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
21448
|
+
`, isInline: true, styles: [".pptx-ng-canvas-stage.is-editable{touch-action:none}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: ElementRendererComponent, selector: "pptx-element-renderer", inputs: ["element", "mediaDataUrls", "zIndex", "obstacles", "canvasWidth", "canvasHeight"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
19228
21449
|
}
|
|
19229
21450
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SlideCanvasComponent, decorators: [{
|
|
19230
21451
|
type: Component,
|
|
@@ -19247,6 +21468,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
19247
21468
|
[element]="element"
|
|
19248
21469
|
[mediaDataUrls]="mediaDataUrls()"
|
|
19249
21470
|
[zIndex]="i"
|
|
21471
|
+
[obstacles]="connectorObstacles()"
|
|
21472
|
+
[canvasWidth]="canvasSize().width"
|
|
21473
|
+
[canvasHeight]="canvasSize().height"
|
|
19250
21474
|
/>
|
|
19251
21475
|
}
|
|
19252
21476
|
@for (box of selectionBoxes(); track box.id) {
|
|
@@ -26030,5 +28254,5 @@ function cn(...values) {
|
|
|
26030
28254
|
* Generated bundle index. Do not edit.
|
|
26031
28255
|
*/
|
|
26032
28256
|
|
|
26033
|
-
export { AccessibilityPanelComponent, AccessibilityService, AnimationPanelComponent, AnimationPlaybackService, BroadcastDialogComponent, CURSOR_PALETTE, ChartRendererComponent, CollaborationCursorsComponent, CollaborationService, CommentsPanelComponent, CommentsService, ConnectorRendererComponent, DEFAULT_BROADCAST_SERVER_URL, DEFAULT_CANVAS_HEIGHT, DEFAULT_CANVAS_WIDTH, DEFAULT_FILL_COLOR, DEFAULT_PRINT_SETTINGS, DEFAULT_SLIDE_BACKGROUND, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_COLOR$1 as DEFAULT_TEXT_COLOR, EMBEDDED_FONTS_STYLE_ID, EditorContextMenuComponent, EditorHistory, EditorStateService, EditorToolbarComponent, ElementRendererComponent, EmbeddedFontsService, EquationRendererComponent, ExportService, FindBarComponent, HANDOUT_OPTIONS, HyperlinkDialogComponent, InkRendererComponent, InspectorPanelComponent, LoadContentService, ModalDialogComponent, Model3DRendererComponent, OleRendererComponent, PowerPointViewerComponent, PresentationOverlayComponent, PresentationTransitionOverlayComponent, PresenterViewComponent, PrintDialogComponent, PrintService, PrintSettingsPanelComponent, PropertiesDialogComponent, RESIZE_HANDLES, SEVERITY_GROUPS, SEVERITY_LABELS, SLIDE_TRANSITION_KEYFRAMES, ShareDialogComponent, SignaturesPanelComponent, SignaturesService, SlideCanvasComponent, SlideSorterOverlayComponent, SlidesPanelComponent, SmartArtRendererComponent, TYPE_LABELS$1 as TYPE_LABELS, TableRendererComponent, VIEWER_THEME, ZoomRendererComponent, addCommentToList, advanceStep, applyMove, applyResize, assignUserColor, bringForward, bringToFront, buildBroadcastConfig, buildBroadcastViewerUrl, buildClearHyperlinkPatch, buildClickGroups, buildCollaborationConfig, buildCssGradientFromShapeStyle, buildEmbeddedFontStyles, buildFontFaceRule, buildHyperlinkPatch, buildPatternFillCss, buildPrintDocument, buildPropertiesPatch, buildShareUrl, bulletIndentPx, canStartBroadcast, canStartShare, canUseClipboard, clampCursorPosition, clampNotesFontSize, clampStep, cn, collectAccessibilityIssues, collectElementText, collectSlideText, computeHandoutLayout, computePageCount, computeSlideIndices, computeTimerProgress, convertOmmlToMathMl, countAccessibilityIssues, defaultCssVars, defaultRadius, defaultThemeColors, deleteElementsByIds, derivePresenceList, duplicateElementById, durationOf, estimatePageCount, fontMimeForFormat, formatAutoNumber, formatCursorLabel, formatElapsed, formatPropertyDate, formatTime, generateBroadcastRoomId, generateCommentId, getContainerStyle, getImageSrc, getPatternSvg, getResolvedShapeClipPath, getResolvedShapeClipPathFor, getShapeFillStrokeStyle, getSlideBackgroundStyle, getSlideTransitionAnimations, getTextBlockStyle, groupIssuesBySeverity, hasExistingLink, headerLabel, isInjectableUrl, isPpactionUrl, isSigned, isUrlSafe, isValidRoomId, issueTrackKey, issueTypeLabel, moveElementBy, normalizeFontFormat, normalizeSlidesPerPage, ommlToMathml, overallStatus, pendingElementStyles, presenceToCursors, provideViewerTheme, removeCommentFromList, renderToCanvas, resizeElement, resolveFontVariant, resolveHyperlinkHref, resolveParagraphBullet, resolvePresenterNotes, resolveTransitionDuration, revealedElementStyles, sanitizeColor, sanitizeSlideIndex, sanitizeUserName, searchSlides, seedBroadcastFields, seedHyperlinkDraft, seedPropertiesDraft, seedShareFields, sendBackward, sendToBack, setElementPosition, signatureCountLabel, signatureKey, signatureTimestamp, signerName, statusKind, statusLabel, themeStyle, themeToCssVars, toggleCommentResolvedInList, updateElementById, validatePrintSettings, validateRoomId, worstStatus };
|
|
28257
|
+
export { AccessibilityPanelComponent, AccessibilityService, AnimationPanelComponent, AnimationPlaybackService, BroadcastDialogComponent, CURSOR_PALETTE, ChartRendererComponent, CollaborationCursorsComponent, CollaborationService, CommentsPanelComponent, CommentsService, ConnectorRendererComponent, ConnectorTextOverlayComponent, DEFAULT_BROADCAST_SERVER_URL, DEFAULT_CANVAS_HEIGHT, DEFAULT_CANVAS_WIDTH, DEFAULT_FILL_COLOR, DEFAULT_PRINT_SETTINGS, DEFAULT_SLIDE_BACKGROUND, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_COLOR$1 as DEFAULT_TEXT_COLOR, EMBEDDED_FONTS_STYLE_ID, EditorContextMenuComponent, EditorHistory, EditorStateService, EditorToolbarComponent, ElementRendererComponent, EmbeddedFontsService, EquationRendererComponent, ExportService, FindBarComponent, HANDOUT_OPTIONS, HyperlinkDialogComponent, InkRendererComponent, InspectorPanelComponent, LoadContentService, ModalDialogComponent, Model3DRendererComponent, OleRendererComponent, PowerPointViewerComponent, PresentationOverlayComponent, PresentationTransitionOverlayComponent, PresenterViewComponent, PrintDialogComponent, PrintService, PrintSettingsPanelComponent, PropertiesDialogComponent, RESIZE_HANDLES, SEVERITY_GROUPS, SEVERITY_LABELS, SLIDE_TRANSITION_KEYFRAMES, ShareDialogComponent, SignaturesPanelComponent, SignaturesService, SlideCanvasComponent, SlideSorterOverlayComponent, SlidesPanelComponent, SmartArtRendererComponent, TYPE_LABELS$1 as TYPE_LABELS, TableRendererComponent, VIEWER_THEME, ZoomRendererComponent, addCommentToList, advanceStep, applyMove, applyResize, assignUserColor, bringForward, bringToFront, buildBroadcastConfig, buildBroadcastViewerUrl, buildClearHyperlinkPatch, buildClickGroups, buildCollaborationConfig, buildCssGradientFromShapeStyle, buildEmbeddedFontStyles, buildFontFaceRule, buildHyperlinkPatch, buildPatternFillCss, buildPrintDocument, buildPropertiesPatch, buildShareUrl, bulletIndentPx, canStartBroadcast, canStartShare, canUseClipboard, clampCursorPosition, clampNotesFontSize, clampStep, cn, collectAccessibilityIssues, collectElementText, collectSlideText, computeHandoutLayout, computePageCount, computeSlideIndices, computeTimerProgress, convertOmmlToMathMl, countAccessibilityIssues, defaultCssVars, defaultRadius, defaultThemeColors, deleteElementsByIds, derivePresenceList, duplicateElementById, durationOf, estimatePageCount, fontMimeForFormat, formatAutoNumber, formatCursorLabel, formatElapsed, formatPropertyDate, formatTime, generateBroadcastRoomId, generateCommentId, getContainerStyle, getImageSrc, getPatternSvg, getResolvedShapeClipPath, getResolvedShapeClipPathFor, getShapeFillStrokeStyle, getSlideBackgroundStyle, getSlideTransitionAnimations, getTextBlockStyle, groupIssuesBySeverity, hasExistingLink, headerLabel, isInjectableUrl, isPpactionUrl, isSigned, isUrlSafe, isValidRoomId, issueTrackKey, issueTypeLabel, moveElementBy, normalizeFontFormat, normalizeSlidesPerPage, ommlToMathml, overallStatus, pendingElementStyles, presenceToCursors, provideViewerTheme, removeCommentFromList, renderToCanvas, resizeElement, resolveFontVariant, resolveHyperlinkHref, resolveParagraphBullet, resolvePresenterNotes, resolveTransitionDuration, revealedElementStyles, routeOrthogonalConnector, sanitizeColor, sanitizeSlideIndex, sanitizeUserName, searchSlides, seedBroadcastFields, seedHyperlinkDraft, seedPropertiesDraft, seedShareFields, sendBackward, sendToBack, setElementPosition, signatureCountLabel, signatureKey, signatureTimestamp, signerName, statusKind, statusLabel, themeStyle, themeToCssVars, toggleCommentResolvedInList, updateElementById, validatePrintSettings, validateRoomId, waypointsToPathD, worstStatus };
|
|
26034
28258
|
//# sourceMappingURL=pptx-angular-viewer.mjs.map
|