mirror of
https://github.com/apache/superset.git
synced 2026-04-20 16:44:46 +00:00
[New Viz] Nightingale Rose Chart (#3676)
* Nightingale Rose Chart * Review comments
This commit is contained in:
committed by
Maxime Beauchemin
parent
a616bf4082
commit
fdd42ef4b6
BIN
superset/assets/images/viz_thumbnails/rose.png
Normal file
BIN
superset/assets/images/viz_thumbnails/rose.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 494 KiB |
@@ -1746,6 +1746,17 @@ export const controls = {
|
||||
controlName: 'TimeSeriesColumnControl',
|
||||
},
|
||||
|
||||
rose_area_proportion: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Use Area Proportions'),
|
||||
description: t(
|
||||
'Check if the Rose Chart should use segment area instead of ' +
|
||||
'segment radius for proportioning',
|
||||
),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
},
|
||||
|
||||
time_series_option: {
|
||||
type: 'SelectControl',
|
||||
label: t('Options'),
|
||||
|
||||
@@ -1541,6 +1541,25 @@ export const visTypes = {
|
||||
],
|
||||
},
|
||||
|
||||
rose: {
|
||||
label: t('Time Series - Nightingale Rose Chart'),
|
||||
showOnExplore: true,
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
sections.NVD3TimeSeries[0],
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: false,
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
['number_format', 'date_time_format'],
|
||||
['rich_tooltip', 'rose_area_proportion'],
|
||||
],
|
||||
},
|
||||
sections.NVD3TimeSeries[1],
|
||||
],
|
||||
},
|
||||
|
||||
partition: {
|
||||
label: 'Partition Diagram',
|
||||
showOnExplore: true,
|
||||
|
||||
@@ -48,6 +48,7 @@ export const VIZ_TYPES = {
|
||||
deck_multi: 'deck_multi',
|
||||
deck_arc: 'deck_arc',
|
||||
deck_polygon: 'deck_polygon',
|
||||
rose: 'rose',
|
||||
};
|
||||
|
||||
const vizMap = {
|
||||
@@ -97,5 +98,6 @@ const vizMap = {
|
||||
[VIZ_TYPES.deck_arc]: require('./deckgl/layers/arc.jsx').default,
|
||||
[VIZ_TYPES.deck_polygon]: require('./deckgl/layers/polygon.jsx').default,
|
||||
[VIZ_TYPES.deck_multi]: require('./deckgl/multi.jsx'),
|
||||
[VIZ_TYPES.rose]: require('./rose.js'),
|
||||
};
|
||||
export default vizMap;
|
||||
|
||||
24
superset/assets/visualizations/rose.css
Normal file
24
superset/assets/visualizations/rose.css
Normal file
@@ -0,0 +1,24 @@
|
||||
.rose path {
|
||||
transition: fill-opacity 180ms linear;
|
||||
stroke: #fff;
|
||||
stroke-width: 1px;
|
||||
stroke-opacity: 1;
|
||||
fill-opacity: 0.75;
|
||||
}
|
||||
|
||||
.rose text {
|
||||
font: 400 12px Arial, sans-serif;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rose .clickable path {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rose .hover path {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.nv-legend .nv-series {
|
||||
cursor: pointer;
|
||||
}
|
||||
540
superset/assets/visualizations/rose.js
Normal file
540
superset/assets/visualizations/rose.js
Normal file
@@ -0,0 +1,540 @@
|
||||
/* eslint no-use-before-define: ["error", { "functions": false }] */
|
||||
import d3 from 'd3';
|
||||
import nv from 'nvd3';
|
||||
import { d3TimeFormatPreset } from '../javascripts/modules/utils';
|
||||
import { getColorFromScheme } from '../javascripts/modules/colors';
|
||||
|
||||
import './rose.css';
|
||||
|
||||
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 roseVis(slice, payload) {
|
||||
const data = payload.data;
|
||||
const fd = slice.formData;
|
||||
const div = d3.select(slice.selector);
|
||||
|
||||
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 = d3.format(fd.number_format);
|
||||
const timeFormat = d3TimeFormatPreset(fd.date_time_format);
|
||||
|
||||
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 color = name => getColorFromScheme(name, fd.color_scheme);
|
||||
|
||||
const svg = div
|
||||
.append('svg')
|
||||
.attr('width', slice.width())
|
||||
.attr('height', slice.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 = fd.rich_tooltip ?
|
||||
adatum[times[timeIndex]]
|
||||
.filter(v => !state.disabled[v.id % numGroups])
|
||||
.map(v => ({
|
||||
key: v.name,
|
||||
value: v.value,
|
||||
color: color(v.name),
|
||||
highlight: v.id === d.arcId,
|
||||
})) : [{ key: d.name, value: d.val, color: color(d.name) }];
|
||||
return {
|
||||
key: 'Date',
|
||||
value: d.time,
|
||||
series,
|
||||
};
|
||||
}
|
||||
|
||||
legend
|
||||
.width(slice.width())
|
||||
.color(d => getColorFromScheme(d.key, fd.color_scheme));
|
||||
legendWrap
|
||||
.datum(legendData(datum))
|
||||
.call(legend);
|
||||
|
||||
tooltip
|
||||
.headerFormatter(timeFormat)
|
||||
.valueFormatter(format);
|
||||
|
||||
// Compute max radius, which the largest value will occupy
|
||||
const width = slice.width();
|
||||
const height = slice.height() - legend.height();
|
||||
const margin = { top: legend.height() };
|
||||
const edgeMargin = 35; // space between outermost radius and slice edge
|
||||
const maxRadius = Math.min(width, height) / 2 - edgeMargin;
|
||||
const labelThreshold = 0.05;
|
||||
const gro = 8; // mouseover radius growth in pixels
|
||||
const mini = 0.075;
|
||||
|
||||
const centerTranslate = `translate(${width / 2},${height / 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) => fd.rose_area_proportion ?
|
||||
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 = v.name;
|
||||
const time = v.time;
|
||||
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 = Object.assign({}, 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 (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 => color(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', function (newState) {
|
||||
if (state.disabled !== newState.disabled) {
|
||||
state.disabled = newState.disabled;
|
||||
updateActive();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = roseVis;
|
||||
@@ -15,6 +15,7 @@ import hashlib
|
||||
import inspect
|
||||
from itertools import product
|
||||
import logging
|
||||
import math
|
||||
import traceback
|
||||
import uuid
|
||||
import zlib
|
||||
@@ -2184,6 +2185,32 @@ class PairedTTestViz(BaseViz):
|
||||
return data
|
||||
|
||||
|
||||
class RoseViz(NVD3TimeSeriesViz):
|
||||
|
||||
viz_type = 'rose'
|
||||
verbose_name = _('Time Series - Nightingale Rose Chart')
|
||||
sort_series = False
|
||||
is_timeseries = True
|
||||
|
||||
def get_data(self, df):
|
||||
data = super(RoseViz, self).get_data(df)
|
||||
result = {}
|
||||
for datum in data:
|
||||
key = datum['key']
|
||||
for val in datum['values']:
|
||||
timestamp = val['x'].value
|
||||
if not result.get(timestamp):
|
||||
result[timestamp] = []
|
||||
value = 0 if math.isnan(val['y']) else val['y']
|
||||
result[timestamp].append({
|
||||
'key': key,
|
||||
'value': value,
|
||||
'name': ', '.join(key) if isinstance(key, list) else key,
|
||||
'time': val['x'],
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
class PartitionViz(NVD3TimeSeriesViz):
|
||||
|
||||
"""
|
||||
|
||||
@@ -591,3 +591,43 @@ class PartitionVizTestCase(unittest.TestCase):
|
||||
test_viz.get_data(df)
|
||||
self.assertEqual('agg_sum', test_viz.levels_for.mock_calls[3][1][0])
|
||||
self.assertEqual(7, len(test_viz.nest_values.mock_calls))
|
||||
|
||||
|
||||
class RoseVisTestCase(unittest.TestCase):
|
||||
|
||||
def test_rose_vis_get_data(self):
|
||||
raw = {}
|
||||
t1 = pd.Timestamp('2000')
|
||||
t2 = pd.Timestamp('2002')
|
||||
t3 = pd.Timestamp('2004')
|
||||
raw[DTTM_ALIAS] = [t1, t2, t3, t1, t2, t3, t1, t2, t3]
|
||||
raw['groupA'] = ['a1', 'a1', 'a1', 'b1', 'b1', 'b1', 'c1', 'c1', 'c1']
|
||||
raw['groupB'] = ['a2', 'a2', 'a2', 'b2', 'b2', 'b2', 'c2', 'c2', 'c2']
|
||||
raw['groupC'] = ['a3', 'a3', 'a3', 'b3', 'b3', 'b3', 'c3', 'c3', 'c3']
|
||||
raw['metric1'] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
df = pd.DataFrame(raw)
|
||||
fd = {
|
||||
'metrics': ['metric1'],
|
||||
'groupby': ['groupA'],
|
||||
}
|
||||
test_viz = viz.RoseViz(Mock(), fd)
|
||||
test_viz.metrics = fd['metrics']
|
||||
res = test_viz.get_data(df)
|
||||
expected = {
|
||||
946684800000000000: [
|
||||
{'time': t1, 'value': 1, 'key': ('a1',), 'name': ('a1',)},
|
||||
{'time': t1, 'value': 4, 'key': ('b1',), 'name': ('b1',)},
|
||||
{'time': t1, 'value': 7, 'key': ('c1',), 'name': ('c1',)},
|
||||
],
|
||||
1009843200000000000: [
|
||||
{'time': t2, 'value': 2, 'key': ('a1',), 'name': ('a1',)},
|
||||
{'time': t2, 'value': 5, 'key': ('b1',), 'name': ('b1',)},
|
||||
{'time': t2, 'value': 8, 'key': ('c1',), 'name': ('c1',)},
|
||||
],
|
||||
1072915200000000000: [
|
||||
{'time': t3, 'value': 3, 'key': ('a1',), 'name': ('a1',)},
|
||||
{'time': t3, 'value': 6, 'key': ('b1',), 'name': ('b1',)},
|
||||
{'time': t3, 'value': 9, 'key': ('c1',), 'name': ('c1',)},
|
||||
],
|
||||
}
|
||||
self.assertEqual(expected, res)
|
||||
|
||||
Reference in New Issue
Block a user