toggle-corp/react-store

View on GitHub
components/Visualization/SparkLine/index.js

Summary

Maintainability
A
0 mins
Test Coverage
import PropTypes from 'prop-types';
import React from 'react';

import { scaleLinear } from 'd3-scale';
import { select } from 'd3-selection';
import { extent } from 'd3-array';
import {
    line as d3Line,
    curveCardinal,
} from 'd3-shape';

import Responsive from '../../General/Responsive';
import Tooltip from '../Tooltip';

import styles from './styles.scss';

const propTypes = {
    className: PropTypes.string,
    /*
     * Data: [
           {
                value: value
                label: label, // tooltip text/node
           },..
        ]
     */
    data: PropTypes.arrayOf(PropTypes.shape({
        value: PropTypes.number.isRequired,
        label: PropTypes.oneOfType([
            PropTypes.string,
            PropTypes.number,
            PropTypes.node,
        ]),
    })).isRequired,
    /*
     * Chart Margins
     */
    margins: PropTypes.shape({
        top: PropTypes.number,
        right: PropTypes.number,
        bottom: PropTypes.number,
        left: PropTypes.number,
    }),
    circleRadius: PropTypes.number,
    boundingClientRect: PropTypes.object.isRequired, // eslint-disable-line
};

const defaultProps = {
    className: 'spark-lines',
    circleRadius: 3,
    margins: {
        top: 1,
        right: 1,
        bottom: 1,
        left: 1,
    },
};

class SparkLine extends React.PureComponent {
    static propTypes = propTypes;

    static defaultProps = defaultProps;

    constructor(props) {
        super(props);

        this.scaleX = scaleLinear();
        this.scaleY = scaleLinear();
    }

    componentDidMount() {
        this.updateRender();
    }

    // eslint-disable-next-line camelcase
    UNSAFE_componentWillReceiveProps(nextProps) {
        if (nextProps !== this.props) {
            this.updateRender();
        }
    }

    componentDidUpdate() {
        this.updateRender();
    }

    onMouseOver = (label) => {
        this.tooltip.setTooltip(label);
        this.tooltip.show();
    }

    onMouseMove = () => {
        this.tooltip.move();
    }

    onMouseOut = () => {
        this.tooltip.hide();
    }

    setTooltip = (tooltip) => { this.tooltip = tooltip; }

    updateRender() {
        const {
            margins: {
                right,
                top,
                left,
                bottom,
            },
            boundingClientRect: {
                height,
                width,
            },
        } = this.props;

        if (!width) {
            return;
        }

        const svgHeight = height - bottom;
        const svgWidth = width - right;

        this.scaleX.range([left, svgWidth]);
        this.scaleY.range([svgHeight, top]);

        this.renderSparkLines();
    }

    renderSparkLines() {
        const { data, circleRadius } = this.props;

        this.scaleX.domain([0, data.length - 1]);
        this.scaleY.domain(extent(data.map(d => d.value)));

        const svg = select(this.svg);

        svg.select('*').remove();

        const root = svg.append('g');

        const line = d3Line()
            .x((d, index) => this.scaleX(index))
            .y(d => this.scaleY(d.value))
            .curve(curveCardinal);

        root.append('path')
            .attr('d', line(data));

        if (circleRadius > 0) {
            root.append('g').selectAll('circle')
                .data(data)
                .enter()
                .append('circle')
                .attr('cx', (d, index) => this.scaleX(index))
                .attr('cy', d => this.scaleY(d.value))
                .attr('r', circleRadius)
                .on('mouseenter', d => this.onMouseOver(d.label))
                .on('mousemove', this.onMouseMove)
                .on('mouseleave', this.onMouseOut);
        }
    }

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

        return (
            <div
                className={`${className} ${styles.sparkLines}`}
                style={{
                    width,
                    height,
                }}
            >
                <svg ref={(svg) => { this.svg = svg; }} />
                <Tooltip setTooltipApi={this.setTooltip} />
            </div>
        );
    }
}

export default Responsive(SparkLine);