Source: visual-layer/src/layers/line/line.js

import {
    Voronoi,
    getQualifiedClassName,
    selectElement,
    makeElement,
    FieldType,
    getObjProp
} from 'muze-utils';
import { BaseLayer } from '../../base-layer';
import { drawLine } from './renderer';
import { defaultConfig } from './default-config';
import { ENCODING } from '../../enums/constants';
import * as PROPS from '../../enums/props';
import {
    attachDataToVoronoi,
    animateGroup,
    getLayerColor,
    positionPoints,
    getIndividualClassName
} from '../../helpers';

import './styles.scss';

/**
 * This layer is used to render straight or smoothed line paths. The mark type of this layer is ```line```.
 *
 * @public
 *
 * @class
 * @module LineLayer
 * @extends BaseLayer
 */
export default class LineLayer extends BaseLayer {

    /**
     * Creates an instance of LineLayer.
     * @param {*} args
     * @memberof LineLayer
     */
    constructor (...args) {
        super(...args);
        this._voronoi = new Voronoi();
    }

    /**
     *
     *
     * @static
     *
     * @memberof LineLayer
     */
    static formalName () {
        return 'line';
    }

    /**
     *
     *
     *
     * @memberof LineLayer
     */
    elemType () {
        return 'path';
    }

    /**
     * Default configuration of line layer
     * @return {Object} Default configuration of layer
     */
    static defaultConfig () {
        return defaultConfig;
    }

    /**
     *
     *
     * @static
     * @param {*} conf
     * @param {*} userConf
     *
     * @memberof LineLayer
     */
    static defaultPolicy (conf, userConf) {
        const config = BaseLayer.defaultPolicy(conf, userConf);
        const encoding = config.encoding;
        const transform = config.transform;
        const colorField = encoding.color && encoding.color.field;

        if (colorField && !transform.groupBy) {
            transform.groupBy = colorField;
        }
        return config;
    }

    /**
     * Returns the draw method for line
     * @return {Function} Draw method of line layer
     */
    getDrawFn () {
        return drawLine;
    }

    /**
     * Applies selection styles to the elements that fall within the selection set.
     * @param {Array} selectionSet Array of tuple ids.
     * @param {Object} config Configuration for selection.
     * @return {BarLayer} Instance of bar layer.
     */
    highlightPoint () {
        return this;
    }

    /**
     * Removes selection styles to the elements that fall within the selection set.
     * @param {Array} selectionSet Array of tuple ids.
     * @param {Object} config Configuration for selection.
     * @return {BarLayer} Instance of bar layer.
     */
    dehighlightPoint () {
        return this;
    }

    focusSelection () {
        return this;
    }

    focusOutSelection () {
        return this;
    }

    fadeOutSelection () {
        return this;
    }

    unfadeSelection () {
        return this;
    }

    shouldDrawAnchors () {
        return true;
    }

    /**
     * Generates the x and y positions for each point
     * @param {Array} data Data Array
     * @param {Object} encoding Visual Encodings of the layer
     * @param {Object} axes Contains the axis
     * @param {number} seriesIndex index of series
     * @return {Array} Array of points
     */
    translatePoints (data, encodingFieldsInf, axes) {
        let points = [];
        const xAxis = axes.x;
        const yAxis = axes.y;
        const colorAxis = axes.color;
        const encoding = this.config().encoding;
        const { xFieldType, yFieldType } = encodingFieldsInf;
        const isXDim = xFieldType === FieldType.DIMENSION;
        const isYDim = yFieldType === FieldType.DIMENSION;
        const key = isXDim ? ENCODING.X : (isYDim ? ENCODING.Y : null);
        const colorEncoding = encoding.color;
        const colorField = colorEncoding.field;
        const fieldsConfig = this.data().getFieldsConfig();
        const colorFieldIndex = colorField && fieldsConfig[colorField].index;
        const style = {};
        const meta = {};

        points = data.map((d, i) => {
            const xPx = xAxis.getScaleValue(d.x) + xAxis.getUnitWidth() / 2;
            const yPx = yAxis.getScaleValue(d.y);
            const { color, rawColor } = getLayerColor({ datum: d, index: i }, {
                colorEncoding, colorAxis, colorFieldIndex });

            style.stroke = color;
            style['fill-opacity'] = 0;
            meta.stateColor = {};
            meta.originalColor = rawColor;
            meta.colorTransform = {};

            const point = {
                enter: {},
                update: {
                    x: xPx,
                    y: d.y === null ? null : yPx
                },
                style,
                _data: d._data,
                _id: d._id,
                rowId: d._id,
                source: d._data,
                meta
            };
            point.className = getIndividualClassName(d, i, data, this);
            this.cachePoint(d[key], point);
            return point;
        });
        points = positionPoints(this, points);
        return points;
    }

