Custom High Precision Date Labels

Demonstrates how to customize specific label formats on a High Precision Date Axis, using external libraries like date-fns.

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.tsx

theme.ts

Copy to clipboard
Minimise
Fullscreen
1import {
2    DateTimeNumericAxis,
3    EllipsePointMarker,
4    MouseWheelZoomModifier,
5    ZoomExtentsModifier,
6    ZoomPanModifier,
7    XyDataSeries,
8    NumericAxis,
9    FastLineRenderableSeries,
10    SciChartSurface,
11    EAutoRange,
12    SmartDateLabelProvider,
13    EResamplingMode,
14    EDatePrecision,
15    EHighPrecisionLabelMode,
16    ETradeChartLabelFormat,
17    NumberRange,
18    RubberBandXyZoomModifier,
19    NativeTextAnnotation,
20    EHorizontalAnchorPoint,
21    ECoordinateMode,
22} from "scichart";
23import { format, fromUnixTime } from "date-fns";
24
25const toUTC = (date: Date) => {
26    // SciChart internally uses UTC dates, so convert to UTC
27    return new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000);
28};
29
30export const drawExample = async (rootElement: string | HTMLDivElement) => {
31    const { wasmContext, sciChartSurface } = await SciChartSurface.create(rootElement);
32
33    const startDate = new Date("2025-01-01T00:00:00Z");
34    const startTimeSeconds = startDate.getTime() / 1000;
35
36    const xAxis = new DateTimeNumericAxis(wasmContext, {
37        axisTitle: "Time",
38        datePrecision: EDatePrecision.Nanoseconds, // Very important -> 1 x-value increment == 1 nanosecond
39        highPrecisionLabelMode: EHighPrecisionLabelMode.Suffix,
40        dateOffset: startTimeSeconds, // in seconds
41
42        showWiderDateOnFirstLabel: true,
43        showYearOnWiderDate: true,
44
45        splitWideDateWithComma: false,
46
47        showSecondsOnWideDate: true, // Usually these 2 should be opposites: when wide date shows seconds, precise date shouldn't.
48        showSecondsOnPreciseDate: true,
49    });
50    sciChartSurface.xAxes.add(xAxis);
51
52    const labelProvider = xAxis.labelProvider as SmartDateLabelProvider;
53
54    // 1. Capture the original SciChart implementations
55    const defaultFormatDateWide = labelProvider.formatDateWide.bind(labelProvider);
56    const defaultFormatDatePrecise = labelProvider.formatDatePrecise.bind(labelProvider);
57
58    // 2. Define the Custom "date-fns" implementations
59    // Wide dates -> e.g. "Jan 01, 2025 12:00:00"
60    const customFormatDateWide = (labelRange: ETradeChartLabelFormat | string, valueInSeconds: number) => {
61        const date = toUTC(fromUnixTime(valueInSeconds));
62        if (
63            labelRange === ETradeChartLabelFormat.Nanoseconds ||
64            labelRange === ETradeChartLabelFormat.Microseconds ||
65            labelRange === ETradeChartLabelFormat.MilliSeconds
66        ) {
67            return format(date, "MMM dd, yyyy HH:mm:ss");
68        } else if (labelRange === ETradeChartLabelFormat.Seconds || labelRange === ETradeChartLabelFormat.Minutes) {
69            return format(date, "MMM dd, yyyy");
70        } else if (labelRange === ETradeChartLabelFormat.Days) {
71            return format(date, "MMM yyyy");
72        } else {
73            return format(date, "yyyy");
74        }
75    };
76
77    // Precise dates -> e.g. "12:59:59" or "03s12345ns"
78    const customFormatDatePrecise = (
79        labelRange: ETradeChartLabelFormat | string,
80        valueInSeconds: number,
81        rawValue?: number
82    ) => {
83        const date = toUTC(fromUnixTime(valueInSeconds));
84
85        // High precision logic
86        if (
87            labelRange === ETradeChartLabelFormat.Nanoseconds ||
88            labelRange === ETradeChartLabelFormat.Microseconds ||
89            labelRange === ETradeChartLabelFormat.MilliSeconds
90        ) {
91            const mode = labelProvider.highPrecisionLabelMode;
92
93            if (mode === EHighPrecisionLabelMode.Suffix && rawValue !== undefined) {
94                const tps = labelProvider.datePrecision;
95                const wholeSeconds = Math.floor(rawValue / tps);
96                const ticksWithinSecond = rawValue - wholeSeconds * tps;
97                const subSecondOffset = ticksWithinSecond / tps;
98
99                const seconds = date.getUTCSeconds();
100                const secondsStr = seconds.toString().padStart(2, "0");
101
102                if (labelRange === ETradeChartLabelFormat.Nanoseconds) {
103                    const ns = Math.round(subSecondOffset * 1_000_000_000);
104                    return `${secondsStr}:${ns}ns`;
105                }
106                if (labelRange === ETradeChartLabelFormat.Microseconds) {
107                    const us = Math.round(subSecondOffset * 1_000_000);
108                    return `${secondsStr}:${us}µs`;
109                }
110                const ms = Math.round(subSecondOffset * 1_000);
111                return `${secondsStr}:${ms}ms`;
112            }
113            return format(date, "ss.SSS");
114        }
115
116        if (labelRange === ETradeChartLabelFormat.Seconds) return format(date, "HH:mm:ss");
117        if (labelRange === ETradeChartLabelFormat.Minutes) return format(date, "HH:mm");
118        if (labelRange === ETradeChartLabelFormat.Days || labelRange === ETradeChartLabelFormat.Months)
119            return format(date, "dd");
120
121        return format(date, "dd/MM/yy");
122    };
123
124    // 3. Logic to toggle between them
125    const setUseDateFns = (useCustom: boolean) => {
126        if (useCustom) {
127            labelProvider.formatDateWide = customFormatDateWide;
128            labelProvider.formatDatePrecise = customFormatDatePrecise;
129        } else {
130            labelProvider.formatDateWide = defaultFormatDateWide;
131            labelProvider.formatDatePrecise = defaultFormatDatePrecise;
132        }
133        // Force redraw to apply changes to existing labels
134        sciChartSurface.invalidateElement();
135    };
136
137    // Apply custom by default initially
138    setUseDateFns(true);
139
140    sciChartSurface.yAxes.add(
141        new NumericAxis(wasmContext, {
142            axisTitle: "Signal (mV)",
143            autoRange: EAutoRange.Always,
144            growBy: new NumberRange(0.1, 0.1),
145        })
146    );
147
148    // Data Generation
149    const xValues: number[] = [];
150    const yValues: number[] = [];
151    type Generator = (i: number) => number;
152
153    const addCluster = (startOffsetNano: number, count: number, generator: Generator) => {
154        for (let i = 0; i < count; i++) {
155            const x = startOffsetNano + i * 10_000_000;
156            const noise = (Math.random() - 0.5) * 0.05;
157            const y = generator(i) + noise;
158
159            xValues.push(x);
160            yValues.push(y);
161        }
162    };
163
164    const ONE_SEC = 1_000_000_000; // Because we use `datePrecision = Nanoseconds`
165    const ONE_MIN = ONE_SEC * 60;
166
167    // 1. Damped Ringing
168    addCluster(0, 2000, (i) => Math.sin(i * 0.1) * Math.exp(-i * 0.002) * 2);
169
170    // 2. Frequency Chirp
171    addCluster(1 * ONE_MIN, 2000, (i) => {
172        const freq = 0.01 + i * 0.00005;
173        return Math.sin(i * freq);
174    });
175
176    // 3. Amplitude Modulated Packet
177    addCluster(3 * ONE_MIN, 2000, (i) => {
178        const carrier = Math.sin(i * 0.1);
179        const envelope = Math.sin(i * 0.03); // Slow envelope
180        return carrier * envelope * 1.5;
181    });
182
183    // 4. Noisy "Heartbeat"
184    addCluster(4 * ONE_MIN, 2000, (i) => {
185        // spike every 200 points
186        if (i % 200 < 20) return 1.0 + Math.random();
187        return Math.random() * 0.2;
188    });
189
190    const lineSeries = new FastLineRenderableSeries(wasmContext, {
191        dataSeries: new XyDataSeries(wasmContext, {
192            xValues,
193            yValues,
194            containsNaN: false,
195            isSorted: true,
196            dataSeriesName: "Sensor A",
197        }),
198        stroke: "#50C7E0",
199        strokeThickness: 2,
200        resamplingMode: EResamplingMode.None,
201        pointMarker: new EllipsePointMarker(wasmContext, {
202            width: 4,
203            height: 4,
204            fill: "#2e3a59",
205            stroke: "#50C7E0",
206            strokeThickness: 1,
207        }),
208    });
209    lineSeries.rolloverModifierProps.tooltipLabelX = "X";
210    sciChartSurface.renderableSeries.add(lineSeries);
211
212    sciChartSurface.chartModifiers.add(new MouseWheelZoomModifier(), new ZoomExtentsModifier(), new ZoomPanModifier());
213    sciChartSurface.zoomExtents();
214
215    sciChartSurface.annotations.add(
216        new NativeTextAnnotation({
217            xCoordinateMode: ECoordinateMode.Relative,
218            yCoordinateMode: ECoordinateMode.Relative,
219            x1: 0.5,
220            y1: 0.08,
221            horizontalAnchorPoint: EHorizontalAnchorPoint.Center,
222            text: `Zoom in & out while toggling the switch to see different axis label formats
223            \nSee how you can also customize date formatting at lines 60, 80 in drawExample.ts!`,
224            lineSpacing: 10,
225            fontSize: 16,
226            opacity: 0.7,
227            textColor: "#FFFFFF",
228        })
229    );
230
231    return {
232        wasmContext,
233        sciChartSurface,
234        controls: { setUseDateFns },
235    };
236};
237

