mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
[SIP-5] Refactor sunburst (#5699)
* extract slice and formData * fix small issues * Update visual * update css
This commit is contained in:
parent
d7f06cbc26
commit
8d01c84c54
@ -2,33 +2,33 @@
|
|||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
.sunburst path {
|
.sunburst path {
|
||||||
stroke: #333;
|
stroke: #ddd;
|
||||||
stroke-width: 0.5px;
|
stroke-width: 0.5px;
|
||||||
}
|
}
|
||||||
.sunburst .center-label {
|
.sunburst .center-label {
|
||||||
text-anchor: middle;
|
text-anchor: middle;
|
||||||
fill: #000;
|
fill: #333;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.sunburst .path-abs-percent {
|
.sunburst .path-abs-percent {
|
||||||
font-size: 3.5em;
|
font-size: 3em;
|
||||||
font-weight: 400;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.sunburst .path-cond-percent {
|
.sunburst .path-cond-percent {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
}
|
}
|
||||||
.sunburst .path-metrics {
|
.sunburst .path-metrics {
|
||||||
font-size: 1.5em;
|
color: #777;
|
||||||
}
|
}
|
||||||
.sunburst .path-ratio {
|
.sunburst .path-ratio {
|
||||||
font-size: 1.2em;
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sunburst .breadcrumbs text {
|
.sunburst .breadcrumbs text {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
text-anchor: middle;
|
text-anchor: middle;
|
||||||
fill: #000;
|
fill: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* dashboard specific */
|
/* dashboard specific */
|
||||||
@ -36,10 +36,11 @@
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
.dashboard .sunburst .path-abs-percent {
|
.dashboard .sunburst .path-abs-percent {
|
||||||
font-size: 2.5em;
|
font-size: 2em;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.dashboard .sunburst .path-cond-percent {
|
.dashboard .sunburst .path-cond-percent {
|
||||||
font-size: 1.75em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
.dashboard .sunburst .path-metrics {
|
.dashboard .sunburst .path-metrics {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
@ -1,18 +1,58 @@
|
|||||||
/* eslint-disable no-underscore-dangle, no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import d3 from 'd3';
|
import d3 from 'd3';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { getColorFromScheme } from '../modules/colors';
|
import { getColorFromScheme } from '../modules/colors';
|
||||||
import { wrapSvgText } from '../modules/utils';
|
import { wrapSvgText } from '../modules/utils';
|
||||||
|
|
||||||
import './sunburst.css';
|
import './sunburst.css';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
// Each row is an array of [hierarchy-lvl1, hierarchy-lvl2, metric1, metric2]
|
||||||
|
// hierarchy-lvls are string. metrics are number
|
||||||
|
data: PropTypes.arrayOf(PropTypes.array),
|
||||||
|
width: PropTypes.number,
|
||||||
|
height: PropTypes.number,
|
||||||
|
colorScheme: PropTypes.string,
|
||||||
|
metrics: PropTypes.arrayOf(PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.object, // The metric object
|
||||||
|
])),
|
||||||
|
};
|
||||||
|
|
||||||
|
function metricLabel(metric) {
|
||||||
|
return ((typeof metric) === 'string' || metric instanceof String)
|
||||||
|
? metric
|
||||||
|
: metric.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a node in a partition layout, return an array of all of its ancestor
|
||||||
|
// nodes, highest first, but excluding the root.
|
||||||
|
function getAncestors(node) {
|
||||||
|
const path = [];
|
||||||
|
let current = node;
|
||||||
|
while (current.parent) {
|
||||||
|
path.unshift(current);
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
// Modified from http://bl.ocks.org/kerryrodden/7090426
|
// Modified from http://bl.ocks.org/kerryrodden/7090426
|
||||||
function sunburstVis(slice, payload) {
|
function Sunburst(element, props) {
|
||||||
const container = d3.select(slice.selector);
|
PropTypes.checkPropTypes(propTypes, props, 'prop', 'Sunburst');
|
||||||
|
|
||||||
|
const container = d3.select(element);
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
colorScheme,
|
||||||
|
metrics,
|
||||||
|
} = props;
|
||||||
|
|
||||||
// vars with shared scope within this function
|
// vars with shared scope within this function
|
||||||
const margin = { top: 10, right: 5, bottom: 10, left: 5 };
|
const margin = { top: 10, right: 5, bottom: 10, left: 5 };
|
||||||
const containerWidth = slice.width();
|
const containerWidth = width;
|
||||||
const containerHeight = slice.height();
|
const containerHeight = height;
|
||||||
const breadcrumbHeight = containerHeight * 0.085;
|
const breadcrumbHeight = containerHeight * 0.085;
|
||||||
const visWidth = containerWidth - margin.left - margin.right;
|
const visWidth = containerWidth - margin.left - margin.right;
|
||||||
const visHeight = containerHeight - margin.top - margin.bottom - breadcrumbHeight;
|
const visHeight = containerHeight - margin.top - margin.bottom - breadcrumbHeight;
|
||||||
@ -36,12 +76,8 @@ function sunburstVis(slice, payload) {
|
|||||||
const arc = d3.svg.arc()
|
const arc = d3.svg.arc()
|
||||||
.startAngle(d => d.x)
|
.startAngle(d => d.x)
|
||||||
.endAngle(d => d.x + d.dx)
|
.endAngle(d => d.x + d.dx)
|
||||||
.innerRadius(function (d) {
|
.innerRadius(d => Math.sqrt(d.y))
|
||||||
return Math.sqrt(d.y);
|
.outerRadius(d => Math.sqrt(d.y + d.dy));
|
||||||
})
|
|
||||||
.outerRadius(function (d) {
|
|
||||||
return Math.sqrt(d.y + d.dy);
|
|
||||||
});
|
|
||||||
|
|
||||||
const formatNum = d3.format('.1s');
|
const formatNum = d3.format('.1s');
|
||||||
const formatPerc = d3.format('.1p');
|
const formatPerc = d3.format('.1p');
|
||||||
@ -52,8 +88,7 @@ function sunburstVis(slice, payload) {
|
|||||||
.attr('width', containerWidth)
|
.attr('width', containerWidth)
|
||||||
.attr('height', containerHeight);
|
.attr('height', containerHeight);
|
||||||
|
|
||||||
function createBreadcrumbs(rawData) {
|
function createBreadcrumbs(firstRowData) {
|
||||||
const firstRowData = rawData.data[0];
|
|
||||||
// -2 bc row contains 2x metrics, +extra for %label and buffer
|
// -2 bc row contains 2x metrics, +extra for %label and buffer
|
||||||
maxBreadcrumbs = (firstRowData.length - 2) + 1;
|
maxBreadcrumbs = (firstRowData.length - 2) + 1;
|
||||||
breadcrumbDims = {
|
breadcrumbDims = {
|
||||||
@ -71,18 +106,6 @@ function sunburstVis(slice, payload) {
|
|||||||
.attr('class', 'end-label');
|
.attr('class', 'end-label');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Given a node in a partition layout, return an array of all of its ancestor
|
|
||||||
// nodes, highest first, but excluding the root.
|
|
||||||
function getAncestors(node) {
|
|
||||||
const path = [];
|
|
||||||
let current = node;
|
|
||||||
while (current.parent) {
|
|
||||||
path.unshift(current);
|
|
||||||
current = current.parent;
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a string that describes the points of a breadcrumb polygon.
|
// Generate a string that describes the points of a breadcrumb polygon.
|
||||||
function breadcrumbPoints(d, i) {
|
function breadcrumbPoints(d, i) {
|
||||||
const points = [];
|
const points = [];
|
||||||
@ -100,9 +123,7 @@ function sunburstVis(slice, payload) {
|
|||||||
|
|
||||||
function updateBreadcrumbs(sequenceArray, percentageString) {
|
function updateBreadcrumbs(sequenceArray, percentageString) {
|
||||||
const g = breadcrumbs.selectAll('g')
|
const g = breadcrumbs.selectAll('g')
|
||||||
.data(sequenceArray, function (d) {
|
.data(sequenceArray, d => d.name + d.depth);
|
||||||
return d.name + d.depth;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add breadcrumb and label for entering nodes.
|
// Add breadcrumb and label for entering nodes.
|
||||||
const entering = g.enter().append('svg:g');
|
const entering = g.enter().append('svg:g');
|
||||||
@ -111,7 +132,7 @@ function sunburstVis(slice, payload) {
|
|||||||
.attr('points', breadcrumbPoints)
|
.attr('points', breadcrumbPoints)
|
||||||
.style('fill', function (d) {
|
.style('fill', function (d) {
|
||||||
return colorByCategory ?
|
return colorByCategory ?
|
||||||
getColorFromScheme(d.name, slice.formData.color_scheme) :
|
getColorFromScheme(d.name, colorScheme) :
|
||||||
colorScale(d.m2 / d.m1);
|
colorScale(d.m2 / d.m1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -122,7 +143,7 @@ function sunburstVis(slice, payload) {
|
|||||||
.style('fill', function (d) {
|
.style('fill', function (d) {
|
||||||
// Make text white or black based on the lightness of the background
|
// Make text white or black based on the lightness of the background
|
||||||
const col = d3.hsl(colorByCategory ?
|
const col = d3.hsl(colorByCategory ?
|
||||||
getColorFromScheme(d.name, slice.formData.color_scheme) :
|
getColorFromScheme(d.name, colorScheme) :
|
||||||
colorScale(d.m2 / d.m1));
|
colorScale(d.m2 / d.m1));
|
||||||
return col.l < 0.5 ? 'white' : 'black';
|
return col.l < 0.5 ? 'white' : 'black';
|
||||||
})
|
})
|
||||||
@ -166,6 +187,7 @@ function sunburstVis(slice, payload) {
|
|||||||
|
|
||||||
// If metrics match, assume we are coloring by category
|
// If metrics match, assume we are coloring by category
|
||||||
const metricsMatch = Math.abs(d.m1 - d.m2) < 0.00001;
|
const metricsMatch = Math.abs(d.m1 - d.m2) < 0.00001;
|
||||||
|
console.log('metrics', metrics);
|
||||||
|
|
||||||
gMiddleText.selectAll('*').remove();
|
gMiddleText.selectAll('*').remove();
|
||||||
|
|
||||||
@ -184,27 +206,24 @@ function sunburstVis(slice, payload) {
|
|||||||
gMiddleText.append('text')
|
gMiddleText.append('text')
|
||||||
.attr('class', 'path-metrics')
|
.attr('class', 'path-metrics')
|
||||||
.attr('y', yOffsets[offsetIndex++])
|
.attr('y', yOffsets[offsetIndex++])
|
||||||
.text('m1: ' + formatNum(d.m1) + (metricsMatch ? '' : ', m2: ' + formatNum(d.m2)));
|
.text(`${metricLabel(metrics[0])}: ${formatNum(d.m1)}` + (metricsMatch ? '' : `, ${metricLabel(metrics[1])}: ${formatNum(d.m2)}`));
|
||||||
|
|
||||||
gMiddleText.append('text')
|
gMiddleText.append('text')
|
||||||
.attr('class', 'path-ratio')
|
.attr('class', 'path-ratio')
|
||||||
.attr('y', yOffsets[offsetIndex++])
|
.attr('y', yOffsets[offsetIndex++])
|
||||||
.text((metricsMatch ? '' : ('m2/m1: ' + formatPerc(d.m2 / d.m1))));
|
.text((metricsMatch ? '' : (`${metricLabel(metrics[1])}/${metricLabel(metrics[0])}: ${formatPerc(d.m2 / d.m1)}`)));
|
||||||
|
|
||||||
// Reset and fade all the segments.
|
// Reset and fade all the segments.
|
||||||
arcs.selectAll('path')
|
arcs.selectAll('path')
|
||||||
.style('stroke-width', null)
|
.style('stroke-width', null)
|
||||||
.style('stroke', null)
|
.style('stroke', null)
|
||||||
.style('opacity', 0.7);
|
.style('opacity', 0.3);
|
||||||
|
|
||||||
// Then highlight only those that are an ancestor of the current segment.
|
// Then highlight only those that are an ancestor of the current segment.
|
||||||
arcs.selectAll('path')
|
arcs.selectAll('path')
|
||||||
.filter(function (node) {
|
.filter(node => (sequenceArray.indexOf(node) >= 0))
|
||||||
return (sequenceArray.indexOf(node) >= 0);
|
|
||||||
})
|
|
||||||
.style('opacity', 1)
|
.style('opacity', 1)
|
||||||
.style('stroke-width', '2px')
|
.style('stroke', '#aaa');
|
||||||
.style('stroke', '#000');
|
|
||||||
|
|
||||||
updateBreadcrumbs(sequenceArray, absolutePercString);
|
updateBreadcrumbs(sequenceArray, absolutePercString);
|
||||||
}
|
}
|
||||||
@ -244,7 +263,7 @@ function sunburstVis(slice, payload) {
|
|||||||
const m1 = Number(row[row.length - 2]);
|
const m1 = Number(row[row.length - 2]);
|
||||||
const m2 = Number(row[row.length - 1]);
|
const m2 = Number(row[row.length - 1]);
|
||||||
const levels = row.slice(0, row.length - 2);
|
const levels = row.slice(0, row.length - 2);
|
||||||
if (isNaN(m1)) { // e.g. if this is a header row
|
if (Number.isNaN(m1)) { // e.g. if this is a header row
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let currentNode = root;
|
let currentNode = root;
|
||||||
@ -263,8 +282,7 @@ function sunburstVis(slice, payload) {
|
|||||||
currChild = children[k];
|
currChild = children[k];
|
||||||
if (currChild.name === nodeName &&
|
if (currChild.name === nodeName &&
|
||||||
currChild.level === level) {
|
currChild.level === level) {
|
||||||
// must match name AND level
|
// must match name AND level
|
||||||
|
|
||||||
childNode = currChild;
|
childNode = currChild;
|
||||||
foundChild = true;
|
foundChild = true;
|
||||||
break;
|
break;
|
||||||
@ -313,8 +331,8 @@ function sunburstVis(slice, payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Main function to draw and set up the visualization, once we have the data.
|
// Main function to draw and set up the visualization, once we have the data.
|
||||||
function createVisualization(rawData) {
|
function createVisualization(rows) {
|
||||||
const tree = buildHierarchy(rawData.data);
|
const root = buildHierarchy(rows);
|
||||||
|
|
||||||
vis = svg.append('svg:g')
|
vis = svg.append('svg:g')
|
||||||
.attr('class', 'sunburst-vis')
|
.attr('class', 'sunburst-vis')
|
||||||
@ -339,15 +357,12 @@ function sunburstVis(slice, payload) {
|
|||||||
.style('opacity', 0);
|
.style('opacity', 0);
|
||||||
|
|
||||||
// For efficiency, filter nodes to keep only those large enough to see.
|
// For efficiency, filter nodes to keep only those large enough to see.
|
||||||
const nodes = partition.nodes(tree)
|
const nodes = partition.nodes(root)
|
||||||
.filter(function (d) {
|
.filter(d => d.dx > 0.005); // 0.005 radians = 0.29 degrees
|
||||||
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
|
|
||||||
});
|
|
||||||
|
|
||||||
let ext;
|
let ext;
|
||||||
const fd = slice.formData;
|
|
||||||
|
|
||||||
if (fd.metric !== fd.secondary_metric && fd.secondary_metric) {
|
if (metrics[0] !== metrics[1] && metrics[1]) {
|
||||||
colorByCategory = false;
|
colorByCategory = false;
|
||||||
ext = d3.extent(nodes, d => d.m2 / d.m1);
|
ext = d3.extent(nodes, d => d.m2 / d.m1);
|
||||||
colorScale = d3.scale.linear()
|
colorScale = d3.scale.linear()
|
||||||
@ -355,26 +370,40 @@ function sunburstVis(slice, payload) {
|
|||||||
.range(['#00D1C1', 'white', '#FFB400']);
|
.range(['#00D1C1', 'white', '#FFB400']);
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = arcs.data([tree]).selectAll('path')
|
arcs.selectAll('path')
|
||||||
.data(nodes)
|
.data(nodes)
|
||||||
.enter()
|
.enter()
|
||||||
.append('svg:path')
|
.append('svg:path')
|
||||||
.attr('display', function (d) {
|
.attr('display', d => d.depth ? null : 'none')
|
||||||
return d.depth ? null : 'none';
|
.attr('d', arc)
|
||||||
})
|
.attr('fill-rule', 'evenodd')
|
||||||
.attr('d', arc)
|
.style('fill', d => colorByCategory
|
||||||
.attr('fill-rule', 'evenodd')
|
? getColorFromScheme(d.name, colorScheme)
|
||||||
.style('fill', d => colorByCategory ?
|
: colorScale(d.m2 / d.m1))
|
||||||
getColorFromScheme(d.name, fd.color_scheme) :
|
.style('opacity', 1)
|
||||||
colorScale(d.m2 / d.m1))
|
.on('mouseenter', mouseenter);
|
||||||
.style('opacity', 1)
|
|
||||||
.on('mouseenter', mouseenter);
|
|
||||||
|
|
||||||
// Get total size of the tree = value of root node from partition.
|
// Get total size of the tree = value of root node from partition.
|
||||||
totalSize = path.node().__data__.value;
|
totalSize = root.value;
|
||||||
}
|
}
|
||||||
createBreadcrumbs(payload);
|
createBreadcrumbs(data[0]);
|
||||||
createVisualization(payload);
|
createVisualization(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = sunburstVis;
|
Sunburst.propTypes = propTypes;
|
||||||
|
|
||||||
|
function adaptor(slice, payload) {
|
||||||
|
const { selector, formData } = slice;
|
||||||
|
const { color_scheme: colorScheme, metric, secondary_metric: secondaryMetric } = formData;
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
|
||||||
|
return Sunburst(element, {
|
||||||
|
data: payload.data,
|
||||||
|
width: slice.width(),
|
||||||
|
height: slice.height(),
|
||||||
|
colorScheme,
|
||||||
|
metrics: [metric, secondaryMetric],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default adaptor;
|
||||||
|
Loading…
Reference in New Issue
Block a user