Creates a JavaScript Force Directed Graph using SciChart.js, visualizing US airport flight routes with a physics-based force simulation.
drawExample.ts
index.html
vanilla.ts
theme.ts
airportData.ts
nodeModifiers.ts
1import {
2 SciChartSurface,
3 NumericAxis,
4 XyDataSeries,
5 XyxyDataSeries,
6 XyScatterRenderableSeries,
7 FastLineSegmentRenderableSeries,
8 EllipsePointMarker,
9 NumberRange,
10 ZoomPanModifier,
11 ZoomExtentsModifier,
12 MouseWheelZoomModifier,
13 PinchZoomModifier,
14 EAutoRange,
15} from "scichart";
16import { appTheme } from "../../../theme";
17import {
18 SimNode,
19 SimEdge,
20 EdgeHoverState,
21 NodeTooltipModifier,
22 NodeDragModifier,
23 NodeHoverPaletteProvider,
24 DragStateRef,
25} from "./nodeModifiers";
26import { AIRPORTS, ROUTES } from "./airportData";
27
28// ─── Build simulation state ───────────────────────────────────────────────────
29
30function buildSimulation(): { nodes: SimNode[]; edges: SimEdge[] } {
31 const iataToIdx = new Map<string, number>();
32 const centerLon = -96;
33 const centerLat = 38;
34 const scaleX = 4.5;
35 const scaleY = 5.5;
36
37 const nodes: SimNode[] = AIRPORTS.map((a, i) => {
38 iataToIdx.set(a.iata, i);
39 const geoX = (a.lon - centerLon) * scaleX;
40 const geoY = (a.lat - centerLat) * scaleY;
41 return { iata: a.iata, label: `${a.iata} – ${a.city}, ${a.state}`, x: geoX, y: geoY, vx: 0, vy: 0, geoX, geoY };
42 });
43
44 const edges: SimEdge[] = ROUTES.map((r) => {
45 const si = iataToIdx.get(r.origin);
46 const ti = iataToIdx.get(r.destination);
47 if (si === undefined || ti === undefined) return null;
48 return { sourceIdx: si, targetIdx: ti };
49 }).filter((e): e is SimEdge => e !== null);
50
51 return { nodes, edges };
52}
53
54// ─── Force simulation tick ────────────────────────────────────────────────────
55
56const REPULSION_STRENGTH = -120;
57const REPULSION_MIN_DIST = 1;
58const SPRING_K = 0.3;
59const SPRING_REST_LENGTH = 20;
60const GEO_ANCHOR_STRENGTH = 0.12;
61const VELOCITY_DECAY = 0.6;
62
63function tick(nodes: SimNode[], edges: SimEdge[], alpha: number): void {
64 for (let i = 0; i < nodes.length; i++) {
65 for (let j = i + 1; j < nodes.length; j++) {
66 const dx = nodes[j].x - nodes[i].x;
67 const dy = nodes[j].y - nodes[i].y;
68 const dist = Math.max(Math.sqrt(dx * dx + dy * dy), REPULSION_MIN_DIST);
69 const force = (REPULSION_STRENGTH * alpha) / (dist * dist);
70 const fx = force * (dx / dist);
71 const fy = force * (dy / dist);
72 nodes[i].vx += fx;
73 nodes[i].vy += fy;
74 nodes[j].vx -= fx;
75 nodes[j].vy -= fy;
76 }
77 }
78
79 for (const edge of edges) {
80 const src = nodes[edge.sourceIdx];
81 const tgt = nodes[edge.targetIdx];
82 const dx = tgt.x + tgt.vx - (src.x + src.vx);
83 const dy = tgt.y + tgt.vy - (src.y + src.vy);
84 const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
85 const force = SPRING_K * (dist - SPRING_REST_LENGTH) * alpha;
86 const fx = force * (dx / dist);
87 const fy = force * (dy / dist);
88 src.vx += fx * 0.5;
89 src.vy += fy * 0.5;
90 tgt.vx -= fx * 0.5;
91 tgt.vy -= fy * 0.5;
92 }
93
94 for (const node of nodes) {
95 node.vx += (node.geoX - node.x) * GEO_ANCHOR_STRENGTH * alpha;
96 node.vy += (node.geoY - node.y) * GEO_ANCHOR_STRENGTH * alpha;
97 }
98
99 for (const node of nodes) {
100 node.vx *= VELOCITY_DECAY;
101 node.vy *= VELOCITY_DECAY;
102 node.x += node.vx;
103 node.y += node.vy;
104 }
105}
106
107// ─── Chart initialization ─────────────────────────────────────────────────────
108
109export const drawExample = async (rootElement: string | HTMLDivElement) => {
110 const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
111 theme: appTheme.SciChartJsTheme,
112 });
113
114 const xAxis = new NumericAxis(wasmContext, {
115 isVisible: false,
116 autoRange: EAutoRange.Never,
117 visibleRangeLimit: new NumberRange(-600, 600),
118 });
119 const yAxis = new NumericAxis(wasmContext, {
120 isVisible: false,
121 autoRange: EAutoRange.Never,
122 visibleRangeLimit: new NumberRange(-600, 600),
123 });
124 xAxis.visibleRange = new NumberRange(-300, 300);
125 yAxis.visibleRange = new NumberRange(-300, 300);
126 sciChartSurface.xAxes.add(xAxis);
127 sciChartSurface.yAxes.add(yAxis);
128
129 const { nodes, edges } = buildSimulation();
130
131 const edgeHover = new EdgeHoverState();
132
133 const edgeDataSeries = new XyxyDataSeries(wasmContext);
134 sciChartSurface.renderableSeries.add(
135 new FastLineSegmentRenderableSeries(wasmContext, {
136 dataSeries: edgeDataSeries,
137 stroke: "#47bde650",
138 strokeThickness: 2,
139 })
140 );
141
142 const edgeHighlightDataSeries = new XyxyDataSeries(wasmContext);
143 sciChartSurface.renderableSeries.add(
144 new FastLineSegmentRenderableSeries(wasmContext, {
145 dataSeries: edgeHighlightDataSeries,
146 stroke: "#47bde6",
147 strokeThickness: 3,
148 })
149 );
150
151 const nodeDataSeries = new XyDataSeries(wasmContext);
152 sciChartSurface.renderableSeries.add(
153 new XyScatterRenderableSeries(wasmContext, {
154 dataSeries: nodeDataSeries,
155 pointMarker: new EllipsePointMarker(wasmContext, {
156 width: 14,
157 height: 14,
158 fill: "#274b92",
159 stroke: "#47bde6",
160 strokeThickness: 1.5,
161 }),
162 paletteProvider: new NodeHoverPaletteProvider(edgeHover),
163 })
164 );
165
166 const dragState: DragStateRef = { current: null };
167
168 let alpha = 1.0;
169 let running = true;
170 let loopAlive = false;
171 let autoZoomed = false;
172 let animFrameId: number = 0;
173
174 function frame() {
175 if (!running || sciChartSurface.isDeleted) {
176 loopAlive = false;
177 return;
178 }
179
180 const simActive = alpha >= 0.001 || !!dragState.current;
181
182 if (simActive) {
183 tick(nodes, edges, alpha);
184 alpha *= 0.9772;
185
186 if (!autoZoomed && alpha < 0.5) {
187 autoZoomed = true;
188 sciChartSurface.zoomExtents(200);
189 }
190
191 if (dragState.current) {
192 const n = nodes[dragState.current.nodeIdx];
193 n.x = dragState.current.dataX;
194 n.y = dragState.current.dataY;
195 n.vx = 0;
196 n.vy = 0;
197 alpha = Math.max(alpha, 0.1);
198 }
199 }
200
201 const ex: number[] = [],
202 ey: number[] = [],
203 ex1: number[] = [],
204 ey1: number[] = [];
205 const hx: number[] = [],
206 hy: number[] = [],
207 hx1: number[] = [],
208 hy1: number[] = [];
209 const h = edgeHover.hoveredNodeIdx;
210 for (const edge of edges) {
211 const src = nodes[edge.sourceIdx],
212 tgt = nodes[edge.targetIdx];
213 if (h !== -1 && (edge.sourceIdx === h || edge.targetIdx === h)) {
214 hx.push(src.x);
215 hy.push(src.y);
216 hx1.push(tgt.x);
217 hy1.push(tgt.y);
218 } else {
219 ex.push(src.x);
220 ey.push(src.y);
221 ex1.push(tgt.x);
222 ey1.push(tgt.y);
223 }
224 }
225 edgeDataSeries.clear();
226 edgeDataSeries.appendRange(ex, ey, ex1, ey1);
227 edgeHighlightDataSeries.clear();
228 edgeHighlightDataSeries.appendRange(hx, hy, hx1, hy1);
229
230 nodeDataSeries.clear();
231 nodeDataSeries.appendRange(
232 nodes.map((n) => n.x),
233 nodes.map((n) => n.y)
234 );
235
236 if (simActive) {
237 animFrameId = requestAnimationFrame(frame);
238 } else {
239 loopAlive = false;
240 }
241 }
242
243 function startLoop() {
244 if (!loopAlive) {
245 loopAlive = true;
246 animFrameId = requestAnimationFrame(frame);
247 }
248 }
249
250 sciChartSurface.chartModifiers.add(
251 new NodeTooltipModifier(nodes, edges, edgeHover, () => startLoop()),
252 new NodeDragModifier(nodes, dragState, () => {
253 alpha = Math.max(alpha, 0.3);
254 startLoop();
255 }),
256 new ZoomPanModifier(),
257 new ZoomExtentsModifier(),
258 new MouseWheelZoomModifier(),
259 new PinchZoomModifier()
260 );
261
262 startLoop();
263
264 return {
265 sciChartSurface,
266 wasmContext,
267 stopAnimation: () => {
268 running = false;
269 if (animFrameId) cancelAnimationFrame(animFrameId);
270 },
271 };
272};
273This example demonstrates a Force Directed Graph built with SciChart.js, visualizing ~60 US airports connected by ~2300 flight routes. The graph uses a custom physics simulation to position nodes, with geographic anchoring that keeps airports near their real-world lat/lon positions.
The chart is initialized using SciChartSurface.create(). Edges are rendered using FastLineSegmentRenderableSeries with an XyxyDataSeries (two endpoints per segment). Airport nodes are rendered as XyScatterRenderableSeries with EllipsePointMarker. The physics loop uses requestAnimationFrame and applies repulsion, spring, and geographic anchor forces each tick.
Two custom ChartModifierBase2D subclasses provide interactivity: NodeTooltipModifier highlights connected routes and labels neighbours on hover, and NodeDragModifier allows dragging nodes to explore the graph structure. Standard ZoomPanModifier and MouseWheelZoomModifier are also included.

Create an interactive JavaScript trading charts for technical analysis. Trading Drawing Tools Demo, which shows how to use Polylines, Extended Lines, Rays, Channels, Pitchforks, Pitchfans, Fibonnaci Retracements, Measure, Stop Loss and Take Profit chart drawing tools for Technical Analysis.

An example of using JavaScript FreehandDrawingModifier for arbitrary drawing on trading and financial charts. Can be used for drawing trends, arrow, markers, text, etc.

Interactive JavaScript Smith chart for RF impedance matching — place markers, build matching networks step by step with the component chain, and switch between impedance and admittance grids.

Demonstrates how to use the SVG render layer in SciChart.js to maintain smooth cursor interaction on heavy charts with millions of points.

Demonstrates how to create multiple synchronized subcharts with an overview range selector using SciChart.js and SubSurfaces

Create a Javascript heatmap chart showing historical orderbook levels using the high performance SciChart.js chart library. Get free demo now.

Demonstrates 64-bit precision Date Axis in SciChart.js handling Nanoseconds to Billions of Years


Demonstrates BaseValue Axes on a JavaScript Chart using SciChart.js to create non-linear and custom-scaled axes such as log-like scales