mirror of
https://github.com/apache/superset.git
synced 2024-09-12 08:39:45 -04:00
Add rose, sankey, sunburst.
This commit is contained in:
parent
9f91454988
commit
7586951f16
@ -0,0 +1,34 @@
|
||||
## @superset-ui/legacy-plugin-chart-rose
|
||||
|
||||
[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-rose.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-rose.svg?style=flat-square)
|
||||
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui.svg?path=packages%2Fsuperset-ui-legacy-plugin-chart-rose&style=flat-square)](https://david-dm.org/apache-superset/superset-ui?path=packages/superset-ui-legacy-plugin-chart-rose)
|
||||
|
||||
This plugin provides Nightingale Rose Diagram for Superset.
|
||||
|
||||
### Usage
|
||||
|
||||
Configure `key`, which can be any `string`, and register the plugin. This `key` will be used to lookup this chart throughout the app.
|
||||
|
||||
```js
|
||||
import RoseChartPlugin from '@superset-ui/legacy-plugin-chart-rose';
|
||||
|
||||
new RoseChartPlugin()
|
||||
.configure({ key: 'rose' })
|
||||
.register();
|
||||
```
|
||||
|
||||
Then use it via `SuperChart`. See [storybook](https://apache-superset.github.io/superset-ui-legacy/?selectedKind=plugin-chart-rose) for more details.
|
||||
|
||||
```js
|
||||
<SuperChart
|
||||
chartType="rose"
|
||||
chartProps={{
|
||||
width: 600,
|
||||
height: 600,
|
||||
formData: {...},
|
||||
payload: {
|
||||
data: {...},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
```
|
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@superset-ui/legacy-plugin-chart-rose",
|
||||
"version": "0.0.0",
|
||||
"description": "Superset Legacy Chart - Nightingale Rose Diagram",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
"files": [
|
||||
"esm",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apache-superset/superset-ui-legacy.git"
|
||||
},
|
||||
"keywords": [
|
||||
"superset"
|
||||
],
|
||||
"author": "Superset",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache-superset/superset-ui-legacy/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache-superset/superset-ui-legacy#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@superset-ui/core": "^0.9.x",
|
||||
"d3": "^3.5.17",
|
||||
"nvd3": "1.8.6",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@superset-ui/chart": "^0.9.x",
|
||||
"@superset-ui/color": "^0.9.x",
|
||||
"@superset-ui/number-format": "^0.9.x",
|
||||
"@superset-ui/time-format": "^0.9.x",
|
||||
"@superset-ui/translation": "^0.9.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart": "^0.9.x",
|
||||
"@superset-ui/color": "^0.9.x",
|
||||
"@superset-ui/number-format": "^0.9.x",
|
||||
"@superset-ui/time-format": "^0.9.x",
|
||||
"@superset-ui/translation": "^0.9.x"
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import { reactify } from '@superset-ui/chart';
|
||||
import Component from './Rose';
|
||||
|
||||
export default reactify(Component);
|
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
.superset-legacy-chart-rose path {
|
||||
transition: fill-opacity 180ms linear;
|
||||
stroke: #fff;
|
||||
stroke-width: 1px;
|
||||
stroke-opacity: 1;
|
||||
fill-opacity: 0.75;
|
||||
}
|
||||
|
||||
.superset-legacy-chart-rose text {
|
||||
font: 400 12px Arial, sans-serif;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.superset-legacy-chart-rose .clickable path {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.superset-legacy-chart-rose .hover path {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.nv-legend .nv-series {
|
||||
cursor: pointer;
|
||||
}
|
@ -0,0 +1,621 @@
|
||||
/**
|
||||
* 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 sort-keys, no-magic-numbers, no-restricted-syntax, no-plusplus, babel/no-invalid-this */
|
||||
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 = 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 => 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;
|
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
After Width: | Height: | Size: 494 KiB |
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
description: '',
|
||||
name: t('Nightingale Rose Chart'),
|
||||
thumbnail,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class RoseChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactRose.js'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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-disable sort-keys */
|
||||
export default function transformProps(chartProps) {
|
||||
const { width, height, formData, payload } = chartProps;
|
||||
const { colorScheme, dateTimeFormat, numberFormat, richTooltip, roseAreaProportion } = formData;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
data: payload.data,
|
||||
colorScheme,
|
||||
dateTimeFormat,
|
||||
numberFormat,
|
||||
useAreaProportions: roseAreaProportion,
|
||||
useRichTooltip: richTooltip,
|
||||
};
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
## @superset-ui/legacy-plugin-chart-sankey
|
||||
|
||||
[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-sankey.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-sankey.svg?style=flat-square)
|
||||
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui.svg?path=packages%2Fsuperset-ui-legacy-plugin-chart-sankey&style=flat-square)](https://david-dm.org/apache-superset/superset-ui?path=packages/superset-ui-legacy-plugin-chart-sankey)
|
||||
|
||||
This plugin provides Sankey Diagram for Superset.
|
||||
|
||||
### Usage
|
||||
|
||||
Configure `key`, which can be any `string`, and register the plugin. This `key` will be used to lookup this chart throughout the app.
|
||||
|
||||
```js
|
||||
import SankeyChartPlugin from '@superset-ui/legacy-plugin-chart-sankey';
|
||||
|
||||
new SankeyChartPlugin()
|
||||
.configure({ key: 'sankey' })
|
||||
.register();
|
||||
```
|
||||
|
||||
Then use it via `SuperChart`. See [storybook](https://apache-superset.github.io/superset-ui-legacy/?selectedKind=plugin-chart-sankey) for more details.
|
||||
|
||||
```js
|
||||
<SuperChart
|
||||
chartType="sankey"
|
||||
chartProps={{
|
||||
width: 600,
|
||||
height: 600,
|
||||
formData: {...},
|
||||
payload: {
|
||||
data: {...},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
```
|
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@superset-ui/legacy-plugin-chart-sankey",
|
||||
"version": "0.0.0",
|
||||
"description": "Superset Legacy Chart - Sankey Diagram",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
"files": [
|
||||
"esm",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apache-superset/superset-ui-legacy.git"
|
||||
},
|
||||
"keywords": [
|
||||
"superset"
|
||||
],
|
||||
"author": "Superset",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache-superset/superset-ui-legacy/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache-superset/superset-ui-legacy#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@superset-ui/core": "^0.9.x",
|
||||
"d3": "^3.5.17",
|
||||
"d3-sankey": "^0.4.2",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@superset-ui/chart": "^0.9.x",
|
||||
"@superset-ui/color": "^0.9.x",
|
||||
"@superset-ui/number-format": "^0.9.x",
|
||||
"@superset-ui/translation": "^0.9.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart": "^0.9.x",
|
||||
"@superset-ui/color": "^0.9.x",
|
||||
"@superset-ui/number-format": "^0.9.x",
|
||||
"@superset-ui/translation": "^0.9.x"
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import { reactify } from '@superset-ui/chart';
|
||||
import Component from './Sankey';
|
||||
|
||||
export default reactify(Component);
|
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
.superset-legacy-chart-sankey .node rect {
|
||||
cursor: move;
|
||||
fill-opacity: .9;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.superset-legacy-chart-sankey .node text {
|
||||
pointer-events: none;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
}
|
||||
|
||||
.superset-legacy-chart-sankey .link {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
stroke-opacity: .2;
|
||||
}
|
||||
|
||||
.superset-legacy-chart-sankey .link:hover {
|
||||
stroke-opacity: .5;
|
||||
}
|
||||
|
||||
.superset-legacy-chart-sankey-tooltip {
|
||||
position: absolute;
|
||||
width: auto;
|
||||
background: #ddd;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 200;
|
||||
color: #333;
|
||||
border: 1px solid #fff;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 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-disable no-param-reassign, no-magic-numbers, sort-keys, babel/no-invalid-this */
|
||||
import d3 from 'd3';
|
||||
import PropTypes from 'prop-types';
|
||||
import { sankey as d3Sankey } from 'd3-sankey';
|
||||
import { CategoricalColorNamespace } from '@superset-ui/color';
|
||||
import { getNumberFormatter, NumberFormats } from '@superset-ui/number-format';
|
||||
import './Sankey.css';
|
||||
|
||||
const propTypes = {
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
source: PropTypes.string,
|
||||
target: PropTypes.string,
|
||||
value: PropTypes.number,
|
||||
}),
|
||||
),
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
colorScheme: PropTypes.string,
|
||||
};
|
||||
|
||||
const formatNumber = getNumberFormatter(NumberFormats.FLOAT);
|
||||
|
||||
function Sankey(element, props) {
|
||||
const { data, width, height, colorScheme } = props;
|
||||
|
||||
const div = d3.select(element);
|
||||
div.classed('superset-legacy-chart-sankey', true);
|
||||
const margin = {
|
||||
top: 5,
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
left: 5,
|
||||
};
|
||||
const innerWidth = width - margin.left - margin.right;
|
||||
const innerHeight = height - margin.top - margin.bottom;
|
||||
|
||||
div.selectAll('*').remove();
|
||||
const svg = div
|
||||
.append('svg')
|
||||
.attr('width', innerWidth + margin.left + margin.right)
|
||||
.attr('height', innerHeight + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
const tooltip = div
|
||||
.append('div')
|
||||
.attr('class', 'sankey-tooltip')
|
||||
.style('opacity', 0);
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
|
||||
const sankey = d3Sankey()
|
||||
.nodeWidth(15)
|
||||
.nodePadding(10)
|
||||
.size([innerWidth, innerHeight]);
|
||||
|
||||
const path = sankey.link();
|
||||
|
||||
let nodes = {};
|
||||
// Compute the distinct nodes from the links.
|
||||
const links = data.map(row => {
|
||||
const link = Object.assign({}, row);
|
||||
link.source = nodes[link.source] || (nodes[link.source] = { name: link.source });
|
||||
link.target = nodes[link.target] || (nodes[link.target] = { name: link.target });
|
||||
link.value = Number(link.value);
|
||||
|
||||
return link;
|
||||
});
|
||||
nodes = d3.values(nodes);
|
||||
|
||||
sankey
|
||||
.nodes(nodes)
|
||||
.links(links)
|
||||
.layout(32);
|
||||
|
||||
function getTooltipHtml(d) {
|
||||
let html;
|
||||
|
||||
if (d.sourceLinks) {
|
||||
// is node
|
||||
html = `${d.name} Value: <span class='emph'>${formatNumber(d.value)}</span>`;
|
||||
} else {
|
||||
const val = formatNumber(d.value);
|
||||
const sourcePercent = d3.round((d.value / d.source.value) * 100, 1);
|
||||
const targetPercent = d3.round((d.value / d.target.value) * 100, 1);
|
||||
|
||||
html = [
|
||||
"<div class=''>Path Value: <span class='emph'>",
|
||||
val,
|
||||
'</span></div>',
|
||||
"<div class='percents'>",
|
||||
"<span class='emph'>",
|
||||
Number.isFinite(sourcePercent) ? sourcePercent : '100',
|
||||
'%</span> of ',
|
||||
d.source.name,
|
||||
'<br/>',
|
||||
`<span class='emph'>${Number.isFinite(targetPercent) ? targetPercent : '--'}%</span> of `,
|
||||
d.target.name,
|
||||
'target',
|
||||
'</div>',
|
||||
].join('');
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function onmouseover(d) {
|
||||
tooltip
|
||||
.html(() => getTooltipHtml(d))
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('left', `${d3.event.offsetX + 10}px`)
|
||||
.style('top', `${d3.event.offsetY + 10}px`)
|
||||
.style('opacity', 0.95);
|
||||
}
|
||||
|
||||
function onmouseout() {
|
||||
tooltip
|
||||
.transition()
|
||||
.duration(100)
|
||||
.style('opacity', 0);
|
||||
}
|
||||
|
||||
const link = svg
|
||||
.append('g')
|
||||
.selectAll('.link')
|
||||
.data(links)
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('class', 'link')
|
||||
.attr('d', path)
|
||||
.style('stroke-width', d => Math.max(1, d.dy))
|
||||
.sort((a, b) => b.dy - a.dy)
|
||||
.on('mouseover', onmouseover)
|
||||
.on('mouseout', onmouseout);
|
||||
|
||||
function dragmove(d) {
|
||||
d3.select(this).attr(
|
||||
'transform',
|
||||
`translate(${d.x},${(d.y = Math.max(0, Math.min(height - d.dy, d3.event.y)))})`,
|
||||
);
|
||||
sankey.relayout();
|
||||
link.attr('d', path);
|
||||
}
|
||||
|
||||
const node = svg
|
||||
.append('g')
|
||||
.selectAll('.node')
|
||||
.data(nodes)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'node')
|
||||
.attr('transform', d => `translate(${d.x},${d.y})`)
|
||||
.call(
|
||||
d3.behavior
|
||||
.drag()
|
||||
.origin(d => d)
|
||||
.on('dragstart', function dragStart() {
|
||||
this.parentNode.appendChild(this);
|
||||
})
|
||||
.on('drag', dragmove),
|
||||
);
|
||||
const minRectHeight = 5;
|
||||
node
|
||||
.append('rect')
|
||||
.attr('height', d => (d.dy > minRectHeight ? d.dy : minRectHeight))
|
||||
.attr('width', sankey.nodeWidth())
|
||||
.style('fill', d => {
|
||||
const name = d.name || 'N/A';
|
||||
d.color = colorFn(name.replace(/ .*/, ''));
|
||||
|
||||
return d.color;
|
||||
})
|
||||
.style('stroke', d => d3.rgb(d.color).darker(2))
|
||||
.on('mouseover', onmouseover)
|
||||
.on('mouseout', onmouseout);
|
||||
|
||||
node
|
||||
.append('text')
|
||||
.attr('x', -6)
|
||||
.attr('y', d => d.dy / 2)
|
||||
.attr('dy', '.35em')
|
||||
.attr('text-anchor', 'end')
|
||||
.attr('transform', null)
|
||||
.text(d => d.name)
|
||||
.filter(d => d.x < innerWidth / 2)
|
||||
.attr('x', 6 + sankey.nodeWidth())
|
||||
.attr('text-anchor', 'start');
|
||||
}
|
||||
|
||||
Sankey.displayName = 'Sankey';
|
||||
Sankey.propTypes = propTypes;
|
||||
|
||||
export default Sankey;
|
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
After Width: | Height: | Size: 200 KiB |
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
credits: ['https://github.com/d3/d3-sankey'],
|
||||
description: '',
|
||||
name: t('Sankey Diagram'),
|
||||
thumbnail,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class SankeyChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactSankey.js'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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-disable sort-keys */
|
||||
export default function transformProps(chartProps) {
|
||||
const { width, height, formData, payload } = chartProps;
|
||||
const { colorScheme } = formData;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
data: payload.data,
|
||||
colorScheme,
|
||||
};
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
## @superset-ui/legacy-plugin-chart-sunburst
|
||||
|
||||
[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-sunburst.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-sunburst.svg?style=flat-square)
|
||||
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui.svg?path=packages%2Fsuperset-ui-legacy-plugin-chart-sunburst&style=flat-square)](https://david-dm.org/apache-superset/superset-ui?path=packages/superset-ui-legacy-plugin-chart-sunburst)
|
||||
|
||||
This plugin provides Sunburst for Superset.
|
||||
|
||||
### Usage
|
||||
|
||||
Configure `key`, which can be any `string`, and register the plugin. This `key` will be used to lookup this chart throughout the app.
|
||||
|
||||
```js
|
||||
import SunburstChartPlugin from '@superset-ui/legacy-plugin-chart-sunburst';
|
||||
|
||||
new SunburstChartPlugin()
|
||||
.configure({ key: 'sunburst' })
|
||||
.register();
|
||||
```
|
||||
|
||||
Then use it via `SuperChart`. See [storybook](https://apache-superset.github.io/superset-ui-legacy/?selectedKind=plugin-chart-sunburst) for more details.
|
||||
|
||||
```js
|
||||
<SuperChart
|
||||
chartType="sunburst"
|
||||
chartProps={{
|
||||
width: 600,
|
||||
height: 600,
|
||||
formData: {...},
|
||||
payload: {
|
||||
data: {...},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
```
|
@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@superset-ui/legacy-plugin-chart-sunburst",
|
||||
"version": "0.0.0",
|
||||
"description": "Superset Legacy Chart - Sunburst",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
"files": [
|
||||
"esm",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apache-superset/superset-ui-legacy.git"
|
||||
},
|
||||
"keywords": [
|
||||
"superset"
|
||||
],
|
||||
"author": "Superset",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache-superset/superset-ui-legacy/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache-superset/superset-ui-legacy#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@superset-ui/core": "^0.9.x",
|
||||
"d3": "^3.5.17",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@superset-ui/chart": "^0.9.x",
|
||||
"@superset-ui/color": "^0.9.x",
|
||||
"@superset-ui/number-format": "^0.9.x",
|
||||
"@superset-ui/translation": "^0.9.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart": "^0.9.x",
|
||||
"@superset-ui/color": "^0.9.x",
|
||||
"@superset-ui/number-format": "^0.9.x",
|
||||
"@superset-ui/translation": "^0.9.x"
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import { reactify } from '@superset-ui/chart';
|
||||
import Component from './Sunburst';
|
||||
|
||||
export default reactify(Component);
|
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
.superset-legacy-chart-sunburst text {
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
.superset-legacy-chart-sunburst path {
|
||||
stroke: #ddd;
|
||||
stroke-width: 0.5px;
|
||||
}
|
||||
.superset-legacy-chart-sunburst .center-label {
|
||||
text-anchor: middle;
|
||||
fill: #333;
|
||||
pointer-events: none;
|
||||
}
|
||||
.superset-legacy-chart-sunburst .path-abs-percent {
|
||||
font-size: 3em;
|
||||
font-weight: 700;
|
||||
}
|
||||
.superset-legacy-chart-sunburst .path-cond-percent {
|
||||
font-size: 2em;
|
||||
}
|
||||
.superset-legacy-chart-sunburst .path-metrics {
|
||||
color: #777;
|
||||
}
|
||||
.superset-legacy-chart-sunburst .path-ratio {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.superset-legacy-chart-sunburst .breadcrumbs text {
|
||||
font-weight: 600;
|
||||
font-size: 1.2em;
|
||||
text-anchor: middle;
|
||||
fill: #333;
|
||||
}
|
||||
|
||||
/* dashboard specific */
|
||||
.dashboard .superset-legacy-chart-sunburst text {
|
||||
font-size: 1em;
|
||||
}
|
||||
.dashboard .superset-legacy-chart-sunburst .path-abs-percent {
|
||||
font-size: 2em;
|
||||
font-weight: 700;
|
||||
}
|
||||
.dashboard .superset-legacy-chart-sunburst .path-cond-percent {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.dashboard .superset-legacy-chart-sunburst .path-metrics {
|
||||
font-size: 1em;
|
||||
}
|
||||
.dashboard .superset-legacy-chart-sunburst .path-ratio {
|
||||
font-size: 1em;
|
||||
}
|
@ -0,0 +1,431 @@
|
||||
/**
|
||||
* 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-disable no-param-reassign, sort-keys, no-magic-numbers */
|
||||
/* eslint-disable complexity, no-plusplus, no-continue, babel/no-invalid-this */
|
||||
import d3 from 'd3';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CategoricalColorNamespace } from '@superset-ui/color';
|
||||
import { getNumberFormatter, NumberFormats } from '@superset-ui/number-format';
|
||||
import wrapSvgText from './utils/wrapSvgText';
|
||||
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
|
||||
function Sunburst(element, props) {
|
||||
const container = d3.select(element);
|
||||
container.classed('superset-legacy-chart-sunburst', true);
|
||||
const { data, width, height, colorScheme, metrics } = props;
|
||||
|
||||
// vars with shared scope within this function
|
||||
const margin = { top: 10, right: 5, bottom: 10, left: 5 };
|
||||
const containerWidth = width;
|
||||
const containerHeight = height;
|
||||
const breadcrumbHeight = containerHeight * 0.085;
|
||||
const visWidth = containerWidth - margin.left - margin.right;
|
||||
const visHeight = containerHeight - margin.top - margin.bottom - breadcrumbHeight;
|
||||
const radius = Math.min(visWidth, visHeight) / 2;
|
||||
|
||||
let colorByCategory = true; // color by category if primary/secondary metrics match
|
||||
let maxBreadcrumbs;
|
||||
let breadcrumbDims; // set based on data
|
||||
let totalSize; // total size of all segments; set after loading the data.
|
||||
let colorScale;
|
||||
let breadcrumbs;
|
||||
let vis;
|
||||
let arcs;
|
||||
let gMiddleText; // dom handles
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
|
||||
// Helper + path gen functions
|
||||
const partition = d3.layout
|
||||
.partition()
|
||||
.size([2 * Math.PI, radius * radius])
|
||||
.value(d => d.m1);
|
||||
|
||||
const arc = d3.svg
|
||||
.arc()
|
||||
.startAngle(d => d.x)
|
||||
.endAngle(d => d.x + d.dx)
|
||||
.innerRadius(d => Math.sqrt(d.y))
|
||||
.outerRadius(d => Math.sqrt(d.y + d.dy));
|
||||
|
||||
const formatNum = getNumberFormatter(NumberFormats.SI_3_DIGIT);
|
||||
const formatPerc = getNumberFormatter(NumberFormats.PERCENT_3_POINT);
|
||||
|
||||
container.select('svg').remove();
|
||||
|
||||
const svg = container
|
||||
.append('svg:svg')
|
||||
.attr('width', containerWidth)
|
||||
.attr('height', containerHeight);
|
||||
|
||||
function createBreadcrumbs(firstRowData) {
|
||||
// -2 bc row contains 2x metrics, +extra for %label and buffer
|
||||
maxBreadcrumbs = firstRowData.length - 2 + 1;
|
||||
breadcrumbDims = {
|
||||
width: visWidth / maxBreadcrumbs,
|
||||
height: breadcrumbHeight * 0.8, // more margin
|
||||
spacing: 3,
|
||||
tipTailWidth: 10,
|
||||
};
|
||||
|
||||
breadcrumbs = svg
|
||||
.append('svg:g')
|
||||
.attr('class', 'breadcrumbs')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
breadcrumbs.append('svg:text').attr('class', 'end-label');
|
||||
}
|
||||
|
||||
// Generate a string that describes the points of a breadcrumb polygon.
|
||||
function breadcrumbPoints(d, i) {
|
||||
const points = [];
|
||||
points.push('0,0');
|
||||
points.push(`${breadcrumbDims.width},0`);
|
||||
points.push(
|
||||
`${breadcrumbDims.width + breadcrumbDims.tipTailWidth},${breadcrumbDims.height / 2}`,
|
||||
);
|
||||
points.push(`${breadcrumbDims.width},${breadcrumbDims.height}`);
|
||||
points.push(`0,${breadcrumbDims.height}`);
|
||||
if (i > 0) {
|
||||
// Leftmost breadcrumb; don't include 6th vertex.
|
||||
points.push(`${breadcrumbDims.tipTailWidth},${breadcrumbDims.height / 2}`);
|
||||
}
|
||||
|
||||
return points.join(' ');
|
||||
}
|
||||
|
||||
function updateBreadcrumbs(sequenceArray, percentageString) {
|
||||
const g = breadcrumbs.selectAll('g').data(sequenceArray, d => d.name + d.depth);
|
||||
|
||||
// Add breadcrumb and label for entering nodes.
|
||||
const entering = g.enter().append('svg:g');
|
||||
|
||||
entering
|
||||
.append('svg:polygon')
|
||||
.attr('points', breadcrumbPoints)
|
||||
.style('fill', d => (colorByCategory ? colorFn(d.name) : colorScale(d.m2 / d.m1)));
|
||||
|
||||
entering
|
||||
.append('svg:text')
|
||||
.attr('x', (breadcrumbDims.width + breadcrumbDims.tipTailWidth) / 2)
|
||||
.attr('y', breadcrumbDims.height / 4)
|
||||
.attr('dy', '0.85em')
|
||||
.style('fill', d => {
|
||||
// Make text white or black based on the lightness of the background
|
||||
const col = d3.hsl(colorByCategory ? colorFn(d.name) : colorScale(d.m2 / d.m1));
|
||||
|
||||
return col.l < 0.5 ? 'white' : 'black';
|
||||
})
|
||||
.attr('class', 'step-label')
|
||||
.text(d => d.name.replace(/_/g, ' '))
|
||||
.call(wrapSvgText, breadcrumbDims.width, breadcrumbDims.height / 2);
|
||||
|
||||
// Set position for entering and updating nodes.
|
||||
g.attr(
|
||||
'transform',
|
||||
(d, i) => `translate(${i * (breadcrumbDims.width + breadcrumbDims.spacing)}, 0)`,
|
||||
);
|
||||
|
||||
// Remove exiting nodes.
|
||||
g.exit().remove();
|
||||
|
||||
// Now move and update the percentage at the end.
|
||||
breadcrumbs
|
||||
.select('.end-label')
|
||||
.attr('x', (sequenceArray.length + 0.5) * (breadcrumbDims.width + breadcrumbDims.spacing))
|
||||
.attr('y', breadcrumbDims.height / 2)
|
||||
.attr('dy', '0.35em')
|
||||
.text(percentageString);
|
||||
|
||||
// Make the breadcrumb trail visible, if it's hidden.
|
||||
breadcrumbs.style('visibility', null);
|
||||
}
|
||||
|
||||
// Fade all but the current sequence, and show it in the breadcrumb trail.
|
||||
function mouseenter(d) {
|
||||
const sequenceArray = getAncestors(d);
|
||||
const parentOfD = sequenceArray[sequenceArray.length - 2] || null;
|
||||
|
||||
const absolutePercentage = (d.m1 / totalSize).toPrecision(3);
|
||||
const conditionalPercentage = parentOfD ? (d.m1 / parentOfD.m1).toPrecision(3) : null;
|
||||
|
||||
const absolutePercString = formatPerc(absolutePercentage);
|
||||
const conditionalPercString = parentOfD ? formatPerc(conditionalPercentage) : '';
|
||||
|
||||
// 3 levels of text if inner-most level, 4 otherwise
|
||||
const yOffsets = ['-25', '7', '35', '60'];
|
||||
let offsetIndex = 0;
|
||||
|
||||
// If metrics match, assume we are coloring by category
|
||||
const metricsMatch = Math.abs(d.m1 - d.m2) < 0.00001;
|
||||
|
||||
gMiddleText.selectAll('*').remove();
|
||||
|
||||
gMiddleText
|
||||
.append('text')
|
||||
.attr('class', 'path-abs-percent')
|
||||
.attr('y', yOffsets[offsetIndex++])
|
||||
.text(`${absolutePercString} of total`);
|
||||
|
||||
if (conditionalPercString) {
|
||||
gMiddleText
|
||||
.append('text')
|
||||
.attr('class', 'path-cond-percent')
|
||||
.attr('y', yOffsets[offsetIndex++])
|
||||
.text(`${conditionalPercString} of parent`);
|
||||
}
|
||||
|
||||
gMiddleText
|
||||
.append('text')
|
||||
.attr('class', 'path-metrics')
|
||||
.attr('y', yOffsets[offsetIndex++])
|
||||
.text(
|
||||
`${metricLabel(metrics[0])}: ${formatNum(d.m1)}${
|
||||
metricsMatch ? '' : `, ${metricLabel(metrics[1])}: ${formatNum(d.m2)}`
|
||||
}`,
|
||||
);
|
||||
|
||||
gMiddleText
|
||||
.append('text')
|
||||
.attr('class', 'path-ratio')
|
||||
.attr('y', yOffsets[offsetIndex++])
|
||||
.text(
|
||||
metricsMatch
|
||||
? ''
|
||||
: `${metricLabel(metrics[1])}/${metricLabel(metrics[0])}: ${formatPerc(d.m2 / d.m1)}`,
|
||||
);
|
||||
|
||||
// Reset and fade all the segments.
|
||||
arcs
|
||||
.selectAll('path')
|
||||
.style('stroke-width', null)
|
||||
.style('stroke', null)
|
||||
.style('opacity', 0.3);
|
||||
|
||||
// Then highlight only those that are an ancestor of the current segment.
|
||||
arcs
|
||||
.selectAll('path')
|
||||
.filter(node => sequenceArray.indexOf(node) >= 0)
|
||||
.style('opacity', 1)
|
||||
.style('stroke', '#aaa');
|
||||
|
||||
updateBreadcrumbs(sequenceArray, absolutePercString);
|
||||
}
|
||||
|
||||
// Restore everything to full opacity when moving off the visualization.
|
||||
function mouseleave() {
|
||||
// Hide the breadcrumb trail
|
||||
breadcrumbs.style('visibility', 'hidden');
|
||||
|
||||
gMiddleText.selectAll('*').remove();
|
||||
|
||||
// Deactivate all segments during transition.
|
||||
arcs.selectAll('path').on('mouseenter', null);
|
||||
|
||||
// Transition each segment to full opacity and then reactivate it.
|
||||
arcs
|
||||
.selectAll('path')
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('opacity', 1)
|
||||
.style('stroke', null)
|
||||
.style('stroke-width', null)
|
||||
.each('end', function() {
|
||||
d3.select(this).on('mouseenter', mouseenter);
|
||||
});
|
||||
}
|
||||
|
||||
function buildHierarchy(rows) {
|
||||
const root = {
|
||||
name: 'root',
|
||||
children: [],
|
||||
};
|
||||
|
||||
// each record [groupby1val, groupby2val, (<string> or 0)n, m1, m2]
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
const m1 = Number(row[row.length - 2]);
|
||||
const m2 = Number(row[row.length - 1]);
|
||||
const levels = row.slice(0, row.length - 2);
|
||||
if (Number.isNaN(m1)) {
|
||||
// e.g. if this is a header row
|
||||
continue;
|
||||
}
|
||||
let currentNode = root;
|
||||
for (let level = 0; level < levels.length; level++) {
|
||||
const children = currentNode.children || [];
|
||||
const nodeName = levels[level].toString();
|
||||
// If the next node has the name '0', it will
|
||||
const isLeafNode = level >= levels.length - 1 || levels[level + 1] === 0;
|
||||
let childNode;
|
||||
let currChild;
|
||||
|
||||
if (!isLeafNode) {
|
||||
// Not yet at the end of the sequence; move down the tree.
|
||||
let foundChild = false;
|
||||
for (let k = 0; k < children.length; k++) {
|
||||
currChild = children[k];
|
||||
if (currChild.name === nodeName && currChild.level === level) {
|
||||
// must match name AND level
|
||||
childNode = currChild;
|
||||
foundChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If we don't already have a child node for this branch, create it.
|
||||
if (!foundChild) {
|
||||
childNode = {
|
||||
name: nodeName,
|
||||
children: [],
|
||||
level,
|
||||
};
|
||||
children.push(childNode);
|
||||
}
|
||||
currentNode = childNode;
|
||||
} else if (nodeName !== 0) {
|
||||
// Reached the end of the sequence; create a leaf node.
|
||||
childNode = {
|
||||
name: nodeName,
|
||||
m1,
|
||||
m2,
|
||||
};
|
||||
children.push(childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function recurse(node) {
|
||||
if (node.children) {
|
||||
let sums;
|
||||
let m1 = 0;
|
||||
let m2 = 0;
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
sums = recurse(node.children[i]);
|
||||
m1 += sums[0];
|
||||
m2 += sums[1];
|
||||
}
|
||||
node.m1 = m1;
|
||||
node.m2 = m2;
|
||||
}
|
||||
|
||||
return [node.m1, node.m2];
|
||||
}
|
||||
|
||||
recurse(root);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
// Main function to draw and set up the visualization, once we have the data.
|
||||
function createVisualization(rows) {
|
||||
const root = buildHierarchy(rows);
|
||||
|
||||
vis = svg
|
||||
.append('svg:g')
|
||||
.attr('class', 'sunburst-vis')
|
||||
.attr(
|
||||
'transform',
|
||||
'translate(' +
|
||||
`${margin.left + visWidth / 2},` +
|
||||
`${margin.top + breadcrumbHeight + visHeight / 2}` +
|
||||
')',
|
||||
)
|
||||
.on('mouseleave', mouseleave);
|
||||
|
||||
arcs = vis.append('svg:g').attr('id', 'arcs');
|
||||
|
||||
gMiddleText = vis.append('svg:g').attr('class', 'center-label');
|
||||
|
||||
// Bounding circle underneath the sunburst, to make it easier to detect
|
||||
// when the mouse leaves the parent g.
|
||||
arcs
|
||||
.append('svg:circle')
|
||||
.attr('r', radius)
|
||||
.style('opacity', 0);
|
||||
|
||||
// For efficiency, filter nodes to keep only those large enough to see.
|
||||
const nodes = partition.nodes(root).filter(d => d.dx > 0.005); // 0.005 radians = 0.29 degrees
|
||||
|
||||
let ext;
|
||||
|
||||
if (metrics[0] !== metrics[1] && metrics[1]) {
|
||||
colorByCategory = false;
|
||||
ext = d3.extent(nodes, d => d.m2 / d.m1);
|
||||
colorScale = d3.scale
|
||||
.linear()
|
||||
.domain([ext[0], ext[0] + (ext[1] - ext[0]) / 2, ext[1]])
|
||||
.range(['#00D1C1', 'white', '#FFB400']);
|
||||
}
|
||||
|
||||
arcs
|
||||
.selectAll('path')
|
||||
.data(nodes)
|
||||
.enter()
|
||||
.append('svg:path')
|
||||
.attr('display', d => (d.depth ? null : 'none'))
|
||||
.attr('d', arc)
|
||||
.attr('fill-rule', 'evenodd')
|
||||
.style('fill', d => (colorByCategory ? colorFn(d.name) : colorScale(d.m2 / d.m1)))
|
||||
.style('opacity', 1)
|
||||
.on('mouseenter', mouseenter);
|
||||
|
||||
// Get total size of the tree = value of root node from partition.
|
||||
totalSize = root.value;
|
||||
}
|
||||
createBreadcrumbs(data[0]);
|
||||
createVisualization(data);
|
||||
}
|
||||
|
||||
Sunburst.displayName = 'Sunburst';
|
||||
Sunburst.propTypes = propTypes;
|
||||
|
||||
export default Sunburst;
|
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
After Width: | Height: | Size: 170 KiB |
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
credits: ['https://bl.ocks.org/kerryrodden/7090426'],
|
||||
description: '',
|
||||
name: t('Sunburst Chart'),
|
||||
thumbnail,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class SunburstChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactSunburst.js'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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-disable sort-keys */
|
||||
export default function transformProps(chartProps) {
|
||||
const { width, height, formData, payload } = chartProps;
|
||||
const { colorScheme, metric, secondaryMetric } = formData;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
data: payload.data,
|
||||
colorScheme,
|
||||
metrics: [metric, secondaryMetric],
|
||||
};
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/* eslint-disable babel/no-invalid-this, no-plusplus */
|
||||
|
||||
/*
|
||||
Utility function that takes a d3 svg:text selection and a max width, and splits the
|
||||
text's text across multiple tspan lines such that any given line does not exceed max width
|
||||
|
||||
If text does not span multiple lines AND adjustedY is passed,
|
||||
will set the text to the passed val
|
||||
*/
|
||||
import d3 from 'd3';
|
||||
|
||||
export default function wrapSvgText(text, width, adjustedY) {
|
||||
const lineHeight = 1;
|
||||
// ems
|
||||
text.each(function each() {
|
||||
const d3Text = d3.select(this);
|
||||
const words = d3Text.text().split(/\s+/);
|
||||
let word;
|
||||
let line = [];
|
||||
let lineNumber = 0;
|
||||
const x = d3Text.attr('x');
|
||||
const y = d3Text.attr('y');
|
||||
const dy = parseFloat(d3Text.attr('dy'));
|
||||
let tspan = d3Text
|
||||
.text(null)
|
||||
.append('tspan')
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('dy', `${dy}em`);
|
||||
|
||||
let didWrap = false;
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
word = words[i];
|
||||
line.push(word);
|
||||
tspan.text(line.join(' '));
|
||||
if (tspan.node().getComputedTextLength() > width) {
|
||||
line.pop();
|
||||
// remove word that pushes over the limit
|
||||
tspan.text(line.join(' '));
|
||||
line = [word];
|
||||
tspan = d3Text
|
||||
.append('tspan')
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('dy', `${++lineNumber * lineHeight + dy}em`)
|
||||
.text(word);
|
||||
didWrap = true;
|
||||
}
|
||||
}
|
||||
if (!didWrap && typeof adjustedY !== 'undefined') {
|
||||
tspan.attr('y', adjustedY);
|
||||
}
|
||||
});
|
||||
}
|
@ -22,7 +22,7 @@ import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
credits: ['https://d3js.org'],
|
||||
credits: ['https://bl.ocks.org/mbostock/911ad09bdead40ec0061'],
|
||||
description: '',
|
||||
name: t('Treemap'),
|
||||
thumbnail,
|
||||
|
Loading…
Reference in New Issue
Block a user