Skip to content

Commit 66cbdcf

Browse files
feat: Visual configuration - PR #11
Add props: xDividerConfig, highlightPosition, highlightValuePosition, onHighlightChanged
2 parents 0bf5576 + 9bf239c commit 66cbdcf

7 files changed

Lines changed: 571 additions & 69 deletions

File tree

README.md

Lines changed: 141 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -105,20 +105,25 @@ export default function App() {
105105

106106
### Chart Props
107107

108-
| Prop | Type | Required | Description |
109-
| ------------------ | ----------------- | -------- | ---------------------------------------------------------------------------------- |
110-
| `width` | `number` || Chart width in pixels |
111-
| `height` | `number` || Chart height in pixels |
112-
| `datasets` | `Dataset[]` || Array of data series to display |
113-
| `colors` | `ChartColors` || Color configuration for chart elements |
114-
| `timeDomain` | `TimeDomain` || Control intial zoom level / scale of X-axis, doesn't have to fit the whole dataset |
115-
| `noDataString` | `string` || Message to show when no data is available |
116-
| `zoomEnabled` | `boolean` || Enable zoom guesture |
117-
| `locale` | `string` || Locale for date/time formatting (default: 'en') |
118-
| `marginHorizontal` | `number` || Horizontal margin in pixels |
119-
| `calendarStrings` | `CalendarStrings` || Custom calendar strings for localization |
120-
| `onZoomStarted` | `() => void` || Callback when zoom interaction starts |
121-
| `onZoomEnded` | `() => void` || Callback when zoom interaction ends |
108+
| Prop | Type | Required | Description |
109+
| ------------------------ | ------------------------------------- | -------- | ---------------------------------------------------------------------------------- |
110+
| `width` | `number` || Chart width in pixels |
111+
| `height` | `number` || Chart height in pixels |
112+
| `datasets` | `Dataset[]` || Array of data series to display |
113+
| `colors` | `ChartColors` || Color configuration for chart elements |
114+
| `timeDomain` | `TimeDomain` || Control intial zoom level / scale of X-axis, doesn't have to fit the whole dataset |
115+
| `noDataString` | `string` || Message to show when no data is available |
116+
| `zoomEnabled` | `boolean` || Enable zoom guesture |
117+
| `locale` | `string` || Locale for date/time formatting (default: 'en') |
118+
| `marginHorizontal` | `number` || Horizontal margin in pixels |
119+
| `highlightPosition` | `number` || Position of highlight line (0-1, default: 0.5 for center) |
120+
| `highlightValuePosition` | `'top' \| 'tooltip' \| 'none'` || Where to show values: header, floating tooltip, or hidden (default: 'top') |
121+
| `xDividerConfig` | `XDividerConfig` || Style for vertical dividers on X axis (ticks or segments) |
122+
| `errorSegments` | `ErrorSegment[]` || Time ranges with error messages to display |
123+
| `calendarStrings` | `CalendarStrings` || Custom calendar strings for localization |
124+
| `onZoomStarted` | `() => void` || Callback when zoom interaction starts |
125+
| `onZoomEnded` | `() => void` || Callback when zoom interaction ends |
126+
| `onHighlightChanged` | `(payload: HighlightPayload) => void` || Callback when highlight position changes with current values |
122127

123128
### Types
124129

@@ -207,8 +212,130 @@ type CalendarStrings = {
207212
}
208213
```
209214
215+
#### ErrorSegment
216+
217+
```typescript
218+
type ErrorSegment = {
219+
message: string // Error message to display
220+
messageColor: string // Color for the error message
221+
start: number // Start timestamp (ms)
222+
end: number // End timestamp (ms)
223+
}
224+
```
225+
226+
#### XDividerConfig
227+
228+
```typescript
229+
// Option 1: Tick style (lines extending from labels)
230+
type XDividerTick = {
231+
type: 'tick'
232+
color?: string // Defaults to ChartColors.border
233+
strokeWidth?: number // Defaults to 0.5
234+
strokeDasharray?: string // Defaults to '2,2'
235+
}
236+
237+
// Option 2: Segment style (alternating full-height segments)
238+
type XDividerSegment = {
239+
type: 'segment'
240+
variant?: 'hour' | 'day' | { dynamicThreshold: number } // Defaults to dynamic
241+
color?: string // Defaults to '#FBFBFC' with gradient
242+
}
243+
244+
type XDividerConfig = XDividerTick | XDividerSegment
245+
```
246+
247+
#### HighlightPayload
248+
249+
```typescript
250+
type HighlightPayload = {
251+
timestamp: number // Exact timestamp at highlight position
252+
values: Array<{
253+
value: number | null // Data value at this point
254+
timestamp: number // Point timestamp
255+
color: string // Dataset color
256+
errorMessage: string | null // Error message if in error segment
257+
measurementName: string // Dataset name
258+
} | null> // null if dataset has no data at this position
259+
}
260+
```
261+
210262
## Advanced Usage
211263
264+
### Highlight Position and Value Display
265+
266+
Control where the vertical highlight line appears and how values are displayed:
267+
268+
```tsx
269+
<Chart
270+
// ... other props
271+
highlightPosition={0.7} // Position from 0 (left) to 1 (right), default: 0.5 (center)
272+
highlightValuePosition="tooltip" // 'top' (header), 'tooltip' (floating box), or 'none'
273+
onHighlightChanged={useCallback((payload) => {
274+
console.log('Current timestamp:', payload.timestamp)
275+
console.log('Values:', payload.values)
276+
}, [])}
277+
/>
278+
```
279+
280+
**Highlight value position modes:**
281+
282+
- `'top'` (default): Shows values in the chart header area
283+
- `'tooltip'`: Displays a floating tooltip box near the highlight line
284+
- `'none'`: Hides value display, useful with `onHighlightChanged` for custom UI
285+
286+
### X-Axis Dividers
287+
288+
Customize the vertical grid lines on the X-axis:
289+
290+
```tsx
291+
// Dashed tick lines (default style)
292+
<Chart
293+
xDividerConfig={{
294+
type: 'tick',
295+
color: '#999',
296+
strokeWidth: 1,
297+
strokeDasharray: '4,4',
298+
}}
299+
/>
300+
301+
// Alternating background segments
302+
<Chart
303+
xDividerConfig={{
304+
type: 'segment',
305+
variant: 'hour', // or 'day' or { dynamicThreshold: 95040000 }
306+
color: '#F5F5F5',
307+
}}
308+
/>
309+
```
310+
311+
**Segment variants:**
312+
313+
- `'hour'`: Every other hour has a background segment
314+
- `'day'`: Every other day has a background segment
315+
- `{ dynamicThreshold: number }`: Auto-switches between hour/day based on visible time range
316+
317+
### Error Segments
318+
319+
Display error messages and highlight problematic time ranges:
320+
321+
```tsx
322+
const errorSegments = [
323+
{
324+
message: 'Sensor offline',
325+
messageColor: '#FF0000',
326+
start: Date.now() - 3600000,
327+
end: Date.now() - 1800000,
328+
},
329+
]
330+
331+
<Chart
332+
// ... other props
333+
errorSegments={errorSegments}
334+
/>
335+
```
336+
337+
Data points within error segments will show the error message instead of values, and the background will be highlighted.
338+
212339
### Multiple Datasets
213340

214341
```tsx

android/src/main/assets/chart.html

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@
1717
-moz-user-select: none;
1818
-ms-user-select: none;
1919
user-select: none;
20+
font-family: 'Helvetica';
2021
margin: 0;
2122
}
2223
#my_dataviz {
2324
position: relative;
2425
font-size: 0;
2526
}
26-
#highlight_holder {
27+
#top_highlight_holder {
2728
display: flex;
2829
flex-direction: row;
2930
align-items: center;
@@ -61,12 +62,50 @@
6162
font-weight: 700;
6263
margin-left: 8px;
6364
}
65+
#tooltip_holder {
66+
position: absolute;
67+
width: fit-content;
68+
top: 0;
69+
bottom: 0;
70+
pointer-events: none;
71+
z-index: 10;
72+
}
73+
#tooltip_box {
74+
background-color: white;
75+
display: flex;
76+
flex-direction: column;
77+
width: fit-content;
78+
height: fit-content;
79+
pointer-events: all;
80+
border-radius: 6px;
81+
padding: 6px;
82+
margin-top: 4px;
83+
box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
84+
transform: translateX(-50%);
85+
}
86+
#tooltip_holder span {
87+
font-family: Arial, Helvetica, sans-serif;
88+
font-size: medium;
89+
text-align: center;
90+
}
91+
#tooltip_values_holder {
92+
display: flex;
93+
flex-direction: column;
94+
gap: 4px;
95+
}
6496
</style>
6597