    /**
     * Renders the line plot
     * @param {SVGElement} container svg element
     * @return {LineLayer} instance of line layer
     */
    render (container) {
        let points;
        let seriesClassName;
        let style;

        const config = this.config();
        const {
            encoding,
            interpolate,
            className,
            defClassName,
            transition
        } = config;
        const store = this._store;
        const normalizedData = store.get(PROPS.NORMALIZED_DATA);
        const transformedData = store.get(PROPS.TRANSFORMED_DATA);
        const fieldsConfig = this.data().getFieldsConfig();
        const axes = this.axes();
        const keys = transformedData.map(d => d.key);
        const qualifiedClassName = getQualifiedClassName(defClassName, this.id(), config.classPrefix);
        const containerSelection = selectElement(container);
        const colorField = encoding.color.field;
        const colorFieldIndex = fieldsConfig[colorField] && fieldsConfig[colorField].index;

        this._points = [];
        this._pointMap = {};
        containerSelection.classed(qualifiedClassName.join(' '), true);
        containerSelection.classed(className, true);
        makeElement(container, 'g', normalizedData, null, {
            enter: (group) => {
                animateGroup(group, {
                    transition,
                    groupAnimateStyle: {
                        enter: {
                            'stroke-opacity': 0,
                            'fill-opacity': this.getPathStyle()['fill-opacity']
                        },
                        update: {
                            'stroke-opacity': encoding.strokeOpacity.value
                        }
                    }
                });
            },
            update: (group, dataArr, i) => {
                points = this.translatePoints(dataArr, this.encodingFieldsInf(), axes, i);
                this._points.push(points);
                seriesClassName = `${qualifiedClassName[0]}-${keys[i] || i}`.toLowerCase();

                let color;
                const colorValFn = encoding.color.value;
                const colorVal = points.find(d => d._data[colorFieldIndex] !== null &&
                        d._data[colorFieldIndex] !== undefined);

                if (colorValFn) {
                    color = colorValFn(dataArr, i, normalizedData);
                } else {
                    color = axes.color.getColor(colorVal && colorVal._data[colorFieldIndex]);
                }

                style = this.getPathStyle(color);
                this.getDrawFn()({
                    container: group.node(),
                    interpolate,
                    points,
                    className: seriesClassName,
                    transition,
                    style: style || {},
                    connectNullData: config.connectNullData
                });
            }
        }, d => d[0]._data[colorFieldIndex] || d[0]._id);

        attachDataToVoronoi(this._voronoi, this._points);
        return this;
    }

    /**
     * Get the css styles need to be applied on the line path
     * @param {string} color Color value
     * @return {Object} Path styles
     */
    getPathStyle (color) {
        return {
            stroke: color,
            'fill-opacity': '0'
        };
    }

    /**
     * Gets the nearest point closest to the given position
     * @param {number} x x position
     * @param {number} y y position
     * @return {Object} Nearest point information
     */
    getNearestPoint (x, y, config) {
        let searchRadius = config.searchRadius;
        const data = this.data();

        if (!data || (data && data.isEmpty())) {
            return null;
        }

        searchRadius = searchRadius !== undefined ? searchRadius : this.config().nearestPointThreshold;
        const point = this._voronoi.find(x, y, searchRadius);
        const dimensions = getObjProp(point, 'data', 'data', 'update');

        if (point) {
            const { _data, _id } = point.data.data;
            const identifiers = this.getIdentifiersFromData(_data, _id);
            return {
                id: identifiers,
                dimensions: [{
                    x: dimensions.x,
                    y: dimensions.y,
                    width: 2,
                    height: 2
                }],
                layerId: this.id()
            };
        }
        return null;
    }
}