@data-ui/histogram
Version:
React + d3 library for creating histograms
208 lines (192 loc) • 7.12 kB
JavaScript
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 no-param-reassign: 0 */
import React from 'react';
import PropTypes from 'prop-types';
import { extent, max } from 'd3-array';
import { chartTheme } from '@data-ui/theme';
import { AreaClosed, LinePath } from '@vx/shape';
import { curveBasis } from '@vx/curve';
import { Group } from '@vx/group';
import { scaleLinear } from '@vx/scale';
import AnimatedDensitySeries from './animated/AnimatedDensitySeries';
import { binnedDataShape } from '../utils/propShapes';
import kernelDensityEstimator from '../utils/kernelDensityEstimator';
import kernelParabolic from '../utils/kernels/epanechnikov';
import kernelGaussian from '../utils/kernels/gaussian';
var propTypes = {
animated: PropTypes.bool,
rawData: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])),
binnedData: binnedDataShape,
binType: PropTypes.oneOf(['numeric', 'categorical']),
fill: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
fillOpacity: PropTypes.oneOfType([PropTypes.func, PropTypes.number]),
horizontal: PropTypes.bool,
kernel: PropTypes.oneOf(['gaussian', 'parabolic']),
showArea: PropTypes.bool,
showLine: PropTypes.bool,
smoothing: PropTypes.number,
stroke: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
strokeDasharray: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
strokeLinecap: PropTypes.oneOf(['butt', 'square', 'round', 'inherit']),
strokeWidth: PropTypes.oneOfType([PropTypes.func, PropTypes.number]),
useEntireScale: PropTypes.bool,
valueAccessor: PropTypes.func,
valueKey: PropTypes.string,
// likely injected by parent Histogram
binScale: PropTypes.func,
valueScale: PropTypes.func
};
var defaultProps = {
animated: true,
rawData: [],
binnedData: [],
binScale: null,
binType: null,
fill: chartTheme.colors.default,
fillOpacity: 0.3,
horizontal: false,
kernel: 'gaussian',
showArea: true,
showLine: true,
smoothing: 1,
stroke: chartTheme.colors.default,
strokeWidth: 2,
strokeDasharray: null,
strokeLinecap: 'round',
useEntireScale: false,
valueAccessor: function valueAccessor(d) {
return d;
},
valueKey: 'count',
valueScale: null
};
var BIN_OFFSET = 0.5;
var getBin = function getBin(d) {
return typeof d.bin === 'undefined' ? d.bin0 : d.bin;
};
var densityAccessor = function densityAccessor(d) {
return d.value;
};
var cumulativeAccessor = function cumulativeAccessor(d) {
return d.cumulative;
};
function DensitySeries(_ref) {
var animated = _ref.animated,
rawData = _ref.rawData,
binnedData = _ref.binnedData,
binScale = _ref.binScale,
binType = _ref.binType,
fill = _ref.fill,
fillOpacity = _ref.fillOpacity,
horizontal = _ref.horizontal,
kernel = _ref.kernel,
showArea = _ref.showArea,
showLine = _ref.showLine,
smoothing = _ref.smoothing,
stroke = _ref.stroke,
strokeWidth = _ref.strokeWidth,
strokeDasharray = _ref.strokeDasharray,
strokeLinecap = _ref.strokeLinecap,
useEntireScale = _ref.useEntireScale,
valueAccessor = _ref.valueAccessor,
valueKey = _ref.valueKey,
valueScale = _ref.valueScale;
if (!showArea && !showLine) return null;
var binWidth = binScale.bandwidth ? binScale.bandwidth() // categorical
: Math.abs(binScale(binnedData[0].bin1) - binScale(binnedData[0].bin0)); // numeric
var binOffset = BIN_OFFSET * binWidth * (horizontal && binType === 'numeric' ? -1 : 1); // all density estimators require numeric data, so if we're passed categorical data
// or pre-aggregated data, we just draw an area curve using the binned data
var densityScale = valueScale;
var getDensity = function getDensity(d) {
return d[valueKey];
};
var densityData = binnedData;
if (binType === 'numeric' && rawData.length > 0) {
// @TODO cache this with a non-functional component
var cumulative = /cumulative/gi.test(valueKey);
var bins = binnedData.map(getBin);
var kernelFunc = kernel === 'gaussian' ? kernelGaussian() : kernelParabolic(smoothing);
var estimator = kernelDensityEstimator(kernelFunc, bins);
densityData = estimator(rawData.map(valueAccessor)); // area fills become inverted when the last value is less than the first value.
// padding with 0s ensures this never happens
densityData.unshift(_extends({}, densityData[0], {
value: 0
}));
densityData.push(_extends({}, densityData[densityData.length - 1], {
value: 0
}));
var densityRange = valueScale.range();
if (!useEntireScale) {
// set the range of the density scale to match the maximum data value
var maxVal = max(binnedData, function (d) {
return d[valueKey];
});
densityRange[1] = valueScale(maxVal);
}
densityScale = scaleLinear({
domain: extent(densityData, function (d, i) {
var val = densityAccessor(d); // compute cumulative in this loop
d.cumulative = val + (i > 0 ? densityData[i - 1].cumulative : 0);
d.id = i;
return cumulative ? d.cumulative : val;
}),
range: densityRange
});
getDensity = cumulative ? cumulativeAccessor : densityAccessor;
}
var offSetBinScale = binScale.copy();
offSetBinScale.range(binScale.range().map(function (v) {
return v + binOffset;
}));
var getX = horizontal ? getDensity : getBin;
var getY = horizontal ? getBin : getDensity;
var xScale = horizontal ? densityScale : offSetBinScale;
var yScale = horizontal ? offSetBinScale : densityScale;
return React.createElement(Group, {
style: {
pointerEvents: 'none'
}
}, animated && React.createElement(AnimatedDensitySeries, {
densityData: densityData,
fill: fill,
fillOpacity: fillOpacity,
horizontal: horizontal,
getX: getX,
getY: getY,
showArea: showArea,
showLine: showLine,
stroke: stroke,
strokeWidth: strokeWidth,
strokeDasharray: strokeDasharray,
strokeLinecap: strokeLinecap,
xScale: xScale,
yScale: yScale
}), !animated && showArea && React.createElement(AreaClosed, {
data: densityData,
x: getX,
y: getY,
xScale: xScale,
yScale: yScale,
fill: fill,
fillOpacity: fillOpacity,
stroke: "transparent",
strokeWidth: strokeWidth,
curve: curveBasis
}), !animated && showLine && strokeWidth > 0 && React.createElement(LinePath, {
data: densityData,
x: getX,
y: getY,
xScale: xScale,
yScale: yScale,
stroke: stroke,
strokeWidth: strokeWidth,
strokeDasharray: strokeDasharray,
strokeLinecap: strokeLinecap,
curve: curveBasis,
glyph: null
}));
}
DensitySeries.propTypes = propTypes;
DensitySeries.defaultProps = defaultProps;
DensitySeries.displayName = 'DensitySeries';
export default DensitySeries;