Skip to content

Commit 717fd74

Browse files
committed
feat: Add error-segment feature
1 parent 1030ed1 commit 717fd74

File tree

5 files changed

+146
-20
lines changed

5 files changed

+146
-20
lines changed

example/src/App.tsx

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useMemo, useState } from 'react'
22
import { View, StyleSheet, TouchableOpacity, Text, Switch } from 'react-native'
33

4-
import Chart, { ChartProps, Dataset } from 'react-native-d3-chart'
4+
import Chart, { ChartProps, Dataset, ErrorSegment } from 'react-native-d3-chart'
55

66
import { buildSlices } from './helpers/buildSlices'
77
import { generateTimeSeriesData } from './helpers/generateTimeSeriesData'
@@ -138,9 +138,49 @@ export default function App() {
138138
[Measurement.Temperature]
139139
)
140140

141+
const errorSegments = useMemo<ErrorSegment[]>(() => {
142+
const now = Date.now()
143+
return [
144+
{
145+
message: 'Unknown error',
146+
messageColor: '#f0f',
147+
start: now - 50 * 60 * 1000,
148+
end: now - 40 * 60 * 1000,
149+
},
150+
{
151+
message: 'No data yet',
152+
messageColor: '#b22',
153+
start: now - 2 * 60 * 1000,
154+
end: now + 15 * 60 * 1000,
155+
},
156+
]
157+
}, [])
158+
141159
const datasets = useMemo<Dataset[]>(
142-
() => enabledMeasurements.map((m) => measurementsRecords[m]),
143-
[enabledMeasurements]
160+
() =>
161+
enabledMeasurements.map((enabledMeasurement, index) => {
162+
const data = measurementsRecords[enabledMeasurement]
163+
if (index !== 0) return data
164+
165+
const firstErrorSegment = errorSegments[0]
166+
if (!firstErrorSegment) return data
167+
168+
// invalidate points within first error segment for the first measurement for demo purposes
169+
return {
170+
...data,
171+
decimalSeparator: ',',
172+
decimals: 2,
173+
points: data.points.map((point) => ({
174+
timestamp: point.timestamp,
175+
value:
176+
point.timestamp - 60 * 1000 > firstErrorSegment.start &&
177+
point.timestamp + 60 * 1000 < firstErrorSegment.end
178+
? null
179+
: point.value,
180+
})),
181+
}
182+
}),
183+
[enabledMeasurements, errorSegments]
144184
)
145185

