|
| 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 | +}; |
0 commit comments