Rose.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. "use strict";
  2. exports.__esModule = true;
  3. exports.default = void 0;
  4. var _d = _interopRequireDefault(require("d3"));
  5. var _propTypes = _interopRequireDefault(require("prop-types"));
  6. var _nvd = _interopRequireDefault(require("nvd3"));
  7. var _color = require("@superset-ui/color");
  8. var _numberFormat = require("@superset-ui/number-format");
  9. var _timeFormat = require("@superset-ui/time-format");
  10. require("./Rose.css");
  11. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  12. 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); }
  13. const propTypes = {
  14. // Data is an object hashed by numeric value, perhaps timestamp
  15. data: _propTypes.default.objectOf(_propTypes.default.arrayOf(_propTypes.default.shape({
  16. key: _propTypes.default.arrayOf(_propTypes.default.string),
  17. name: _propTypes.default.arrayOf(_propTypes.default.string),
  18. time: _propTypes.default.number,
  19. value: _propTypes.default.number
  20. }))),
  21. width: _propTypes.default.number,
  22. height: _propTypes.default.number,
  23. dateTimeFormat: _propTypes.default.string,
  24. numberFormat: _propTypes.default.string,
  25. useRichTooltip: _propTypes.default.bool,
  26. useAreaProportions: _propTypes.default.bool
  27. };
  28. function copyArc(d) {
  29. return {
  30. startAngle: d.startAngle,
  31. endAngle: d.endAngle,
  32. innerRadius: d.innerRadius,
  33. outerRadius: d.outerRadius
  34. };
  35. }
  36. function sortValues(a, b) {
  37. if (a.value === b.value) {
  38. return a.name > b.name ? 1 : -1;
  39. }
  40. return b.value - a.value;
  41. }
  42. function Rose(element, props) {
  43. const {
  44. data,
  45. width,
  46. height,
  47. colorScheme,
  48. dateTimeFormat,
  49. numberFormat,
  50. useRichTooltip,
  51. useAreaProportions
  52. } = props;
  53. const div = _d.default.select(element);
  54. div.classed('superset-legacy-chart-rose', true);
  55. const datum = data;
  56. const times = Object.keys(datum).map(t => parseInt(t, 10)).sort((a, b) => a - b);
  57. const numGrains = times.length;
  58. const numGroups = datum[times[0]].length;
  59. const format = (0, _numberFormat.getNumberFormatter)(numberFormat);
  60. const timeFormat = (0, _timeFormat.getTimeFormatter)(dateTimeFormat);
  61. const colorFn = _color.CategoricalColorNamespace.getScale(colorScheme);
  62. _d.default.select('.nvtooltip').remove();
  63. div.selectAll('*').remove();
  64. const arc = _d.default.svg.arc();
  65. const legend = _nvd.default.models.legend();
  66. const tooltip = _nvd.default.models.tooltip();
  67. const state = {
  68. disabled: datum[times[0]].map(() => false)
  69. };
  70. const svg = div.append('svg').attr('width', width).attr('height', height);
  71. const g = svg.append('g').attr('class', 'rose').append('g');
  72. const legendWrap = g.append('g').attr('class', 'legendWrap');
  73. function legendData(adatum) {
  74. return adatum[times[0]].map((v, i) => ({
  75. disabled: state.disabled[i],
  76. key: v.name
  77. }));
  78. }
  79. function tooltipData(d, i, adatum) {
  80. const timeIndex = Math.floor(d.arcId / numGroups);
  81. const series = useRichTooltip ? adatum[times[timeIndex]].filter(v => !state.disabled[v.id % numGroups]).map(v => ({
  82. key: v.name,
  83. value: v.value,
  84. color: colorFn(v.name),
  85. highlight: v.id === d.arcId
  86. })) : [{
  87. key: d.name,
  88. value: d.val,
  89. color: colorFn(d.name)
  90. }];
  91. return {
  92. key: 'Date',
  93. value: d.time,
  94. series
  95. };
  96. }
  97. legend.width(width).color(d => colorFn(d.key));
  98. legendWrap.datum(legendData(datum)).call(legend);
  99. tooltip.headerFormatter(timeFormat).valueFormatter(format); // Compute max radius, which the largest value will occupy
  100. const roseHeight = height - legend.height();
  101. const margin = {
  102. top: legend.height()
  103. };
  104. const edgeMargin = 35; // space between outermost radius and slice edge
  105. const maxRadius = Math.min(width, roseHeight) / 2 - edgeMargin;
  106. const labelThreshold = 0.05;
  107. const gro = 8; // mouseover radius growth in pixels
  108. const mini = 0.075;
  109. const centerTranslate = "translate(" + width / 2 + "," + (roseHeight / 2 + margin.top) + ")";
  110. const roseWrap = g.append('g').attr('transform', centerTranslate).attr('class', 'roseWrap');
  111. const labelsWrap = g.append('g').attr('transform', centerTranslate).attr('class', 'labelsWrap');
  112. const groupLabelsWrap = g.append('g').attr('transform', centerTranslate).attr('class', 'groupLabelsWrap'); // Compute inner and outer angles for each data point
  113. function computeArcStates(adatum) {
  114. // Find the max sum of values across all time
  115. let maxSum = 0;
  116. let grain = 0;
  117. const sums = [];
  118. for (const t of times) {
  119. const sum = datum[t].reduce((a, v, i) => a + (state.disabled[i] ? 0 : v.value), 0);
  120. maxSum = sum > maxSum ? sum : maxSum;
  121. sums[grain] = sum;
  122. grain++;
  123. } // Compute angle occupied by each time grain
  124. const dtheta = Math.PI * 2 / numGrains;
  125. const angles = [];
  126. for (let i = 0; i <= numGrains; i++) {
  127. angles.push(dtheta * i - Math.PI / 2);
  128. } // Compute proportion
  129. const P = maxRadius / maxSum;
  130. const Q = P * maxRadius;
  131. const computeOuterRadius = (value, innerRadius) => useAreaProportions ? Math.sqrt(Q * value + innerRadius * innerRadius) : P * value + innerRadius;
  132. const arcSt = {
  133. data: [],
  134. extend: {},
  135. push: {},
  136. pieStart: {},
  137. pie: {},
  138. pieOver: {},
  139. mini: {},
  140. labels: [],
  141. groupLabels: []
  142. };
  143. let arcId = 0;
  144. for (let i = 0; i < numGrains; i++) {
  145. const t = times[i];
  146. const startAngle = angles[i];
  147. const endAngle = angles[i + 1];
  148. const G = 2 * Math.PI / sums[i];
  149. let innerRadius = 0;
  150. let outerRadius;
  151. let pieStartAngle = 0;
  152. let pieEndAngle;
  153. for (const v of adatum[t]) {
  154. const val = state.disabled[arcId % numGroups] ? 0 : v.value;
  155. const {
  156. name,
  157. time
  158. } = v;
  159. v.id = arcId;
  160. outerRadius = computeOuterRadius(val, innerRadius);
  161. arcSt.data.push({
  162. startAngle,
  163. endAngle,
  164. innerRadius,
  165. outerRadius,
  166. name,
  167. arcId,
  168. val,
  169. time
  170. });
  171. arcSt.extend[arcId] = {
  172. startAngle,
  173. endAngle,
  174. innerRadius,
  175. name,
  176. outerRadius: outerRadius + gro
  177. };
  178. arcSt.push[arcId] = {
  179. startAngle,
  180. endAngle,
  181. innerRadius: innerRadius + gro,
  182. outerRadius: outerRadius + gro
  183. };
  184. arcSt.pieStart[arcId] = {
  185. startAngle,
  186. endAngle,
  187. innerRadius: mini * maxRadius,
  188. outerRadius: maxRadius
  189. };
  190. arcSt.mini[arcId] = {
  191. startAngle,
  192. endAngle,
  193. innerRadius: innerRadius * mini,
  194. outerRadius: outerRadius * mini
  195. };
  196. arcId++;
  197. innerRadius = outerRadius;
  198. }
  199. const labelArc = _extends({}, arcSt.data[i * numGroups]);
  200. labelArc.outerRadius = maxRadius + 20;
  201. labelArc.innerRadius = maxRadius + 15;
  202. arcSt.labels.push(labelArc);
  203. for (const v of adatum[t].concat().sort(sortValues)) {
  204. const val = state.disabled[v.id % numGroups] ? 0 : v.value;
  205. pieEndAngle = G * val + pieStartAngle;
  206. arcSt.pie[v.id] = {
  207. startAngle: pieStartAngle,
  208. endAngle: pieEndAngle,
  209. innerRadius: maxRadius * mini,
  210. outerRadius: maxRadius,
  211. percent: v.value / sums[i]
  212. };
  213. arcSt.pieOver[v.id] = {
  214. startAngle: pieStartAngle,
  215. endAngle: pieEndAngle,
  216. innerRadius: maxRadius * mini,
  217. outerRadius: maxRadius + gro
  218. };
  219. pieStartAngle = pieEndAngle;
  220. }
  221. }
  222. arcSt.groupLabels = arcSt.data.slice(0, numGroups);
  223. return arcSt;
  224. }
  225. let arcSt = computeArcStates(datum);
  226. function tween(target, resFunc) {
  227. return function doTween(d) {
  228. const interpolate = _d.default.interpolate(copyArc(d), copyArc(target));
  229. return t => resFunc(Object.assign(d, interpolate(t)));
  230. };
  231. }
  232. function arcTween(target) {
  233. return tween(target, d => arc(d));
  234. }
  235. function translateTween(target) {
  236. return tween(target, d => "translate(" + arc.centroid(d) + ")");
  237. } // Grab the ID range of segments stand between
  238. // this segment and the edge of the circle
  239. const segmentsToEdgeCache = {};
  240. function getSegmentsToEdge(arcId) {
  241. if (segmentsToEdgeCache[arcId]) {
  242. return segmentsToEdgeCache[arcId];
  243. }
  244. const timeIndex = Math.floor(arcId / numGroups);
  245. segmentsToEdgeCache[arcId] = [arcId + 1, numGroups * (timeIndex + 1) - 1];
  246. return segmentsToEdgeCache[arcId];
  247. } // Get the IDs of all segments in a timeIndex
  248. const segmentsInTimeCache = {};
  249. function getSegmentsInTime(arcId) {
  250. if (segmentsInTimeCache[arcId]) {
  251. return segmentsInTimeCache[arcId];
  252. }
  253. const timeIndex = Math.floor(arcId / numGroups);
  254. segmentsInTimeCache[arcId] = [timeIndex * numGroups, (timeIndex + 1) * numGroups - 1];
  255. return segmentsInTimeCache[arcId];
  256. }
  257. let clickId = -1;
  258. let inTransition = false;
  259. const ae = roseWrap.selectAll('g').data(JSON.parse(JSON.stringify(arcSt.data))) // deep copy data state
  260. .enter().append('g').attr('class', 'segment').classed('clickable', true).on('mouseover', mouseover).on('mouseout', mouseout).on('mousemove', mousemove).on('click', click);
  261. const labels = labelsWrap.selectAll('g').data(JSON.parse(JSON.stringify(arcSt.labels))).enter().append('g').attr('class', 'roseLabel').attr('transform', d => "translate(" + arc.centroid(d) + ")");
  262. labels.append('text').style('text-anchor', 'middle').style('fill', '#000').text(d => timeFormat(d.time));
  263. const groupLabels = groupLabelsWrap.selectAll('g').data(JSON.parse(JSON.stringify(arcSt.groupLabels))).enter().append('g');
  264. groupLabels.style('opacity', 0).attr('class', 'roseGroupLabels').append('text').style('text-anchor', 'middle').style('fill', '#000').text(d => d.name);
  265. const arcs = ae.append('path').attr('class', 'arc').attr('fill', d => colorFn(d.name)).attr('d', arc);
  266. function mousemove() {
  267. tooltip();
  268. }
  269. function mouseover(b, i) {
  270. tooltip.data(tooltipData(b, i, datum)).hidden(false);
  271. const $this = _d.default.select(this);
  272. $this.classed('hover', true);
  273. if (clickId < 0 && !inTransition) {
  274. $this.select('path').interrupt().transition().duration(180).attrTween('d', arcTween(arcSt.extend[i]));
  275. const edge = getSegmentsToEdge(i);
  276. arcs.filter(d => edge[0] <= d.arcId && d.arcId <= edge[1]).interrupt().transition().duration(180).attrTween('d', d => arcTween(arcSt.push[d.arcId])(d));
  277. } else if (!inTransition) {
  278. const segments = getSegmentsInTime(clickId);
  279. if (segments[0] <= b.arcId && b.arcId <= segments[1]) {
  280. $this.select('path').interrupt().transition().duration(180).attrTween('d', arcTween(arcSt.pieOver[i]));
  281. }
  282. }
  283. }
  284. function mouseout(b, i) {
  285. tooltip.hidden(true);
  286. const $this = _d.default.select(this);
  287. $this.classed('hover', false);
  288. if (clickId < 0 && !inTransition) {
  289. $this.select('path').interrupt().transition().duration(180).attrTween('d', arcTween(arcSt.data[i]));
  290. const edge = getSegmentsToEdge(i);
  291. arcs.filter(d => edge[0] <= d.arcId && d.arcId <= edge[1]).interrupt().transition().duration(180).attrTween('d', d => arcTween(arcSt.data[d.arcId])(d));
  292. } else if (!inTransition) {
  293. const segments = getSegmentsInTime(clickId);
  294. if (segments[0] <= b.arcId && b.arcId <= segments[1]) {
  295. $this.select('path').interrupt().transition().duration(180).attrTween('d', arcTween(arcSt.pie[i]));
  296. }
  297. }
  298. }
  299. function click(b, i) {
  300. if (inTransition) {
  301. return;
  302. }
  303. const delay = _d.default.event.altKey ? 3750 : 375;
  304. const segments = getSegmentsInTime(i);
  305. if (clickId < 0) {
  306. inTransition = true;
  307. clickId = i;
  308. labels.interrupt().transition().duration(delay).attrTween('transform', d => translateTween({
  309. outerRadius: 0,
  310. innerRadius: 0,
  311. startAngle: d.startAngle,
  312. endAngle: d.endAngle
  313. })(d)).style('opacity', 0);
  314. groupLabels.attr('transform', "translate(" + arc.centroid({
  315. outerRadius: maxRadius + 20,
  316. innerRadius: maxRadius + 15,
  317. startAngle: arcSt.data[i].startAngle,
  318. endAngle: arcSt.data[i].endAngle
  319. }) + ")").interrupt().transition().delay(delay).duration(delay).attrTween('transform', d => translateTween({
  320. outerRadius: maxRadius + 20,
  321. innerRadius: maxRadius + 15,
  322. startAngle: arcSt.pie[segments[0] + d.arcId].startAngle,
  323. endAngle: arcSt.pie[segments[0] + d.arcId].endAngle
  324. })(d)).style('opacity', d => state.disabled[d.arcId] || arcSt.pie[segments[0] + d.arcId].percent < labelThreshold ? 0 : 1);
  325. ae.classed('clickable', d => segments[0] > d.arcId || d.arcId > segments[1]);
  326. arcs.filter(d => segments[0] <= d.arcId && d.arcId <= segments[1]).interrupt().transition().duration(delay).attrTween('d', d => arcTween(arcSt.pieStart[d.arcId])(d)).transition().duration(delay).attrTween('d', d => arcTween(arcSt.pie[d.arcId])(d)).each('end', () => {
  327. inTransition = false;
  328. });
  329. arcs.filter(d => segments[0] > d.arcId || d.arcId > segments[1]).interrupt().transition().duration(delay).attrTween('d', d => arcTween(arcSt.mini[d.arcId])(d));
  330. } else if (clickId < segments[0] || segments[1] < clickId) {
  331. inTransition = true;
  332. const clickSegments = getSegmentsInTime(clickId);
  333. labels.interrupt().transition().delay(delay).duration(delay).attrTween('transform', d => translateTween(arcSt.labels[d.arcId / numGroups])(d)).style('opacity', 1);
  334. groupLabels.interrupt().transition().duration(delay).attrTween('transform', translateTween({
  335. outerRadius: maxRadius + 20,
  336. innerRadius: maxRadius + 15,
  337. startAngle: arcSt.data[clickId].startAngle,
  338. endAngle: arcSt.data[clickId].endAngle
  339. })).style('opacity', 0);
  340. ae.classed('clickable', true);
  341. arcs.filter(d => clickSegments[0] <= d.arcId && d.arcId <= clickSegments[1]).interrupt().transition().duration(delay).attrTween('d', d => arcTween(arcSt.pieStart[d.arcId])(d)).transition().duration(delay).attrTween('d', d => arcTween(arcSt.data[d.arcId])(d)).each('end', () => {
  342. clickId = -1;
  343. inTransition = false;
  344. });
  345. arcs.filter(d => clickSegments[0] > d.arcId || d.arcId > clickSegments[1]).interrupt().transition().delay(delay).duration(delay).attrTween('d', d => arcTween(arcSt.data[d.arcId])(d));
  346. }
  347. }
  348. function updateActive() {
  349. const delay = _d.default.event.altKey ? 3000 : 300;
  350. legendWrap.datum(legendData(datum)).call(legend);
  351. const nArcSt = computeArcStates(datum);
  352. inTransition = true;
  353. if (clickId < 0) {
  354. arcs.style('opacity', 1).interrupt().transition().duration(delay).attrTween('d', d => arcTween(nArcSt.data[d.arcId])(d)).each('end', () => {
  355. inTransition = false;
  356. arcSt = nArcSt;
  357. }).transition().duration(0).style('opacity', d => state.disabled[d.arcId % numGroups] ? 0 : 1);
  358. } else {
  359. const segments = getSegmentsInTime(clickId);
  360. arcs.style('opacity', 1).interrupt().transition().duration(delay).attrTween('d', d => segments[0] <= d.arcId && d.arcId <= segments[1] ? arcTween(nArcSt.pie[d.arcId])(d) : arcTween(nArcSt.mini[d.arcId])(d)).each('end', () => {
  361. inTransition = false;
  362. arcSt = nArcSt;
  363. }).transition().duration(0).style('opacity', d => state.disabled[d.arcId % numGroups] ? 0 : 1);
  364. groupLabels.interrupt().transition().duration(delay).attrTween('transform', d => translateTween({
  365. outerRadius: maxRadius + 20,
  366. innerRadius: maxRadius + 15,
  367. startAngle: nArcSt.pie[segments[0] + d.arcId].startAngle,
  368. endAngle: nArcSt.pie[segments[0] + d.arcId].endAngle
  369. })(d)).style('opacity', d => state.disabled[d.arcId] || arcSt.pie[segments[0] + d.arcId].percent < labelThreshold ? 0 : 1);
  370. }
  371. }
  372. legend.dispatch.on('stateChange', newState => {
  373. if (state.disabled !== newState.disabled) {
  374. state.disabled = newState.disabled;
  375. updateActive();
  376. }
  377. });
  378. }
  379. Rose.displayName = 'Rose';
  380. Rose.propTypes = propTypes;
  381. var _default = Rose;
  382. exports.default = _default;