Skip to content

Commit 80bfe41

Browse files
author
Becca Bailey
authored
Merge pull request #2077 from FormidableLabs/zibs/native-victory-brush-line
feat: Add VictoryNativeBrushLine component
2 parents a75fc74 + 0f86f1f commit 80bfe41

10 files changed

+439
-3
lines changed

demo/rn/src/navigation-config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type RootStackNavigatorParams = {
1010
BoxPlot: undefined;
1111
ErrorBar: undefined;
1212
Voronoi: undefined;
13+
BrushLine: undefined;
1314

1415
Legends: undefined;
1516
Axis: undefined;
+289
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import * as React from "react";
2+
import { Dimensions, ScrollView } from "react-native";
3+
import {
4+
VictoryAxis,
5+
VictoryBrushLine,
6+
VictoryChart,
7+
VictoryScatter,
8+
VictoryLine,
9+
VictoryLabel,
10+
VictoryBar,
11+
VictoryContainer
12+
} from "victory-native";
13+
import { DomainTuple } from "victory-core";
14+
import _ from "lodash";
15+
16+
import viewStyles from "../styles/view-styles";
17+
import { VictoryBrushLineProps } from "victory-brush-line";
18+
19+
type DataType = {
20+
name: string;
21+
strength: number;
22+
intelligence: number;
23+
speed: number;
24+
luck: number;
25+
}[];
26+
27+
interface DataSet {
28+
name: string;
29+
data: { x: string; y: number }[];
30+
}
31+
32+
const data: DataType = [
33+
{ name: "Adrien", strength: 5, intelligence: 30, speed: 500, luck: 3 },
34+
{ name: "Brice", strength: 1, intelligence: 13, speed: 550, luck: 2 },
35+
{ name: "Casey", strength: 4, intelligence: 15, speed: 80, luck: 1 },
36+
{ name: "Drew", strength: 3, intelligence: 25, speed: 600, luck: 5 },
37+
{ name: "Erin", strength: 9, intelligence: 50, speed: 350, luck: 4 },
38+
{ name: "Francis", strength: 2, intelligence: 40, speed: 200, luck: 2 }
39+
];
40+
41+
type Attribute = "strength" | "intelligence" | "speed" | "luck";
42+
const attributes: Attribute[] = ["strength", "intelligence", "speed", "luck"];
43+
44+
type Filter = Record<Attribute, number[] | undefined>;
45+
46+
const width = Dimensions.get("window").width;
47+
const padding: { [key: string]: number } = {
48+
top: 100,
49+
left: 50,
50+
right: 50,
51+
bottom: 50
52+
};
53+
54+
function normalizeData(maximumValues: number[]) {
55+
// construct normalized datasets by dividing the value for each attribute by the maximum value
56+
return data.map((datum) => ({
57+
name: datum.name,
58+
data: attributes.map((attribute, i) => ({
59+
x: attribute,
60+
y: datum[attribute] / maximumValues[i]
61+
}))
62+
}));
63+
}
64+
65+
function getMaximumValues() {
66+
return attributes.map((attribute) => {
67+
return data.reduce((memo, datum) => {
68+
return datum[attribute] > memo ? datum[attribute] : memo;
69+
}, -Infinity);
70+
});
71+
}
72+
73+
function getAxisOffset(index: number) {
74+
const step = (width - padding.left - padding.right) / (attributes.length - 1);
75+
return step * index + padding.left;
76+
}
77+
78+
export const BrushLineScreen: React.FC = () => {
79+
const [scrollEnabled, setScrollEnabled] = React.useState(true);
80+
const [maximumValues] = React.useState(getMaximumValues());
81+
const [datasets] = React.useState<DataSet[]>(normalizeData(maximumValues));
82+
const [filters, setFilters] = React.useState<Filter>({
83+
intelligence: undefined,
84+
strength: undefined,
85+
luck: undefined,
86+
speed: undefined
87+
});
88+
const [isFiltered, setIsFiltered] = React.useState(false);
89+
const [activeDatasets, setActiveDatasets] = React.useState<string[]>([]);
90+
91+
const onDomainChange = (
92+
domain: DomainTuple,
93+
props: VictoryBrushLineProps | undefined
94+
) => {
95+
const filters = addNewFilters(domain, props);
96+
const isFiltered = !_.isEmpty(_.values(filters).filter(Boolean));
97+
const activeDatasets = isFiltered
98+
? getActiveDatasets(filters)
99+
: datasets.map((x) => x.name);
100+
setFilters(filters);
101+
setIsFiltered(isFiltered);
102+
setActiveDatasets(activeDatasets);
103+
};
104+
105+
const addNewFilters = (
106+
domain: DomainTuple,
107+
props: VictoryBrushLineProps | undefined
108+
): Filter => {
109+
if (!domain) return filters;
110+
if (typeof domain[0] != "number") return filters;
111+
if (typeof domain[1] != "number") return filters;
112+
if (!props) return filters;
113+
114+
const dm = domain as [number, number];
115+
const currentFilters = filters;
116+
const extent = dm && Math.abs(dm[1] - dm[0]);
117+
const minVal = 1 / Number.MAX_VALUE;
118+
119+
currentFilters[props.name as Attribute] = extent <= minVal ? undefined : dm;
120+
121+
return currentFilters;
122+
};
123+
124+
const getActiveDatasets = (filters: Filter): string[] => {
125+
// Return the names from all datasets that have values within all filters
126+
const isActive = (dataset: DataSet): (string | null | undefined)[] => {
127+
return Object.keys(filters).reduce((memo: any, name) => {
128+
let filter = name as keyof Filter;
129+
130+
if (!memo || !Array.isArray(filters[filter])) {
131+
return memo;
132+
}
133+
const point = _.find(dataset.data, (d) => d.x === filter);
134+
let tuple = filters[filter];
135+
return (
136+
point &&
137+
tuple &&
138+
Math.max(...tuple) >= point.y &&
139+
Math.min(...tuple) <= point.y
140+
);
141+
}, true);
142+
};
143+
144+
return datasets
145+
.map((dataset) => (isActive(dataset) ? dataset.name : null))
146+
.filter(Boolean) as string[];
147+
};
148+
149+
const isActive = (dataset: DataSet): boolean => {
150+
// Determine whether a given dataset is active
151+
return !isFiltered ? true : _.includes(activeDatasets, dataset.name);
152+
};
153+
154+
const max = maximumValues || [];
155+
156+
return (
157+
<ScrollView scrollEnabled={scrollEnabled} style={viewStyles.container}>
158+
<VictoryChart
159+
containerComponent={
160+
<VictoryContainer
161+
onTouchStart={() => setScrollEnabled(false)}
162+
onTouchEnd={() => setScrollEnabled(true)}
163+
/>
164+
}
165+
domain={{ y: [0, 1.1] }}
166+
width={width}
167+
padding={padding}
168+
>
169+
<VictoryAxis
170+
style={{
171+
tickLabels: { fontSize: 20 },
172+
axis: { stroke: "none" }
173+
}}
174+
tickLabelComponent={<VictoryLabel y={padding.top - 40} />}
175+
/>
176+
{datasets.map((dataset: DataSet) => (
177+
<VictoryLine
178+
key={dataset.name}
179+
name={dataset.name}
180+
data={dataset.data}
181+
style={{
182+
data: {
183+
stroke: "tomato",
184+
opacity: isActive(dataset) ? 1 : 0.2
185+
}
186+
}}
187+
/>
188+
))}
189+
{attributes.map((attribute, index) => (
190+
<VictoryAxis
191+
dependentAxis
192+
name={attribute}
193+
key={index}
194+
axisComponent={
195+
<VictoryBrushLine
196+
name={attribute}
197+
width={20}
198+
onTouchStart={() => setScrollEnabled(false)}
199+
onTouchEnd={() => setScrollEnabled(true)}
200+
onBrushDomainChange={onDomainChange}
201+
/>
202+
}
203+
offsetX={getAxisOffset(index)}
204+
style={{
205+
tickLabels: {
206+
fontSize: 15,
207+
padding: 15,
208+
pointerEvents: "none"
209+
}
210+
}}
211+
tickValues={[0.2, 0.4, 0.6, 0.8, 1]}
212+
tickFormat={(tick) => Math.round(tick * max[index])}
213+
/>
214+
))}
215+
</VictoryChart>
216+
<VictoryChart>
217+
<VictoryScatter
218+
data={[
219+
{ x: "one", y: 0 },
220+
{ x: "two", y: 2 },
221+
{ x: "three", y: 4 }
222+
]}
223+
/>
224+
<VictoryAxis
225+
gridComponent={
226+
<VictoryBrushLine
227+
onTouchStart={() => setScrollEnabled(false)}
228+
onTouchEnd={() => setScrollEnabled(true)}
229+
brushWidth={20}
230+
/>
231+
}
232+
/>
233+
</VictoryChart>
234+
<VictoryChart domainPadding={{ x: 80 }}>
235+
<VictoryBar
236+
data={[
237+
{ x: "one", y: 4 },
238+
{ x: "two", y: 5 },
239+
{ x: "three", y: 6 }
240+
]}
241+
/>
242+
<VictoryAxis
243+
dependentAxis
244+
axisComponent={
245+
<VictoryBrushLine
246+
onTouchStart={() => setScrollEnabled(false)}
247+
onTouchEnd={() => setScrollEnabled(true)}
248+
brushWidth={20}
249+
brushDomain={[2, 3]}
250+
/>
251+
}
252+
/>
253+
</VictoryChart>
254+
<VictoryChart domainPadding={50}>
255+
<VictoryScatter
256+
size={4}
257+
style={{ data: { fill: "tomato" } }}
258+
data={[
259+
{ x: 1, y: 1 },
260+
{ x: 2, y: 2 },
261+
{ x: 3, y: 4 }
262+
]}
263+
/>
264+
<VictoryAxis
265+
tickValues={[1, 2, 3]}
266+
gridComponent={
267+
<VictoryBrushLine
268+
onTouchStart={() => setScrollEnabled(false)}
269+
onTouchEnd={() => setScrollEnabled(true)}
270+
width={30}
271+
/>
272+
}
273+
/>
274+
</VictoryChart>
275+
276+
<VictoryAxis
277+
standalone
278+
gridComponent={
279+
<VictoryBrushLine
280+
onTouchStart={() => setScrollEnabled(false)}
281+
onTouchEnd={() => setScrollEnabled(true)}
282+
brushWidth={10}
283+
brushDomain={[0, 10]}
284+
/>
285+
}
286+
/>
287+
</ScrollView>
288+
);
289+
};

demo/rn/src/screens/components-screen.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ const sections: SectionItem[] = [
7474
{ key: "Scatter", title: "VictoryScatter" },
7575
{ key: "BoxPlot", title: "VictoryBoxPlot" },
7676
{ key: "ErrorBar", title: "VictoryErrorBar" },
77-
{ key: "Voronoi", title: "VictoryVoronoi" }
77+
{ key: "Voronoi", title: "VictoryVoronoi" },
78+
{ key: "BrushLine", title: "VictoryBrushLine" }
7879
],
7980
title: "Charts"
8081
},