146186
return (
@@ -156,6 +196,7 @@ export default function App() {
156196
colors={chartColors}
157197
timeDomain={timeDomain}
158198
marginHorizontal={PADDING}
199+
errorSegments={errorSegments}
159200
noDataString="No data available"
160201
/>
161202
<View style={styles.spacer} />

src/Chart.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
type ChartProps,
1010
type TimeDomain,
1111
type ChartColors,
12+
type ErrorSegment,
1213
type CalendarStrings,
1314
} from './types'
1415

@@ -26,6 +27,7 @@ type HtmlProps = {
2627
timeDomain: TimeDomain
2728
marginHorizontal: number
2829
calendar?: CalendarStrings
30+
errorSegments?: ErrorSegment[]
2931
}
3032

3133
export default function Chart({
@@ -35,6 +37,7 @@ export default function Chart({
3537
timeDomain,
3638
zoomEnabled,
3739
noDataString,
40+
errorSegments,
3841
calendarStrings,
3942
colors: chartColors,
4043
locale = 'en',
@@ -64,6 +67,7 @@ export default function Chart({
6467
datasets,
6568
timeDomain,
6669
noDataString,
70+
errorSegments,
6771
marginHorizontal,
6872
calendar: calendarStrings,
6973
zoomEnabled: !!zoomEnabled,
@@ -95,6 +99,7 @@ export default function Chart({
9599
chartColors,
96100
zoomEnabled,
97101
noDataString,
102+
errorSegments,
98103
calendarStrings,
99104
marginHorizontal,
100105
])

src/drawFunction.js

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ function findClosest(data, target) {
108108
return finalIx
109109
}
110110
111+
function getClosest(data, target) {
112+
const closestIndex = findClosest(data, target)
113+
return data[closestIndex]
114+
}
115+
111116
function postMessage(type, payload) {
112117
if (window.ReactNativeWebView) {
113118
window.ReactNativeWebView.postMessage(JSON.stringify({ type, payload }))
@@ -380,11 +385,61 @@ window.draw = (props) => {
380385
.attr('x', 0)
381386
.attr('y', 0)
382387
388+
defs
389+
.append('linearGradient')
390+
.attr('id', 'error-gradient')
391+
.attr('gradientUnits', 'userSpaceOnUse')
392+
.attr('x1', 0)
393+
.attr('x2', 0)
394+
.attr('y1', 0)
395+
.attr('y2', height)
396+
.selectAll('stop')
397+
.data([
398+
{ offset: '0%', opacity: 0 },
399+
{ offset: '100%', opacity: 0.1 },
400+
])
401+
.enter()
402+
.append('stop')
403+
.attr('offset', (d) => d.offset)
404+
.attr('stop-opacity', (d) => d.opacity)
405+
.attr('stop-color', '#000000')
406+
383407
const chart = selectOrAppend(svg, 'g', 'chart').attr(
384408
'clip-path',
385409
'url(#clip)'
386410
)
387411
412+
var drawErrorSegments = () => {
413+
const errorSegmentsHolder = selectOrAppend(
414+
chart,
415+
'g',
416+
'errorSegmentsHolder'
417+
)
418+
errorSegmentsHolder.selectAll('rect').remove()
419+
if (!props.errorSegments?.length) return
420+
421+
try {
422+
const parseTime = d3.timeParse('%Q')
423+
errorSegmentsHolder
424+
.selectAll('rect')
425+
.data(
426+
props.errorSegments.map(({ start, end }) => ({
427+
startDate: parseTime(start),
428+
endDate: parseTime(end),
429+
}))
430+
)
431+
.enter()
432+
.append('rect')
433+
.attr('fill', 'url(#error-gradient)')
434+
.attr('height', height)
435+
.attr('width', (d) => x(d.endDate) - x(d.startDate))
436+
.attr('x', (d) => x(d.startDate))
437+
} catch (e) {
438+
postMessage('error segment update error', e.message)
439+
}
440+
}
441+
drawErrorSegments()
442+
388443
const getGradientOffset =
389444
(yScale) =>
390445
({ value }) =>
@@ -445,7 +500,7 @@ window.draw = (props) => {
445500
opacity: 0.223958 * baseOpacity,
446501
},
447502
{ value: y.domain()[0], opacity: 0 },
448-
]
503+
]
449504
defs
450505
.append('linearGradient')
451506
.attr('id', 'area-gradient' + index)
@@ -767,6 +822,7 @@ window.draw = (props) => {
767822
768823
updateHighlight()
769824
updateSlices()
825+
drawErrorSegments()
770826
} catch (e) {
771827
postMessage('zoomerror', e.message)
772828
}
@@ -788,6 +844,10 @@ window.draw = (props) => {
788844
const x0 = width * highlightPosition
789845
const highlightExactDate = x.invert(x0)
790846
847+
const highlightedErrorSegment = props.errorSegments?.find(
848+
(s) => s.start < highlightExactDate && s.end > highlightExactDate
849+
)
850+
791851
var highlightTime = null
792852
props.datasets.forEach((dataset, index) => {
793853
const { unit, decimals } = dataset
@@ -802,19 +862,25 @@ window.draw = (props) => {
802862
return
803863
}
804864
805-
const pointIndex = findClosest(definedData, highlightExactDate)
806-
const highlight = definedData[pointIndex]
807-
865+
const highlight = getClosest(definedData, highlightExactDate)
808866
const xValue = x(highlight.date)
809867
810-
const tooFar =
811-
Math.abs(x0 - xValue) > 8 &&
868+
const is10MinutesAway =
812869
Math.abs(highlightExactDate - highlight.date) > 10 * 60 * 1000
870+
const is8PixelsAway = Math.abs(x0 - xValue) > 8
813871
814-
const color = getDatasetColor(
815-
dataset,
816-
tooFar ? undefined : highlight.value
817-
)
872+
// Closest defined point is more than 10 minutes away, or we are in error segment + closest _unfiltered_ point is null
873+
const tooFar =
874+
is8PixelsAway &&
875+
(is10MinutesAway ||
876+
(!!highlightedErrorSegment &&
877+
getClosest(operators[index].domainData, highlightExactDate)
878+
?.value === null))
879+
const isInErrorSegment = highlightedErrorSegment && tooFar
880+
881+
const color = isInErrorSegment
882+
? highlightedErrorSegment.messageColor
883+
: getDatasetColor(dataset, tooFar ? undefined : highlight.value)
818884
819885
if (!tooFar) {
820886
highlightTime = highlight.date
@@ -832,9 +898,11 @@ window.draw = (props) => {
832898
.select('span#highlightvalue' + index)
833899
.style('color', color)
834900
.html(
835-
tooFar
836-
? props.noDataString
837-
: d3.format('.' + decimals + 'f')(highlight.value) + ' ' + unit
901+
isInErrorSegment
902+
? highlightedErrorSegment.message
903+
: tooFar
904+
? props.noDataString
905+
: d3.format('.' + decimals + 'f')(highlight.value) + ' ' + unit
838906
)
839907
})
840908

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export {
44
type Dataset,
55
type ChartProps,
66
type TimeDomain,
7+
type ErrorSegment,
78
type CalendarStrings,
89
} from './types'
910

src/types.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,34 @@ export type Slices = {
4242
}
4343

4444
export type Dataset = {
45-
measurementName: string
46-
color: string | ThresholdColor
47-
points: Point[]
4845
unit: string
46+
points: Point[]
4947
decimals: number
48+
measurementName: string
49+
color: string | ThresholdColor
5050
slices?: Slices
5151
minDeltaY?: number
52+
axisColor?: string
5253
/**
5354
* Background fill color below the line (area chart)
5455
* Set to null to disable area fill.
5556
* @default undefined will use `color` with reduced opacity
5657
*/
5758
areaColor?: string | null
58-
axisColor?: string
5959
decimalSeparator?: '.' | ','
60+
/**
61+
* Override Y axis domain (min/max values)
62+
*/
6063
domain?: { bottom: number; top: number }
6164
}
6265

66+
export type ErrorSegment = {
67+
message: string
68+
messageColor: string
69+
end: Point['timestamp']
70+
start: Point['timestamp']
71+
}
72+
6373
export type ChartProps = {
6474
width: number
6575
height: number
@@ -70,6 +80,7 @@ export type ChartProps = {
7080
locale?: string
7181
zoomEnabled?: boolean
7282
marginHorizontal?: number
83+
errorSegments?: ErrorSegment[]
7384
calendarStrings?: CalendarStrings
7485
onZoomEnded?: () => void
7586
onZoomStarted?: () => void

0 commit comments

Comments
 (0)