pptx-angular-viewer 4.24.2 → 4.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +9 -9
- package/ng-package.json +25 -0
- package/package.json +45 -20
- package/scripts/finalize-dist.mjs +24 -0
- package/scripts/inline-shared.mjs +43 -0
- package/tsconfig.lib.json +13 -0
- package/vitest-setup.ts +12 -0
- package/fesm2022/pptx-angular-viewer-bar-chart-3d-hit-test-B6eCwecE.mjs +0 -9855
- package/fesm2022/pptx-angular-viewer-bar-chart-3d-hit-test-B6eCwecE.mjs.map +0 -1
- package/fesm2022/pptx-angular-viewer-chart-3d-view-scene-IqColhvd.mjs +0 -1594
- package/fesm2022/pptx-angular-viewer-chart-3d-view-scene-IqColhvd.mjs.map +0 -1
- package/fesm2022/pptx-angular-viewer-chat-history-idb-7dzD77Ju.mjs +0 -73
- package/fesm2022/pptx-angular-viewer-chat-history-idb-7dzD77Ju.mjs.map +0 -1
- package/fesm2022/pptx-angular-viewer-chat-history-store-Ckeu-olx.mjs +0 -121
- package/fesm2022/pptx-angular-viewer-chat-history-store-Ckeu-olx.mjs.map +0 -1
- package/fesm2022/pptx-angular-viewer-view-overflow-BBD8nZlq.mjs +0 -1418
- package/fesm2022/pptx-angular-viewer-view-overflow-BBD8nZlq.mjs.map +0 -1
- package/fesm2022/pptx-angular-viewer-view-scene-htTUkvvA.mjs +0 -836
- package/fesm2022/pptx-angular-viewer-view-scene-htTUkvvA.mjs.map +0 -1
- package/fesm2022/pptx-angular-viewer.mjs +0 -169787
- package/fesm2022/pptx-angular-viewer.mjs.map +0 -1
- package/i18n/de.cjs +0 -4165
- package/i18n/de.d.cts +0 -3
- package/i18n/de.d.mts +0 -3
- package/i18n/de.mjs +0 -4138
- package/i18n/es.cjs +0 -4165
- package/i18n/es.d.cts +0 -3
- package/i18n/es.d.mts +0 -3
- package/i18n/es.mjs +0 -4138
- package/i18n/fr.cjs +0 -4165
- package/i18n/fr.d.cts +0 -3
- package/i18n/fr.d.mts +0 -3
- package/i18n/fr.mjs +0 -4138
- package/i18n/zh-CN.cjs +0 -4198
- package/i18n/zh-CN.d.cts +0 -3
- package/i18n/zh-CN.d.mts +0 -3
- package/i18n/zh-CN.mjs +0 -4171
- package/pptx-angular-viewer.css +0 -2
- package/types/pptx-angular-viewer.d.ts +0 -59990
- package/types/pptx-angular-viewer.d.ts.map +0 -1
|
@@ -1,1594 +0,0 @@
|
|
|
1
|
-
import { I as resolvePieSliceShareValue, N as roundDragValue, Rt as DEFAULT_PALETTE, Yt as shade, _ as renderChartLineSvg, a as buildPerspCamera, g as appendChartLegendSvg, h as obliqueBarTaper, i as widenPieViewModel, l as perspScreenToPlaneY, n as computePieChartLayout, r as pieRimPoint, s as perspBoxMatrix, t as buildBarChart3DHoverTooltip, u as perspToScreen, v as renderChartTextSvg, w as isChartInteractionArmed, y as createSvgEl } from "./pptx-angular-viewer-bar-chart-3d-hit-test-B6eCwecE.mjs";
|
|
2
|
-
const FACE_SHADE_AMOUNT = {
|
|
3
|
-
front: 0,
|
|
4
|
-
top: .25,
|
|
5
|
-
right: .36,
|
|
6
|
-
left: .36,
|
|
7
|
-
back: .36,
|
|
8
|
-
bottom: .5
|
|
9
|
-
};
|
|
10
|
-
function shadeChart3DFace(baseColor, face) {
|
|
11
|
-
const amount = FACE_SHADE_AMOUNT[face];
|
|
12
|
-
return amount === 0 ? baseColor : shade(baseColor, amount);
|
|
13
|
-
}
|
|
14
|
-
function buildChart3DBoxFaceColors(baseColor) {
|
|
15
|
-
return [
|
|
16
|
-
shadeChart3DFace(baseColor, "right"),
|
|
17
|
-
shadeChart3DFace(baseColor, "left"),
|
|
18
|
-
shadeChart3DFace(baseColor, "top"),
|
|
19
|
-
shadeChart3DFace(baseColor, "bottom"),
|
|
20
|
-
shadeChart3DFace(baseColor, "front"),
|
|
21
|
-
shadeChart3DFace(baseColor, "back")
|
|
22
|
-
];
|
|
23
|
-
}
|
|
24
|
-
const NORMAL_SHADE = {
|
|
25
|
-
box: [
|
|
26
|
-
.64,
|
|
27
|
-
.75,
|
|
28
|
-
1
|
|
29
|
-
],
|
|
30
|
-
cylinder: [
|
|
31
|
-
.62,
|
|
32
|
-
.75,
|
|
33
|
-
1.07
|
|
34
|
-
],
|
|
35
|
-
cone: [
|
|
36
|
-
.7,
|
|
37
|
-
.75,
|
|
38
|
-
1.16
|
|
39
|
-
],
|
|
40
|
-
pyramid: [
|
|
41
|
-
.68,
|
|
42
|
-
.75,
|
|
43
|
-
1.03
|
|
44
|
-
]
|
|
45
|
-
};
|
|
46
|
-
const UNDERSIDE_SHADE = .5;
|
|
47
|
-
function shadeFamily(shape) {
|
|
48
|
-
switch (shape) {
|
|
49
|
-
case "cylinder": return "cylinder";
|
|
50
|
-
case "cone":
|
|
51
|
-
case "coneToMax": return "cone";
|
|
52
|
-
case "pyramid":
|
|
53
|
-
case "pyramidToMax": return "pyramid";
|
|
54
|
-
default: return "box";
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
function chart3DNormalShade(shape, nx, ny, nz) {
|
|
58
|
-
const [cx, cy, cz] = NORMAL_SHADE[shadeFamily(shape)];
|
|
59
|
-
return cx * nx * nx + (ny >= 0 ? cy : UNDERSIDE_SHADE) * ny * ny + cz * nz * nz;
|
|
60
|
-
}
|
|
61
|
-
const ROUND_SEGMENTS = 48;
|
|
62
|
-
function isShapedObliqueBar(bar) {
|
|
63
|
-
return bar.shape !== "box";
|
|
64
|
-
}
|
|
65
|
-
function obliqueShapeKey(bar, horizontal) {
|
|
66
|
-
return `${bar.shape}:${bar.taper.bottom.toFixed(4)}:${bar.taper.top.toFixed(4)}:${horizontal ? "h" : "v"}`;
|
|
67
|
-
}
|
|
68
|
-
function buildObliqueShapeGeometry(three, bar, horizontal) {
|
|
69
|
-
const pointed = bar.shape === "pyramid" || bar.shape === "pyramidToMax";
|
|
70
|
-
let geometry;
|
|
71
|
-
if (pointed) {
|
|
72
|
-
const r = Math.SQRT1_2;
|
|
73
|
-
const faceted = new three.CylinderGeometry(r * bar.taper.top, r * bar.taper.bottom, 1, 4).rotateY(Math.PI / 4);
|
|
74
|
-
geometry = faceted.toNonIndexed();
|
|
75
|
-
faceted.dispose();
|
|
76
|
-
geometry.computeVertexNormals();
|
|
77
|
-
} else geometry = new three.CylinderGeometry(.5 * bar.taper.top, .5 * bar.taper.bottom, 1, ROUND_SEGMENTS);
|
|
78
|
-
if (horizontal) geometry.rotateZ(-Math.PI / 2);
|
|
79
|
-
return geometry;
|
|
80
|
-
}
|
|
81
|
-
function shadeObliqueShapeGeometry(three, geometry, bar, scale, viewYaw) {
|
|
82
|
-
const round = bar.shape !== "pyramid" && bar.shape !== "pyramidToMax";
|
|
83
|
-
const cos = Math.cos(round ? viewYaw : 0);
|
|
84
|
-
const sin = Math.sin(round ? viewYaw : 0);
|
|
85
|
-
const normals = geometry.getAttribute("normal");
|
|
86
|
-
const count = normals.count;
|
|
87
|
-
let colors = geometry.getAttribute("color");
|
|
88
|
-
if (!colors || colors.count !== count) {
|
|
89
|
-
colors = new three.Float32BufferAttribute(new Float32Array(count * 3), 3);
|
|
90
|
-
geometry.setAttribute("color", colors);
|
|
91
|
-
}
|
|
92
|
-
const srgb = new three.Color(bar.color).getRGB(new three.Color(), three.SRGBColorSpace);
|
|
93
|
-
const out = new three.Color();
|
|
94
|
-
for (let i = 0; i < count; i++) {
|
|
95
|
-
let nx = normals.getX(i) / Math.max(scale.x, 1e-6);
|
|
96
|
-
let ny = normals.getY(i) / Math.max(scale.y, 1e-6);
|
|
97
|
-
let nz = normals.getZ(i) / Math.max(scale.z, 1e-6);
|
|
98
|
-
const len = Math.hypot(nx, ny, nz) || 1;
|
|
99
|
-
nx /= len;
|
|
100
|
-
ny /= len;
|
|
101
|
-
nz /= len;
|
|
102
|
-
const f = chart3DNormalShade(bar.shape, nx * cos - nz * sin, ny, nx * sin + nz * cos);
|
|
103
|
-
out.setRGB(Math.min(1, srgb.r * f), Math.min(1, srgb.g * f), Math.min(1, srgb.b * f), three.SRGBColorSpace);
|
|
104
|
-
colors.setXYZ(i, out.r, out.g, out.b);
|
|
105
|
-
}
|
|
106
|
-
colors.needsUpdate = true;
|
|
107
|
-
}
|
|
108
|
-
const GRIDLINE_COLOR$1 = 14277081;
|
|
109
|
-
function obliqueToScene(layout, svgWidth, svgHeight, p) {
|
|
110
|
-
return [
|
|
111
|
-
layout.origin.x + p[0] - svgWidth / 2,
|
|
112
|
-
svgHeight / 2 - layout.origin.y + p[1],
|
|
113
|
-
-p[2]
|
|
114
|
-
];
|
|
115
|
-
}
|
|
116
|
-
function placeObliqueBarMesh(three, mesh, layout, svgWidth, svgHeight, bar) {
|
|
117
|
-
mesh.position.set(...obliqueToScene(layout, svgWidth, svgHeight, [
|
|
118
|
-
bar.x + bar.w / 2,
|
|
119
|
-
bar.y + bar.h / 2,
|
|
120
|
-
bar.z + bar.d / 2
|
|
121
|
-
]));
|
|
122
|
-
mesh.scale.set(Math.max(bar.w, .001), Math.max(bar.h, .001), bar.d);
|
|
123
|
-
if (!isShapedObliqueBar(bar)) return;
|
|
124
|
-
const key = obliqueShapeKey(bar, layout.horizontal);
|
|
125
|
-
const data = mesh.userData;
|
|
126
|
-
if (data.shapeKey !== key) {
|
|
127
|
-
const previous = mesh.geometry;
|
|
128
|
-
mesh.geometry = buildObliqueShapeGeometry(three, bar, layout.horizontal);
|
|
129
|
-
previous.dispose();
|
|
130
|
-
data.shapeKey = key;
|
|
131
|
-
}
|
|
132
|
-
shadeObliqueShapeGeometry(three, mesh.geometry, bar, mesh.scale, Math.atan(layout.shear.x));
|
|
133
|
-
}
|
|
134
|
-
function buildChart3DBarMeshes(three, layout, svgWidth, svgHeight) {
|
|
135
|
-
const group = new three.Group();
|
|
136
|
-
const meshes = [];
|
|
137
|
-
const lineGeometries = [];
|
|
138
|
-
const materials = [];
|
|
139
|
-
const points = [];
|
|
140
|
-
for (const line of layout.gridlines) points.push(...obliqueToScene(layout, svgWidth, svgHeight, line.from), ...obliqueToScene(layout, svgWidth, svgHeight, line.to));
|
|
141
|
-
if (points.length > 0) {
|
|
142
|
-
const lineGeometry = new three.BufferGeometry();
|
|
143
|
-
lineGeometry.setAttribute("position", new three.Float32BufferAttribute(points, 3));
|
|
144
|
-
const lineMaterial = new three.LineBasicMaterial({ color: GRIDLINE_COLOR$1 });
|
|
145
|
-
lineGeometries.push(lineGeometry);
|
|
146
|
-
materials.push(lineMaterial);
|
|
147
|
-
group.add(new three.LineSegments(lineGeometry, lineMaterial));
|
|
148
|
-
}
|
|
149
|
-
for (const bar of layout.bars) {
|
|
150
|
-
let mesh;
|
|
151
|
-
if (isShapedObliqueBar(bar)) {
|
|
152
|
-
const material = new three.MeshBasicMaterial({ vertexColors: true });
|
|
153
|
-
materials.push(material);
|
|
154
|
-
mesh = new three.Mesh(new three.BufferGeometry(), material);
|
|
155
|
-
} else {
|
|
156
|
-
const faceMaterials = buildChart3DBoxFaceColors(bar.color).map((color) => new three.MeshBasicMaterial({ color }));
|
|
157
|
-
materials.push(...faceMaterials);
|
|
158
|
-
mesh = new three.Mesh(new three.BoxGeometry(1, 1, 1), faceMaterials);
|
|
159
|
-
}
|
|
160
|
-
mesh.userData = {
|
|
161
|
-
seriesIndex: bar.seriesIndex,
|
|
162
|
-
categoryIndex: bar.categoryIndex
|
|
163
|
-
};
|
|
164
|
-
placeObliqueBarMesh(three, mesh, layout, svgWidth, svgHeight, bar);
|
|
165
|
-
group.add(mesh);
|
|
166
|
-
meshes.push(mesh);
|
|
167
|
-
}
|
|
168
|
-
return {
|
|
169
|
-
group,
|
|
170
|
-
meshes,
|
|
171
|
-
dispose: () => {
|
|
172
|
-
for (const geometry of [...lineGeometries, ...meshes.map((m) => m.geometry)]) geometry.dispose();
|
|
173
|
-
for (const material of materials) material.dispose();
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
function renderChart3DChromeOverlaySvg(doc, source, box) {
|
|
178
|
-
const vm = box ? withBoxChromePositions(source) : source;
|
|
179
|
-
const svg = createSvgEl(doc, "svg", {
|
|
180
|
-
class: "pptxv-chart-3d-chrome",
|
|
181
|
-
viewBox: `0 0 ${vm.svgWidth} ${vm.svgHeight}`,
|
|
182
|
-
preserveAspectRatio: "none"
|
|
183
|
-
});
|
|
184
|
-
svg.style.setProperty("width", "100%");
|
|
185
|
-
svg.style.setProperty("height", "100%");
|
|
186
|
-
svg.style.setProperty("display", "block");
|
|
187
|
-
svg.style.setProperty("position", "absolute");
|
|
188
|
-
svg.style.setProperty("inset", "0");
|
|
189
|
-
svg.style.setProperty("pointer-events", "none");
|
|
190
|
-
if (vm.areaFill) svg.appendChild(createSvgEl(doc, "rect", {
|
|
191
|
-
x: 0,
|
|
192
|
-
y: 0,
|
|
193
|
-
width: vm.svgWidth,
|
|
194
|
-
height: vm.svgHeight,
|
|
195
|
-
rx: vm.areaRadius,
|
|
196
|
-
fill: vm.areaFill
|
|
197
|
-
}));
|
|
198
|
-
if (vm.title) {
|
|
199
|
-
const title = createSvgEl(doc, "text", {
|
|
200
|
-
x: vm.titleX,
|
|
201
|
-
y: vm.titleY,
|
|
202
|
-
"text-anchor": "middle",
|
|
203
|
-
"font-size": vm.titleStyle?.fontSize ?? 12,
|
|
204
|
-
"font-weight": vm.titleStyle?.fontWeight ?? 600,
|
|
205
|
-
"font-family": vm.titleStyle?.fontFamily,
|
|
206
|
-
fill: vm.titleStyle?.fill ?? "#1e293b",
|
|
207
|
-
"data-chart-part": "title"
|
|
208
|
-
});
|
|
209
|
-
if (vm.titleRunSpans && vm.titleRunSpans.length > 0) for (const run of vm.titleRunSpans) {
|
|
210
|
-
const tspan = createSvgEl(doc, "tspan", {
|
|
211
|
-
"font-size": run.fontSize,
|
|
212
|
-
"font-weight": run.fontWeight,
|
|
213
|
-
"font-style": run.fontStyle,
|
|
214
|
-
"font-family": run.fontFamily,
|
|
215
|
-
fill: run.fill
|
|
216
|
-
});
|
|
217
|
-
tspan.textContent = run.text;
|
|
218
|
-
title.appendChild(tspan);
|
|
219
|
-
}
|
|
220
|
-
else title.textContent = vm.title;
|
|
221
|
-
svg.appendChild(title);
|
|
222
|
-
}
|
|
223
|
-
if (box) {
|
|
224
|
-
appendBoxLabels(doc, svg, vm, box.labels);
|
|
225
|
-
const legend = box.legend ?? vm.legend.map((item) => ({
|
|
226
|
-
...item,
|
|
227
|
-
lineSwatch: void 0
|
|
228
|
-
}));
|
|
229
|
-
const legendVm = {
|
|
230
|
-
...vm,
|
|
231
|
-
legend: box.reverseLegend ? legend.reverse() : legend
|
|
232
|
-
};
|
|
233
|
-
appendChartLegendSvg(doc, svg, legendVm);
|
|
234
|
-
return svg;
|
|
235
|
-
}
|
|
236
|
-
for (const gl of vm.gridlines) svg.appendChild(renderChartLineSvg(doc, gl));
|
|
237
|
-
for (const gl of vm.secondaryGridlines ?? []) svg.appendChild(renderChartLineSvg(doc, gl));
|
|
238
|
-
for (const lbl of vm.axisLabels) svg.appendChild(renderChartTextSvg(doc, lbl));
|
|
239
|
-
for (const lbl of vm.secondaryAxisLabels ?? []) svg.appendChild(renderChartTextSvg(doc, lbl));
|
|
240
|
-
if (vm.zeroLine) svg.appendChild(renderChartLineSvg(doc, vm.zeroLine));
|
|
241
|
-
for (const lbl of vm.categoryLabels) svg.appendChild(renderChartTextSvg(doc, lbl));
|
|
242
|
-
appendChartLegendSvg(doc, svg, vm);
|
|
243
|
-
return svg;
|
|
244
|
-
}
|
|
245
|
-
const PT = 4 / 3;
|
|
246
|
-
const OBLIQUE_TITLE_BASELINE = 24 * PT;
|
|
247
|
-
const OBLIQUE_LEGEND_BASELINE = 14 * PT;
|
|
248
|
-
function withBoxChromePositions(vm) {
|
|
249
|
-
const legendBottom = (vm.legendY ?? vm.svgHeight - 8) >= vm.svgHeight / 2;
|
|
250
|
-
return {
|
|
251
|
-
...vm,
|
|
252
|
-
titleY: OBLIQUE_TITLE_BASELINE,
|
|
253
|
-
legendY: legendBottom ? vm.svgHeight - OBLIQUE_LEGEND_BASELINE - 3 : vm.legendY
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
function appendBoxLabels(doc, svg, vm, labels) {
|
|
257
|
-
const style = vm.axisLabels[0];
|
|
258
|
-
for (const label of labels) svg.appendChild(renderChartTextSvg(doc, {
|
|
259
|
-
kind: "text",
|
|
260
|
-
x: label.x,
|
|
261
|
-
y: label.y,
|
|
262
|
-
text: label.text,
|
|
263
|
-
fontSize: label.fontSize,
|
|
264
|
-
fill: style?.fill ?? "#595959",
|
|
265
|
-
fontFamily: style?.fontFamily,
|
|
266
|
-
textAnchor: label.anchor,
|
|
267
|
-
dominantBaseline: label.baseline ?? "central"
|
|
268
|
-
}));
|
|
269
|
-
}
|
|
270
|
-
function partOf(point) {
|
|
271
|
-
return point ? {
|
|
272
|
-
role: "dataPoint",
|
|
273
|
-
seriesIndex: point.seriesIndex,
|
|
274
|
-
pointIndex: point.pointIndex
|
|
275
|
-
} : null;
|
|
276
|
-
}
|
|
277
|
-
function attachChart3DMarkInteraction(options) {
|
|
278
|
-
const { ctx, camera, adapter, chartData } = options;
|
|
279
|
-
const three = ctx.three;
|
|
280
|
-
const target = ctx.eventTarget;
|
|
281
|
-
const raycaster = new three.Raycaster();
|
|
282
|
-
const far = new three.Vector3();
|
|
283
|
-
let interactive = ctx.interactive;
|
|
284
|
-
let press = null;
|
|
285
|
-
let hoveredTooltip;
|
|
286
|
-
function pointAt(clientX, clientY) {
|
|
287
|
-
const rect = target.getBoundingClientRect();
|
|
288
|
-
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
289
|
-
options.scene.updateMatrixWorld();
|
|
290
|
-
camera.updateMatrixWorld();
|
|
291
|
-
const x = (clientX - rect.left) / rect.width * 2 - 1;
|
|
292
|
-
const y = -((clientY - rect.top) / rect.height) * 2 + 1;
|
|
293
|
-
raycaster.ray.origin.set(x, y, -1).unproject(camera);
|
|
294
|
-
far.set(x, y, 1).unproject(camera);
|
|
295
|
-
raycaster.ray.direction.copy(far).sub(raycaster.ray.origin).normalize();
|
|
296
|
-
const hit = raycaster.intersectObjects(adapter.targets, false)[0];
|
|
297
|
-
return hit ? adapter.pointAt(hit) : null;
|
|
298
|
-
}
|
|
299
|
-
const onPointerMove = (event) => {
|
|
300
|
-
if (press?.drag) {
|
|
301
|
-
const dx = event.clientX - press.startX;
|
|
302
|
-
const dy = event.clientY - press.startY;
|
|
303
|
-
if (!press.drag.moved && Math.hypot(dx, dy) < 3) return;
|
|
304
|
-
press.drag.moved = true;
|
|
305
|
-
const k = press.chartPxPerClientPx;
|
|
306
|
-
const rect = target.getBoundingClientRect();
|
|
307
|
-
const value = press.drag.session.preview(dx * k, dy * k, {
|
|
308
|
-
x: (event.clientX - rect.left) * k,
|
|
309
|
-
y: (event.clientY - rect.top) * k
|
|
310
|
-
});
|
|
311
|
-
press.lastValue = value;
|
|
312
|
-
ctx.requestRender();
|
|
313
|
-
const part = partOf(press.point);
|
|
314
|
-
if (part) ctx.emit({
|
|
315
|
-
type: "drag",
|
|
316
|
-
detail: {
|
|
317
|
-
part,
|
|
318
|
-
value,
|
|
319
|
-
phase: "move"
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
const point = pointAt(event.clientX, event.clientY);
|
|
325
|
-
const value = point ? adapter.valueOf(point) : void 0;
|
|
326
|
-
const tooltip = point && value !== void 0 ? buildBarChart3DHoverTooltip({
|
|
327
|
-
seriesIndex: point.seriesIndex,
|
|
328
|
-
categoryIndex: point.pointIndex,
|
|
329
|
-
value
|
|
330
|
-
}, {
|
|
331
|
-
categoryLabels: options.categoryLabels,
|
|
332
|
-
seriesNames: options.seriesNames,
|
|
333
|
-
numberFormats: chartData.series.map((s) => s.numberFormat)
|
|
334
|
-
}) : void 0;
|
|
335
|
-
if (tooltip !== hoveredTooltip) {
|
|
336
|
-
hoveredTooltip = tooltip;
|
|
337
|
-
target.title = tooltip ?? "";
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
const onPointerDown = (event) => {
|
|
341
|
-
if (!interactive) return;
|
|
342
|
-
const point = pointAt(event.clientX, event.clientY);
|
|
343
|
-
const armed = point !== null && isChartInteractionArmed(target);
|
|
344
|
-
if (armed) event.stopPropagation();
|
|
345
|
-
const session = armed && point ? adapter.beginDrag(point) : null;
|
|
346
|
-
if (session) {
|
|
347
|
-
event.preventDefault();
|
|
348
|
-
target.setPointerCapture?.(event.pointerId);
|
|
349
|
-
}
|
|
350
|
-
const rect = target.getBoundingClientRect();
|
|
351
|
-
press = {
|
|
352
|
-
point,
|
|
353
|
-
drag: session ? {
|
|
354
|
-
session,
|
|
355
|
-
moved: false
|
|
356
|
-
} : null,
|
|
357
|
-
startX: event.clientX,
|
|
358
|
-
startY: event.clientY,
|
|
359
|
-
chartPxPerClientPx: rect.height > 0 ? options.svgHeight / rect.height : 1,
|
|
360
|
-
lastValue: null
|
|
361
|
-
};
|
|
362
|
-
};
|
|
363
|
-
const onPointerUp = (event) => {
|
|
364
|
-
const current = press;
|
|
365
|
-
press = null;
|
|
366
|
-
if (!current || !interactive) return;
|
|
367
|
-
const part = partOf(current.point);
|
|
368
|
-
if (current.drag?.moved && current.lastValue !== null && part) {
|
|
369
|
-
ctx.emit({
|
|
370
|
-
type: "drag",
|
|
371
|
-
detail: {
|
|
372
|
-
part,
|
|
373
|
-
value: current.lastValue,
|
|
374
|
-
phase: "commit"
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
if (Math.hypot(event.clientX - current.startX, event.clientY - current.startY) < 3) ctx.emit({
|
|
380
|
-
type: "select",
|
|
381
|
-
part
|
|
382
|
-
});
|
|
383
|
-
};
|
|
384
|
-
const onPointerLeave = () => {
|
|
385
|
-
if (hoveredTooltip !== void 0) {
|
|
386
|
-
hoveredTooltip = void 0;
|
|
387
|
-
target.title = "";
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
target.addEventListener("pointermove", onPointerMove);
|
|
391
|
-
target.addEventListener("pointerdown", onPointerDown);
|
|
392
|
-
target.addEventListener("pointerup", onPointerUp);
|
|
393
|
-
target.addEventListener("pointerleave", onPointerLeave);
|
|
394
|
-
return {
|
|
395
|
-
setInteractive(on) {
|
|
396
|
-
interactive = on;
|
|
397
|
-
if (!on) press = null;
|
|
398
|
-
},
|
|
399
|
-
setSelectedPart(part) {
|
|
400
|
-
adapter.highlight(part?.role === "dataPoint" && part.pointIndex !== void 0 ? {
|
|
401
|
-
seriesIndex: part.seriesIndex,
|
|
402
|
-
pointIndex: part.pointIndex
|
|
403
|
-
} : null);
|
|
404
|
-
ctx.requestRender();
|
|
405
|
-
},
|
|
406
|
-
dispose() {
|
|
407
|
-
target.removeEventListener("pointermove", onPointerMove);
|
|
408
|
-
target.removeEventListener("pointerdown", onPointerDown);
|
|
409
|
-
target.removeEventListener("pointerup", onPointerUp);
|
|
410
|
-
target.removeEventListener("pointerleave", onPointerLeave);
|
|
411
|
-
adapter.dispose?.();
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
function isObliqueBarDraggable(layout) {
|
|
416
|
-
return layout.grouping === "clustered" || layout.grouping === "standard";
|
|
417
|
-
}
|
|
418
|
-
function obliqueDragValue(layout, startValue, dx, dy) {
|
|
419
|
-
const raw = startValue + (layout.horizontal ? dx : -dy) / layout.valueScale;
|
|
420
|
-
const span = layout.range.max - layout.range.min;
|
|
421
|
-
return roundDragValue(raw, {
|
|
422
|
-
min: layout.range.min,
|
|
423
|
-
max: layout.range.max,
|
|
424
|
-
span
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
function obliqueDraggedExtent(layout, value) {
|
|
428
|
-
const { min, max } = layout.range;
|
|
429
|
-
const base = Math.min(Math.max(0, min), max);
|
|
430
|
-
const clamp = (v) => Math.min(Math.max(v, min), max);
|
|
431
|
-
const lo = (clamp(Math.min(base, value)) - min) * layout.valueScale;
|
|
432
|
-
return {
|
|
433
|
-
from: lo,
|
|
434
|
-
length: (clamp(Math.max(base, value)) - min) * layout.valueScale - lo
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
function obliqueBarAtValue(layout, bar, value) {
|
|
438
|
-
const { from, length } = obliqueDraggedExtent(layout, value);
|
|
439
|
-
const extent = (layout.range.max - layout.range.min) * layout.valueScale;
|
|
440
|
-
const taper = obliqueBarTaper(bar.shape, from, from + length, extent, value);
|
|
441
|
-
return layout.horizontal ? {
|
|
442
|
-
...bar,
|
|
443
|
-
x: from,
|
|
444
|
-
w: length,
|
|
445
|
-
value,
|
|
446
|
-
taper
|
|
447
|
-
} : {
|
|
448
|
-
...bar,
|
|
449
|
-
y: from,
|
|
450
|
-
h: length,
|
|
451
|
-
value,
|
|
452
|
-
taper
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
const SELECTED_OUTLINE_COLOR = 2450411;
|
|
456
|
-
function attachObliqueBarInteraction(options) {
|
|
457
|
-
const { ctx, meshes, layout } = options;
|
|
458
|
-
const bars = layout.bars;
|
|
459
|
-
const three = ctx.three;
|
|
460
|
-
const outline = new three.LineSegments(new three.EdgesGeometry(new three.BoxGeometry(1, 1, 1)), new three.LineBasicMaterial({ color: SELECTED_OUTLINE_COLOR }));
|
|
461
|
-
outline.visible = false;
|
|
462
|
-
options.scene.add(outline);
|
|
463
|
-
let selected = -1;
|
|
464
|
-
const indexOf = (point) => bars.findIndex((b) => b.seriesIndex === point.seriesIndex && b.categoryIndex === point.pointIndex);
|
|
465
|
-
function syncOutline() {
|
|
466
|
-
outline.visible = selected >= 0;
|
|
467
|
-
if (selected >= 0) {
|
|
468
|
-
outline.position.copy(meshes[selected].position);
|
|
469
|
-
outline.scale.copy(meshes[selected].scale);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return attachChart3DMarkInteraction({
|
|
473
|
-
ctx,
|
|
474
|
-
camera: options.camera,
|
|
475
|
-
scene: options.scene,
|
|
476
|
-
svgHeight: options.svgHeight,
|
|
477
|
-
chartData: options.chartData,
|
|
478
|
-
categoryLabels: options.categoryLabels,
|
|
479
|
-
seriesNames: options.seriesNames,
|
|
480
|
-
adapter: {
|
|
481
|
-
targets: meshes,
|
|
482
|
-
pointAt(hit) {
|
|
483
|
-
const data = hit.object.userData;
|
|
484
|
-
if (data.seriesIndex === void 0 || data.categoryIndex === void 0) return null;
|
|
485
|
-
return {
|
|
486
|
-
seriesIndex: data.seriesIndex,
|
|
487
|
-
pointIndex: data.categoryIndex
|
|
488
|
-
};
|
|
489
|
-
},
|
|
490
|
-
valueOf(point) {
|
|
491
|
-
return bars[indexOf(point)]?.value;
|
|
492
|
-
},
|
|
493
|
-
beginDrag(point) {
|
|
494
|
-
const index = indexOf(point);
|
|
495
|
-
if (index < 0 || !isObliqueBarDraggable(layout)) return null;
|
|
496
|
-
const start = bars[index];
|
|
497
|
-
return { preview(dx, dy) {
|
|
498
|
-
const value = obliqueDragValue(layout, start.value, dx, dy);
|
|
499
|
-
placeObliqueBarMesh(three, meshes[index], layout, options.svgWidth, options.svgHeight, obliqueBarAtValue(layout, start, value));
|
|
500
|
-
syncOutline();
|
|
501
|
-
return value;
|
|
502
|
-
} };
|
|
503
|
-
},
|
|
504
|
-
highlight(point) {
|
|
505
|
-
selected = point ? indexOf(point) : -1;
|
|
506
|
-
syncOutline();
|
|
507
|
-
},
|
|
508
|
-
dispose() {
|
|
509
|
-
outline.geometry.dispose();
|
|
510
|
-
outline.material.dispose();
|
|
511
|
-
outline.removeFromParent();
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
const DEFAULT_GAP_WIDTH = 150;
|
|
517
|
-
function valueY(layout, v) {
|
|
518
|
-
return (Math.min(Math.max(v, layout.range.min), layout.range.max) - layout.range.min) * layout.valueScale;
|
|
519
|
-
}
|
|
520
|
-
function buildPerspBarPrisms(chartData, layout, override) {
|
|
521
|
-
const nCat = layout.categoryX.length;
|
|
522
|
-
const nSer = chartData.series.length;
|
|
523
|
-
const catExtent = layout.horizontal ? layout.view.box.h : layout.view.box.w;
|
|
524
|
-
const slot = nCat > 0 ? catExtent / nCat : catExtent;
|
|
525
|
-
const gapWidth = (chartData.barGapWidth ?? DEFAULT_GAP_WIDTH) / 100;
|
|
526
|
-
const barW = slot / ((layout.grouping === "clustered" ? nSer : 1) + gapWidth);
|
|
527
|
-
const depth = Math.min(barW, layout.rowDepth);
|
|
528
|
-
const stacked = layout.grouping === "stacked" || layout.grouping === "percentStacked";
|
|
529
|
-
const baseline = Math.min(Math.max(0, layout.range.min), layout.range.max);
|
|
530
|
-
const prisms = [];
|
|
531
|
-
for (let c = 0; c < nCat; c++) {
|
|
532
|
-
const valueOf = (s) => {
|
|
533
|
-
if (override && override.seriesIndex === s && override.pointIndex === c) return override.value;
|
|
534
|
-
const v = chartData.series[s]?.values[c];
|
|
535
|
-
return v !== void 0 && Number.isFinite(v) ? v : 0;
|
|
536
|
-
};
|
|
537
|
-
const total = chartData.series.reduce((t, _s, s) => t + Math.abs(valueOf(s)), 0);
|
|
538
|
-
let pos = baseline;
|
|
539
|
-
let neg = baseline;
|
|
540
|
-
for (let s = 0; s < nSer; s++) {
|
|
541
|
-
const raw = valueOf(s);
|
|
542
|
-
const value = layout.grouping === "percentStacked" ? total ? raw / total : 0 : raw;
|
|
543
|
-
let from = baseline;
|
|
544
|
-
let to = value;
|
|
545
|
-
if (stacked) {
|
|
546
|
-
from = value >= 0 ? pos : neg;
|
|
547
|
-
to = from + value;
|
|
548
|
-
if (value >= 0) pos = to;
|
|
549
|
-
else neg = to;
|
|
550
|
-
}
|
|
551
|
-
const shape = chartData.series[s]?.shape ?? chartData.barShape ?? "box";
|
|
552
|
-
const slotIndex = layout.grouping === "clustered" ? s : 0;
|
|
553
|
-
const row = layout.grouping === "standard" ? s : 0;
|
|
554
|
-
const c0 = c * slot + gapWidth * barW / 2 + slotIndex * barW;
|
|
555
|
-
const v0 = valueY(layout, Math.min(from, to));
|
|
556
|
-
const v1 = Math.max(valueY(layout, Math.max(from, to)), v0 + 1e-6);
|
|
557
|
-
const [x0, x1, y0, y1] = layout.horizontal ? [
|
|
558
|
-
v0,
|
|
559
|
-
v1,
|
|
560
|
-
c0,
|
|
561
|
-
c0 + barW
|
|
562
|
-
] : [
|
|
563
|
-
c0,
|
|
564
|
-
c0 + barW,
|
|
565
|
-
v0,
|
|
566
|
-
v1
|
|
567
|
-
];
|
|
568
|
-
const z0 = row * layout.rowDepth + (layout.rowDepth - depth) / 2;
|
|
569
|
-
prisms.push({
|
|
570
|
-
seriesIndex: s,
|
|
571
|
-
pointIndex: c,
|
|
572
|
-
color: layout.colors[s],
|
|
573
|
-
outline: [
|
|
574
|
-
[x0, y0],
|
|
575
|
-
[x1, y0],
|
|
576
|
-
[x1, y1],
|
|
577
|
-
[x0, y1]
|
|
578
|
-
],
|
|
579
|
-
z0,
|
|
580
|
-
z1: z0 + depth,
|
|
581
|
-
shape,
|
|
582
|
-
taper: obliqueBarTaper(shape, v0, v1, (layout.range.max - layout.range.min) * layout.valueScale, raw),
|
|
583
|
-
horizontal: layout.horizontal
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
return prisms;
|
|
588
|
-
}
|
|
589
|
-
const RIBBON_THICKNESS = .012;
|
|
590
|
-
function perspValueY(layout, v) {
|
|
591
|
-
return (Math.min(Math.max(v, layout.range.min), layout.range.max) - layout.range.min) * layout.valueScale;
|
|
592
|
-
}
|
|
593
|
-
function perspRowSpan(layout, row) {
|
|
594
|
-
const z0 = row * layout.rowDepth + (layout.rowDepth - layout.markDepth) / 2;
|
|
595
|
-
return [z0, z0 + layout.markDepth];
|
|
596
|
-
}
|
|
597
|
-
function finite(v) {
|
|
598
|
-
return v !== void 0 && Number.isFinite(v) ? v : 0;
|
|
599
|
-
}
|
|
600
|
-
function plottedValues(chartData, layout, nCat) {
|
|
601
|
-
const raw = chartData.series.map((s) => Array.from({ length: nCat }, (_, c) => finite(s.values[c])));
|
|
602
|
-
if (layout.grouping !== "percentStacked") return raw;
|
|
603
|
-
const totals = Array.from({ length: nCat }, (_, c) => raw.reduce((t, s) => t + Math.abs(s[c]), 0));
|
|
604
|
-
return raw.map((s) => s.map((v, c) => totals[c] ? v / totals[c] : 0));
|
|
605
|
-
}
|
|
606
|
-
function buildPerspPrisms(chartData, layout, override) {
|
|
607
|
-
if (layout.kind === "bar") return buildPerspBarPrisms(chartData, layout, override);
|
|
608
|
-
const nCat = layout.categoryX.length;
|
|
609
|
-
const values = plottedValues(chartData, layout, nCat);
|
|
610
|
-
if (override) {
|
|
611
|
-
const row = values[override.seriesIndex];
|
|
612
|
-
if (row && override.pointIndex < nCat) row[override.pointIndex] = override.value;
|
|
613
|
-
}
|
|
614
|
-
const xs = layout.categoryX;
|
|
615
|
-
const baseY = perspValueY(layout, Math.max(0, layout.range.min));
|
|
616
|
-
const prisms = [];
|
|
617
|
-
if (layout.kind === "line") {
|
|
618
|
-
const t = layout.view.box.h * RIBBON_THICKNESS / 2;
|
|
619
|
-
values.forEach((series, s) => {
|
|
620
|
-
const ys = series.map((v) => perspValueY(layout, v));
|
|
621
|
-
const [z0, z1] = perspRowSpan(layout, s);
|
|
622
|
-
prisms.push({
|
|
623
|
-
seriesIndex: s,
|
|
624
|
-
color: layout.colors[s],
|
|
625
|
-
outline: [...xs.map((x, c) => [x, ys[c] - t]), ...xs.map((x, c) => [x, ys[c] + t]).reverse()],
|
|
626
|
-
z0,
|
|
627
|
-
z1
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
return prisms;
|
|
631
|
-
}
|
|
632
|
-
const stacked = layout.grouping !== "standard";
|
|
633
|
-
const running = new Array(nCat).fill(0);
|
|
634
|
-
values.forEach((series, s) => {
|
|
635
|
-
const lower = stacked ? running.map((v) => perspValueY(layout, v)) : xs.map(() => baseY);
|
|
636
|
-
const upper = series.map((v, c) => perspValueY(layout, stacked ? running[c] + v : v));
|
|
637
|
-
if (stacked) series.forEach((v, c) => {
|
|
638
|
-
running[c] += v;
|
|
639
|
-
});
|
|
640
|
-
const [z0, z1] = perspRowSpan(layout, stacked ? 0 : s);
|
|
641
|
-
prisms.push({
|
|
642
|
-
seriesIndex: s,
|
|
643
|
-
color: layout.colors[s],
|
|
644
|
-
outline: [...xs.map((x, c) => [x, lower[c]]), ...xs.map((x, c) => [x, upper[c]]).reverse()],
|
|
645
|
-
z0,
|
|
646
|
-
z1
|
|
647
|
-
});
|
|
648
|
-
});
|
|
649
|
-
return prisms;
|
|
650
|
-
}
|
|
651
|
-
function isShapedPerspPrism(prism) {
|
|
652
|
-
return prism.shape !== void 0 && prism.shape !== "box";
|
|
653
|
-
}
|
|
654
|
-
function outlineBounds(prism) {
|
|
655
|
-
const xs = prism.outline.map(([x]) => x);
|
|
656
|
-
const ys = prism.outline.map(([, y]) => y);
|
|
657
|
-
return [
|
|
658
|
-
Math.min(...xs),
|
|
659
|
-
Math.max(...xs),
|
|
660
|
-
Math.min(...ys),
|
|
661
|
-
Math.max(...ys)
|
|
662
|
-
];
|
|
663
|
-
}
|
|
664
|
-
function buildShapedPrismGeometry(three, prism) {
|
|
665
|
-
const shape = prism.shape ?? "box";
|
|
666
|
-
const unit = buildObliqueShapeGeometry(three, {
|
|
667
|
-
shape,
|
|
668
|
-
taper: prism.taper ?? {
|
|
669
|
-
bottom: 1,
|
|
670
|
-
top: 1
|
|
671
|
-
}
|
|
672
|
-
}, Boolean(prism.horizontal));
|
|
673
|
-
const geometry = unit.index ? unit.toNonIndexed() : unit;
|
|
674
|
-
if (geometry !== unit) unit.dispose();
|
|
675
|
-
const [x0, x1, y0, y1] = outlineBounds(prism);
|
|
676
|
-
const sx = Math.max(x1 - x0, 1e-6);
|
|
677
|
-
const sy = Math.max(y1 - y0, 1e-6);
|
|
678
|
-
const sz = Math.max(prism.z1 - prism.z0, 1e-6);
|
|
679
|
-
geometry.scale(sx, sy, sz);
|
|
680
|
-
geometry.translate((x0 + x1) / 2, (y0 + y1) / 2, (prism.z0 + prism.z1) / 2);
|
|
681
|
-
geometry.computeVertexNormals();
|
|
682
|
-
const normals = geometry.getAttribute("normal");
|
|
683
|
-
const colors = new Float32Array(normals.count * 3);
|
|
684
|
-
const base = new three.Color(prism.color).getRGB(new three.Color(), three.SRGBColorSpace);
|
|
685
|
-
const out = new three.Color();
|
|
686
|
-
for (let i = 0; i < normals.count; i++) {
|
|
687
|
-
const f = chart3DNormalShade(shape, normals.getX(i), normals.getY(i), -normals.getZ(i));
|
|
688
|
-
out.setRGB(Math.min(1, base.r * f), Math.min(1, base.g * f), Math.min(1, base.b * f), three.SRGBColorSpace);
|
|
689
|
-
colors.set([
|
|
690
|
-
out.r,
|
|
691
|
-
out.g,
|
|
692
|
-
out.b
|
|
693
|
-
], i * 3);
|
|
694
|
-
}
|
|
695
|
-
geometry.setAttribute("color", new three.Float32BufferAttribute(colors, 3));
|
|
696
|
-
return geometry;
|
|
697
|
-
}
|
|
698
|
-
const GRIDLINE_COLOR = 14277081;
|
|
699
|
-
function shadePerspGeometry(three, geometry, color) {
|
|
700
|
-
const normals = geometry.getAttribute("normal");
|
|
701
|
-
const colors = new Float32Array(normals.count * 3);
|
|
702
|
-
const base = new three.Color(color).getRGB(new three.Color(), three.SRGBColorSpace);
|
|
703
|
-
const out = new three.Color();
|
|
704
|
-
for (let i = 0; i < normals.count; i++) {
|
|
705
|
-
const f = chart3DNormalShade("box", normals.getX(i), normals.getY(i), -normals.getZ(i));
|
|
706
|
-
out.setRGB(Math.min(1, base.r * f), Math.min(1, base.g * f), Math.min(1, base.b * f), three.SRGBColorSpace);
|
|
707
|
-
colors[i * 3] = out.r;
|
|
708
|
-
colors[i * 3 + 1] = out.g;
|
|
709
|
-
colors[i * 3 + 2] = out.b;
|
|
710
|
-
}
|
|
711
|
-
geometry.setAttribute("color", new three.Float32BufferAttribute(colors, 3));
|
|
712
|
-
}
|
|
713
|
-
function buildPrismGeometry(three, prism) {
|
|
714
|
-
if (isShapedPerspPrism(prism)) return buildShapedPrismGeometry(three, prism);
|
|
715
|
-
const shape = new three.Shape(prism.outline.map(([x, y]) => new three.Vector2(x, y)));
|
|
716
|
-
const extruded = new three.ExtrudeGeometry(shape, {
|
|
717
|
-
depth: Math.max(prism.z1 - prism.z0, 1e-6),
|
|
718
|
-
bevelEnabled: false,
|
|
719
|
-
steps: 1
|
|
720
|
-
});
|
|
721
|
-
const geometry = extruded.index ? extruded.toNonIndexed() : extruded;
|
|
722
|
-
if (geometry !== extruded) extruded.dispose();
|
|
723
|
-
geometry.translate(0, 0, prism.z0);
|
|
724
|
-
geometry.computeVertexNormals();
|
|
725
|
-
shadePerspGeometry(three, geometry, prism.color);
|
|
726
|
-
return geometry;
|
|
727
|
-
}
|
|
728
|
-
function buildPerspGridlines(three, layout) {
|
|
729
|
-
const points = [];
|
|
730
|
-
for (const line of layout.gridlines) points.push(...line.from, ...line.to);
|
|
731
|
-
const geometry = new three.BufferGeometry();
|
|
732
|
-
geometry.setAttribute("position", new three.Float32BufferAttribute(points, 3));
|
|
733
|
-
return new three.LineSegments(geometry, new three.LineBasicMaterial({ color: GRIDLINE_COLOR }));
|
|
734
|
-
}
|
|
735
|
-
function buildPerspMeshes(three, layout, prisms) {
|
|
736
|
-
const group = new three.Group();
|
|
737
|
-
group.matrixAutoUpdate = false;
|
|
738
|
-
group.matrix.copy(perspBoxMatrix(three, layout.view));
|
|
739
|
-
const gridlines = buildPerspGridlines(three, layout);
|
|
740
|
-
group.add(gridlines);
|
|
741
|
-
const material = new three.MeshBasicMaterial({
|
|
742
|
-
vertexColors: true,
|
|
743
|
-
side: three.DoubleSide
|
|
744
|
-
});
|
|
745
|
-
const meshes = prisms.map((prism) => {
|
|
746
|
-
const mesh = new three.Mesh(buildPrismGeometry(three, prism), material);
|
|
747
|
-
mesh.userData = {
|
|
748
|
-
seriesIndex: prism.seriesIndex,
|
|
749
|
-
pointIndex: prism.pointIndex
|
|
750
|
-
};
|
|
751
|
-
group.add(mesh);
|
|
752
|
-
return mesh;
|
|
753
|
-
});
|
|
754
|
-
return {
|
|
755
|
-
group,
|
|
756
|
-
meshes,
|
|
757
|
-
setPrism(i, prism) {
|
|
758
|
-
const mesh = meshes[i];
|
|
759
|
-
if (!mesh) return;
|
|
760
|
-
const previous = mesh.geometry;
|
|
761
|
-
mesh.geometry = buildPrismGeometry(three, prism);
|
|
762
|
-
previous.dispose();
|
|
763
|
-
},
|
|
764
|
-
dispose() {
|
|
765
|
-
gridlines.geometry.dispose();
|
|
766
|
-
gridlines.material.dispose();
|
|
767
|
-
material.dispose();
|
|
768
|
-
for (const mesh of meshes) mesh.geometry.dispose();
|
|
769
|
-
}
|
|
770
|
-
};
|
|
771
|
-
}
|
|
772
|
-
function surfaceRowZ(layout, s, n) {
|
|
773
|
-
return n > 1 ? s / (n - 1) * layout.view.box.d : layout.view.box.d / 2;
|
|
774
|
-
}
|
|
775
|
-
function bandEdges(range) {
|
|
776
|
-
const unit = range.majorUnit > 0 ? range.majorUnit : range.max - range.min || 1;
|
|
777
|
-
const edges = [];
|
|
778
|
-
for (let v = range.min; v < range.max - unit * 1e-9; v += unit) edges.push(Number(v.toPrecision(12)));
|
|
779
|
-
edges.push(range.max);
|
|
780
|
-
return edges;
|
|
781
|
-
}
|
|
782
|
-
function formatEdge(v) {
|
|
783
|
-
return String(Number(v.toPrecision(10)));
|
|
784
|
-
}
|
|
785
|
-
function surfaceBands(chartData, range, palette) {
|
|
786
|
-
const edges = bandEdges(range);
|
|
787
|
-
return edges.slice(0, -1).map((lo, i) => {
|
|
788
|
-
const fmt = chartData.bandFmts?.find((b) => b.index === i)?.spPr;
|
|
789
|
-
const cycle = palette.length > 0 ? palette : ["#4472C4"];
|
|
790
|
-
const base = cycle[i % cycle.length];
|
|
791
|
-
const fallback = Math.floor(i / cycle.length) % 2 === 1 ? shade(base, .4) : base;
|
|
792
|
-
return {
|
|
793
|
-
label: `${formatEdge(lo)}-${formatEdge(edges[i + 1])}`,
|
|
794
|
-
color: fmt?.fillColor ?? fmt?.strokeColor ?? fallback
|
|
795
|
-
};
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
function clipY(poly, lo, hi) {
|
|
799
|
-
const clip = (pts, keep, edge) => {
|
|
800
|
-
const out = [];
|
|
801
|
-
for (let i = 0; i < pts.length; i++) {
|
|
802
|
-
const a = pts[i];
|
|
803
|
-
const b = pts[(i + 1) % pts.length];
|
|
804
|
-
const ina = keep(a);
|
|
805
|
-
const inb = keep(b);
|
|
806
|
-
if (ina) out.push(a);
|
|
807
|
-
if (ina !== inb) {
|
|
808
|
-
const t = (edge - a[1]) / (b[1] - a[1]);
|
|
809
|
-
out.push([
|
|
810
|
-
a[0] + (b[0] - a[0]) * t,
|
|
811
|
-
edge,
|
|
812
|
-
a[2] + (b[2] - a[2]) * t
|
|
813
|
-
]);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
return out;
|
|
817
|
-
};
|
|
818
|
-
return clip(clip(poly, (p) => p[1] >= lo, lo), (p) => p[1] <= hi, hi);
|
|
819
|
-
}
|
|
820
|
-
function cellTriangles(point, s, c) {
|
|
821
|
-
return [[
|
|
822
|
-
point(s, c),
|
|
823
|
-
point(s, c + 1),
|
|
824
|
-
point(s + 1, c + 1)
|
|
825
|
-
], [
|
|
826
|
-
point(s, c),
|
|
827
|
-
point(s + 1, c + 1),
|
|
828
|
-
point(s + 1, c)
|
|
829
|
-
]];
|
|
830
|
-
}
|
|
831
|
-
function clampedBandSegments(a, b, edgesY, out, keepFlat) {
|
|
832
|
-
for (let band = 0; band + 1 < edgesY.length; band++) {
|
|
833
|
-
const lo = edgesY[band];
|
|
834
|
-
const hi = edgesY[band + 1];
|
|
835
|
-
const cuts = [0, 1];
|
|
836
|
-
for (const e of [lo, hi]) {
|
|
837
|
-
const t = (e - a[1]) / (b[1] - a[1]);
|
|
838
|
-
if (t > 0 && t < 1) cuts.push(t);
|
|
839
|
-
}
|
|
840
|
-
cuts.sort((x, y) => x - y);
|
|
841
|
-
const at = (t) => [
|
|
842
|
-
a[0] + (b[0] - a[0]) * t,
|
|
843
|
-
Math.min(hi, Math.max(lo, a[1] + (b[1] - a[1]) * t)),
|
|
844
|
-
a[2] + (b[2] - a[2]) * t
|
|
845
|
-
];
|
|
846
|
-
for (let i = 0; i + 1 < cuts.length; i++) {
|
|
847
|
-
const from = at(cuts[i]);
|
|
848
|
-
const to = at(cuts[i + 1]);
|
|
849
|
-
const flat = from[1] === to[1] && (from[1] === lo || from[1] === hi);
|
|
850
|
-
if (keepFlat || !flat) out.push({
|
|
851
|
-
from,
|
|
852
|
-
to,
|
|
853
|
-
band
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
function buildSurfaceGeometry(chartData, layout, palette, override) {
|
|
859
|
-
const nSer = chartData.series.length;
|
|
860
|
-
const nCat = layout.categoryX.length;
|
|
861
|
-
const bands = surfaceBands(chartData, layout.range, palette);
|
|
862
|
-
const edgesY = bandEdges(layout.range).map((v) => perspValueY(layout, v));
|
|
863
|
-
const point = (s, c) => {
|
|
864
|
-
let v = chartData.series[s]?.values[c];
|
|
865
|
-
if (override && override.seriesIndex === s && override.pointIndex === c) v = override.value;
|
|
866
|
-
const value = v !== void 0 && Number.isFinite(v) ? v : layout.range.min;
|
|
867
|
-
return [
|
|
868
|
-
layout.categoryX[c],
|
|
869
|
-
perspValueY(layout, value),
|
|
870
|
-
surfaceRowZ(layout, s, nSer)
|
|
871
|
-
];
|
|
872
|
-
};
|
|
873
|
-
const geometry = {
|
|
874
|
-
bands,
|
|
875
|
-
triangles: [],
|
|
876
|
-
segments: []
|
|
877
|
-
};
|
|
878
|
-
if (chartData.wireframe) {
|
|
879
|
-
for (let s = 0; s < nSer; s++) for (let c = 0; c < nCat; c++) {
|
|
880
|
-
if (c + 1 < nCat) clampedBandSegments(point(s, c), point(s, c + 1), edgesY, geometry.segments, s === 0);
|
|
881
|
-
if (s + 1 < nSer) clampedBandSegments(point(s, c), point(s + 1, c), edgesY, geometry.segments, false);
|
|
882
|
-
}
|
|
883
|
-
return geometry;
|
|
884
|
-
}
|
|
885
|
-
for (let s = 0; s + 1 < nSer; s++) for (let c = 0; c + 1 < nCat; c++) for (const tri of cellTriangles(point, s, c)) for (let b = 0; b < bands.length; b++) {
|
|
886
|
-
const piece = clipY(tri, edgesY[b], edgesY[b + 1]);
|
|
887
|
-
for (let i = 1; i + 1 < piece.length; i++) geometry.triangles.push({
|
|
888
|
-
points: [
|
|
889
|
-
piece[0],
|
|
890
|
-
piece[i],
|
|
891
|
-
piece[i + 1]
|
|
892
|
-
],
|
|
893
|
-
band: b
|
|
894
|
-
});
|
|
895
|
-
}
|
|
896
|
-
return geometry;
|
|
897
|
-
}
|
|
898
|
-
function srgb(three, hex, f) {
|
|
899
|
-
const base = new three.Color(hex).getRGB(new three.Color(), three.SRGBColorSpace);
|
|
900
|
-
return new three.Color().setRGB(Math.min(1, base.r * f), Math.min(1, base.g * f), Math.min(1, base.b * f), three.SRGBColorSpace);
|
|
901
|
-
}
|
|
902
|
-
function buildVisual(three, chartData, layout, palette, override) {
|
|
903
|
-
const geo = buildSurfaceGeometry(chartData, layout, palette, override);
|
|
904
|
-
const positions = [];
|
|
905
|
-
const colors = [];
|
|
906
|
-
if (chartData.wireframe) {
|
|
907
|
-
for (const seg of geo.segments) {
|
|
908
|
-
const c = srgb(three, geo.bands[seg.band].color, 1);
|
|
909
|
-
positions.push(...seg.from, ...seg.to);
|
|
910
|
-
colors.push(c.r, c.g, c.b, c.r, c.g, c.b);
|
|
911
|
-
}
|
|
912
|
-
const g = new three.BufferGeometry();
|
|
913
|
-
g.setAttribute("position", new three.Float32BufferAttribute(positions, 3));
|
|
914
|
-
g.setAttribute("color", new three.Float32BufferAttribute(colors, 3));
|
|
915
|
-
return new three.LineSegments(g, new three.LineBasicMaterial({ vertexColors: true }));
|
|
916
|
-
}
|
|
917
|
-
const a = new three.Vector3();
|
|
918
|
-
const b = new three.Vector3();
|
|
919
|
-
const n = new three.Vector3();
|
|
920
|
-
for (const tri of geo.triangles) {
|
|
921
|
-
const [p0, p1, p2] = tri.points;
|
|
922
|
-
a.set(p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]);
|
|
923
|
-
b.set(p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]);
|
|
924
|
-
n.crossVectors(a, b).normalize();
|
|
925
|
-
if (n.y < 0) n.negate();
|
|
926
|
-
const c = srgb(three, geo.bands[tri.band].color, chart3DNormalShade("box", n.x, n.y, -n.z));
|
|
927
|
-
positions.push(...p0, ...p1, ...p2);
|
|
928
|
-
colors.push(c.r, c.g, c.b, c.r, c.g, c.b, c.r, c.g, c.b);
|
|
929
|
-
}
|
|
930
|
-
const g = new three.BufferGeometry();
|
|
931
|
-
g.setAttribute("position", new three.Float32BufferAttribute(positions, 3));
|
|
932
|
-
g.setAttribute("color", new three.Float32BufferAttribute(colors, 3));
|
|
933
|
-
return new three.Mesh(g, new three.MeshBasicMaterial({
|
|
934
|
-
vertexColors: true,
|
|
935
|
-
side: three.DoubleSide
|
|
936
|
-
}));
|
|
937
|
-
}
|
|
938
|
-
function buildPickGeometry(three, chartData, layout) {
|
|
939
|
-
const nSer = chartData.series.length;
|
|
940
|
-
const nCat = layout.categoryX.length;
|
|
941
|
-
const at = (s, c) => {
|
|
942
|
-
const v = chartData.series[s]?.values[c];
|
|
943
|
-
return [
|
|
944
|
-
layout.categoryX[c],
|
|
945
|
-
perspValueY(layout, v !== void 0 && Number.isFinite(v) ? v : layout.range.min),
|
|
946
|
-
surfaceRowZ(layout, s, nSer)
|
|
947
|
-
];
|
|
948
|
-
};
|
|
949
|
-
const positions = [];
|
|
950
|
-
for (let s = 0; s + 1 < nSer; s++) for (let c = 0; c + 1 < nCat; c++) {
|
|
951
|
-
positions.push(...at(s, c), ...at(s, c + 1), ...at(s + 1, c + 1));
|
|
952
|
-
positions.push(...at(s, c), ...at(s + 1, c + 1), ...at(s + 1, c));
|
|
953
|
-
}
|
|
954
|
-
const g = new three.BufferGeometry();
|
|
955
|
-
g.setAttribute("position", new three.Float32BufferAttribute(positions, 3));
|
|
956
|
-
return g;
|
|
957
|
-
}
|
|
958
|
-
function disposeObject(object) {
|
|
959
|
-
const withGeometry = object;
|
|
960
|
-
withGeometry.geometry?.dispose();
|
|
961
|
-
withGeometry.material?.dispose();
|
|
962
|
-
}
|
|
963
|
-
function buildSurfaceMarks(three, chartData, layout, palette) {
|
|
964
|
-
const group = new three.Group();
|
|
965
|
-
group.matrixAutoUpdate = false;
|
|
966
|
-
group.matrix.copy(perspBoxMatrix(three, layout.view));
|
|
967
|
-
const gridlines = buildPerspGridlines(three, layout);
|
|
968
|
-
group.add(gridlines);
|
|
969
|
-
let visual = buildVisual(three, chartData, layout, palette);
|
|
970
|
-
group.add(visual);
|
|
971
|
-
const pickMaterial = new three.MeshBasicMaterial({
|
|
972
|
-
side: three.DoubleSide,
|
|
973
|
-
visible: false
|
|
974
|
-
});
|
|
975
|
-
const pickTarget = new three.Mesh(buildPickGeometry(three, chartData, layout), pickMaterial);
|
|
976
|
-
group.add(pickTarget);
|
|
977
|
-
return {
|
|
978
|
-
group,
|
|
979
|
-
pickTarget,
|
|
980
|
-
rebuild(override) {
|
|
981
|
-
group.remove(visual);
|
|
982
|
-
disposeObject(visual);
|
|
983
|
-
visual = buildVisual(three, chartData, layout, palette, override);
|
|
984
|
-
group.add(visual);
|
|
985
|
-
},
|
|
986
|
-
dispose() {
|
|
987
|
-
disposeObject(visual);
|
|
988
|
-
disposeObject(gridlines);
|
|
989
|
-
pickTarget.geometry.dispose();
|
|
990
|
-
pickMaterial.dispose();
|
|
991
|
-
}
|
|
992
|
-
};
|
|
993
|
-
}
|
|
994
|
-
function nearestIndex(xs, x) {
|
|
995
|
-
let best = 0;
|
|
996
|
-
xs.forEach((v, i) => {
|
|
997
|
-
if (Math.abs(v - x) < Math.abs(xs[best] - x)) best = i;
|
|
998
|
-
});
|
|
999
|
-
return best;
|
|
1000
|
-
}
|
|
1001
|
-
function buildPerspMarkSet(three, chartData, layout, palette) {
|
|
1002
|
-
if (layout.kind === "surface") {
|
|
1003
|
-
const nSer = chartData.series.length;
|
|
1004
|
-
const zs = Array.from({ length: nSer }, (_, s) => surfaceRowZ(layout, s, nSer));
|
|
1005
|
-
const surface = buildSurfaceMarks(three, chartData, layout, palette);
|
|
1006
|
-
return {
|
|
1007
|
-
group: surface.group,
|
|
1008
|
-
targets: [surface.pickTarget],
|
|
1009
|
-
pointAt: (_object, local) => ({
|
|
1010
|
-
seriesIndex: nearestIndex(zs, local.z),
|
|
1011
|
-
pointIndex: nearestIndex(layout.categoryX, local.x)
|
|
1012
|
-
}),
|
|
1013
|
-
anchor: (point, value) => [
|
|
1014
|
-
layout.categoryX[point.pointIndex],
|
|
1015
|
-
perspValueY(layout, value),
|
|
1016
|
-
zs[point.seriesIndex]
|
|
1017
|
-
],
|
|
1018
|
-
draggable: true,
|
|
1019
|
-
preview: (point, value) => surface.rebuild({
|
|
1020
|
-
...point,
|
|
1021
|
-
value
|
|
1022
|
-
}),
|
|
1023
|
-
dispose: () => surface.dispose()
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
|
-
if (layout.kind === "bar") return buildBarMarkSet(three, chartData, layout);
|
|
1027
|
-
const stacked = layout.grouping !== "standard" && layout.grouping !== "clustered";
|
|
1028
|
-
const meshes = buildPerspMeshes(three, layout, buildPerspPrisms(chartData, layout));
|
|
1029
|
-
return {
|
|
1030
|
-
group: meshes.group,
|
|
1031
|
-
targets: meshes.meshes,
|
|
1032
|
-
pointAt(object, local) {
|
|
1033
|
-
const seriesIndex = object.userData.seriesIndex;
|
|
1034
|
-
return seriesIndex === void 0 ? null : {
|
|
1035
|
-
seriesIndex,
|
|
1036
|
-
pointIndex: nearestIndex(layout.categoryX, local.x)
|
|
1037
|
-
};
|
|
1038
|
-
},
|
|
1039
|
-
anchor(point, value) {
|
|
1040
|
-
const [z0, z1] = perspRowSpan(layout, stacked ? 0 : point.seriesIndex);
|
|
1041
|
-
return [
|
|
1042
|
-
layout.categoryX[point.pointIndex],
|
|
1043
|
-
perspValueY(layout, value),
|
|
1044
|
-
(z0 + z1) / 2
|
|
1045
|
-
];
|
|
1046
|
-
},
|
|
1047
|
-
draggable: !stacked,
|
|
1048
|
-
preview(point, value) {
|
|
1049
|
-
meshes.setPrism(point.seriesIndex, buildPerspPrisms(chartData, layout, {
|
|
1050
|
-
...point,
|
|
1051
|
-
value
|
|
1052
|
-
})[point.seriesIndex]);
|
|
1053
|
-
},
|
|
1054
|
-
dispose: () => meshes.dispose()
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
function buildBarMarkSet(three, chartData, layout) {
|
|
1058
|
-
const prisms = buildPerspPrisms(chartData, layout);
|
|
1059
|
-
const meshes = buildPerspMeshes(three, layout, prisms);
|
|
1060
|
-
const indexOf = (point) => prisms.findIndex((p) => p.seriesIndex === point.seriesIndex && p.pointIndex === point.pointIndex);
|
|
1061
|
-
return {
|
|
1062
|
-
group: meshes.group,
|
|
1063
|
-
targets: meshes.meshes,
|
|
1064
|
-
pointAt(object) {
|
|
1065
|
-
const data = object.userData;
|
|
1066
|
-
return data.seriesIndex === void 0 || data.pointIndex === void 0 ? null : {
|
|
1067
|
-
seriesIndex: data.seriesIndex,
|
|
1068
|
-
pointIndex: data.pointIndex
|
|
1069
|
-
};
|
|
1070
|
-
},
|
|
1071
|
-
anchor(point, value) {
|
|
1072
|
-
const prism = prisms[indexOf(point)];
|
|
1073
|
-
if (!prism) return [
|
|
1074
|
-
0,
|
|
1075
|
-
perspValueY(layout, value),
|
|
1076
|
-
0
|
|
1077
|
-
];
|
|
1078
|
-
const along = prism.outline.map(([x, y]) => layout.horizontal ? y : x);
|
|
1079
|
-
const mid = (Math.min(...along) + Math.max(...along)) / 2;
|
|
1080
|
-
const v = perspValueY(layout, value);
|
|
1081
|
-
const z = (prism.z0 + prism.z1) / 2;
|
|
1082
|
-
return layout.horizontal ? [
|
|
1083
|
-
v,
|
|
1084
|
-
mid,
|
|
1085
|
-
z
|
|
1086
|
-
] : [
|
|
1087
|
-
mid,
|
|
1088
|
-
v,
|
|
1089
|
-
z
|
|
1090
|
-
];
|
|
1091
|
-
},
|
|
1092
|
-
draggable: layout.grouping === "clustered" || layout.grouping === "standard",
|
|
1093
|
-
preview(point, value) {
|
|
1094
|
-
const i = indexOf(point);
|
|
1095
|
-
const next = buildPerspPrisms(chartData, layout, {
|
|
1096
|
-
...point,
|
|
1097
|
-
value
|
|
1098
|
-
})[i];
|
|
1099
|
-
if (next) meshes.setPrism(i, next);
|
|
1100
|
-
},
|
|
1101
|
-
dispose: () => meshes.dispose()
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
const SELECTED_COLOR$1 = 2450411;
|
|
1105
|
-
function surfaceLegend(chartData, layout, palette, background, textStyle) {
|
|
1106
|
-
return surfaceBands(chartData, layout.range, palette).map((band) => ({
|
|
1107
|
-
label: band.label,
|
|
1108
|
-
color: band.color,
|
|
1109
|
-
...textStyle ? { textStyle } : {},
|
|
1110
|
-
...chartData.wireframe ? { lineSwatch: { primitives: [{
|
|
1111
|
-
kind: "rect",
|
|
1112
|
-
x: 0,
|
|
1113
|
-
y: -7,
|
|
1114
|
-
w: 10,
|
|
1115
|
-
h: 10,
|
|
1116
|
-
fill: band.color
|
|
1117
|
-
}, {
|
|
1118
|
-
kind: "rect",
|
|
1119
|
-
x: 1,
|
|
1120
|
-
y: -6,
|
|
1121
|
-
w: 8,
|
|
1122
|
-
h: 8,
|
|
1123
|
-
fill: background
|
|
1124
|
-
}] } } : {}
|
|
1125
|
-
}));
|
|
1126
|
-
}
|
|
1127
|
-
function mountPerspChartView(input, ctx) {
|
|
1128
|
-
const three = ctx.three;
|
|
1129
|
-
const { vm, layout, chartData } = input;
|
|
1130
|
-
const palette = chartData.colorPalette?.length ? chartData.colorPalette : DEFAULT_PALETTE;
|
|
1131
|
-
const camera = buildPerspCamera(three, layout.view, vm.svgWidth, vm.svgHeight);
|
|
1132
|
-
const scene = new three.Scene();
|
|
1133
|
-
const marks = buildPerspMarkSet(three, chartData, layout, palette);
|
|
1134
|
-
scene.add(marks.group);
|
|
1135
|
-
const markerGeometry = new three.OctahedronGeometry(layout.view.box.w * .012);
|
|
1136
|
-
const markerMaterial = new three.MeshBasicMaterial({ color: SELECTED_COLOR$1 });
|
|
1137
|
-
const marker = new three.Mesh(markerGeometry, markerMaterial);
|
|
1138
|
-
marker.visible = false;
|
|
1139
|
-
marks.group.add(marker);
|
|
1140
|
-
const overlay = renderChart3DChromeOverlaySvg(ctx.document, vm, {
|
|
1141
|
-
labels: layout.labels,
|
|
1142
|
-
reverseLegend: layout.horizontal && layout.grouping === "clustered",
|
|
1143
|
-
legend: layout.kind === "surface" ? surfaceLegend(chartData, layout, palette, vm.areaFill ?? "#ffffff", vm.legend[0]?.textStyle) : void 0
|
|
1144
|
-
});
|
|
1145
|
-
ctx.overlay.appendChild(overlay);
|
|
1146
|
-
const valueAt = (point) => {
|
|
1147
|
-
const v = chartData.series[point.seriesIndex]?.values[point.pointIndex];
|
|
1148
|
-
return v !== void 0 && Number.isFinite(v) ? v : void 0;
|
|
1149
|
-
};
|
|
1150
|
-
const placeMarker = (point, value) => {
|
|
1151
|
-
marker.position.set(...marks.anchor(point, value));
|
|
1152
|
-
};
|
|
1153
|
-
const interaction = attachChart3DMarkInteraction({
|
|
1154
|
-
ctx,
|
|
1155
|
-
camera,
|
|
1156
|
-
scene,
|
|
1157
|
-
svgHeight: vm.svgHeight,
|
|
1158
|
-
chartData,
|
|
1159
|
-
categoryLabels: input.categoryLabels,
|
|
1160
|
-
seriesNames: chartData.series.map((s) => s.name),
|
|
1161
|
-
adapter: {
|
|
1162
|
-
targets: marks.targets,
|
|
1163
|
-
pointAt: (hit) => marks.pointAt(hit.object, marks.group.worldToLocal(hit.point.clone())),
|
|
1164
|
-
valueOf: valueAt,
|
|
1165
|
-
beginDrag(point) {
|
|
1166
|
-
const start = valueAt(point);
|
|
1167
|
-
if (!marks.draggable || start === void 0) return null;
|
|
1168
|
-
const unit = layout.range.majorUnit || 1;
|
|
1169
|
-
const a = marks.anchor(point, start);
|
|
1170
|
-
const step = unit * layout.valueScale;
|
|
1171
|
-
const b = layout.horizontal ? [
|
|
1172
|
-
a[0] + step,
|
|
1173
|
-
a[1],
|
|
1174
|
-
a[2]
|
|
1175
|
-
] : [
|
|
1176
|
-
a[0],
|
|
1177
|
-
a[1] + step,
|
|
1178
|
-
a[2]
|
|
1179
|
-
];
|
|
1180
|
-
const s0 = perspToScreen(layout.view, a);
|
|
1181
|
-
const s1 = perspToScreen(layout.view, b);
|
|
1182
|
-
const ux = (s1.x - s0.x) / unit;
|
|
1183
|
-
const uy = (s1.y - s0.y) / unit;
|
|
1184
|
-
const len2 = ux * ux + uy * uy || 1;
|
|
1185
|
-
const span = layout.range.max - layout.range.min;
|
|
1186
|
-
return { preview(dx, dy) {
|
|
1187
|
-
const value = roundDragValue(start + (dx * ux + dy * uy) / len2, {
|
|
1188
|
-
min: layout.range.min,
|
|
1189
|
-
max: layout.range.max,
|
|
1190
|
-
span
|
|
1191
|
-
});
|
|
1192
|
-
marks.preview(point, value);
|
|
1193
|
-
if (marker.visible) placeMarker(point, value);
|
|
1194
|
-
return value;
|
|
1195
|
-
} };
|
|
1196
|
-
},
|
|
1197
|
-
highlight(point) {
|
|
1198
|
-
const value = point ? valueAt(point) : void 0;
|
|
1199
|
-
marker.visible = point !== null && value !== void 0;
|
|
1200
|
-
if (point && value !== void 0) placeMarker(point, value);
|
|
1201
|
-
},
|
|
1202
|
-
dispose() {
|
|
1203
|
-
markerGeometry.dispose();
|
|
1204
|
-
markerMaterial.dispose();
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
});
|
|
1208
|
-
let disposed = false;
|
|
1209
|
-
return {
|
|
1210
|
-
render(renderer) {
|
|
1211
|
-
renderer.render(scene, camera);
|
|
1212
|
-
},
|
|
1213
|
-
resize() {},
|
|
1214
|
-
isAnimating: () => false,
|
|
1215
|
-
setInteractive: (on) => interaction.setInteractive(on),
|
|
1216
|
-
setSelectedPart: (part) => interaction.setSelectedPart(part),
|
|
1217
|
-
dispose() {
|
|
1218
|
-
if (disposed) return;
|
|
1219
|
-
disposed = true;
|
|
1220
|
-
interaction.dispose();
|
|
1221
|
-
marks.dispose();
|
|
1222
|
-
overlay.remove();
|
|
1223
|
-
}
|
|
1224
|
-
};
|
|
1225
|
-
}
|
|
1226
|
-
const SEAM_COLOR = 16777215;
|
|
1227
|
-
const SEGMENTS_PER_TURN = 144;
|
|
1228
|
-
function pieFaceShade(nx, ny, nz, cut = false) {
|
|
1229
|
-
if (ny > .5) return 1;
|
|
1230
|
-
if (ny < -.5) return .3;
|
|
1231
|
-
const phi = Math.atan2(nx, -nz);
|
|
1232
|
-
if (cut) return .5 + .2 * Math.sin(phi);
|
|
1233
|
-
return Math.max(.385, .57 - .275 * phi);
|
|
1234
|
-
}
|
|
1235
|
-
function sliceBuffers(three, layout, slice) {
|
|
1236
|
-
const base = new three.Color(slice.color).getRGB(new three.Color(), three.SRGBColorSpace);
|
|
1237
|
-
const out = new three.Color();
|
|
1238
|
-
const positions = [];
|
|
1239
|
-
const colors = [];
|
|
1240
|
-
const cx = layout.center.x + slice.offset.x;
|
|
1241
|
-
const cz = layout.center.z + slice.offset.z;
|
|
1242
|
-
const top = layout.center.y;
|
|
1243
|
-
const r = layout.radius;
|
|
1244
|
-
const push = (p, n, cut = false) => {
|
|
1245
|
-
const f = pieFaceShade(n[0], n[1], n[2], cut);
|
|
1246
|
-
out.setRGB(Math.min(1, base.r * f), Math.min(1, base.g * f), Math.min(1, base.b * f), three.SRGBColorSpace);
|
|
1247
|
-
positions.push(...p);
|
|
1248
|
-
colors.push(out.r, out.g, out.b);
|
|
1249
|
-
};
|
|
1250
|
-
const span = slice.endAngle - slice.startAngle;
|
|
1251
|
-
const steps = Math.max(2, Math.ceil(span / (Math.PI * 2) * SEGMENTS_PER_TURN));
|
|
1252
|
-
const angles = Array.from({ length: steps + 1 }, (_, i) => slice.startAngle + span * i / steps);
|
|
1253
|
-
const rim = (a, y) => {
|
|
1254
|
-
const p = pieRimPoint(cx, cz, r, a);
|
|
1255
|
-
return [
|
|
1256
|
-
p.x,
|
|
1257
|
-
y,
|
|
1258
|
-
p.z
|
|
1259
|
-
];
|
|
1260
|
-
};
|
|
1261
|
-
const radial = (a) => [
|
|
1262
|
-
Math.sin(a),
|
|
1263
|
-
0,
|
|
1264
|
-
Math.cos(a)
|
|
1265
|
-
];
|
|
1266
|
-
for (let i = 0; i < steps; i++) {
|
|
1267
|
-
const a0 = angles[i];
|
|
1268
|
-
const a1 = angles[i + 1];
|
|
1269
|
-
push([
|
|
1270
|
-
cx,
|
|
1271
|
-
top,
|
|
1272
|
-
cz
|
|
1273
|
-
], [
|
|
1274
|
-
0,
|
|
1275
|
-
1,
|
|
1276
|
-
0
|
|
1277
|
-
]);
|
|
1278
|
-
push(rim(a0, top), [
|
|
1279
|
-
0,
|
|
1280
|
-
1,
|
|
1281
|
-
0
|
|
1282
|
-
]);
|
|
1283
|
-
push(rim(a1, top), [
|
|
1284
|
-
0,
|
|
1285
|
-
1,
|
|
1286
|
-
0
|
|
1287
|
-
]);
|
|
1288
|
-
push([
|
|
1289
|
-
cx,
|
|
1290
|
-
0,
|
|
1291
|
-
cz
|
|
1292
|
-
], [
|
|
1293
|
-
0,
|
|
1294
|
-
-1,
|
|
1295
|
-
0
|
|
1296
|
-
]);
|
|
1297
|
-
push(rim(a1, 0), [
|
|
1298
|
-
0,
|
|
1299
|
-
-1,
|
|
1300
|
-
0
|
|
1301
|
-
]);
|
|
1302
|
-
push(rim(a0, 0), [
|
|
1303
|
-
0,
|
|
1304
|
-
-1,
|
|
1305
|
-
0
|
|
1306
|
-
]);
|
|
1307
|
-
push(rim(a0, top), radial(a0));
|
|
1308
|
-
push(rim(a0, 0), radial(a0));
|
|
1309
|
-
push(rim(a1, 0), radial(a1));
|
|
1310
|
-
push(rim(a0, top), radial(a0));
|
|
1311
|
-
push(rim(a1, 0), radial(a1));
|
|
1312
|
-
push(rim(a1, top), radial(a1));
|
|
1313
|
-
}
|
|
1314
|
-
for (const [a, sign] of [[slice.startAngle, -1], [slice.endAngle, 1]]) {
|
|
1315
|
-
const n = [
|
|
1316
|
-
sign * Math.cos(a),
|
|
1317
|
-
0,
|
|
1318
|
-
-sign * Math.sin(a)
|
|
1319
|
-
];
|
|
1320
|
-
push([
|
|
1321
|
-
cx,
|
|
1322
|
-
top,
|
|
1323
|
-
cz
|
|
1324
|
-
], n, true);
|
|
1325
|
-
push([
|
|
1326
|
-
cx,
|
|
1327
|
-
0,
|
|
1328
|
-
cz
|
|
1329
|
-
], n, true);
|
|
1330
|
-
push(rim(a, 0), n, true);
|
|
1331
|
-
push([
|
|
1332
|
-
cx,
|
|
1333
|
-
top,
|
|
1334
|
-
cz
|
|
1335
|
-
], n, true);
|
|
1336
|
-
push(rim(a, 0), n, true);
|
|
1337
|
-
push(rim(a, top), n, true);
|
|
1338
|
-
}
|
|
1339
|
-
const seams = [];
|
|
1340
|
-
for (let i = 0; i < steps; i++) seams.push(...rim(angles[i], top), ...rim(angles[i + 1], top));
|
|
1341
|
-
for (const a of [slice.startAngle, slice.endAngle]) seams.push(cx, top, cz, ...rim(a, top), ...rim(a, top), ...rim(a, 0));
|
|
1342
|
-
return {
|
|
1343
|
-
positions,
|
|
1344
|
-
colors,
|
|
1345
|
-
seams
|
|
1346
|
-
};
|
|
1347
|
-
}
|
|
1348
|
-
function buildPieMarks(three, layout) {
|
|
1349
|
-
const group = new three.Group();
|
|
1350
|
-
group.matrixAutoUpdate = false;
|
|
1351
|
-
group.matrix.copy(perspBoxMatrix(three, layout.view));
|
|
1352
|
-
const material = new three.MeshBasicMaterial({
|
|
1353
|
-
vertexColors: true,
|
|
1354
|
-
side: three.DoubleSide,
|
|
1355
|
-
polygonOffset: true,
|
|
1356
|
-
polygonOffsetFactor: 1,
|
|
1357
|
-
polygonOffsetUnits: 1
|
|
1358
|
-
});
|
|
1359
|
-
const seamMaterial = new three.LineBasicMaterial({ color: SEAM_COLOR });
|
|
1360
|
-
const meshes = [];
|
|
1361
|
-
let seams = null;
|
|
1362
|
-
const clear = () => {
|
|
1363
|
-
for (const mesh of meshes) {
|
|
1364
|
-
group.remove(mesh);
|
|
1365
|
-
mesh.geometry.dispose();
|
|
1366
|
-
}
|
|
1367
|
-
if (seams) {
|
|
1368
|
-
group.remove(seams);
|
|
1369
|
-
seams.geometry.dispose();
|
|
1370
|
-
}
|
|
1371
|
-
};
|
|
1372
|
-
const build = (slices) => {
|
|
1373
|
-
clear();
|
|
1374
|
-
const seamPoints = [];
|
|
1375
|
-
const built = slices.map((slice) => {
|
|
1376
|
-
const b = sliceBuffers(three, layout, slice);
|
|
1377
|
-
const geometry = new three.BufferGeometry();
|
|
1378
|
-
geometry.setAttribute("position", new three.Float32BufferAttribute(b.positions, 3));
|
|
1379
|
-
geometry.setAttribute("color", new three.Float32BufferAttribute(b.colors, 3));
|
|
1380
|
-
const mesh = new three.Mesh(geometry, material);
|
|
1381
|
-
mesh.userData = {
|
|
1382
|
-
seriesIndex: 0,
|
|
1383
|
-
pointIndex: slice.pointIndex
|
|
1384
|
-
};
|
|
1385
|
-
group.add(mesh);
|
|
1386
|
-
seamPoints.push(...b.seams);
|
|
1387
|
-
return mesh;
|
|
1388
|
-
});
|
|
1389
|
-
meshes.splice(0, meshes.length, ...built);
|
|
1390
|
-
const seamGeometry = new three.BufferGeometry();
|
|
1391
|
-
seamGeometry.setAttribute("position", new three.Float32BufferAttribute(seamPoints, 3));
|
|
1392
|
-
seams = new three.LineSegments(seamGeometry, seamMaterial);
|
|
1393
|
-
group.add(seams);
|
|
1394
|
-
};
|
|
1395
|
-
build(layout.slices);
|
|
1396
|
-
return {
|
|
1397
|
-
group,
|
|
1398
|
-
meshes,
|
|
1399
|
-
setSlices: build,
|
|
1400
|
-
dispose() {
|
|
1401
|
-
clear();
|
|
1402
|
-
material.dispose();
|
|
1403
|
-
seamMaterial.dispose();
|
|
1404
|
-
}
|
|
1405
|
-
};
|
|
1406
|
-
}
|
|
1407
|
-
const SELECTED_COLOR = 2450411;
|
|
1408
|
-
function slicesWith(input, pointIndex, value) {
|
|
1409
|
-
const series = input.chartData.series.map((s, i) => i === 0 ? {
|
|
1410
|
-
...s,
|
|
1411
|
-
values: s.values.map((v, j) => j === pointIndex ? value : v)
|
|
1412
|
-
} : s);
|
|
1413
|
-
const element = {
|
|
1414
|
-
...input.element,
|
|
1415
|
-
chartData: {
|
|
1416
|
-
...input.chartData,
|
|
1417
|
-
series
|
|
1418
|
-
}
|
|
1419
|
-
};
|
|
1420
|
-
return computePieChartLayout(element, input.vm)?.slices ?? input.layout.slices;
|
|
1421
|
-
}
|
|
1422
|
-
function mountPieChartView(input, ctx) {
|
|
1423
|
-
const three = ctx.three;
|
|
1424
|
-
const { vm, layout, chartData } = input;
|
|
1425
|
-
const camera = buildPerspCamera(three, layout.view, vm.svgWidth, vm.svgHeight);
|
|
1426
|
-
const scene = new three.Scene();
|
|
1427
|
-
const marks = buildPieMarks(three, layout);
|
|
1428
|
-
scene.add(marks.group);
|
|
1429
|
-
const markerGeometry = new three.OctahedronGeometry(.03);
|
|
1430
|
-
const markerMaterial = new three.MeshBasicMaterial({ color: SELECTED_COLOR });
|
|
1431
|
-
const marker = new three.Mesh(markerGeometry, markerMaterial);
|
|
1432
|
-
marker.visible = false;
|
|
1433
|
-
marks.group.add(marker);
|
|
1434
|
-
const overlay = renderChart3DChromeOverlaySvg(ctx.document, vm, {
|
|
1435
|
-
labels: [],
|
|
1436
|
-
reverseLegend: false
|
|
1437
|
-
});
|
|
1438
|
-
ctx.overlay.appendChild(overlay);
|
|
1439
|
-
const values = () => chartData.series[0]?.values.slice() ?? [];
|
|
1440
|
-
const interaction = attachChart3DMarkInteraction({
|
|
1441
|
-
ctx,
|
|
1442
|
-
camera,
|
|
1443
|
-
scene,
|
|
1444
|
-
svgHeight: vm.svgHeight,
|
|
1445
|
-
chartData,
|
|
1446
|
-
categoryLabels: input.categoryLabels,
|
|
1447
|
-
seriesNames: chartData.series.map((s) => s.name),
|
|
1448
|
-
adapter: {
|
|
1449
|
-
targets: marks.meshes,
|
|
1450
|
-
pointAt(hit) {
|
|
1451
|
-
const pointIndex = hit.object.userData.pointIndex;
|
|
1452
|
-
return pointIndex === void 0 ? null : {
|
|
1453
|
-
seriesIndex: 0,
|
|
1454
|
-
pointIndex
|
|
1455
|
-
};
|
|
1456
|
-
},
|
|
1457
|
-
valueOf(point) {
|
|
1458
|
-
const v = chartData.series[0]?.values[point.pointIndex];
|
|
1459
|
-
return v !== void 0 && Number.isFinite(v) ? v : void 0;
|
|
1460
|
-
},
|
|
1461
|
-
beginDrag(point) {
|
|
1462
|
-
const slice = layout.slices[point.pointIndex];
|
|
1463
|
-
if (!slice) return null;
|
|
1464
|
-
const start = values();
|
|
1465
|
-
return { preview(_dx, _dy, at) {
|
|
1466
|
-
const hit = perspScreenToPlaneY(layout.view, at.x, at.y, layout.center.y);
|
|
1467
|
-
if (!hit) return start[point.pointIndex];
|
|
1468
|
-
const angle = Math.atan2(hit.x - layout.center.x - slice.offset.x, hit.z - layout.center.z - slice.offset.z);
|
|
1469
|
-
const value = resolvePieSliceShareValue(angle, slice.startAngle, start, point.pointIndex);
|
|
1470
|
-
marks.setSlices(slicesWith(input, point.pointIndex, value));
|
|
1471
|
-
return value;
|
|
1472
|
-
} };
|
|
1473
|
-
},
|
|
1474
|
-
highlight(point) {
|
|
1475
|
-
const slice = point ? layout.slices[point.pointIndex] : void 0;
|
|
1476
|
-
marker.visible = slice !== void 0;
|
|
1477
|
-
if (slice) {
|
|
1478
|
-
const mid = (slice.startAngle + slice.endAngle) / 2;
|
|
1479
|
-
const p = pieRimPoint(layout.center.x + slice.offset.x, layout.center.z + slice.offset.z, layout.radius * .6, mid);
|
|
1480
|
-
marker.position.set(p.x, layout.center.y, p.z);
|
|
1481
|
-
}
|
|
1482
|
-
},
|
|
1483
|
-
dispose() {
|
|
1484
|
-
markerGeometry.dispose();
|
|
1485
|
-
markerMaterial.dispose();
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
});
|
|
1489
|
-
let disposed = false;
|
|
1490
|
-
return {
|
|
1491
|
-
render(renderer) {
|
|
1492
|
-
renderer.render(scene, camera);
|
|
1493
|
-
},
|
|
1494
|
-
resize() {},
|
|
1495
|
-
isAnimating: () => false,
|
|
1496
|
-
setInteractive: (on) => interaction.setInteractive(on),
|
|
1497
|
-
setSelectedPart: (part) => interaction.setSelectedPart(part),
|
|
1498
|
-
dispose() {
|
|
1499
|
-
if (disposed) return;
|
|
1500
|
-
disposed = true;
|
|
1501
|
-
interaction.dispose();
|
|
1502
|
-
marks.dispose();
|
|
1503
|
-
overlay.remove();
|
|
1504
|
-
}
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
function buildObliqueCamera(three, svgWidth, svgHeight, shearX, shearY) {
|
|
1508
|
-
const halfW = svgWidth / 2;
|
|
1509
|
-
const halfH = svgHeight / 2;
|
|
1510
|
-
const distance = halfW + halfH + 500;
|
|
1511
|
-
const camera = new three.OrthographicCamera(-halfW, halfW, halfH, -halfH, 1, distance + 2e3);
|
|
1512
|
-
camera.position.set(0, 0, distance);
|
|
1513
|
-
camera.lookAt(0, 0, 0);
|
|
1514
|
-
camera.updateProjectionMatrix();
|
|
1515
|
-
const m = camera.projectionMatrix.elements;
|
|
1516
|
-
const ndcScaleX = m[0];
|
|
1517
|
-
const ndcScaleY = m[5];
|
|
1518
|
-
m[8] = -shearX * ndcScaleX;
|
|
1519
|
-
m[9] = shearY * ndcScaleY;
|
|
1520
|
-
m[12] += m[8] * distance;
|
|
1521
|
-
m[13] += m[9] * distance;
|
|
1522
|
-
camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
|
|
1523
|
-
return camera;
|
|
1524
|
-
}
|
|
1525
|
-
async function mountChart3DView(spec, ctx) {
|
|
1526
|
-
if (spec.geometry?.kind === "pie") {
|
|
1527
|
-
const chartData = spec.element.chartData;
|
|
1528
|
-
if (!chartData) throw new Error("3D chart spec without chart data");
|
|
1529
|
-
return mountPieChartView({
|
|
1530
|
-
element: spec.element,
|
|
1531
|
-
vm: widenPieViewModel(spec.vm, spec.element),
|
|
1532
|
-
layout: spec.geometry.layout,
|
|
1533
|
-
chartData,
|
|
1534
|
-
categoryLabels: spec.categoryLabels
|
|
1535
|
-
}, ctx);
|
|
1536
|
-
}
|
|
1537
|
-
if (spec.geometry?.kind === "perspective") {
|
|
1538
|
-
const chartData = spec.element.chartData;
|
|
1539
|
-
if (!chartData) throw new Error("3D chart spec without chart data");
|
|
1540
|
-
return mountPerspChartView({
|
|
1541
|
-
vm: spec.vm,
|
|
1542
|
-
layout: spec.geometry.layout,
|
|
1543
|
-
chartData,
|
|
1544
|
-
categoryLabels: spec.categoryLabels
|
|
1545
|
-
}, ctx);
|
|
1546
|
-
}
|
|
1547
|
-
if (spec.projection.mode !== "oblique" || spec.geometry?.kind !== "oblique") throw new Error(`no 3D chart scene for chartType=${spec.chartType}`);
|
|
1548
|
-
const three = ctx.three;
|
|
1549
|
-
const { vm, geometry } = spec;
|
|
1550
|
-
const chartData = spec.element.chartData;
|
|
1551
|
-
if (!chartData) throw new Error("3D chart spec without chart data");
|
|
1552
|
-
const camera = buildObliqueCamera(three, vm.svgWidth, vm.svgHeight, spec.projection.shearX, spec.projection.shearY);
|
|
1553
|
-
const scene = new three.Scene();
|
|
1554
|
-
const { layout } = geometry;
|
|
1555
|
-
const bars = buildChart3DBarMeshes(three, layout, vm.svgWidth, vm.svgHeight);
|
|
1556
|
-
scene.add(bars.group);
|
|
1557
|
-
const overlay = renderChart3DChromeOverlaySvg(ctx.document, vm, {
|
|
1558
|
-
labels: layout.labels,
|
|
1559
|
-
reverseLegend: layout.horizontal && layout.grouping === "clustered"
|
|
1560
|
-
});
|
|
1561
|
-
ctx.overlay.appendChild(overlay);
|
|
1562
|
-
const interaction = attachObliqueBarInteraction({
|
|
1563
|
-
ctx,
|
|
1564
|
-
camera,
|
|
1565
|
-
meshes: bars.meshes,
|
|
1566
|
-
layout,
|
|
1567
|
-
svgWidth: vm.svgWidth,
|
|
1568
|
-
svgHeight: vm.svgHeight,
|
|
1569
|
-
chartData,
|
|
1570
|
-
categoryLabels: spec.categoryLabels,
|
|
1571
|
-
seriesNames: chartData.series.map((series) => series.name),
|
|
1572
|
-
scene
|
|
1573
|
-
});
|
|
1574
|
-
let disposed = false;
|
|
1575
|
-
return {
|
|
1576
|
-
render(renderer) {
|
|
1577
|
-
renderer.render(scene, camera);
|
|
1578
|
-
},
|
|
1579
|
-
resize() {},
|
|
1580
|
-
isAnimating: () => false,
|
|
1581
|
-
setInteractive: (on) => interaction.setInteractive(on),
|
|
1582
|
-
setSelectedPart: (part) => interaction.setSelectedPart(part),
|
|
1583
|
-
dispose() {
|
|
1584
|
-
if (disposed) return;
|
|
1585
|
-
disposed = true;
|
|
1586
|
-
interaction.dispose();
|
|
1587
|
-
bars.dispose();
|
|
1588
|
-
overlay.remove();
|
|
1589
|
-
}
|
|
1590
|
-
};
|
|
1591
|
-
}
|
|
1592
|
-
export { mountChart3DView };
|
|
1593
|
-
|
|
1594
|
-
//# sourceMappingURL=pptx-angular-viewer-chart-3d-view-scene-IqColhvd.mjs.map
|