demo/rn/src/screens/root-navigator.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { BoxPlotScreen } from "./box-plot-screen";
1515
import { ErrorBarScreen } from "./error-bar-screen";
1616
import { PolarAxisScreen } from "./polar-axis-screen";
1717
import { VoronoiScreen } from "./voronoi-screen";
18+
import { BrushLineScreen } from "./brush-line-screen";
1819

1920
export const RootNavigator: React.FC = () => {
2021
return (
@@ -70,6 +71,11 @@ export const RootNavigator: React.FC = () => {
7071
component={VoronoiScreen}
7172
options={{ title: "VictoryVoronoi" }}
7273
/>
74+
<RootStack.Screen
75+
name="BrushLine"
76+
component={BrushLineScreen}
77+
options={{ title: "VictoryBrushLine" }}
78+
/>
7379

7480
{/* Other */}
7581
<RootStack.Screen

packages/victory-native/src/components/victory-brush-container.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ RectWithStyle.propTypes = {
2020
};
2121

2222
const nativeBrushMixin = (base) =>
23-
class VictoryNativeSelectionContainer extends base {
23+
class VictoryNativeBrushContainer extends base {
2424
// eslint-disable-line max-len
2525
// assign native specific defaultProps over web `VictoryBrushContainer` defaultProps
2626
static defaultProps = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from "react";
2+
import { VictoryBrushLineProps } from "victory-brush-line";
3+
4+
export interface VictoryNativeBrushLine extends VictoryBrushLineProps {
5+
onTouchStart?: Function;
6+
onTouchEnd?: Function;
7+
}
8+
9+
export default class extends React.Component<VictoryNativeBrushLine, any> {}

0 commit comments

Comments
 (0)