Demonstrates a Angular Parallel Coordinate Plot, which visualises multivariate data as polylines crossing one vertical axis per dimension. Switch between a multi-series chart and a single-series chart of 100,000 records.
This chart type is also known as a Parallel Coordinates Chart or a PCP
drawExample.ts
index.tsx
theme.ts
ParallelCoordinateAxisReorderModifier.ts
ParallelCoordinateCursorModifier.ts
ParallelCoordinateDataSource.ts
ParallelCoordinatePlotHighlightModifier.ts
ParallelCoordinateRecordSelectionModifier.ts
ParallelCoordinateSingleHighlightModifier.ts
parallelCoordinateTooltipDataTemplate.ts
SplineLineHitTestProvider.ts
1import {
2 EAxisType,
3 EXyDirection,
4 IRenderableSeries,
5 MouseWheelZoomModifier,
6 NumberRange,
7 NumericAxis,
8 RubberBandXyZoomModifier,
9 SciChartSurface,
10 SeriesSelectionModifier,
11 SplineLineRenderableSeries,
12 ZoomExtentsModifier,
13 ZoomPanModifier,
14} from "scichart";
15import { appTheme } from "../../../theme";
16import { ParallelCoordinateAxisReorderModifier } from "./ParallelCoordinateAxisReorderModifier";
17import { ParallelCoordinateCursorModifier } from "./ParallelCoordinateCursorModifier";
18import { ParallelCoordinateDataSource, PCP_HIDDEN_YAXIS_ID } from "./ParallelCoordinateDataSource";
19import { ParallelCoordinatePlotHighlightModifier } from "./ParallelCoordinatePlotHighlightModifier";
20import { ParallelCoordinateRecordSelectionModifier } from "./ParallelCoordinateRecordSelectionModifier";
21import { ParallelCoordinateSingleHighlightModifier } from "./ParallelCoordinateSingleHighlightModifier";
22import { SplineLineHitTestProvider } from "./SplineLineHitTestProvider";
23import { createParallelCoordinateTooltipDataTemplate } from "./parallelCoordinateTooltipDataTemplate";
24
25const originList = ["USA", "Europe", "Japan"];
26
27// Per-origin engine profiles reproduce the classic auto-mpg relationships:
28// USA cars trend to big engines / low mpg, European and Japanese cars to smaller engines / higher mpg.
29const originProfiles: Record<string, { minCyl: number; maxCyl: number }> = {
30 USA: { minCyl: 6, maxCyl: 8 },
31 Europe: { minCyl: 4, maxCyl: 6 },
32 Japan: { minCyl: 3, maxCyl: 4 },
33};
34
35const originColors: Record<string, string> = {
36 USA: appTheme.VividOrange,
37 Europe: appTheme.VividSkyBlue,
38 Japan: appTheme.VividTeal,
39};
40
41// Mulberry32 - a small deterministic PRNG so the generated dataset is reproducible
42const createSeededRandom = (seed: number) => {
43 let state = seed;
44 return () => {
45 state |= 0;
46 state = (state + 0x6d2b79f5) | 0;
47 let t = Math.imul(state ^ (state >>> 15), 1 | state);
48 t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
49 return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
50 };
51};
52
53interface ICarsData {
54 origins: string[];
55 mpg: number[];
56 cylinders: number[];
57 displacement: number[];
58 horsepower: number[];
59 weight: number[];
60 acceleration: number[];
61}
62
63// Generate a synthetic "Cars" dataset of recordCount records. Uses a seeded PRNG so the data is reproducible.
64const generateData = (recordCount: number): ICarsData => {
65 const random = createSeededRandom(1972);
66 const data: ICarsData = {
67 origins: [],
68 mpg: [],
69 cylinders: [],
70 displacement: [],
71 horsepower: [],
72 weight: [],
73 acceleration: [],
74 };
75 for (let i = 0; i < recordCount; i++) {
76 const origin = originList[i % originList.length];
77 const profile = originProfiles[origin];
78 // Correlated values: cylinders drives displacement -> horsepower -> weight; mpg and acceleration fall
79 // as the engine grows.
80 const cyl = Math.round(profile.minCyl + random() * (profile.maxCyl - profile.minCyl));
81 const disp = Math.round((60 + cyl * 45) * (0.85 + random() * 0.3));
82 const hp = Math.round(disp * (0.4 + random() * 0.25));
83 const wt = Math.round(1400 + disp * 6 * (0.85 + random() * 0.3));
84 const mpgVal = Math.max(9, Math.round(46 - hp * 0.12 - cyl * 1.5 + random() * 6));
85 const accel = Math.round((26 - hp * 0.05 + random() * 4) * 10) / 10;
86 data.origins.push(origin);
87 data.cylinders.push(cyl);
88 data.displacement.push(disp);
89 data.horsepower.push(hp);
90 data.weight.push(wt);
91 data.mpg.push(mpgVal);
92 data.acceleration.push(accel);
93 }
94 return data;
95};
96
97const addCategories = (source: ParallelCoordinateDataSource, data: ICarsData): void => {
98 source.addCategory("mpg", data.mpg, EAxisType.NumericAxis);
99 source.addCategory("cylinders", data.cylinders, EAxisType.NumericAxis);
100 source.addCategory("displacement", data.displacement, EAxisType.NumericAxis);
101 source.addCategory("horsepower", data.horsepower, EAxisType.NumericAxis);
102 source.addCategory("weight", data.weight, EAxisType.NumericAxis);
103 source.addCategory("acceleration", data.acceleration, EAxisType.NumericAxis);
104 source.addCategory("origin", data.origins, EAxisType.CategoryAxis);
105};
106
107export const MULTI_SERIES_RECORD_COUNT = 100;
108export const SINGLE_SERIES_RECORD_COUNT = 100000;
109
110/**
111 * Multi-series: one renderable series per record (100 records). Each line can be hovered/clicked with
112 * the {@link SeriesSelectionModifier}, which highlights that record via onSelectedChanged/onHoveredChanged.
113 * Drag a range on one of the category axes with the {@link ParallelCoordinatePlotHighlightModifier} to highlight
114 * all records passing through that range (click to clear).
115 */
116export const drawMultiSeriesExample = async (rootElement: string | HTMLDivElement, interpolate: boolean = false) => {
117 const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
118 theme: appTheme.SciChartJsTheme,
119 });
120 const xAxis = new NumericAxis(wasmContext, { drawLabels: false, growBy: new NumberRange(0.07, 0.07) });
121 sciChartSurface.xAxes.add(xAxis);
122
123 const carsData = generateData(MULTI_SERIES_RECORD_COUNT);
124 const data = new ParallelCoordinateDataSource(wasmContext, sciChartSurface, {
125 useSingleSeries: false,
126 pointMarkerSize: 0,
127 });
128 addCategories(data, carsData);
129
130 // Bold + opaque when selected or hovered, faint otherwise; keeps the origin colour.
131 const applySelectionStyle = (series: IRenderableSeries) => {
132 series.strokeThickness = series.isSelected ? 4 : series.isHovered ? 3 : 2;
133 series.opacity = series.isSelected || series.isHovered ? 1 : 0.5;
134 };
135 data.createSeriesFn = (index) => ({
136 // interpolationPoints > 0 -> SplineLineRenderableSeries (smooth curves); 0 -> straight segments
137 interpolationPoints: interpolate ? 5 : 0,
138 stroke: originColors[carsData.origins[index]],
139 strokeThickness: 2,
140 opacity: 0.5,
141 onSelectedChanged: applySelectionStyle,
142 onHoveredChanged: applySelectionStyle,
143 });
144 data.build();
145
146 // Spline series render a curve that bows away from the straight chords the default line hit-test uses, so
147 // hover/selection would miss the visible line. Swap in a provider that tests against the spline points.
148 if (interpolate) {
149 data.renderableSeries.forEach((series) => {
150 if (series instanceof SplineLineRenderableSeries) {
151 series.hitTestProvider = new SplineLineHitTestProvider(series, wasmContext);
152 }
153 });
154 }
155
156 // Both left-drag modifiers share the same gesture, so they are exposed as independent toggles in the demo
157 // UI and both start disabled - enable at most one at a time.
158 const highlightModifier = new ParallelCoordinatePlotHighlightModifier(data);
159 highlightModifier.isEnabled = false;
160 // X-only rubber-band zoom: EXyDirection.XDirection makes getRubberBandRect span the full Y height, so only
161 // the horizontal drag extent matters.
162 const rubberBandZoomModifier = new RubberBandXyZoomModifier({ xyDirection: EXyDirection.XDirection });
163 rubberBandZoomModifier.isEnabled = false;
164 // Drag a category axis left/right to reorder the dimensions (reorders categories + rebuilds the source).
165 const axisReorderModifier = new ParallelCoordinateAxisReorderModifier(data);
166 axisReorderModifier.isEnabled = false;
167 const panModifier = new ZoomPanModifier({ xyDirection: EXyDirection.XDirection });
168 panModifier.isEnabled = false;
169 // Hover/click a record line to select it (drives the per-series onHovered/onSelectedChanged styling above).
170 const seriesSelectionModifier = new SeriesSelectionModifier({ enableHover: true, enableSelection: true });
171 seriesSelectionModifier.isEnabled = false;
172 // Hover tooltip showing the nearest record's values (independent of the left-drag group).
173 const cursorModifier = new ParallelCoordinateCursorModifier({
174 showAxisLabels: false,
175 showTooltip: true,
176 hitTestRadius: 2000,
177 tooltipDataTemplate: createParallelCoordinateTooltipDataTemplate(data),
178 isSvgOnly: true,
179 });
180 cursorModifier.isEnabled = false;
181
182 sciChartSurface.chartModifiers.add(
183 highlightModifier,
184 rubberBandZoomModifier,
185 axisReorderModifier,
186 panModifier,
187 new MouseWheelZoomModifier({ xyDirection: EXyDirection.XDirection }),
188 new ZoomExtentsModifier({ includedYAxisIds: [PCP_HIDDEN_YAXIS_ID] }),
189 seriesSelectionModifier,
190 cursorModifier
191 );
192
193 return {
194 wasmContext,
195 sciChartSurface,
196 highlightModifier,
197 rubberBandZoomModifier,
198 axisReorderModifier,
199 panModifier,
200 seriesSelectionModifier,
201 cursorModifier,
202 };
203};
204
205/**
206 * Single-series: all 100,000 records drawn by one renderable series (NaN gaps between records) for
207 * performance. The {@link ParallelCoordinateCursorModifier} hit-tests the nearest record and shows all its values.
208 */
209export const drawSingleSeriesExample = async (rootElement: string | HTMLDivElement, interpolate: boolean = false) => {
210 const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
211 theme: appTheme.SciChartJsTheme,
212 });
213 const xAxis = new NumericAxis(wasmContext, { drawLabels: false, growBy: new NumberRange(0.07, 0.07) });
214 sciChartSurface.xAxes.add(xAxis);
215
216 const carsData = generateData(SINGLE_SERIES_RECORD_COUNT);
217 const data = new ParallelCoordinateDataSource(wasmContext, sciChartSurface, { useSingleSeries: true });
218 addCategories(data, carsData);
219 data.createSeriesFn = (index) => ({
220 // interpolationPoints > 0 -> spline curves; 0 -> straight segments
221 interpolationPoints: interpolate ? 5 : 0,
222 stroke: originColors[carsData.origins[index]],
223 strokeThickness: 2,
224 opacity: 0.02,
225 });
226 data.build();
227
228 // In spline mode the drawn curve bows away from the straight chords the default hit-test uses, so swap in a
229 // provider that tests the spline points (and maps the hit back to a source point -> record for selection).
230 if (interpolate) {
231 const single = data.renderableSeries[0];
232 if (single instanceof SplineLineRenderableSeries) {
233 single.hitTestProvider = new SplineLineHitTestProvider(single, wasmContext);
234 }
235 }
236
237 // All modifiers start disabled and are driven by the demo UI toggles (see index.tsx).
238 const cursorModifier = new ParallelCoordinateCursorModifier({
239 showAxisLabels: false,
240 showTooltip: true,
241 // hit-test the nearest data point in coordinate space, then show every value for its record
242 hitTestRadius: 2000,
243 tooltipDataTemplate: createParallelCoordinateTooltipDataTemplate(data),
244 isSvgOnly: true,
245 });
246 cursorModifier.isEnabled = false;
247 // Left-drag group (mutually exclusive, enforced in the UI): pan, X-only rubber-band zoom, axis reorder.
248 const panModifier = new ZoomPanModifier({ xyDirection: EXyDirection.XDirection });
249 panModifier.isEnabled = false;
250 const rubberBandZoomModifier = new RubberBandXyZoomModifier({ xyDirection: EXyDirection.XDirection });
251 rubberBandZoomModifier.isEnabled = false;
252 const axisReorderModifier = new ParallelCoordinateAxisReorderModifier(data);
253 axisReorderModifier.isEnabled = false;
254 // Click a record polyline to select it: highlights that record and dims the rest via the palette provider
255 // (single-series has no per-record series, so selection is done by paletting a subset of data points).
256 const recordSelectionModifier = new ParallelCoordinateRecordSelectionModifier(data);
257 recordSelectionModifier.isEnabled = false;
258 // Range highlight: drag over a Y axis to highlight records passing through the range (palette-based).
259 const highlightModifier = new ParallelCoordinateSingleHighlightModifier(data);
260 highlightModifier.isEnabled = false;
261
262 sciChartSurface.chartModifiers.add(
263 cursorModifier,
264 panModifier,
265 rubberBandZoomModifier,
266 axisReorderModifier,
267 highlightModifier,
268 recordSelectionModifier,
269 new MouseWheelZoomModifier({ xyDirection: EXyDirection.XDirection }),
270 new ZoomExtentsModifier({ includedYAxisIds: [PCP_HIDDEN_YAXIS_ID] })
271 );
272
273 return {
274 wasmContext,
275 sciChartSurface,
276 cursorModifier,
277 panModifier,
278 rubberBandZoomModifier,
279 axisReorderModifier,
280 highlightModifier,
281 recordSelectionModifier,
282 };
283};
284
285export const drawExample = drawMultiSeriesExample;
286
Draw custom lit 3D geometry in Angular with SciChart.js using ImmediateLitMeshContext. Emit per-vertex positions, normals, texture coordinates and colours from a custom 3D scene entity.

Load a 3D model in Angular with SciChart.js. Add the model to a SciChart3DSurface as a 3D scene entity, frame it with the camera, then orbit and zoom it. Any 3D model can be loaded.