High-Precision DateTimeNumericAxis with Custom Label Formatting - in Angular

Overview

This example demonstrates how to use DateTimeNumericAxis with nanosecond-precision data and the built-in SmartDateLabelProvider, while safely overriding date formatting logic using date-fns.

It highlights how SciChart dynamically switches between wide and precise date labels as you zoom, and how you can fully customize that behavior without breaking cursor labels, tick calculations, or high-precision rendering.

Use the toggle above the chart to switch between SciChart’s default formatting and a custom date-fns implementation.

Key Concepts Demonstrated

DateTimeNumericAxis with Nanosecond Precision

The X-axis is configured with:

  • datePrecision = EDatePrecision.Nanoseconds
  • A fixed dateOffset (Unix seconds)

This allows raw X values to represent nanosecond ticks, while still displaying full calendar dates and times on the axis. Each increment of 1 on the X-axis equals 1 nanosecond, making this suitable for high-frequency sensor data, trading systems, or scientific signals.

SmartDateLabelProvider

The SmartDateLabelProvider automatically adapts label formats based on the visible time range:

  • Wide labels provide context (e.g. Jan 01, 2025 12:00:00)
  • Precise labels show incremental detail (e.g. 59s345ms, 123456ns)

As you zoom in and out, the provider dynamically switches between: Nanoseconds, Microseconds, Milliseconds, Seconds, Minutes, Days / Months.

This logic is driven by internal label thresholds and the current visible range.

Custom Formatting with date-fns

This demo overrides two key methods on the SmartDateLabelProvider:

  • formatDateWide
  • formatDatePrecise

Instead of replacing the entire label provider, the original SciChart implementations are preserved and can be restored instantly. This ensures:

  • Cursor and rollover labels remain correct
  • Tick spacing and delta calculations are unaffected
  • High-precision math stays intact

The toggle switches between:

  • Default SciChart formatting
  • Custom date-fns formatting for both wide and precise labels

This pattern is recommended when integrating third-party date libraries.

When to Use This Approach

Use DateTimeNumericAxis with SmartDateLabelProvider when:

  • You need sub-millisecond or nanosecond resolution
  • You want automatic, readable date formatting
  • You need full control over label appearance
  • Cursor and tooltip accuracy is critical

This pattern is ideal for high-frequency trading, telemetry, scientific measurement, and real-time monitoring applications.

Related Documentation:

SciChart Ltd, 16 Beaufort Court, Admirals Way, Docklands, London, E14 9XL.