UNPKG

react-draggin-charts

Version:
366 lines (318 loc) 9.92 kB
import React from "react"; import { debounce } from "lodash"; import PropTypes from "prop-types"; import Animation from "react-vis/dist/animation"; import { ANIMATED_SERIES_PROPS } from "react-vis/dist/utils/series-utils"; import { warning } from "react-vis/dist/utils/react-utils"; import { DEFAULT_SIZE, DEFAULT_OPACITY } from "react-vis/dist/theme"; import AbstractSeries from "react-vis/dist/plot/series/abstract-series"; import { passiveCaptureEventObj } from '../utils'; const predefinedClassName = "rv-xy-plot__series rv-xy-plot__series--mark"; const DEFAULT_STROKE_WIDTH = 1; function getAdjToZeroVal(low) { let adjVal = 0; // if bottom of range is less than zero, need to add abs value to all if (low < 0) { adjVal = Math.abs(low); } return adjVal; } function toLowHigh([val1, val2]) { const low = Math.min(val1, val2); const high = low === val1 ? val2 : val1; return [low, high]; } function toPercentage(value, [low, high]) { const adjToZeroVal = getAdjToZeroVal(low); value += adjToZeroVal; low += adjToZeroVal; // low will be zero if it was below zero high += adjToZeroVal; return value / (high - low); } function fromPercentage(percentage, [low, high]) { const adjToZeroVal = getAdjToZeroVal(low); return percentage * (high - Math.max(low, 0) + adjToZeroVal) + low; // don't question it... } function mapToDomain(value, domain, range) { const sortedDomain = toLowHigh(domain); const sortedRange = toLowHigh(range); const domainVal = fromPercentage( toPercentage(value, sortedRange), sortedDomain, ); const [domainLowerBound, domainUpperBound] = sortedDomain; if (domainVal < domainLowerBound) { return domainLowerBound; } if (domainVal > domainUpperBound) { return domainUpperBound; } return domainVal; } export default class MarkSeries extends AbstractSeries { static displayName = "MarkSeries"; static propTypes = { ...AbstractSeries.propTypes, getNull: PropTypes.func, strokeWidth: PropTypes.number, }; static defaultProps = { getNull: () => true, }; _valueMouseDownHandler = (d, event) => { this.dragIndex = this.indexCache[d.id]; this._valueDragStartHandler(d, event, this.dragIndex); const { onValueMouseDown, onSeriesMouseDown } = this.props; if (onValueMouseDown) { onValueMouseDown(d, { event, index: this.dragIndex }); } if (onSeriesMouseDown) { onSeriesMouseDown({ event }); } if (super._valueMouseDownHandler) { super._valueMouseDownHandler(d, event); } }; _valueMouseUpHandler = (d, event) => { const { onValueMouseUp, onSeriesMouseUp } = this.props; if (onValueMouseUp) { onValueMouseUp(d, { event, index: this.indexCache[d.id] }); } if (onSeriesMouseUp) { onSeriesMouseUp({ event }); } if (super._valueMouseUpHandler) { super._valueMouseUpHandler(d, event); } }; _valueMouseMoveHandler = (d, event) => { const { onValueMouseMove, onSeriesMouseMove } = this.props; if (onValueMouseMove) { onValueMouseMove(d, { event, index: this.indexCache[d.id] }); } if (onSeriesMouseMove) { onSeriesMouseMove({ event }); } if (super._valueMouseMoveHandler) { super._valueMouseMoveHandler(d, event); } }; _valueDragStartHandler = (d, event, index) => { const { onValueDragStart, onSeriesDragStart } = this.props; if (onValueDragStart) { onValueDragStart(d, { event, index }); } if (onSeriesDragStart) { onSeriesDragStart({ event }); } if (super._valueDragStartHandler) { super._valueDragStartHandler(d, event); } }; _valueDragEndHandler = (d, event, index) => { const { onValueDragEnd, onSeriesDragEnd } = this.props; if (onValueDragEnd) { onValueDragEnd(d, { event, index }); } if (onSeriesDragEnd) { onSeriesDragEnd({ event }); } if (super._valueDragEndHandler) { super._valueDragEndHandler(d, event); } }; _valueDragHandler = (d, event, index, chartCoords) => { const { onValueDrag, onSeriesDrag } = this.props; if (onValueDrag) { onValueDrag(d, { event, index, chartCoords }); } if (onSeriesDrag) { onSeriesDrag({ event, chartCoords }); } if (super._valueDragHandler) { super._valueDragHandler(d, event); } }; componentDidMount() { document.addEventListener("mousemove", this.handleMouseMove, passiveCaptureEventObj); document.addEventListener("mouseup", this.handleMouseUp, passiveCaptureEventObj); window.addEventListener("resize", this.updateOffsetCoords, passiveCaptureEventObj); window.addEventListener("scroll", this.updateOffsetCoords, passiveCaptureEventObj); } componentWillUnmount() { document.removeEventListener("mousemove", this.handleMouseMove, passiveCaptureEventObj); document.removeEventListener("mouseup", this.handleMouseUp, passiveCaptureEventObj); window.removeEventListener("resize", this.updateOffsetCoords, passiveCaptureEventObj); window.removeEventListener("scroll", this.updateOffsetCoords, passiveCaptureEventObj); } componentDidUpdate(prevProps) { if (this.props.data !== prevProps.data) { this.indexCache = this.createIndexCache(); } } createIndexCache = () => { const { data } = this.props; const indexCache = {}; const dataLength = data.length; for (let i = 0; i < dataLength; i++) { indexCache[data[i].id] = i; } return indexCache; }; indexCache = this.createIndexCache(); updateOffsetCoords = debounce(() => { if (this.svgElem) { const { top: svgElemOffsetTop, left: svgElemOffsetLeft, } = this.svgElem.getBoundingClientRect(); this.svgElemOffsetTop = svgElemOffsetTop; this.svgElemOffsetLeft = svgElemOffsetLeft; } }, 16); refFunc = elem => { if (elem) { // we need container SVG for offsets while (elem.tagName.toUpperCase() !== "SVG") { elem = elem.parentNode; } this.svgElem = elem; this.updateOffsetCoords(); } }; dragIndex = -1; // null equivalent handleMouseUp = e => { if (this.dragIndex !== -1) { this._valueDragEndHandler( this.props.data[this.dragIndex], e, this.dragIndex, ); this.dragIndex = -1; } }; handleMouseMove = e => { if (this.dragIndex !== -1) { this._valueDragHandler( this.props.data[this.dragIndex], e, this.dragIndex, this.toChartCoords(e), ); } }; fromGlobalToChartCoords = e => { const { clientX, clientY } = e; const { svgElemOffsetTop, svgElemOffsetLeft } = this; const strokeWidthProp = this.props.style.strokeWidth; const strokeWidth = ( typeof strokeWidthProp === "string" ? +strokeWidthProp.replace("px", "") : strokeWidthProp ) || 0; const size = this.props.size || 5; const radius = size + strokeWidth; return { x: clientX - svgElemOffsetLeft - this.props.marginLeft - radius, y: clientY - svgElemOffsetTop - this.props.marginTop, // - radius, screwed something up }; }; toChartCoords = e => { if (!this.svgElem) { return null; } const { xRange, yRange, xDomain, yDomain, innerHeight, } = this.props; const { x: adjX, y: adjY, } = this.fromGlobalToChartCoords(e); const yFromBottomOfChart = innerHeight - adjY; return { x: mapToDomain(adjX, xDomain, xRange), y: mapToDomain(yFromBottomOfChart, yDomain, yRange), }; }; _renderCircle(d, i, strokeWidth, style, scalingFunctions) { const { fill, opacity, size, stroke, x, y, } = scalingFunctions; const attrs = { r: size ? size(d) : DEFAULT_SIZE, cx: x(d), cy: y(d), style: { cursor: "pointer", opacity: opacity ? opacity(d) : DEFAULT_OPACITY, stroke: stroke && stroke(d), fill: fill && fill(d), strokeWidth: strokeWidth || DEFAULT_STROKE_WIDTH, ...style, }, key: i, onClick: e => this._valueClickHandler(d, e), onMouseDown: e => this._valueMouseDownHandler(d, e), onMouseUp: e => this._valueMouseUpHandler(d, e), onMouseMove: e => this._valueMouseMoveHandler(d, e), onContextMenu: e => this._valueRightClickHandler(d, e), onMouseOver: e => this._valueMouseOverHandler(d, e), onMouseOut: e => this._valueMouseOutHandler(d, e), }; return <circle {...attrs} />; } render() { const { animation, className, data, marginLeft, marginTop, strokeWidth, style, } = this.props; if (this.props.nullAccessor) { warning("nullAccessor has been renamed to getNull", true); } const getNull = this.props.nullAccessor || this.props.getNull; if (!data) { return null; } if (animation) { return ( <Animation {...this.props} animatedProps={ANIMATED_SERIES_PROPS}> <MarkSeries {...this.props} animation={null} /> </Animation> ); } const scalingFunctions = { fill: this._getAttributeFunctor("fill") || this._getAttributeFunctor("color"), opacity: this._getAttributeFunctor("opacity"), size: this._getAttributeFunctor("size"), stroke: this._getAttributeFunctor("stroke") || this._getAttributeFunctor("color"), x: this._getAttributeFunctor("x"), y: this._getAttributeFunctor("y"), }; return ( <g className={`${predefinedClassName} ${className}`} transform={`translate(${marginLeft},${marginTop})`} ref={this.refFunc} > {data.map( (d, i) => getNull(d) && this._renderCircle(d, i, strokeWidth, style, scalingFunctions) )} </g> ); } }