6698
<!-- Create a div where the graph will take place -->
6799
<body>
68100
<div id="my_dataviz">
69-
<div id="highlight_holder">
101+
<div id="tooltip_holder" style="display: none">
102+
<div id="tooltip_box">
103+
<div id="tooltip_values_holder"></div>
104+
<span id="tooltip_time"></span>
105+
<span id="tooltip_date"></span>
106+
</div>
107+
</div>
108+
<div id="top_highlight_holder" style="display: none">
70109
<div id="labels_holder" class="row_holder"></div>
71110
<div id="values_holder" class="row_holder"></div>
72111
<div id="timeholder">

example/src/App.tsx

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

4-
import Chart, { ChartProps, Dataset, ErrorSegment } from 'react-native-d3-chart'
4+
import Chart, {
5+
type Dataset,
6+
type ChartProps,
7+
type ErrorSegment,
8+
} from 'react-native-d3-chart'
59

610
import { buildSlices } from './helpers/buildSlices'
711
import { generateTimeSeriesData } from './helpers/generateTimeSeriesData'
812
import { temperatureData, visits } from './mockData'
13+
import type { HighlightPayload } from '../../src/types'
914

1015
type TimeDomainType = 'hour' | 'day' | 'week' | 'month'
1116

@@ -138,6 +143,12 @@ export default function App() {
138143
[Measurement.Temperature]
139144
)
140145

