mistercrunch closed pull request #3676: [New Viz] Nightingale Rose Chart
URL: https://github.com/apache/incubator-superset/pull/3676
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/superset/assets/images/viz_thumbnails/rose.png 
b/superset/assets/images/viz_thumbnails/rose.png
new file mode 100644
index 0000000000..763fa2b120
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 2c18f4b0fa..360220d21d 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 f935a5b6c6..38ff52d23a 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 40b6592d90..4aaae3df24 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 0000000000..809df93349
--- /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 0000000000..e385f1a7eb
--- /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 e59835b60b..6fc8cf9887 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -15,6 +15,7 @@
 import inspect
 from itertools import product
 import logging
+import math
 import traceback
 import uuid
 import zlib
@@ -2184,6 +2185,32 @@ def get_data(self, df):
         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 abf29adb62..e9e8d6b9c1 100644
--- a/tests/viz_tests.py
+++ b/tests/viz_tests.py
@@ -591,3 +591,43 @@ def test_get_data_calls_correct_method(self):
         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)


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
[email protected]


With regards,
Apache Git Services

Reply via email to