UNPKG

@data-ui/histogram

Version:

React + d3 library for creating histograms

265 lines (235 loc) 9.11 kB
function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } 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); } /* eslint complexity: ['error', 17] */ import PropTypes from 'prop-types'; import React from 'react'; import { Group } from '@vx/group'; import { scaleBand, scaleLinear } from '@vx/scale'; import { WithTooltip, withTooltipPropTypes } from '@data-ui/shared'; import { isAxis, isSeries } from '../utils/componentIsX'; import collectBinnedDataFromChildSeries from '../utils/collectBinnedDataFromChildSeries'; import componentName from '../utils/componentName'; import computeDomainsFromBins from '../utils/computeDomainsFromBins'; import getValueKey from '../utils/getValueKey'; import shallowCompareObjectEntries from '../utils/shallowCompareObjectEntries'; import { themeShape } from '../utils/propShapes'; export var propTypes = _extends({}, withTooltipPropTypes, { ariaLabel: PropTypes.string.isRequired, binValues: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])), binCount: PropTypes.number, binType: PropTypes.oneOf(['numeric', 'categorical']), children: PropTypes.node.isRequired, cumulative: PropTypes.bool, height: PropTypes.number.isRequired, horizontal: PropTypes.bool, limits: PropTypes.arrayOf(PropTypes.number), // values outside the limits are ignored margin: PropTypes.shape({ top: PropTypes.number, right: PropTypes.number, bottom: PropTypes.number, left: PropTypes.number }), normalized: PropTypes.bool, renderTooltip: PropTypes.func, theme: themeShape, width: PropTypes.number.isRequired, valueAccessor: PropTypes.func }); var defaultProps = { binCount: 10, binType: 'numeric', binValues: null, cumulative: false, horizontal: false, limits: null, margin: { top: 32, right: 32, bottom: 64, left: 64 }, normalized: false, renderTooltip: null, theme: {}, valueAccessor: function valueAccessor(d) { return d; } }; var Histogram = /*#__PURE__*/ function (_React$PureComponent) { _inheritsLoose(Histogram, _React$PureComponent); function Histogram(props) { var _this; _this = _React$PureComponent.call(this, props) || this; _this.state = _this.getStateFromProps(props); return _this; } var _proto = Histogram.prototype; _proto.componentWillReceiveProps = function componentWillReceiveProps(nextProps) { var _this2 = this; var shouldComputeBinsAndScales = false; // eslint-disable-next-line react/destructuring-assignment if (['width', 'height', 'children'].some(function (prop) { return _this2.props[prop] !== nextProps[prop]; })) { shouldComputeBinsAndScales = true; } if (['margin'].some( // eslint-disable-next-line react/destructuring-assignment function (prop) { return !shallowCompareObjectEntries(_this2.props[prop], nextProps[prop]); })) { shouldComputeBinsAndScales = true; } if (shouldComputeBinsAndScales) this.setState(this.getStateFromProps(nextProps)); }; _proto.getStateFromProps = function getStateFromProps(props) { var dimensions = this.getDimmensions(props); var binsByIndex = this.getBinnedData(props); var scales = this.getScales(props, binsByIndex, dimensions); return _extends({ binsByIndex: binsByIndex }, dimensions, scales); }; _proto.getDimmensions = function getDimmensions(props) { var _ref = props || this.props, margin = _ref.margin, width = _ref.width, height = _ref.height; var completeMargin = _extends({}, defaultProps.margin, margin); return { margin: completeMargin, innerHeight: height - completeMargin.top - completeMargin.bottom, innerWidth: width - completeMargin.left - completeMargin.right }; }; _proto.getBinnedData = function getBinnedData(props) { var _ref2 = props || this.props, children = _ref2.children, binCount = _ref2.binCount, binType = _ref2.binType, binValues = _ref2.binValues, limits = _ref2.limits, valueAccessor = _ref2.valueAccessor; return collectBinnedDataFromChildSeries({ children: children, binCount: binCount, binType: binType, binValues: binValues, limits: limits, valueAccessor: valueAccessor }); }; _proto.getScales = function getScales(props, binsByIndex, _ref3) { var innerHeight = _ref3.innerHeight, innerWidth = _ref3.innerWidth; var _ref4 = props || this.props, binType = _ref4.binType, binValues = _ref4.binValues, cumulative = _ref4.cumulative, horizontal = _ref4.horizontal, normalized = _ref4.normalized; var binScaleFunc = binType === 'numeric' ? scaleLinear : scaleBand; var valueKey = getValueKey({ normalized: normalized, cumulative: cumulative }); var _computeDomainsFromBi = computeDomainsFromBins({ binsByIndex: binsByIndex, binType: binType, binValues: binValues, valueKey: valueKey }), binDomain = _computeDomainsFromBi.binDomain, valueDomain = _computeDomainsFromBi.valueDomain; var binRange = horizontal ? [innerHeight, 0] : [0, innerWidth]; var valueRange = horizontal ? [0, innerWidth] : [innerHeight, 0]; // when viewing categorical data horizontally it is more natural to // read alphabetical from top down if (horizontal && binType === 'categorical') binRange.reverse(); return { binScale: binScaleFunc({ range: binRange, domain: binDomain }), valueScale: scaleLinear({ range: valueRange, domain: valueDomain }), valueKey: valueKey }; }; _proto.render = function render() { var renderTooltip = this.props.renderTooltip; if (renderTooltip) { return React.createElement(WithTooltip, { renderTooltip: renderTooltip }, React.createElement(Histogram, _extends({}, this.props, { renderTooltip: null }))); } var _this$props = this.props, ariaLabel = _this$props.ariaLabel, binType = _this$props.binType, binValues = _this$props.binValues, children = _this$props.children, height = _this$props.height, horizontal = _this$props.horizontal, onMouseLeave = _this$props.onMouseLeave, onMouseMove = _this$props.onMouseMove, theme = _this$props.theme, valueAccessor = _this$props.valueAccessor, width = _this$props.width; var _this$state = this.state, binsByIndex = _this$state.binsByIndex, binScale = _this$state.binScale, innerHeight = _this$state.innerHeight, innerWidth = _this$state.innerWidth, margin = _this$state.margin, valueKey = _this$state.valueKey, valueScale = _this$state.valueScale; return React.createElement("svg", { "aria-label": ariaLabel, role: "img", width: width, height: height }, React.createElement(Group, { left: margin.left, top: margin.top }, React.Children.map(children, function (Child, index) { var name = componentName(Child); if (isSeries(name)) { var binnedData = binsByIndex[index]; return React.cloneElement(Child, { binScale: binScale, binType: binType, binnedData: binnedData, horizontal: horizontal, valueAccessor: valueAccessor, valueKey: valueKey, valueScale: valueScale, onMouseLeave: onMouseLeave, onMouseMove: onMouseMove }); } else if (isAxis(name)) { var styleKey = name[0].toLowerCase(); var binOrValue = name === 'XAxis' && !horizontal || name === 'YAxis' && horizontal ? 'bin' : 'value'; var tickValues = Child.props.tickValues || (binOrValue === 'bin' && binValues ? binValues : null); return React.cloneElement(Child, { top: name === 'YAxis' || Child.props.orientation === 'top' ? 0 : innerHeight, left: name === 'XAxis' || Child.props.orientation === 'left' ? 0 : innerWidth, label: Child.props.label || (binOrValue === 'value' ? valueKey : null), scale: binOrValue === 'value' ? valueScale : binScale, axisStyles: _extends({}, theme[styleKey + "AxisStyles"], Child.props.axisStyles), tickStyles: _extends({}, theme[styleKey + "TickStyles"], Child.props.tickStyles), tickValues: tickValues }); } return Child; }))); }; return Histogram; }(React.PureComponent); Histogram.propTypes = propTypes; Histogram.defaultProps = defaultProps; Histogram.displayName = 'Histogram'; export default Histogram;