Skip to content

Commit 64e825c

Browse files
author
Niilo Keinänen
committed
Update examples
1 parent 8e9b890 commit 64e825c

8 files changed

Lines changed: 104 additions & 40 deletions

README.md

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,11 @@ The demo can be used as an example or a seed project. Local execution requires t
1919

2020
## Description
2121

22-
This example demonstrates **Scrolling Heatmap Aggregation** in `HeatmapScrollingGridSeries`. You can also configure aggregation for a static `HeatmapGridSeries`.
22+
This example demonstrates Scrolling Heatmap Aggregation in `HeatmapScrollingGridSeries`. You can also configure aggregation for a static `HeatmapGridSeries`.
2323

24-
By default, heatmaps display the closest heatmap cell value to each pixel that is rendered on the screen. With dense, high resolution heatmaps, this can mean that there is no guarantee which data value is displayed in a pixel.
24+
By default, heatmaps display the closest heatmap cell value to each pixel that is rendered on the screen. With dense, high resolution heatmaps, this can mean that there is no guarantee which data value is displayed in a pixel. Heatmap aggregation can be enabled to specify this behavior (which value to show when multiple cell values are contained by single pixel) at the expense of a performance hit.
2525

26-
Heatmap aggregation can be enabled to specify this behavior (which value to show when multiple cell values are contained by single pixel) at the expense of a performance hit.
27-
28-
In this example, the lower heatmap uses the `max` aggregation mode, making sudden **power spikes (shown in red)** much easier to identify in the data stream.
29-
30-
# Heatmap Scrolling Grid Series aggregation
26+
In this example, the lower heatmap uses the `max` aggregation mode, making it much easier to identify signals in the data stream.
3127

3228
In LightningChart JS, aggregation mode is configured with the `setAggregation` method. By default the aggregation mode is `undefined`. Bilinear interpolation is not supported simultaneously, so remember to disable it with `setIntensityInterpolation('disabled')`.
3329

