superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From maximebeauche...@apache.org
Subject [incubator-superset] branch master updated: [New Viz] Nightingale Rose Chart (#3676)
Date Sun, 04 Feb 2018 04:18:26 GMT
This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new fdd42ef  [New Viz] Nightingale Rose Chart (#3676)
fdd42ef is described below

commit fdd42ef4b62c931f91febe242c12986d1780bf0d
Author: Jeff Niu <jeffniu22@gmail.com>
AuthorDate: Sat Feb 3 23:18:24 2018 -0500

    [New Viz] Nightingale Rose Chart (#3676)
    
    * Nightingale Rose Chart
    
    * Review comments
---
 superset/assets/images/viz_thumbnails/rose.png     | Bin 0 -> 506254 bytes
 .../assets/javascripts/explore/stores/controls.jsx |  11 +
 .../assets/javascripts/explore/stores/visTypes.js  |  19 +
 superset/assets/visualizations/main.js             |   2 +
 superset/assets/visualizations/rose.css            |  24 +
 superset/assets/visualizations/rose.js             | 540 +++++++++++++++++++++
 superset/viz.py                                    |  27 ++
 tests/viz_tests.py                                 |  40 ++
 8 files changed, 663 insertions(+)

diff --git a/superset/assets/images/viz_thumbnails/rose.png b/superset/assets/images/viz_thumbnails/rose.png
new file mode 100644
index 0000000..763fa2b
Binary files /dev/null and b/superset/assets/images/viz_thumbnails/rose.png differ
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index 2c18f4b..360220d 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -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'),
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js
index f935a5b..38ff52d 100644
--- a/superset/assets/javascripts/explore/stores/visTypes.js
+++ b/superset/assets/javascripts/explore/stores/visTypes.js
@@ -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,
diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js
index 40b6592..4aaae3d 100644
--- a/superset/assets/visualizations/main.js
+++ b/superset/assets/visualizations/main.js
@@ -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;
diff --git a/superset/assets/visualizations/rose.css b/superset/assets/visualizations/rose.css
new file mode 100644
index 0000000..809df93
--- /dev/null
+++ b/superset/assets/visualizations/rose.css
@@ -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;
+}
diff --git a/superset/assets/visualizations/rose.js b/superset/assets/visualizations/rose.js
new file mode 100644
index 0000000..e385f1a
--- /dev/null
+++ b/superset/assets/visualizations/rose.js
@@ -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;
diff --git a/superset/viz.py b/superset/viz.py
index e59835b..6fc8cf9 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -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):
 
     """
diff --git a/tests/viz_tests.py b/tests/viz_tests.py
index abf29ad..e9e8d6b 100644
--- a/tests/viz_tests.py
+++ b/tests/viz_tests.py
@@ -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)

-- 
To stop receiving notification emails like this one, please contact
maximebeauchemin@apache.org.

Mime
View raw message