Skip to content

Commit 1030ed1

Browse files
Merge pull request #5 from mitigate-dev/develop
feat: Support slices
2 parents b7b21b0 + 88da23b commit 1030ed1

8 files changed

Lines changed: 406 additions & 59 deletions

File tree

README.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,9 @@ type Dataset = {
132132
unit: string // Unit symbol (e.g., '°C', 'kg', 'm/s')
133133
decimals: number // Number of decimal places to show
134134
minDeltaY?: number // Minimum Y-axis change to show, limit Y-zoom
135-
areaColor?: string // Optional area fill color (defaults to base color)
135+
areaColor?: string | null // Area fill color (null to disable, defaults to base color)
136136
axisColor?: string // Optional Y-axis text color (defaults to base color)
137+
slices?: Slices // Optional background regions/zones
137138
decimalSeparator?: '.' | ',' // Decimal separator
138139
domain?: {
139140
// Custom Y-axis range
@@ -142,6 +143,16 @@ type Dataset = {
142143
}
143144
}
144145

146+
type Slices = {
147+
start: number // Start timestamp for the slices
148+
end: number // End timestamp for the slices
149+
items: Array<{
150+
color: string // Background color (use alpha for transparency)
151+
start: { top: number; bottom: number } // Y-values at start time
152+
end: { top: number; bottom: number } // Y-values at end time
153+
}>
154+
}
155+
145156
type ThresholdColor = {
146157
type: 'thresholds'
147158
baseColor: string // Default color for values below all thresholds
@@ -258,6 +269,48 @@ const datasetWithThresholds = {
258269
- **Battery levels**: Red (critical) → Orange (low) → Green (healthy)
259270
- **Network latency**: Green (fast) → Yellow (moderate) → Red (slow)
260271

272+
### Background Slices/Zones
273+
274+
Add colored background regions to highlight acceptable ranges, warning zones, or targets:
275+
276+
```tsx
277+
const datasetWithSlices = {
278+
measurementName: 'CPU Usage',
279+
color: '#333',
280+
unit: '%',
281+
decimals: 1,
282+
points: cpuData,
283+
slices: {
284+
start: startTimestamp,
285+
end: endTimestamp,
286+
items: [
287+
{
288+
color: '#00FF0020', // Green with transparency (healthy zone)
289+
start: { bottom: 0, top: 50 },
290+
end: { bottom: 0, top: 50 },
291+
},
292+
{
293+
color: '#FFA50020', // Orange with transparency (warning zone)
294+
start: { bottom: 50, top: 80 },
295+
end: { bottom: 50, top: 80 },
296+
},
297+
{
298+
color: '#FF000020', // Red with transparency (critical zone)
299+
start: { bottom: 80, top: 100 },
300+
end: { bottom: 80, top: 100 },
301+
},
302+
],
303+
},
304+
}
305+
```
306+
307+
**Features:**
308+
309+
- **Horizontal zones**: Use same top/bottom values for start and end
310+
- **Diagonal zones**: Use different values to create slanted regions
311+
- **Transparency**: Use alpha channel (e.g., `#FF000020`) for subtle backgrounds
312+
- **Multiple regions**: Stack different colored zones for complex visualizations
313+
261314
### Zoom Callbacks
262315

263316
```tsx

example/src/App.tsx

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { View, StyleSheet, TouchableOpacity, Text, Switch } from 'react-native'
33

44
import Chart, { ChartProps, Dataset } from 'react-native-d3-chart'
55

6+
import { buildSlices } from './helpers/buildSlices'
7+
import { generateTimeSeriesData } from './helpers/generateTimeSeriesData'
8+
import { temperatureData, visits } from './mockData'
9+
610
type TimeDomainType = 'hour' | 'day' | 'week' | 'month'
711

812
const TIME_DOMAIN_TYPES: TimeDomainType[] = ['hour', 'day', 'week', 'month']
@@ -16,53 +20,22 @@ const chartColors: ChartProps['colors'] = {
1620
highlightTime: '#444',
1721
}
1822

19-
// Generate data points every minute from a month ago to now
20-
const generateDataPoints = ({
21-
startingValue = 400,
22-
minimum = 0,
23-
maximum = 3000,
24-
radomFactor = 20,
25-
} = {}) => {
26-
const points = []
27-
const now = Date.now()
28-
const monthAgo = now - 30 * 24 * 60 * 60 * 1000 // 30 days ago
29-
let value = startingValue
30-
31-
for (let timestamp = monthAgo; timestamp <= now; timestamp += 60 * 1000) {
32-
const randomVariation = (Math.random() - 0.5) * radomFactor
33-
value += randomVariation
34-
35-
// either randomVariation was negative and value went below minimum
36-
// or randomVariation was positive and value went above maximum
37-
if (value < minimum || value > maximum) {
38-
// invert direction to keep within bounds
39-
value -= 2 * randomVariation
40-
}
41-
42-
points.push({ timestamp, value })
43-
}
44-
45-
return points
46-
}
47-
4823
enum Measurement {
4924
Temperature = 'Temperature',
5025
Blue = 'Blue',
5126
Green = 'Green',
5227
Pink = 'Pink',
28+
Visits = 'Visits',
29+
VisitRate = 'Visit Rate',
5330
}
31+
5432
const measurementKeys = Object.values(Measurement)
5533
const measurementsRecords: Record<Measurement, Dataset> = {
5634
[Measurement.Temperature]: {
5735
unit: '°C',
58-
points: generateDataPoints({
59-
maximum: 40,
60-
minimum: -10,
61-
radomFactor: 1,
62-
startingValue: -8,
63-
}),
36+
points: temperatureData,
6437
decimals: 0,
65-
areaColor: '#83cba8',
38+
areaColor: '#c4deff',
6639
color: {
6740
type: 'thresholds',
6841
baseColor: '#3d91ff',
@@ -79,7 +52,7 @@ const measurementsRecords: Record<Measurement, Dataset> = {
7952
},
8053
[Measurement.Blue]: {
8154
unit: 'l',
82-
points: generateDataPoints({
55+
points: generateTimeSeriesData({
8356
startingValue: 160,
8457
minimum: 50,
8558
radomFactor: 6,
@@ -90,21 +63,50 @@ const measurementsRecords: Record<Measurement, Dataset> = {
9063
},
9164
[Measurement.Green]: {
9265
unit: 'kg',
93-
points: generateDataPoints(),
66+
points: generateTimeSeriesData(),
9467
decimals: 0,
9568
color: '#6e6',
9669
measurementName: Measurement.Green,
9770
},
9871
[Measurement.Pink]: {
9972
unit: 'm/s',
100-
points: generateDataPoints({
73+
points: generateTimeSeriesData({
10174
startingValue: 20,
10275
minimum: 100,
10376
}),
10477
decimals: 1,
10578
color: '#e0e',
10679
measurementName: Measurement.Pink,
10780
},
81+
82+
[Measurement.VisitRate]: {
83+
unit: 'visits/h',
84+
points: visits.movingAveregeData,
85+
slices: buildSlices('horizontal', {
86+
end: visits.latestTimestamp,
87+
start: visits.oldestTimestamp,
88+
yellowThreshold: visits.averageVisitRatePerHour,
89+
redThreshold: visits.averageVisitRatePerHour * 1.1,
90+
}),
91+
decimals: 0,
92+
color: '#000',
93+
areaColor: null,
94+
measurementName: Measurement.VisitRate,
95+
},
96+
[Measurement.Visits]: {
97+
unit: 'pulses',
98+
points: visits.culmulativeData,
99+
decimals: 0,
100+
color: '#000',
101+
areaColor: null,
102+
measurementName: 'Visits cumulative',
103+
slices: buildSlices('axial', {
104+
end: visits.latestTimestamp,
105+
start: visits.oldestTimestamp,
106+
yellowThreshold: visits.averageVisitRatePerHour,
107+
redThreshold: visits.averageVisitRatePerHour * 1.1,
108+
}),
109+
},
108110
}
109111

110112
export default function App() {

example/src/helpers/buildSlices.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { Slices } from '../../../src/types'
2+
3+
export function buildSlices(
4+
variant: 'horizontal' | 'axial',
5+
{
6+
end,
7+
start,
8+
redThreshold,
9+
yellowThreshold,
10+
}: {
11+
end: number
12+
start: number
13+
redThreshold: number
14+
yellowThreshold: number
15+
}
16+
): Slices {
17+
if (variant === 'horizontal') {
18+
return {
19+
end,
20+
start,
21+
items: [
22+
{
23+
color: '#08985115',
24+
start: { bottom: 0, top: yellowThreshold },
25+
end: { bottom: 0, top: yellowThreshold },
26+
},
27+
{
28+
color: '#ffc40015',
29+
start: {
30+
bottom: yellowThreshold,
31+
top: redThreshold,
32+
},
33+
end: {
34+
bottom: yellowThreshold,
35+
top: redThreshold,
36+
},
37+
},
38+
{
39+
color: '#bb222215',
40+
start: {
41+
bottom: redThreshold,
42+
top: redThreshold * 10,
43+
},
44+
end: {
45+
bottom: redThreshold,
46+
top: redThreshold * 10,
47+
},
48+
},
49+
],
50+
}
51+
}
52+
53+
const dataDurationMs = end - start
54+
const dataDurationHours = dataDurationMs / (60 * 60 * 1000)
55+
56+
const warningEnd = dataDurationHours * yellowThreshold
57+
const dangerEnd = dataDurationHours * redThreshold
58+
59+
const topEdge = redThreshold * 2 * dataDurationHours
60+
61+
return {
62+
end,
63+
start,
64+
items: [
65+
{
66+
color: '#08985115',
67+
start: { bottom: 0, top: 0 },
68+
end: { bottom: 0, top: warningEnd },
69+
},
70+
{
71+
color: '#ffc40015',
72+
start: { bottom: 0, top: 0 },
73+
end: { bottom: warningEnd, top: dangerEnd },
74+
},
75+
{
76+
color: '#bb222215',
77+
start: { bottom: 0, top: topEdge },
78+
end: { bottom: dangerEnd, top: topEdge },
79+
},
80+
],
81+
}
82+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Generates random pulse data points over a specified time range.
3+
* @param maxAgeDays - The maximum age of the data points in days.
4+
* @param intervalMs - Time interval in milliseconds between consecutive data points. Default is 60,000 ms (1 minute).
5+
* @param averageRatePerHour - Average number of pulses per hour. Default is 120.
6+
* @param burstFactor - Factor by which the pulse rate increases during bursts. Default is 3.
7+
* @param burstProbability - Probability of a burst occurring at each interval. Default is 0.1.
8+
* @returns An array of random pulse data points.
9+
*/
10+
export function generateRandomPulses({
11+
maxAgeDays = 30,
12+
intervalMs = 60 * 1000,
13+
averageRatePerHour = 120,
14+
burstFactor = 3,
15+
burstProbability = 0.1,
16+
} = {}) {
17+
const points = []
18+
const now = Date.now()
19+
const oldestTimestamp = now - maxAgeDays * 24 * 60 * 60 * 1000 // maxAgeDays ago
20+
const averageRate = averageRatePerHour / 60 // Convert to per minute
21+
22+
for (
23+
let timestamp = oldestTimestamp;
24+
timestamp <= now;
25+
timestamp += intervalMs
26+
) {
27+
// Generate clustered/bursty Poisson data with higher short-term variance
28+
// Calculate rates to maintain target average: baseRate * (1-p) + burstRate * p = averageRate
29+
const burstRate = averageRate * burstFactor
30+
const baseRate =
31+
(averageRate - burstRate * burstProbability) / (1 - burstProbability)
32+
33+
let pulses = 0
34+
35+
// Determine if this is a burst period
36+
const isBurst = Math.random() < burstProbability
37+
const lambda = isBurst ? burstRate : baseRate
38+
39+
if (lambda < 30) {
40+
// Knuth's algorithm for small λ
41+
const L = Math.exp(-lambda)
42+
let k = 0
43+
let p = 1
44+
45+
do {
46+
k++
47+
p *= Math.random()
48+
} while (p > L)
49+
50+
pulses = k - 1
51+
} else {
52+
// Normal approximation for large λ
53+
const u1 = Math.random()
54+
const u2 = Math.random()
55+
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2)
56+
pulses = Math.max(0, Math.round(lambda + Math.sqrt(lambda) * z))
57+
}
58+
59+
points.push({ timestamp, value: pulses })
60+
}
61+
62+
return points
63+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Generates time series data points, each point deviating slightly from the previous one.
3+
* @param startingValue Initial value for the first data point. Default is 400.
4+
* @param minimum Minimum value for the data points. Default is 0.
5+
* @param maximum Maximum value for the data points. Default is 3000.
6+
* @param maxAgeDays Number of days in the past to start generating data from. Default is 30 days.
7+
* @param intervalMs Time interval in milliseconds between consecutive data points. Default is 60,000 ms (1 minute).
8+
* @param radomFactor Maximum random variation applied to each point. Default is 20.
9+
* @returns Array of data points with timestamps and values.
10+
*/
11+
export function generateTimeSeriesData({
12+
startingValue = 400,
13+
minimum = 0,
14+
maximum = 3000,
15+
maxAgeDays = 30,
16+
intervalMs = 60 * 1000,
17+
radomFactor = 20,
18+
} = {}) {
19+
const points = []
20+
const now = Date.now()
21+
const monthAgo = now - maxAgeDays * 24 * 60 * 60 * 1000
22+
let value = startingValue
23+
24+
for (let timestamp = monthAgo; timestamp <= now; timestamp += intervalMs) {
25+
const randomVariation = (Math.random() - 0.5) * radomFactor
26+
value += randomVariation
27+
28+
// either randomVariation was negative and value went below minimum
29+
// or randomVariation was positive and value went above maximum
30+
if (value < minimum || value > maximum) {
31+
// invert direction to keep within bounds
32+
value -= 2 * randomVariation
33+
}
34+
35+
points.push({ timestamp, value })
36+
}
37+
38+
return points
39+
}

0 commit comments

Comments
 (0)