@data-ui/histogram
Version:
React + d3 library for creating histograms
265 lines (235 loc) • 9.11 kB
JavaScript
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;