@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
JavaScript
"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;