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 = 500;
const padding = { top: 100, left: 50, right: 50, bottom: 50 };

class App extends React.Component {
  constructor() {
    super();
    const maximumValues = this.getMaximumValues();
    const datasets = this.normalizeData(maximumValues);
    this.state = {
      maximumValues, datasets, filters: {}, activeDatasets: [], isFiltered: false
    };
  }

  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);
    });
  }

  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] }
      ))
    }));
  }

  addNewFilters(domain, props) {
    const filters = this.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;
  }

  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 this.state.datasets.map((dataset) => {
      return isActive(dataset, filters) ? dataset.name : null;
    }).filter(Boolean);
  }

  onDomainChange(domain, props) {
    const filters = this.addNewFilters(domain, props);
    const isFiltered = !_.isEmpty(_.values(filters).filter(Boolean));
    const activeDatasets = isFiltered ? this.getActiveDatasets(filters) : this.state.datasets;
    this.setState({ activeDatasets, filters, isFiltered });
  }

  isActive(dataset) {
    // Determine whether a given dataset is active
    return !this.state.isFiltered ? true : _.includes(this.state.activeDatasets, dataset.name);
  }

  getAxisOffset(index) {
    const step = (width - padding.left - padding.right) / (attributes.length - 1);
    return step * index + padding.left;
  }

  render() {
    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}/>}
        />
        {this.state.datasets.map((dataset) => (
          <VictoryLine
            key={dataset.name} name={dataset.name} data={dataset.data}
            groupComponent={<g/>}
            style={{ data: {
              stroke: "tomato",
              opacity: this.isActive(dataset) ? 1 : 0.2
            } }}
          />
        ))}
        {attributes.map((attribute, index) => (
          <VictoryAxis dependentAxis
            key={index}
            axisComponent={
              <VictoryBrushLine name={attribute}
                width={20}
                onBrushDomainChange={this.onDomainChange.bind(this)}
              />
            }
            offsetX={this.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 * this.state.maximumValues[index])}
          />
        ))}
      </VictoryChart>
    );
  }
}

render(<App/>);