146+
const [highlightValuePosition, setHighlightValuePosition] = useState<
147+
'top' | 'tooltip' | 'none'
148+
>('tooltip')
149+
150+
const [currentHighlight, setCurrentHighlight] = useState<HighlightPayload>()
151+
141152
const errorSegments = useMemo<ErrorSegment[]>(() => {
142153
const now = Date.now()
143154
return [
@@ -188,6 +199,21 @@ export default function App() {
188199
style={styles.holder}
189200
onLayout={(e) => setWidth(e.nativeEvent.layout.width)}
190201
>
202+
<View style={styles.optionsRow}>
203+
{(['top', 'tooltip', 'none'] as const).map((type) => (
204+
<TouchableOpacity
205+
key={type}
206+
style={[
207+
styles.optionsItem,
208+
type === highlightValuePosition &&
209+
styles.highlightPositionItemActive,
210+
]}
211+
onPress={() => setHighlightValuePosition(type)}
212+
>
213+
<Text style={styles.optionsItemText}>{type}</Text>
214+
</TouchableOpacity>
215+
))}
216+
</View>
191217
<Chart
192218
zoomEnabled
193219
width={width}
@@ -198,22 +224,38 @@ export default function App() {
198224
marginHorizontal={PADDING}
199225
errorSegments={errorSegments}
200226
noDataString="No data available"
227+
highlightValuePosition={highlightValuePosition}
228+
xDividerConfig={{ type: 'segment', color: '#F2F2FF' }}
229+
onHighlightChanged={setCurrentHighlight}
201230
/>
202231
<View style={styles.spacer} />
203-
<View style={styles.timeDomainRow}>
232+
<View style={styles.optionsRow}>
204233
{TIME_DOMAIN_TYPES.map((type) => (
205234
<TouchableOpacity
206235
key={type}
207236
style={[
208-
styles.timeDomainItem,
237+
styles.optionsItem,
209238
type === timeDomainType && styles.timeDomainItemActive,
210239
]}
211240
onPress={() => setTimeDomainType(type)}
212241
>
213-
<Text style={styles.timeDomainItemText}>{type}</Text>
242+
<Text style={styles.optionsItemText}>{type}</Text>
214243
</TouchableOpacity>
215244
))}
216245
</View>
246+
<View style={styles.highlightContainer}>
247+
<Text>Highlight listener:</Text>
248+
<Text>
249+
Exact timestamp:
250+
{currentHighlight?.timestamp &&
251+
new Date(currentHighlight.timestamp).toTimeString()}
252+
</Text>
253+
{currentHighlight?.values.map((value, index) => (
254+
<Text key={index} style={{ color: value?.color || '#000' }}>
255+
{`${value?.measurementName}: ${value?.errorMessage ?? value?.value}`}
256+
</Text>
257+
))}
258+
</View>
217259
{/* Measurement toggles */}
218260
{measurementKeys.map((measurement) => (
219261
<View key={measurement} style={styles.switchContainer}>
@@ -253,28 +295,40 @@ const styles = StyleSheet.create({
253295
width: '100%',
254296
flex: 1,
255297
borderRadius: 10,
256-
paddingVertical: PADDING,
298+
paddingTop: 30,
299+
paddingBottom: PADDING,
257300
backgroundColor: '#fff',
258301
},
259302
spacer: {
260303
height: 10,
261304
},
262-
timeDomainRow: {
305+
highlightContainer: {
306+
right: 20,
307+
bottom: 40,
308+
opacity: 0.9,
309+
position: 'absolute',
310+
backgroundColor: '#dfe',
311+
},
312+
optionsRow: {
263313
flexDirection: 'row',
264314
paddingHorizontal: PADDING,
315+
marginVertical: 10,
265316
},
266-
timeDomainItem: {
317+
optionsItem: {
267318
flex: 1,
268319
borderRadius: 11,
269320
paddingVertical: 2,
270321
justifyContent: 'center',
271322
alignItems: 'center',
272323
},
324+
optionsItemText: {
325+
fontSize: 13,
326+
},
273327
timeDomainItemActive: {
274328
backgroundColor: '#b22',
275329
},
276-
timeDomainItemText: {
277-
fontSize: 13,
330+
highlightPositionItemActive: {
331+
backgroundColor: '#2b2',
278332
},
279333
switchContainer: {
280334
marginVertical: 10,

0 commit comments

Comments
 (0)