@@ -39,8 +35,6 @@ heatmapSeries
3935
.setIntensityInterpolation('disabled')
4036
```
4137

42-
The data used in this example: [ElectroSense PSD Spectrum Dataset](https://doi.org/10.5281/zenodo.7521246).
43-
4438
## API Links
4539

4640
* [Scrolling Heatmap Grid Series]
@@ -69,13 +63,13 @@ Direct developer email support can be purchased through a [Support Plan][4] or b
6963
[3]: https://stackoverflow.com/questions/tagged/lightningchart
7064
[4]: https://lightningchart.com/support-services/
7165

72-
© LightningChart Ltd 2009-2025. All rights reserved.
66+
© LightningChart Ltd 2009-2026. All rights reserved.
7367

7468

75-
[Scrolling Heatmap Grid Series]: https://lightningchart.com/js-charts/api-documentation/v8.1.0/classes/HeatmapScrollingGridSeriesIntensityValues.html
76-
[Paletted Fill Style]: https://lightningchart.com/js-charts/api-documentation/v8.1.0/classes/PalettedFill.html
77-
[Color Lookup Table]: https://lightningchart.com/js-charts/api-documentation/v8.1.0/classes/LUT.html
78-
[Chart XY]: https://lightningchart.com/js-charts/api-documentation/v8.1.0/classes/ChartXY.html
79-
[Axis XY]: https://lightningchart.com/js-charts/api-documentation/v8.1.0/classes/Axis.html
80-
[Axis Automatic Scroll Strategies]: https://lightningchart.com/js-charts/api-documentation/v8.1.0/variables/AxisScrollStrategies.html
69+
[Scrolling Heatmap Grid Series]: https://lightningchart.com/js-charts/api-documentation/v8.2.0/classes/HeatmapScrollingGridSeriesIntensityValues.html
70+
[Paletted Fill Style]: https://lightningchart.com/js-charts/api-documentation/v8.2.0/classes/PalettedFill.html
71+
[Color Lookup Table]: https://lightningchart.com/js-charts/api-documentation/v8.2.0/classes/LUT.html
72+
[Chart XY]: https://lightningchart.com/js-charts/api-documentation/v8.2.0/classes/ChartXY.html
73+
[Axis XY]: https://lightningchart.com/js-charts/api-documentation/v8.2.0/classes/Axis.html
74+
[Axis Automatic Scroll Strategies]: https://lightningchart.com/js-charts/api-documentation/v8.2.0/variables/AxisScrollStrategies.html
8175

assets/spectrum.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.
616 KB
Loading
648 KB
Loading
379 KB
Loading
423 KB
Loading
449 KB
Loading

src/index.js

Lines changed: 93 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ const {
99
emptyLine,
1010
synchronizeAxisIntervals,
1111
AxisTickStrategies,
12-
htmlTextRenderer,
1312
} = lcjs
1413

1514
// General configuration for this data set
1615
const config = {
1716
framesPerSecond: 20,
1817
frameIntervalMs: (0.5 * 1000) / 20,
19-
freqStartMHz: 920,
20-
freqEndMHz: 935,
21-
resolution: 1612,
22-
freqStepMHz: (935 - 920) / (1612 - 1),
23-
visibleFrameCount: 150,
18+
freqStartMHz: 0,
19+
freqEndMHz: 400,
20+
resolution: 1612,
21+
freqStepMHz: (400 - 0) / (1612 - 1),
22+
visibleFrameCount: 400,
2423
}
24+
const charts = []
2525

2626
// Initialize LightningChart JS
2727
const lc = lightningChart({
@@ -45,24 +45,43 @@ const chart1 = lc
4545
container: containerChart1,
4646
defaultAxisX: { type: 'linear-highPrecision' },
4747
defaultAxisY: { type: 'linear-highPrecision' },
48-
textRenderer: htmlTextRenderer,
49-
theme: Themes[new URLSearchParams(window.location.search).get('theme') || 'darkGold'] || undefined,
48+
theme: (() => {
49+
const t = Themes[new URLSearchParams(window.location.search).get('theme') || 'darkGold'] || undefined
50+
const smallView = Math.min(window.innerWidth, window.innerHeight) < 500
51+
if (!window.__lcjsDebugOverlay) {
52+
window.__lcjsDebugOverlay = document.createElement('div')
53+
window.__lcjsDebugOverlay.style.cssText = 'position:fixed;top:0;left:0;background:rgba(0,0,0,0.7);color:#fff;padding:4px 8px;z-index:99999;font:12px monospace;pointer-events:none'
54+
if (document.body) document.body.appendChild(window.__lcjsDebugOverlay)
55+
setInterval(() => {
56+
if (!window.__lcjsDebugOverlay.parentNode && document.body) document.body.appendChild(window.__lcjsDebugOverlay)
57+
window.__lcjsDebugOverlay.textContent = window.innerWidth + 'x' + window.innerHeight + ' dpr=' + window.devicePixelRatio + ' small=' + (Math.min(window.innerWidth, window.innerHeight) < 500)
58+
}, 500)
59+
}
60+
return t && smallView ? lcjs.scaleTheme(t, 0.5) : t
61+
})(),
5062
})
5163
.setTitle('Scrolling Heatmap - No Aggregation')
5264
.setTitleMargin({ top: 10, bottom: 10 })
65+
.setCursorMode(undefined)
5366
containerChart1.style.flex = '1'
5467

5568
// Setup progressive scrolling Axis
5669
chart1.axisY
5770
.setScrollStrategy(AxisScrollStrategies.scrolling)
58-
.setTitle('History (Frames)')
71+
.setTitle('Time (s)')
5972
.setDefaultInterval((state) => ({
6073
start: state.dataMax ?? 0,
6174
end: (state.dataMax ?? 0) - config.visibleFrameCount,
6275
stopAxisAfter: false,
6376
}))
6477
.setPointerEvents(false)
65-
.setTickStrategy(AxisTickStrategies.Empty)
78+
// .setTickStrategy(AxisTickStrategies.Empty)
79+
.setTickStrategy(AxisTickStrategies.Numeric, (tickStrategy) => tickStrategy
80+
.setFormattingFunction((frameIndex) => {
81+
const seconds = frameIndex / config.framesPerSecond
82+
return seconds.toFixed(1)
83+
})
84+
)
6685
.setAnimationsEnabled(false)
6786

6887
chart1.axisX
@@ -98,24 +117,29 @@ const chart2 = lc
98117
container: containerChart2,
99118
defaultAxisX: { type: 'linear-highPrecision' },
100119
defaultAxisY: { type: 'linear-highPrecision' },
101-
textRenderer: htmlTextRenderer,
102120
// theme: Themes.darkGold
103121
})
104122
.setTitle('Scrolling Heatmap - Aggregation (Max)')
105123
.setTitleMargin({ top: 10, bottom: 10 })
124+
.setCursorMode(undefined)
106125
containerChart2.style.flex = '1'
107126

108127
// Setup progressive scrolling Axis
109128
chart2.axisY
110129
.setScrollStrategy(AxisScrollStrategies.scrolling)
111-
.setTitle('History (Frames)')
130+
.setTitle('Time (s)')
112131
.setDefaultInterval((state) => ({
113132
start: state.dataMax ?? 0,
114133
end: (state.dataMax ?? 0) - config.visibleFrameCount,
115134
stopAxisAfter: false,
116135
}))
117136
.setPointerEvents(false)
118-
.setTickStrategy(AxisTickStrategies.Empty)
137+
.setTickStrategy(AxisTickStrategies.Numeric, (tickStrategy) => tickStrategy
138+
.setFormattingFunction((frameIndex) => {
139+
const seconds = frameIndex / config.framesPerSecond
140+
return seconds.toFixed(1)
141+
})
142+
)
119143
.setAnimationsEnabled(false)
120144

121145
chart2.axisX
@@ -146,8 +170,59 @@ const heatmapSeries2 = chart2
146170
synchronizeAxisIntervals(chart1.axisX, chart2.axisX)
147171
synchronizeAxisIntervals(chart1.axisY, chart2.axisY)
148172

173+
// Add manual cursors to charts
174+
const cursor1 = chart1.addCursor()
175+
const cursor2 = chart2.addCursor()
176+
charts.push({ chart: chart1, series: heatmapSeries1, cursor: cursor1 })
177+
charts.push({ chart: chart2, series: heatmapSeries2, cursor: cursor2 })
178+
179+
const hideCursor = () => {
180+
charts.forEach((chart) => chart.cursor.setVisible(false))
181+
}
182+
183+
// Display cursor at given x coordinate and solve nearest for all series in both charts
184+
const displayCursorAt = (x, y, value) => {
185+
charts.forEach((chart) => {
186+
const solveResults = chart.chart
187+
.getSeries()
188+
// NOTE: Heatmap series doesn't currently have direct API syntax to solve nearest from axis coordinate - for it, you have to first translate axis coordinate to client coordinate and then use solve nearest
189+
.map((series) => series.getCursorEnabled() && series.solveNearest({ x, y: 0 }))
190+
.filter((solve) => !!solve)
191+
if (solveResults.length > 0) {
192+
solveResults[0].x = x
193+
solveResults[0].y = y
194+
solveResults[0].cursorPosition.pointMarker.x = x
195+
solveResults[0].cursorPosition.pointMarker.y = y
196+
solveResults[0].intensity = value
197+
chart.cursor
198+
.setVisible(true)
199+
.setPosition({
200+
pointMarker: { x: x, y: y },
201+
pointMarkerScale: chart1.coordsAxis,
202+
resultTable: { x: x, y: y },
203+
resultTableScale: chart1.coordsAxis,
204+
})
205+
.setResultTable((rt) => rt.setContent(chart1.getCursorFormatting()(chart1, solveResults[0], solveResults)))
206+
} else {
207+
chart.cursor.setVisible(false)
208+
}
209+
})
210+
211+
}
212+
213+
charts.forEach((chart) => {
214+
chart.series.addEventListener('pointermove', (event, info) => {
215+
let intensity = 0
216+
if (info) {
217+
try { intensity = info.intensity } catch (error) { intensity = 0}
218+
}
219+
displayCursorAt(chart.chart.translateCoordinate(event, chart.chart.coordsAxis).x, chart.chart.translateCoordinate(event, chart.chart.coordsAxis).y, intensity)
220+
})
221+
chart.series.addEventListener('pointerleave', (event) => hideCursor())
222+
})
223+
149224
const streamData = () => {
150-
fetch(document.head.baseURI + 'examples/assets/1708/spectrum_920_935.json')
225+
fetch(document.head.baseURI + 'examples/assets/1708/spectrum.json')
151226
.then((r) => r.json())
152227
.then((data) => {
153228
// Find min and max values of data for LUT
@@ -161,13 +236,10 @@ const streamData = () => {
161236
}
162237
}
163238

164-
const A = Math.floor(min)
165-
const B = Math.ceil(max)
166-
167239
const lut = new LUT({
168-
steps: regularColorSteps(A, B, theme.examples.coldHotColorPalette),
169-
units: 'dBm',
170240
interpolate: true,
241+
steps: regularColorSteps(Math.floor(min), Math.ceil(max), theme.examples.coldHotColorPalette),
242+
units: 'dBm',
171243
})
172244
const paletteFill = new PalettedFill({ lut, lookUpProperty: 'value' })
173245
heatmapSeries1.setFillStyle(paletteFill)
@@ -176,10 +248,7 @@ const streamData = () => {
176248
let frameIndex = 0
177249

178250
const interval = setInterval(() => {
179-
if (frameIndex >= data.length) {
180-
// Loop the data set
181-
frameIndex = 0
182-
}
251+
if (frameIndex >= data.length) frameIndex = 0
183252

184253
const sample = data[frameIndex]
185254
heatmapSeries1.addIntensityValues([sample])
@@ -193,4 +262,4 @@ const streamData = () => {
193262
})
194263
}
195264

196-
streamData()
265+
streamData()

0 commit comments

Comments
 (0)