UNPKG

react-vis

Version:

Data visualization library based on React and d3.

389 lines (364 loc) 14 kB
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } // Copyright (c) 2016 - 2017 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import React from 'react'; import PropTypes from 'prop-types'; import { scaleLinear } from 'd3-scale'; import { format } from 'd3-format'; import { AnimationPropType } from "../animation"; import XYPlot from "../plot/xy-plot"; import { DISCRETE_COLOR_RANGE } from "../theme"; import { MarginPropType } from "../utils/chart-utils"; import { getCombinedClassName } from "../utils/styling-utils"; import MarkSeries from "../plot/series/mark-series"; import PolygonSeries from "../plot/series/polygon-series"; import LabelSeries from "../plot/series/label-series"; import DecorativeAxis from "../plot/axis/decorative-axis"; var predefinedClassName = 'rv-radar-chart'; var DEFAULT_FORMAT = format('.2r'); /** * Generate axes for each of the domains * @param {Object} props - props.animation {Boolean} - props.domains {Array} array of object specifying the way each axis is to be plotted - props.style {object} style object for the whole chart - props.tickFormat {Function} formatting function for axes - props.startingAngle {number} the initial angle offset * @return {Array} the plotted axis components */ function getAxes(props) { var animation = props.animation, domains = props.domains, startingAngle = props.startingAngle, style = props.style, tickFormat = props.tickFormat, hideInnerMostValues = props.hideInnerMostValues; return domains.map(function (domain, index) { var angle = index / domains.length * Math.PI * 2 + startingAngle; var sortedDomain = domain.domain; var domainTickFormat = function domainTickFormat(t) { if (hideInnerMostValues && t === sortedDomain[0]) { return ''; } return domain.tickFormat ? domain.tickFormat(t) : tickFormat(t); }; return /*#__PURE__*/React.createElement(DecorativeAxis, { animation: animation, key: "".concat(index, "-axis"), axisStart: { x: 0, y: 0 }, axisEnd: { x: getCoordinate(Math.cos(angle)), y: getCoordinate(Math.sin(angle)) }, axisDomain: sortedDomain, numberOfTicks: 5, tickValue: domainTickFormat, style: style.axes }); }); } /** * Generate x or y coordinate for axisEnd * @param {Number} axisEndPoint - epsilon is an arbitrarily chosen small number to approximate axisEndPoints - to true values resulting from trigonometry functions (sin, cos) on angles * @return {Number} the x or y coordinate accounting for exact trig values */ function getCoordinate(axisEndPoint) { var epsilon = 10e-13; if (Math.abs(axisEndPoint) <= epsilon) { axisEndPoint = 0; } else if (axisEndPoint > 0) { if (Math.abs(axisEndPoint - 0.5) <= epsilon) { axisEndPoint = 0.5; } } else if (axisEndPoint < 0) { if (Math.abs(axisEndPoint + 0.5) <= epsilon) { axisEndPoint = -0.5; } } return axisEndPoint; } /** * Generate labels for the ends of the axes * @param {Object} props - props.domains {Array} array of object specifying the way each axis is to be plotted - props.startingAngle {number} the initial angle offset - props.style {object} style object for just the labels * @return {Array} the prepped data for the labelSeries */ function getLabels(props) { var domains = props.domains, startingAngle = props.startingAngle, style = props.style; return domains.map(function (_ref, index) { var name = _ref.name; var angle = index / domains.length * Math.PI * 2 + startingAngle; var radius = 1.2; return { x: radius * Math.cos(angle), y: radius * Math.sin(angle), label: name, style: style }; }); } /** * Generate the actual polygons to be plotted * @param {Object} props - props.animation {Boolean} - props.data {Array} array of object specifying what values are to be plotted - props.domains {Array} array of object specifying the way each axis is to be plotted - props.startingAngle {number} the initial angle offset - props.style {object} style object for the whole chart * @return {Array} the plotted axis components */ function getPolygons(props) { var animation = props.animation, colorRange = props.colorRange, domains = props.domains, data = props.data, style = props.style, startingAngle = props.startingAngle, onSeriesMouseOver = props.onSeriesMouseOver, onSeriesMouseOut = props.onSeriesMouseOut; var scales = domains.reduce(function (acc, _ref2) { var domain = _ref2.domain, name = _ref2.name; acc[name] = scaleLinear().domain(domain).range([0, 1]); return acc; }, {}); return data.map(function (row, rowIndex) { var mappedData = domains.map(function (_ref3, index) { var name = _ref3.name, getValue = _ref3.getValue; var dataPoint = getValue ? getValue(row) : row[name]; // error handling if point doesn't exist var angle = index / domains.length * Math.PI * 2 + startingAngle; // dont let the radius become negative var radius = Math.max(scales[name](dataPoint), 0); return { x: radius * Math.cos(angle), y: radius * Math.sin(angle), name: row.name }; }); return /*#__PURE__*/React.createElement(PolygonSeries, { animation: animation, className: "".concat(predefinedClassName, "-polygon"), key: "".concat(rowIndex, "-polygon"), data: mappedData, style: _objectSpread({ stroke: row.color || row.stroke || colorRange[rowIndex % colorRange.length], fill: row.color || row.fill || colorRange[rowIndex % colorRange.length] }, style.polygons), onSeriesMouseOver: onSeriesMouseOver, onSeriesMouseOut: onSeriesMouseOut }); }); } /** * Generate circles at the polygon points for Hover functionality * @param {Object} props - props.animation {Boolean} - props.data {Array} array of object specifying what values are to be plotted - props.domains {Array} array of object specifying the way each axis is to be plotted - props.startingAngle {number} the initial angle offset - props.style {object} style object for the whole chart - props.onValueMouseOver {function} function to call on mouse over a polygon point - props.onValueMouseOver {function} function to call when mouse leaves a polygon point * @return {Array} the plotted axis components */ function getPolygonPoints(props) { var animation = props.animation, domains = props.domains, data = props.data, startingAngle = props.startingAngle, style = props.style, onValueMouseOver = props.onValueMouseOver, onValueMouseOut = props.onValueMouseOut; if (!onValueMouseOver) { return; } var scales = domains.reduce(function (acc, _ref4) { var domain = _ref4.domain, name = _ref4.name; acc[name] = scaleLinear().domain(domain).range([0, 1]); return acc; }, {}); return data.map(function (row, rowIndex) { var mappedData = domains.map(function (_ref5, index) { var name = _ref5.name, getValue = _ref5.getValue; var dataPoint = getValue ? getValue(row) : row[name]; // error handling if point doesn't exist var angle = index / domains.length * Math.PI * 2 + startingAngle; // dont let the radius become negative var radius = Math.max(scales[name](dataPoint), 0); return { x: radius * Math.cos(angle), y: radius * Math.sin(angle), domain: name, value: dataPoint, dataName: row.name }; }); return /*#__PURE__*/React.createElement(MarkSeries, { animation: animation, className: "".concat(predefinedClassName, "-polygonPoint"), key: "".concat(rowIndex, "-polygonPoint"), data: mappedData, size: 10, style: _objectSpread(_objectSpread({}, style.polygons), {}, { fill: 'transparent', stroke: 'transparent' }), onValueMouseOver: onValueMouseOver, onValueMouseOut: onValueMouseOut }); }); } function RadarChart(props) { var animation = props.animation, className = props.className, children = props.children, colorRange = props.colorRange, data = props.data, domains = props.domains, height = props.height, hideInnerMostValues = props.hideInnerMostValues, margin = props.margin, onMouseLeave = props.onMouseLeave, onMouseEnter = props.onMouseEnter, startingAngle = props.startingAngle, style = props.style, tickFormat = props.tickFormat, width = props.width, renderAxesOverPolygons = props.renderAxesOverPolygons, onValueMouseOver = props.onValueMouseOver, onValueMouseOut = props.onValueMouseOut, onSeriesMouseOver = props.onSeriesMouseOver, onSeriesMouseOut = props.onSeriesMouseOut; var axes = getAxes({ domains: domains, animation: animation, hideInnerMostValues: hideInnerMostValues, startingAngle: startingAngle, style: style, tickFormat: tickFormat }); var polygons = getPolygons({ animation: animation, colorRange: colorRange, domains: domains, data: data, startingAngle: startingAngle, style: style, onSeriesMouseOver: onSeriesMouseOver, onSeriesMouseOut: onSeriesMouseOut }); var polygonPoints = getPolygonPoints({ animation: animation, colorRange: colorRange, domains: domains, data: data, startingAngle: startingAngle, style: style, onValueMouseOver: onValueMouseOver, onValueMouseOut: onValueMouseOut }); var labelSeries = /*#__PURE__*/React.createElement(LabelSeries, { animation: animation, key: className, className: "".concat(predefinedClassName, "-label"), data: getLabels({ domains: domains, style: style.labels, startingAngle: startingAngle }) }); return /*#__PURE__*/React.createElement(XYPlot, { height: height, width: width, margin: margin, dontCheckIfEmpty: true, className: getCombinedClassName(className, predefinedClassName), onMouseLeave: onMouseLeave, onMouseEnter: onMouseEnter, xDomain: [-1, 1], yDomain: [-1, 1] }, children, !renderAxesOverPolygons && axes.concat(polygons).concat(labelSeries).concat(polygonPoints), renderAxesOverPolygons && polygons.concat(labelSeries).concat(axes).concat(polygonPoints)); } RadarChart.displayName = 'RadarChart'; RadarChart.propTypes = { animation: AnimationPropType, className: PropTypes.string, colorType: PropTypes.string, colorRange: PropTypes.arrayOf(PropTypes.string), data: PropTypes.arrayOf(PropTypes.object).isRequired, domains: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string.isRequired, domain: PropTypes.arrayOf(PropTypes.number).isRequired, tickFormat: PropTypes.func })).isRequired, height: PropTypes.number.isRequired, hideInnerMostValues: PropTypes.bool, margin: MarginPropType, startingAngle: PropTypes.number, style: PropTypes.shape({ axes: PropTypes.object, labels: PropTypes.object, polygons: PropTypes.object }), tickFormat: PropTypes.func, width: PropTypes.number.isRequired, renderAxesOverPolygons: PropTypes.bool, onValueMouseOver: PropTypes.func, onValueMouseOut: PropTypes.func, onSeriesMouseOver: PropTypes.func, onSeriesMouseOut: PropTypes.func }; RadarChart.defaultProps = { className: '', colorType: 'category', colorRange: DISCRETE_COLOR_RANGE, hideInnerMostValues: true, startingAngle: Math.PI / 2, style: { axes: { line: {}, ticks: {}, text: {} }, labels: { fontSize: 10, textAnchor: 'middle' }, polygons: { strokeWidth: 0.5, strokeOpacity: 1, fillOpacity: 0.1 } }, tickFormat: DEFAULT_FORMAT, renderAxesOverPolygons: false }; export default RadarChart;