UNPKG

@data-ui/xy-chart

Version:

A package of charts with standard x- and y- axes. https://williaster.github.io/data-ui

500 lines (436 loc) 18.8 kB
"use strict"; exports.__esModule = true; exports.default = exports.defaultProps = exports.propTypes = exports.VORONOI_TRIGGER = exports.SERIES_TRIGGER = exports.CONTAINER_TRIGGER = void 0; var _react = _interopRequireDefault(require("react")); var _propTypes = _interopRequireDefault(require("prop-types")); var _grid = require("@vx/grid"); var _group = require("@vx/group"); var _shared = require("@data-ui/shared"); var _collectVoronoiData = _interopRequireDefault(require("../utils/collectVoronoiData")); var _findClosestDatums2 = _interopRequireDefault(require("../utils/findClosestDatums")); var _shallowCompareObjectEntries = _interopRequireDefault(require("../utils/shallowCompareObjectEntries")); var _Voronoi = _interopRequireDefault(require("./Voronoi")); var _chartUtils = require("../utils/chartUtils"); var _collectScalesFromProps = _interopRequireDefault(require("../utils/collectScalesFromProps")); var _getChartDimensions2 = _interopRequireDefault(require("../utils/getChartDimensions")); var _propShapes = require("../utils/propShapes"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } var CONTAINER_TRIGGER = 'container'; exports.CONTAINER_TRIGGER = CONTAINER_TRIGGER; var SERIES_TRIGGER = 'series'; exports.SERIES_TRIGGER = SERIES_TRIGGER; var VORONOI_TRIGGER = 'voronoi'; exports.VORONOI_TRIGGER = VORONOI_TRIGGER; var Y_LABEL_OFFSET = 0.7; var propTypes = { ariaLabel: _propTypes.default.string.isRequired, children: _propTypes.default.node, disableMouseEvents: _propTypes.default.bool, eventTrigger: _propTypes.default.oneOf([CONTAINER_TRIGGER, SERIES_TRIGGER, VORONOI_TRIGGER]), eventTriggerRefs: _propTypes.default.func, height: _propTypes.default.number.isRequired, innerRef: _propTypes.default.func, margin: _propTypes.default.shape({ top: _propTypes.default.number, right: _propTypes.default.number, bottom: _propTypes.default.number, left: _propTypes.default.number }), renderTooltip: _propTypes.default.func, showXGrid: _propTypes.default.bool, xGridValues: _propTypes.default.arrayOf(_propShapes.stringNumberDateObjectPropType), xGridOffset: _propTypes.default.number, showYGrid: _propTypes.default.bool, yGridValues: _propTypes.default.arrayOf(_propShapes.stringNumberDateObjectPropType), yGridOffset: _propTypes.default.number, showVoronoi: _propTypes.default.bool, snapTooltipToDataX: _propTypes.default.bool, snapTooltipToDataY: _propTypes.default.bool, theme: _propShapes.themeShape, width: _propTypes.default.number.isRequired, xScale: _propShapes.scaleShape.isRequired, yScale: _propShapes.scaleShape.isRequired, // these may be passed from WithTooltip onClick: _propTypes.default.func, // expects to be called like func({ event, datum }) onMouseMove: _propTypes.default.func, // expects to be called like func({ event, datum }) onMouseLeave: _propTypes.default.func, // expects to be called like func({ event, datum }) tooltipData: _propTypes.default.shape({ event: _propTypes.default.object, datum: _propTypes.default.object, series: _propTypes.default.object }) }; exports.propTypes = propTypes; var defaultProps = { children: null, disableMouseEvents: false, eventTrigger: SERIES_TRIGGER, eventTriggerRefs: null, innerRef: null, margin: _chartUtils.DEFAULT_CHART_MARGIN, renderTooltip: null, showVoronoi: false, showXGrid: false, xGridValues: null, xGridOffset: null, showYGrid: false, yGridValues: null, yGridOffset: null, snapTooltipToDataX: false, snapTooltipToDataY: false, theme: {}, onClick: null, onMouseMove: null, onMouseLeave: null, tooltipData: null }; // accessors exports.defaultProps = defaultProps; var getX = function getX(d) { return d && d.x; }; var getY = function getY(d) { return d && d.y; }; var XYChart = /*#__PURE__*/ function (_React$PureComponent) { _inheritsLoose(XYChart, _React$PureComponent); function XYChart(props) { var _this; _this = _React$PureComponent.call(this, props) || this; // if renderTooltip is passed we return another XYChart wrapped in WithTooltip // therefore we don't want to compute state if the nested chart will do so _this.state = props.renderTooltip ? {} : XYChart.getStateFromProps(props); _this.getDatumCoords = _this.getDatumCoords.bind(_assertThisInitialized(_assertThisInitialized(_this))); _this.handleClick = _this.handleClick.bind(_assertThisInitialized(_assertThisInitialized(_this))); _this.handleMouseLeave = _this.handleMouseLeave.bind(_assertThisInitialized(_assertThisInitialized(_this))); _this.handleMouseMove = _this.handleMouseMove.bind(_assertThisInitialized(_assertThisInitialized(_this))); _this.handleMouseDown = _this.handleMouseDown.bind(_assertThisInitialized(_assertThisInitialized(_this))); _this.handleContainerEvent = _this.handleContainerEvent.bind(_assertThisInitialized(_assertThisInitialized(_this))); return _this; } var _proto = XYChart.prototype; _proto.componentDidMount = function componentDidMount() { var _this$props = this.props, renderTooltip = _this$props.renderTooltip, eventTriggerRefs = _this$props.eventTriggerRefs; if (!renderTooltip && eventTriggerRefs) { eventTriggerRefs({ mousemove: this.handleMouseMove, mouseleave: this.handleMouseLeave, click: this.handleClick }); } }; _proto.componentWillReceiveProps = function componentWillReceiveProps(nextProps) { var _this2 = this; var shouldComputeScales = false; if (['width', 'height', 'children'].some(function (prop) { return _this2.props[prop] !== nextProps[prop]; } // eslint-disable-line react/destructuring-assignment )) { shouldComputeScales = true; } if (['margin', 'xScale', 'yScale'].some( // eslint-disable-next-line react/destructuring-assignment function (prop) { return !(0, _shallowCompareObjectEntries.default)(_this2.props[prop], nextProps[prop]); })) { shouldComputeScales = true; } if (shouldComputeScales) this.setState(XYChart.getStateFromProps(nextProps)); }; XYChart.getStateFromProps = function getStateFromProps(props) { var _getChartDimensions = (0, _getChartDimensions2.default)(props), margin = _getChartDimensions.margin, innerWidth = _getChartDimensions.innerWidth, innerHeight = _getChartDimensions.innerHeight; var _collectScalesFromPro = (0, _collectScalesFromProps.default)(props), xScale = _collectScalesFromPro.xScale, yScale = _collectScalesFromPro.yScale; var voronoiData = (0, _collectVoronoiData.default)({ children: props.children, getX: getX, getY: getY }); return { innerHeight: innerHeight, innerWidth: innerWidth, margin: margin, xScale: xScale, yScale: yScale, voronoiData: voronoiData, voronoiX: function voronoiX(d) { return xScale(getX(d)); }, voronoiY: function voronoiY(d) { return yScale(getY(d)); } }; }; _proto.getNumTicksAndGridValues = function getNumTicksAndGridValues(innerWidth, innerHeight) { var _this$props2 = this.props, children = _this$props2.children, xGridValues = _this$props2.xGridValues, yGridValues = _this$props2.yGridValues; var xAxis = (0, _chartUtils.getChildWithName)('XAxis', children); var yAxis = (0, _chartUtils.getChildWithName)('YAxis', children); // use num ticks and tickValues defined on Axes, if relevant return { numXTicks: (0, _chartUtils.propOrFallback)(xAxis && xAxis.props, 'numTicks', (0, _chartUtils.numTicksForWidth)(innerWidth)), numYTicks: (0, _chartUtils.propOrFallback)(yAxis && yAxis.props, 'numTicks', (0, _chartUtils.numTicksForHeight)(innerHeight)), xGridValues: xGridValues || (xAxis && xAxis.props && xAxis.props.tickValues ? xAxis.props.tickValues : null), yGridValues: yGridValues || (yAxis && yAxis.props && yAxis.props.tickValues ? yAxis.props.tickValues : null) }; }; _proto.getDatumCoords = function getDatumCoords(datum) { var _this$state = this.state, xScale = _this$state.xScale, yScale = _this$state.yScale, margin = _this$state.margin; var coords = {}; // tooltip operates in full width/height space so we must account for margins if (datum) coords.x = xScale(getX(datum)) + margin.left; if (datum) coords.y = yScale(getY(datum)) + margin.top; return coords; }; _proto.handleContainerEvent = function handleContainerEvent(event) { var _this$state2 = this.state, xScale = _this$state2.xScale, yScale = _this$state2.yScale, margin = _this$state2.margin; var children = this.props.children; var _findClosestDatums = (0, _findClosestDatums2.default)({ children: children, event: event, getX: getX, getY: getY, xScale: xScale, yScale: yScale, margin: margin }), closestDatum = _findClosestDatums.closestDatum, series = _findClosestDatums.series; if (closestDatum || Object.keys(series).length > 0) { event.persist(); var args = { event: event, datum: closestDatum, series: series }; if (event.type === 'mousemove') this.handleMouseMove(args);else if (event.type === 'click') this.handleClick(args); } }; _proto.handleMouseDown = function handleMouseDown(event) { if (this.fireBrushStart) { this.fireBrushStart(event); } }; _proto.handleMouseMove = function handleMouseMove(args) { var _this$props3 = this.props, snapTooltipToDataX = _this$props3.snapTooltipToDataX, snapTooltipToDataY = _this$props3.snapTooltipToDataY, onMouseMove = _this$props3.onMouseMove; var isFocusEvent = args.event && args.event.type === 'focus'; if (onMouseMove) { var _this$getDatumCoords = this.getDatumCoords(args.datum), x = _this$getDatumCoords.x, y = _this$getDatumCoords.y; onMouseMove(_extends({}, args, { coords: _extends({}, (isFocusEvent || snapTooltipToDataX) && { x: x }, (isFocusEvent || snapTooltipToDataY) && { y: y }, args.coords) })); } }; _proto.handleMouseLeave = function handleMouseLeave(args) { var onMouseLeave = this.props.onMouseLeave; if (onMouseLeave) onMouseLeave(args); }; _proto.handleClick = function handleClick(args) { var _this$props4 = this.props, snapTooltipToDataX = _this$props4.snapTooltipToDataX, snapTooltipToDataY = _this$props4.snapTooltipToDataY, onClick = _this$props4.onClick; if (onClick) { var coords = this.getDatumCoords(args.datum); onClick(_extends({}, args, { coords: _extends({ x: snapTooltipToDataX ? coords.x : undefined, y: snapTooltipToDataY ? coords.y : undefined }, args.coords) })); } }; _proto.render = function render() { var _this3 = this; var renderTooltip = this.props.renderTooltip; if (renderTooltip) { return _react.default.createElement(_shared.WithTooltip, { renderTooltip: renderTooltip }, _react.default.createElement(XYChart, _extends({}, this.props, { renderTooltip: null }))); } var _this$props5 = this.props, ariaLabel = _this$props5.ariaLabel, eventTrigger = _this$props5.eventTrigger, children = _this$props5.children, showXGrid = _this$props5.showXGrid, showYGrid = _this$props5.showYGrid, theme = _this$props5.theme, height = _this$props5.height, width = _this$props5.width, innerRef = _this$props5.innerRef, tooltipData = _this$props5.tooltipData, showVoronoi = _this$props5.showVoronoi, xGridOffset = _this$props5.xGridOffset, yGridOffset = _this$props5.yGridOffset; var _this$state3 = this.state, innerWidth = _this$state3.innerWidth, innerHeight = _this$state3.innerHeight, margin = _this$state3.margin, voronoiData = _this$state3.voronoiData, voronoiX = _this$state3.voronoiX, voronoiY = _this$state3.voronoiY, xScale = _this$state3.xScale, yScale = _this$state3.yScale; var _this$getNumTicksAndG = this.getNumTicksAndGridValues(innerWidth, innerHeight), numXTicks = _this$getNumTicksAndG.numXTicks, numYTicks = _this$getNumTicksAndG.numYTicks, xGridValues = _this$getNumTicksAndG.xGridValues, yGridValues = _this$getNumTicksAndG.yGridValues; var CrossHairs = []; // ensure these are the top-most layer var Brush = null; var xAxisOrientation; var yAxisOrientation; return innerWidth > 0 && innerHeight > 0 && _react.default.createElement("svg", { "aria-label": ariaLabel, role: "img", width: width, height: height, ref: innerRef }, _react.default.createElement(_group.Group, { left: margin.left, top: margin.top }, showXGrid && _react.default.createElement(_grid.GridColumns, { scale: xScale, height: innerHeight, numTicks: numXTicks, stroke: theme.gridStyles && theme.gridStyles.stroke, strokeWidth: theme.gridStyles && theme.gridStyles.strokeWidth, tickValues: xGridValues, offset: (0, _chartUtils.isDefined)(xGridOffset) ? xGridOffset : xScale.bandwidth && xScale.bandwidth() / 2 || 0 }), showYGrid && _react.default.createElement(_grid.GridRows, { scale: yScale, width: innerWidth, numTicks: numYTicks, stroke: theme.gridStyles && theme.gridStyles.stroke, strokeWidth: theme.gridStyles && theme.gridStyles.strokeWidth, tickValues: yGridValues, offset: (0, _chartUtils.isDefined)(yGridOffset) ? yGridOffset : yScale.bandwidth && yScale.bandwidth() / 2 || 0 }), _react.default.Children.map(children, function (Child) { var name = (0, _chartUtils.componentName)(Child); if ((0, _chartUtils.isAxis)(name)) { var styleKey = name[0].toLowerCase(); var labelOffset = typeof Child.props.labelOffset === 'number' ? Child.props.labelOffset : name === 'YAxis' && Y_LABEL_OFFSET * margin[Child.props.orientation] || 0; if (name === 'XAxis') { xAxisOrientation = Child.props.orientation; } else { yAxisOrientation = Child.props.orientation; } return _react.default.cloneElement(Child, { innerHeight: innerHeight, innerWidth: innerWidth, height: height, width: width, labelOffset: labelOffset, numTicks: name === 'XAxis' ? numXTicks : numYTicks, scale: name === 'XAxis' ? xScale : yScale, rangePadding: Child.props.rangePadding || (name === 'XAxis' ? xScale.offset : undefined), axisStyles: _extends({}, theme[styleKey + "AxisStyles"], Child.props.axisStyles), tickStyles: _extends({}, theme[styleKey + "TickStyles"], Child.props.tickStyles) }); } else if ((0, _chartUtils.isSeries)(name)) { return _react.default.cloneElement(Child, { xScale: xScale, yScale: yScale, margin: margin, onClick: Child.props.onClick || (Child.props.disableMouseEvents ? undefined : _this3.handleClick), onMouseLeave: Child.props.onMouseLeave || (Child.props.disableMouseEvents ? undefined : _this3.handleMouseLeave), onMouseMove: Child.props.onMouseMove || (Child.props.disableMouseEvents ? undefined : _this3.handleMouseMove) }); } else if ((0, _chartUtils.isCrossHair)(name)) { CrossHairs.push(Child); return null; } else if ((0, _chartUtils.isReferenceLine)(name)) { return _react.default.cloneElement(Child, { xScale: xScale, yScale: yScale }); } else if ((0, _chartUtils.isBrush)(name)) { Brush = Child; return null; } return Child; }), eventTrigger === VORONOI_TRIGGER && _react.default.createElement(_Voronoi.default, { data: voronoiData, x: voronoiX, y: voronoiY, width: innerWidth, height: innerHeight, onClick: this.handleClick, onMouseDown: this.handleMouseDown, onMouseMove: this.handleMouseMove, onMouseLeave: this.handleMouseLeave, showVoronoi: showVoronoi }), eventTrigger === CONTAINER_TRIGGER && _react.default.createElement("rect", { x: 0, y: 0, width: innerWidth, height: innerHeight, fill: "transparent", fillOpacity: 0, onMouseDown: this.handleMouseDown, onClick: this.handleContainerEvent, onMouseMove: this.handleContainerEvent, onMouseLeave: this.handleMouseLeave }), Brush && _react.default.cloneElement(Brush, { xScale: xScale, yScale: yScale, innerHeight: innerHeight, innerWidth: innerWidth, margin: margin, onMouseMove: this.handleContainerEvent, onMouseLeave: this.handleMouseLeave, onClick: this.handleContainerEvent, xAxisOrientation: xAxisOrientation, yAxisOrientation: yAxisOrientation }), tooltipData && CrossHairs.length > 0 && CrossHairs.map(function (CrossHair, i) { return _react.default.cloneElement(CrossHair, { key: "crosshair-" + i, // eslint-disable-line react/no-array-index-key datum: tooltipData.datum, series: tooltipData.series, getScaledX: function getScaledX(d) { return xScale(getX(d) || 0) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0); }, getScaledY: function getScaledY(d) { return yScale(getY(d) || 0) + (yScale.bandwidth ? yScale.bandwidth() / 2 : 0); }, xScale: xScale, yScale: yScale }); }))); }; return XYChart; }(_react.default.PureComponent); XYChart.propTypes = propTypes; XYChart.defaultProps = defaultProps; XYChart.displayName = 'XYChart'; var _default = XYChart; exports.default = _default;