123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454 |
- 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); }
- /**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
- /* eslint no-use-before-define: ["error", { "functions": false }] */
- /* eslint-disable no-restricted-syntax, no-plusplus */
- /* eslint-disable react/sort-prop-types */
- import d3 from 'd3';
- import PropTypes from 'prop-types';
- import nv from 'nvd3';
- import { CategoricalColorNamespace } from '@superset-ui/color';
- import { getNumberFormatter } from '@superset-ui/number-format';
- import { getTimeFormatter } from '@superset-ui/time-format';
- import './Rose.css';
- const propTypes = {
- // Data is an object hashed by numeric value, perhaps timestamp
- data: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.shape({
- key: PropTypes.arrayOf(PropTypes.string),
- name: PropTypes.arrayOf(PropTypes.string),
- time: PropTypes.number,
- value: PropTypes.number
- }))),
- width: PropTypes.number,
- height: PropTypes.number,
- dateTimeFormat: PropTypes.string,
- numberFormat: PropTypes.string,
- useRichTooltip: PropTypes.bool,
- useAreaProportions: PropTypes.bool
- };
- function copyArc(d) {
- return {
- startAngle: d.startAngle,
- endAngle: d.endAngle,
- innerRadius: d.innerRadius,
- outerRadius: d.outerRadius
- };
- }
- function sortValues(a, b) {
- if (a.value === b.value) {
- return a.name > b.name ? 1 : -1;
- }
- return b.value - a.value;
- }
- function Rose(element, props) {
- const {
- data,
- width,
- height,
- colorScheme,
- dateTimeFormat,
- numberFormat,
- useRichTooltip,
- useAreaProportions
- } = props;
- const div = d3.select(element);
- div.classed('superset-legacy-chart-rose', true);
- const datum = data;
- const times = Object.keys(datum).map(t => parseInt(t, 10)).sort((a, b) => a - b);
- const numGrains = times.length;
- const numGroups = datum[times[0]].length;
- const format = getNumberFormatter(numberFormat);
- const timeFormat = getTimeFormatter(dateTimeFormat);
- const colorFn = CategoricalColorNamespace.getScale(colorScheme);
- d3.select('.nvtooltip').remove();
- div.selectAll('*').remove();
- const arc = d3.svg.arc();
- const legend = nv.models.legend();
- const tooltip = nv.models.tooltip();
- const state = {
- disabled: datum[times[0]].map(() => false)
- };
- const svg = div.append('svg').attr('width', width).attr('height', height);
- const g = svg.append('g').attr('class', 'rose').append('g');
- const legendWrap = g.append('g').attr('class', 'legendWrap');
- function legendData(adatum) {
- return adatum[times[0]].map((v, i) => ({
- disabled: state.disabled[i],
- key: v.name
- }));
- }
- function tooltipData(d, i, adatum) {
- const timeIndex = Math.floor(d.arcId / numGroups);
- const series = useRichTooltip ? adatum[times[timeIndex]].filter(v => !state.disabled[v.id % numGroups]).map(v => ({
- key: v.name,
- value: v.value,
- color: colorFn(v.name),
- highlight: v.id === d.arcId
- })) : [{
- key: d.name,
- value: d.val,
- color: colorFn(d.name)
- }];
- return {
- key: 'Date',
- value: d.time,
- series
- };
- }
- legend.width(width).color(d => colorFn(d.key));
- legendWrap.datum(legendData(datum)).call(legend);
- tooltip.headerFormatter(timeFormat).valueFormatter(format); // Compute max radius, which the largest value will occupy
- const roseHeight = height - legend.height();
- const margin = {
- top: legend.height()
- };
- const edgeMargin = 35; // space between outermost radius and slice edge
- const maxRadius = Math.min(width, roseHeight) / 2 - edgeMargin;
- const labelThreshold = 0.05;
- const gro = 8; // mouseover radius growth in pixels
- const mini = 0.075;
- const centerTranslate = "translate(" + width / 2 + "," + (roseHeight / 2 + margin.top) + ")";
- const roseWrap = g.append('g').attr('transform', centerTranslate).attr('class', 'roseWrap');
- const labelsWrap = g.append('g').attr('transform', centerTranslate).attr('class', 'labelsWrap');
- const groupLabelsWrap = g.append('g').attr('transform', centerTranslate).attr('class', 'groupLabelsWrap'); // Compute inner and outer angles for each data point
- function computeArcStates(adatum) {
- // Find the max sum of values across all time
- let maxSum = 0;
- let grain = 0;
- const sums = [];
- for (const t of times) {
- const sum = datum[t].reduce((a, v, i) => a + (state.disabled[i] ? 0 : v.value), 0);
- maxSum = sum > maxSum ? sum : maxSum;
- sums[grain] = sum;
- grain++;
- } // Compute angle occupied by each time grain
- const dtheta = Math.PI * 2 / numGrains;
- const angles = [];
- for (let i = 0; i <= numGrains; i++) {
- angles.push(dtheta * i - Math.PI / 2);
- } // Compute proportion
- const P = maxRadius / maxSum;
- const Q = P * maxRadius;
- const computeOuterRadius = (value, innerRadius) => useAreaProportions ? Math.sqrt(Q * value + innerRadius * innerRadius) : P * value + innerRadius;
- const arcSt = {
- data: [],
- extend: {},
- push: {},
- pieStart: {},
- pie: {},
- pieOver: {},
- mini: {},
- labels: [],
- groupLabels: []
- };
- let arcId = 0;
- for (let i = 0; i < numGrains; i++) {
- const t = times[i];
- const startAngle = angles[i];
- const endAngle = angles[i + 1];
- const G = 2 * Math.PI / sums[i];
- let innerRadius = 0;
- let outerRadius;
- let pieStartAngle = 0;
- let pieEndAngle;
- for (const v of adatum[t]) {
- const val = state.disabled[arcId % numGroups] ? 0 : v.value;
- const {
- name,
- time
- } = v;
- v.id = arcId;
- outerRadius = computeOuterRadius(val, innerRadius);
- arcSt.data.push({
- startAngle,
- endAngle,
- innerRadius,
- outerRadius,
- name,
- arcId,
- val,
- time
- });
- arcSt.extend[arcId] = {
- startAngle,
- endAngle,
- innerRadius,
- name,
- outerRadius: outerRadius + gro
- };
- arcSt.push[arcId] = {
- startAngle,
- endAngle,
- innerRadius: innerRadius + gro,
- outerRadius: outerRadius + gro
- };
- arcSt.pieStart[arcId] = {
- startAngle,
- endAngle,
- innerRadius: mini * maxRadius,
- outerRadius: maxRadius
- };
- arcSt.mini[arcId] = {
- startAngle,
- endAngle,
- innerRadius: innerRadius * mini,
- outerRadius: outerRadius * mini
- };
- arcId++;
- innerRadius = outerRadius;
- }
- const labelArc = _extends({}, arcSt.data[i * numGroups]);
- labelArc.outerRadius = maxRadius + 20;
- labelArc.innerRadius = maxRadius + 15;
- arcSt.labels.push(labelArc);
- for (const v of adatum[t].concat().sort(sortValues)) {
- const val = state.disabled[v.id % numGroups] ? 0 : v.value;
- pieEndAngle = G * val + pieStartAngle;
- arcSt.pie[v.id] = {
- startAngle: pieStartAngle,
- endAngle: pieEndAngle,
- innerRadius: maxRadius * mini,
- outerRadius: maxRadius,
- percent: v.value / sums[i]
- };
- arcSt.pieOver[v.id] = {
- startAngle: pieStartAngle,
- endAngle: pieEndAngle,
- innerRadius: maxRadius * mini,
- outerRadius: maxRadius + gro
- };
- pieStartAngle = pieEndAngle;
- }
- }
- arcSt.groupLabels = arcSt.data.slice(0, numGroups);
- return arcSt;
- }
- let arcSt = computeArcStates(datum);
- function tween(target, resFunc) {
- return function doTween(d) {
- const interpolate = d3.interpolate(copyArc(d), copyArc(target));
- return t => resFunc(Object.assign(d, interpolate(t)));
- };
- }
- function arcTween(target) {
- return tween(target, d => arc(d));
- }
- function translateTween(target) {
- return tween(target, d => "translate(" + arc.centroid(d) + ")");
- } // Grab the ID range of segments stand between
- // this segment and the edge of the circle
- const segmentsToEdgeCache = {};
- function getSegmentsToEdge(arcId) {
- if (segmentsToEdgeCache[arcId]) {
- return segmentsToEdgeCache[arcId];
- }
- const timeIndex = Math.floor(arcId / numGroups);
- segmentsToEdgeCache[arcId] = [arcId + 1, numGroups * (timeIndex + 1) - 1];
- return segmentsToEdgeCache[arcId];
- } // Get the IDs of all segments in a timeIndex
- const segmentsInTimeCache = {};
- function getSegmentsInTime(arcId) {
- if (segmentsInTimeCache[arcId]) {
- return segmentsInTimeCache[arcId];
- }
- const timeIndex = Math.floor(arcId / numGroups);
- segmentsInTimeCache[arcId] = [timeIndex * numGroups, (timeIndex + 1) * numGroups - 1];
- return segmentsInTimeCache[arcId];
- }
- let clickId = -1;
- let inTransition = false;
- const ae = roseWrap.selectAll('g').data(JSON.parse(JSON.stringify(arcSt.data))) // deep copy data state
- .enter().append('g').attr('class', 'segment').classed('clickable', true).on('mouseover', mouseover).on('mouseout', mouseout).on('mousemove', mousemove).on('click', click);
- 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) + ")");
- labels.append('text').style('text-anchor', 'middle').style('fill', '#000').text(d => timeFormat(d.time));
- const groupLabels = groupLabelsWrap.selectAll('g').data(JSON.parse(JSON.stringify(arcSt.groupLabels))).enter().append('g');
- groupLabels.style('opacity', 0).attr('class', 'roseGroupLabels').append('text').style('text-anchor', 'middle').style('fill', '#000').text(d => d.name);
- const arcs = ae.append('path').attr('class', 'arc').attr('fill', d => colorFn(d.name)).attr('d', arc);
- function mousemove() {
- tooltip();
- }
- function mouseover(b, i) {
- tooltip.data(tooltipData(b, i, datum)).hidden(false);
- const $this = d3.select(this);
- $this.classed('hover', true);
- if (clickId < 0 && !inTransition) {
- $this.select('path').interrupt().transition().duration(180).attrTween('d', arcTween(arcSt.extend[i]));
- const edge = getSegmentsToEdge(i);
- arcs.filter(d => edge[0] <= d.arcId && d.arcId <= edge[1]).interrupt().transition().duration(180).attrTween('d', d => arcTween(arcSt.push[d.arcId])(d));
- } else if (!inTransition) {
- const segments = getSegmentsInTime(clickId);
- if (segments[0] <= b.arcId && b.arcId <= segments[1]) {
- $this.select('path').interrupt().transition().duration(180).attrTween('d', arcTween(arcSt.pieOver[i]));
- }
- }
- }
- function mouseout(b, i) {
- tooltip.hidden(true);
- const $this = d3.select(this);
- $this.classed('hover', false);
- if (clickId < 0 && !inTransition) {
- $this.select('path').interrupt().transition().duration(180).attrTween('d', arcTween(arcSt.data[i]));
- const edge = getSegmentsToEdge(i);
- arcs.filter(d => edge[0] <= d.arcId && d.arcId <= edge[1]).interrupt().transition().duration(180).attrTween('d', d => arcTween(arcSt.data[d.arcId])(d));
- } else if (!inTransition) {
- const segments = getSegmentsInTime(clickId);
- if (segments[0] <= b.arcId && b.arcId <= segments[1]) {
- $this.select('path').interrupt().transition().duration(180).attrTween('d', arcTween(arcSt.pie[i]));
- }
- }
- }
- function click(b, i) {
- if (inTransition) {
- return;
- }
- const delay = d3.event.altKey ? 3750 : 375;
- const segments = getSegmentsInTime(i);
- if (clickId < 0) {
- inTransition = true;
- clickId = i;
- labels.interrupt().transition().duration(delay).attrTween('transform', d => translateTween({
- outerRadius: 0,
- innerRadius: 0,
- startAngle: d.startAngle,
- endAngle: d.endAngle
- })(d)).style('opacity', 0);
- groupLabels.attr('transform', "translate(" + arc.centroid({
- outerRadius: maxRadius + 20,
- innerRadius: maxRadius + 15,
- startAngle: arcSt.data[i].startAngle,
- endAngle: arcSt.data[i].endAngle
- }) + ")").interrupt().transition().delay(delay).duration(delay).attrTween('transform', d => translateTween({
- outerRadius: maxRadius + 20,
- innerRadius: maxRadius + 15,
- startAngle: arcSt.pie[segments[0] + d.arcId].startAngle,
- endAngle: arcSt.pie[segments[0] + d.arcId].endAngle
- })(d)).style('opacity', d => state.disabled[d.arcId] || arcSt.pie[segments[0] + d.arcId].percent < labelThreshold ? 0 : 1);
- ae.classed('clickable', d => segments[0] > d.arcId || d.arcId > segments[1]);
- 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', () => {
- inTransition = false;
- });
- arcs.filter(d => segments[0] > d.arcId || d.arcId > segments[1]).interrupt().transition().duration(delay).attrTween('d', d => arcTween(arcSt.mini[d.arcId])(d));
- } else if (clickId < segments[0] || segments[1] < clickId) {
- inTransition = true;
- const clickSegments = getSegmentsInTime(clickId);
- labels.interrupt().transition().delay(delay).duration(delay).attrTween('transform', d => translateTween(arcSt.labels[d.arcId / numGroups])(d)).style('opacity', 1);
- groupLabels.interrupt().transition().duration(delay).attrTween('transform', translateTween({
- outerRadius: maxRadius + 20,
- innerRadius: maxRadius + 15,
- startAngle: arcSt.data[clickId].startAngle,
- endAngle: arcSt.data[clickId].endAngle
- })).style('opacity', 0);
- ae.classed('clickable', true);
- 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', () => {
- clickId = -1;
- inTransition = false;
- });
- 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));
- }
- }
- function updateActive() {
- const delay = d3.event.altKey ? 3000 : 300;
- legendWrap.datum(legendData(datum)).call(legend);
- const nArcSt = computeArcStates(datum);
- inTransition = true;
- if (clickId < 0) {
- arcs.style('opacity', 1).interrupt().transition().duration(delay).attrTween('d', d => arcTween(nArcSt.data[d.arcId])(d)).each('end', () => {
- inTransition = false;
- arcSt = nArcSt;
- }).transition().duration(0).style('opacity', d => state.disabled[d.arcId % numGroups] ? 0 : 1);
- } else {
- const segments = getSegmentsInTime(clickId);
- 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', () => {
- inTransition = false;
- arcSt = nArcSt;
- }).transition().duration(0).style('opacity', d => state.disabled[d.arcId % numGroups] ? 0 : 1);
- groupLabels.interrupt().transition().duration(delay).attrTween('transform', d => translateTween({
- outerRadius: maxRadius + 20,
- innerRadius: maxRadius + 15,
- startAngle: nArcSt.pie[segments[0] + d.arcId].startAngle,
- endAngle: nArcSt.pie[segments[0] + d.arcId].endAngle
- })(d)).style('opacity', d => state.disabled[d.arcId] || arcSt.pie[segments[0] + d.arcId].percent < labelThreshold ? 0 : 1);
- }
- }
- legend.dispatch.on('stateChange', newState => {
- if (state.disabled !== newState.disabled) {
- state.disabled = newState.disabled;
- updateActive();
- }
- });
- }
- Rose.displayName = 'Rose';
- Rose.propTypes = propTypes;
- export default Rose;
|