Back to Gallery
Parallel Brush Axes
const data = [ { name: "Adrien", strength: 5, intelligence: 30, speed: 500, luck: 3 }, { name: "Brice", strength: 1, intelligence: 13, speed: 550, luck: 2 }, { name: "Casey", strength: 4, intelligence: 15, speed: 80, luck: 1 }, { name: "Drew", strength: 3, intelligence: 25, speed: 600, luck: 5 }, { name: "Erin", strength: 9, intelligence: 50, speed: 350, luck: 4 }, { name: "Francis", strength: 2, intelligence: 40, speed: 200, luck: 2 } ]; const attributes = ["strength", "intelligence", "speed", "luck"]; const height = 500; const width = 700; const padding = { top: 100, left: 50, right: 50, bottom: 50 }; function getMaximumValues() { // Find the maximum value for each axis. This will be used to normalize data and re-scale axis ticks return attributes.map((attribute) => { return data.reduce((memo, datum) => { return datum[attribute] > memo ? datum[attribute] : memo; }, -Infinity); }); } function normalizeData(maximumValues) { // construct normalized datasets by dividing the value for each attribute by the maximum value return data.map((datum) => ({ name: datum.name, data: attributes.map((attribute, i) => ( { x: attribute, y: datum[attribute] / maximumValues[i] } )) })); } function App() { const maximumValues = getMaximumValues(); const datasets = normalizeData(maximumValues); const [state, setState] = React.useState({ maximumValues, datasets, filters: {}, activeDatasets: [], isFiltered: false }); function addNewFilters(domain, props) { const filters = state.filters || {}; const extent = domain && Math.abs(domain[1] - domain[0]); const minVal = 1 / Number.MAX_SAFE_INTEGER; filters[props.name] = extent <= minVal ? undefined : domain; return filters; } function getActiveDatasets(filters) { // Return the names from all datasets that have values within all filters const isActive = (dataset) => { return _.keys(filters).reduce((memo, name) => { if (!memo || !Array.isArray(filters[name])) { return memo; } const point = _.find(dataset.data, (d) => d.x === name); return point && Math.max(...filters[name]) >= point.y && Math.min(...filters[name]) <= point.y; }, true); }; return state.datasets.map((dataset) => { return isActive(dataset, filters) ? dataset.name : null; }).filter(Boolean); } function onDomainChange(domain, props) { const filters = addNewFilters(domain, props); const isFiltered = !_.isEmpty(_.values(filters).filter(Boolean)); const activeDatasets = isFiltered ? getActiveDatasets(filters) : state.datasets; setState({ activeDatasets, filters, isFiltered }); } function isActive(dataset) { // Determine whether a given dataset is active return !state.isFiltered ? true : _.includes(state.activeDatasets, dataset.name); } function getAxisOffset(index) { const step = (width - padding.left - padding.right) / (attributes.length - 1); return step * index + padding.left; } return ( <VictoryChart domain={{ y: [0, 1.1] }} height={height} width={width} padding={padding} > <VictoryAxis style={{ tickLabels: { fontSize: 20 }, axis: { stroke: "none" } }} tickLabelComponent={<VictoryLabel y={padding.top - 40}/>} /> {state.datasets.map((dataset) => ( <VictoryLine key={dataset.name} name={dataset.name} data={dataset.data} groupComponent={<g/>} style={{ data: { stroke: "tomato", opacity: isActive(dataset) ? 1 : 0.2 } }} /> ))} {attributes.map((attribute, index) => ( <VictoryAxis dependentAxis key={index} axisComponent={ <VictoryBrushLine name={attribute} width={20} onBrushDomainChange={onDomainChange.bind(this)} /> } offsetX={getAxisOffset(index)} style={{ tickLabels: { fontSize: 15, padding: 15, pointerEvents: "none" }, }} tickValues={[0.2, 0.4, 0.6, 0.8, 1]} tickFormat={(tick) => Math.round(tick * state.maximumValues[index])} /> ))} </VictoryChart> ); } render(<App/>);