UNPKG

react-vis

Version:

Data visualization library based on React and d3.

388 lines (358 loc) 13 kB
var _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; }; // 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 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 React.createElement(DecorativeAxis, { animation: animation, key: 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 React.createElement(PolygonSeries, { animation: animation, className: predefinedClassName + '-polygon', key: rowIndex + '-polygon', data: mappedData, style: _extends({ 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 React.createElement(MarkSeries, { animation: animation, className: predefinedClassName + '-polygonPoint', key: rowIndex + '-polygonPoint', data: mappedData, size: 10, style: _extends({}, 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 = React.createElement(LabelSeries, { animation: animation, key: className, className: predefinedClassName + '-label', data: getLabels({ domains: domains, style: style.labels, startingAngle: startingAngle }) }); return React.createElement( XYPlot, { height: height, width: width, margin: margin, dontCheckIfEmpty: true, className: 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;