Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

3 лет назад
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. 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; };
  2. // Copyright (c) 2016 - 2017 Uber Technologies, Inc.
  3. //
  4. // Permission is hereby granted, free of charge, to any person obtaining a copy
  5. // of this software and associated documentation files (the "Software"), to deal
  6. // in the Software without restriction, including without limitation the rights
  7. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. // copies of the Software, and to permit persons to whom the Software is
  9. // furnished to do so, subject to the following conditions:
  10. //
  11. // The above copyright notice and this permission notice shall be included in
  12. // all copies or substantial portions of the Software.
  13. //
  14. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20. // THE SOFTWARE.
  21. import React from 'react';
  22. import PropTypes from 'prop-types';
  23. import { scaleLinear } from 'd3-scale';
  24. import { format } from 'd3-format';
  25. import { AnimationPropType } from '../animation';
  26. import XYPlot from '../plot/xy-plot';
  27. import { DISCRETE_COLOR_RANGE } from '../theme';
  28. import { MarginPropType } from '../utils/chart-utils';
  29. import MarkSeries from '../plot/series/mark-series';
  30. import PolygonSeries from '../plot/series/polygon-series';
  31. import LabelSeries from '../plot/series/label-series';
  32. import DecorativeAxis from '../plot/axis/decorative-axis';
  33. var predefinedClassName = 'rv-radar-chart';
  34. var DEFAULT_FORMAT = format('.2r');
  35. /**
  36. * Generate axes for each of the domains
  37. * @param {Object} props
  38. - props.animation {Boolean}
  39. - props.domains {Array} array of object specifying the way each axis is to be plotted
  40. - props.style {object} style object for the whole chart
  41. - props.tickFormat {Function} formatting function for axes
  42. - props.startingAngle {number} the initial angle offset
  43. * @return {Array} the plotted axis components
  44. */
  45. function getAxes(props) {
  46. var animation = props.animation,
  47. domains = props.domains,
  48. startingAngle = props.startingAngle,
  49. style = props.style,
  50. tickFormat = props.tickFormat,
  51. hideInnerMostValues = props.hideInnerMostValues;
  52. return domains.map(function (domain, index) {
  53. var angle = index / domains.length * Math.PI * 2 + startingAngle;
  54. var sortedDomain = domain.domain;
  55. var domainTickFormat = function domainTickFormat(t) {
  56. if (hideInnerMostValues && t === sortedDomain[0]) {
  57. return '';
  58. }
  59. return domain.tickFormat ? domain.tickFormat(t) : tickFormat(t);
  60. };
  61. return React.createElement(DecorativeAxis, {
  62. animation: animation,
  63. key: index + '-axis',
  64. axisStart: { x: 0, y: 0 },
  65. axisEnd: {
  66. x: getCoordinate(Math.cos(angle)),
  67. y: getCoordinate(Math.sin(angle))
  68. },
  69. axisDomain: sortedDomain,
  70. numberOfTicks: 5,
  71. tickValue: domainTickFormat,
  72. style: style.axes
  73. });
  74. });
  75. }
  76. /**
  77. * Generate x or y coordinate for axisEnd
  78. * @param {Number} axisEndPoint
  79. - epsilon is an arbitrarily chosen small number to approximate axisEndPoints
  80. - to true values resulting from trigonometry functions (sin, cos) on angles
  81. * @return {Number} the x or y coordinate accounting for exact trig values
  82. */
  83. function getCoordinate(axisEndPoint) {
  84. var epsilon = 10e-13;
  85. if (Math.abs(axisEndPoint) <= epsilon) {
  86. axisEndPoint = 0;
  87. } else if (axisEndPoint > 0) {
  88. if (Math.abs(axisEndPoint - 0.5) <= epsilon) {
  89. axisEndPoint = 0.5;
  90. }
  91. } else if (axisEndPoint < 0) {
  92. if (Math.abs(axisEndPoint + 0.5) <= epsilon) {
  93. axisEndPoint = -0.5;
  94. }
  95. }
  96. return axisEndPoint;
  97. }
  98. /**
  99. * Generate labels for the ends of the axes
  100. * @param {Object} props
  101. - props.domains {Array} array of object specifying the way each axis is to be plotted
  102. - props.startingAngle {number} the initial angle offset
  103. - props.style {object} style object for just the labels
  104. * @return {Array} the prepped data for the labelSeries
  105. */
  106. function getLabels(props) {
  107. var domains = props.domains,
  108. startingAngle = props.startingAngle,
  109. style = props.style;
  110. return domains.map(function (_ref, index) {
  111. var name = _ref.name;
  112. var angle = index / domains.length * Math.PI * 2 + startingAngle;
  113. var radius = 1.2;
  114. return {
  115. x: radius * Math.cos(angle),
  116. y: radius * Math.sin(angle),
  117. label: name,
  118. style: style
  119. };
  120. });
  121. }
  122. /**
  123. * Generate the actual polygons to be plotted
  124. * @param {Object} props
  125. - props.animation {Boolean}
  126. - props.data {Array} array of object specifying what values are to be plotted
  127. - props.domains {Array} array of object specifying the way each axis is to be plotted
  128. - props.startingAngle {number} the initial angle offset
  129. - props.style {object} style object for the whole chart
  130. * @return {Array} the plotted axis components
  131. */
  132. function getPolygons(props) {
  133. var animation = props.animation,
  134. colorRange = props.colorRange,
  135. domains = props.domains,
  136. data = props.data,
  137. style = props.style,
  138. startingAngle = props.startingAngle,
  139. onSeriesMouseOver = props.onSeriesMouseOver,
  140. onSeriesMouseOut = props.onSeriesMouseOut;
  141. var scales = domains.reduce(function (acc, _ref2) {
  142. var domain = _ref2.domain,
  143. name = _ref2.name;
  144. acc[name] = scaleLinear().domain(domain).range([0, 1]);
  145. return acc;
  146. }, {});
  147. return data.map(function (row, rowIndex) {
  148. var mappedData = domains.map(function (_ref3, index) {
  149. var name = _ref3.name,
  150. getValue = _ref3.getValue;
  151. var dataPoint = getValue ? getValue(row) : row[name];
  152. // error handling if point doesn't exist
  153. var angle = index / domains.length * Math.PI * 2 + startingAngle;
  154. // dont let the radius become negative
  155. var radius = Math.max(scales[name](dataPoint), 0);
  156. return { x: radius * Math.cos(angle), y: radius * Math.sin(angle), name: row.name };
  157. });
  158. return React.createElement(PolygonSeries, {
  159. animation: animation,
  160. className: predefinedClassName + '-polygon',
  161. key: rowIndex + '-polygon',
  162. data: mappedData,
  163. style: _extends({
  164. stroke: row.color || row.stroke || colorRange[rowIndex % colorRange.length],
  165. fill: row.color || row.fill || colorRange[rowIndex % colorRange.length]
  166. }, style.polygons),
  167. onSeriesMouseOver: onSeriesMouseOver,
  168. onSeriesMouseOut: onSeriesMouseOut
  169. });
  170. });
  171. }
  172. /**
  173. * Generate circles at the polygon points for Hover functionality
  174. * @param {Object} props
  175. - props.animation {Boolean}
  176. - props.data {Array} array of object specifying what values are to be plotted
  177. - props.domains {Array} array of object specifying the way each axis is to be plotted
  178. - props.startingAngle {number} the initial angle offset
  179. - props.style {object} style object for the whole chart
  180. - props.onValueMouseOver {function} function to call on mouse over a polygon point
  181. - props.onValueMouseOver {function} function to call when mouse leaves a polygon point
  182. * @return {Array} the plotted axis components
  183. */
  184. function getPolygonPoints(props) {
  185. var animation = props.animation,
  186. domains = props.domains,
  187. data = props.data,
  188. startingAngle = props.startingAngle,
  189. style = props.style,
  190. onValueMouseOver = props.onValueMouseOver,
  191. onValueMouseOut = props.onValueMouseOut;
  192. if (!onValueMouseOver) {
  193. return;
  194. }
  195. var scales = domains.reduce(function (acc, _ref4) {
  196. var domain = _ref4.domain,
  197. name = _ref4.name;
  198. acc[name] = scaleLinear().domain(domain).range([0, 1]);
  199. return acc;
  200. }, {});
  201. return data.map(function (row, rowIndex) {
  202. var mappedData = domains.map(function (_ref5, index) {
  203. var name = _ref5.name,
  204. getValue = _ref5.getValue;
  205. var dataPoint = getValue ? getValue(row) : row[name];
  206. // error handling if point doesn't exist
  207. var angle = index / domains.length * Math.PI * 2 + startingAngle;
  208. // dont let the radius become negative
  209. var radius = Math.max(scales[name](dataPoint), 0);
  210. return {
  211. x: radius * Math.cos(angle),
  212. y: radius * Math.sin(angle),
  213. domain: name,
  214. value: dataPoint,
  215. dataName: row.name
  216. };
  217. });
  218. return React.createElement(MarkSeries, {
  219. animation: animation,
  220. className: predefinedClassName + '-polygonPoint',
  221. key: rowIndex + '-polygonPoint',
  222. data: mappedData,
  223. size: 10,
  224. style: _extends({}, style.polygons, {
  225. fill: 'transparent',
  226. stroke: 'transparent'
  227. }),
  228. onValueMouseOver: onValueMouseOver,
  229. onValueMouseOut: onValueMouseOut
  230. });
  231. });
  232. }
  233. function RadarChart(props) {
  234. var animation = props.animation,
  235. className = props.className,
  236. children = props.children,
  237. colorRange = props.colorRange,
  238. data = props.data,
  239. domains = props.domains,
  240. height = props.height,
  241. hideInnerMostValues = props.hideInnerMostValues,
  242. margin = props.margin,
  243. onMouseLeave = props.onMouseLeave,
  244. onMouseEnter = props.onMouseEnter,
  245. startingAngle = props.startingAngle,
  246. style = props.style,
  247. tickFormat = props.tickFormat,
  248. width = props.width,
  249. renderAxesOverPolygons = props.renderAxesOverPolygons,
  250. onValueMouseOver = props.onValueMouseOver,
  251. onValueMouseOut = props.onValueMouseOut,
  252. onSeriesMouseOver = props.onSeriesMouseOver,
  253. onSeriesMouseOut = props.onSeriesMouseOut;
  254. var axes = getAxes({
  255. domains: domains,
  256. animation: animation,
  257. hideInnerMostValues: hideInnerMostValues,
  258. startingAngle: startingAngle,
  259. style: style,
  260. tickFormat: tickFormat
  261. });
  262. var polygons = getPolygons({
  263. animation: animation,
  264. colorRange: colorRange,
  265. domains: domains,
  266. data: data,
  267. startingAngle: startingAngle,
  268. style: style,
  269. onSeriesMouseOver: onSeriesMouseOver,
  270. onSeriesMouseOut: onSeriesMouseOut
  271. });
  272. var polygonPoints = getPolygonPoints({
  273. animation: animation,
  274. colorRange: colorRange,
  275. domains: domains,
  276. data: data,
  277. startingAngle: startingAngle,
  278. style: style,
  279. onValueMouseOver: onValueMouseOver,
  280. onValueMouseOut: onValueMouseOut
  281. });
  282. var labelSeries = React.createElement(LabelSeries, {
  283. animation: animation,
  284. key: className,
  285. className: predefinedClassName + '-label',
  286. data: getLabels({ domains: domains, style: style.labels, startingAngle: startingAngle }) });
  287. return React.createElement(
  288. XYPlot,
  289. {
  290. height: height,
  291. width: width,
  292. margin: margin,
  293. dontCheckIfEmpty: true,
  294. className: className + ' ' + predefinedClassName,
  295. onMouseLeave: onMouseLeave,
  296. onMouseEnter: onMouseEnter,
  297. xDomain: [-1, 1],
  298. yDomain: [-1, 1] },
  299. children,
  300. !renderAxesOverPolygons && axes.concat(polygons).concat(labelSeries).concat(polygonPoints),
  301. renderAxesOverPolygons && polygons.concat(labelSeries).concat(axes).concat(polygonPoints)
  302. );
  303. }
  304. RadarChart.displayName = 'RadarChart';
  305. RadarChart.propTypes = {
  306. animation: AnimationPropType,
  307. className: PropTypes.string,
  308. colorType: PropTypes.string,
  309. colorRange: PropTypes.arrayOf(PropTypes.string),
  310. data: PropTypes.arrayOf(PropTypes.object).isRequired,
  311. domains: PropTypes.arrayOf(PropTypes.shape({
  312. name: PropTypes.string.isRequired,
  313. domain: PropTypes.arrayOf(PropTypes.number).isRequired,
  314. tickFormat: PropTypes.func
  315. })).isRequired,
  316. height: PropTypes.number.isRequired,
  317. hideInnerMostValues: PropTypes.bool,
  318. margin: MarginPropType,
  319. startingAngle: PropTypes.number,
  320. style: PropTypes.shape({
  321. axes: PropTypes.object,
  322. labels: PropTypes.object,
  323. polygons: PropTypes.object
  324. }),
  325. tickFormat: PropTypes.func,
  326. width: PropTypes.number.isRequired,
  327. renderAxesOverPolygons: PropTypes.bool,
  328. onValueMouseOver: PropTypes.func,
  329. onValueMouseOut: PropTypes.func,
  330. onSeriesMouseOver: PropTypes.func,
  331. onSeriesMouseOut: PropTypes.func
  332. };
  333. RadarChart.defaultProps = {
  334. className: '',
  335. colorType: 'category',
  336. colorRange: DISCRETE_COLOR_RANGE,
  337. hideInnerMostValues: true,
  338. startingAngle: Math.PI / 2,
  339. style: {
  340. axes: {
  341. line: {},
  342. ticks: {},
  343. text: {}
  344. },
  345. labels: {
  346. fontSize: 10,
  347. textAnchor: 'middle'
  348. },
  349. polygons: {
  350. strokeWidth: 0.5,
  351. strokeOpacity: 1,
  352. fillOpacity: 0.1
  353. }
  354. },
  355. tickFormat: DEFAULT_FORMAT,
  356. renderAxesOverPolygons: false
  357. };
  358. export default RadarChart;