UNPKG

@data-ui/histogram

Version:

React + d3 library for creating histograms

208 lines (192 loc) 7.12 kB
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;