airbnb/caravel

View on GitHub
superset-frontend/plugins/legacy-plugin-chart-horizon/src/HorizonRow.jsx

Summary

Maintainability
C
1 day
Test Coverage
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/* eslint-disable no-continue, no-bitwise */
/* eslint-disable react/jsx-sort-default-props */
/* eslint-disable react/sort-prop-types */
import { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { extent as d3Extent } from 'd3-array';
import { scaleLinear } from 'd3-scale';

export const DEFAULT_COLORS = [
  '#313695',
  '#4575b4',
  '#74add1',
  '#abd9e9',
  '#fee090',
  '#fdae61',
  '#f46d43',
  '#d73027',
];

const propTypes = {
  className: PropTypes.string,
  width: PropTypes.number,
  height: PropTypes.number,
  data: PropTypes.arrayOf(
    PropTypes.shape({
      y: PropTypes.number,
    }),
  ).isRequired,
  bands: PropTypes.number,
  colors: PropTypes.arrayOf(PropTypes.string),
  colorScale: PropTypes.string,
  mode: PropTypes.string,
  offsetX: PropTypes.number,
  title: PropTypes.string,
  yDomain: PropTypes.arrayOf(PropTypes.number),
};

const defaultProps = {
  className: '',
  width: 800,
  height: 20,
  bands: DEFAULT_COLORS.length >> 1,
  colors: DEFAULT_COLORS,
  colorScale: 'series',
  mode: 'offset',
  offsetX: 0,
  title: '',
  yDomain: undefined,
};

class HorizonRow extends PureComponent {
  componentDidMount() {
    this.drawChart();
  }

  componentDidUpdate() {
    this.drawChart();
  }

  componentWillUnmount() {
    this.canvas = null;
  }

  drawChart() {
    if (this.canvas) {
      const {
        data: rawData,
        yDomain,
        width,
        height,
        bands,
        colors,
        colorScale,
        offsetX,
        mode,
      } = this.props;

      const data =
        colorScale === 'change'
          ? rawData.map(d => ({ ...d, y: d.y - rawData[0].y }))
          : rawData;

      const context = this.canvas.getContext('2d');
      context.imageSmoothingEnabled = false;
      context.clearRect(0, 0, width, height);
      // Reset transform
      context.setTransform(1, 0, 0, 1, 0, 0);
      context.translate(0.5, 0.5);

      const step = width / data.length;
      // the data frame currently being shown:
      const startIndex = Math.floor(Math.max(0, -(offsetX / step)));
      const endIndex = Math.floor(
        Math.min(data.length, startIndex + width / step),
      );

      // skip drawing if there's no data to be drawn
      if (startIndex > data.length) {
        return;
      }

      // Create y-scale
      const [min, max] = yDomain || d3Extent(data, d => d.y);
      const y = scaleLinear()
        .domain([0, Math.max(-min, max)])
        .range([0, height]);

      // we are drawing positive & negative bands separately to avoid mutating canvas state
      // http://www.html5rocks.com/en/tutorials/canvas/performance/
      let hasNegative = false;
      // draw positive bands
      let value;
      let bExtents;
      for (let b = 0; b < bands; b += 1) {
        context.fillStyle = colors[bands + b];

        // Adjust the range based on the current band index.
        bExtents = (b + 1 - bands) * height;
        y.range([bands * height + bExtents, bExtents]);

        // only the current data frame is being drawn i.e. what's visible:
        for (let i = startIndex; i < endIndex; i += 1) {
          value = data[i].y;
          if (value <= 0) {
            hasNegative = true;
            continue;
          }
          if (value !== undefined) {
            context.fillRect(
              offsetX + i * step,
              y(value),
              step + 1,
              y(0) - y(value),
            );
          }
        }
      }

      // draw negative bands
      if (hasNegative) {
        // mirror the negative bands, by flipping the canvas
        if (mode === 'offset') {
          context.translate(0, height);
          context.scale(1, -1);
        }

        for (let b = 0; b < bands; b += 1) {
          context.fillStyle = colors[bands - b - 1];

          // Adjust the range based on the current band index.
          bExtents = (b + 1 - bands) * height;
          y.range([bands * height + bExtents, bExtents]);

          // only the current data frame is being drawn i.e. what's visible:
          for (let ii = startIndex; ii < endIndex; ii += 1) {
            value = data[ii].y;
            if (value >= 0) {
              continue;
            }
            context.fillRect(
              offsetX + ii * step,
              y(-value),
              step + 1,
              y(0) - y(-value),
            );
          }
        }
      }
    }
  }

  render() {
    const { className, title, width, height } = this.props;

    return (
      <div className={`horizon-row ${className}`}>
        <span className="title">{title}</span>
        <canvas
          ref={c => {
            this.canvas = c;
          }}
          width={width}
          height={height}
        />
      </div>
    );
  }
}

HorizonRow.propTypes = propTypes;
HorizonRow.defaultProps = defaultProps;

export default HorizonRow;