diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-sankey/Stories.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-sankey/Stories.tsx
index 0a7f01a05957..6af47a80b0c2 100644
--- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-sankey/Stories.tsx
+++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-sankey/Stories.tsx
@@ -2,6 +2,7 @@
import React from 'react';
import { SuperChart } from '@superset-ui/core';
import SankeyChartPlugin from '@superset-ui/legacy-plugin-chart-sankey';
+import ResizableChartDemo from '../../../shared/components/ResizableChartDemo';
import data from './data';
new SankeyChartPlugin().configure({ key: 'sankey' }).register();
@@ -21,3 +22,19 @@ export const basic = () => (
}}
/>
);
+
+export const resizable = () => (
+
+ {({ width, height }) => (
+
+ )}
+
+);
diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-sunburst/Stories.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-sunburst/Stories.tsx
index ce4153c8d90d..56c5422f2ce0 100644
--- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-sunburst/Stories.tsx
+++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-sunburst/Stories.tsx
@@ -2,6 +2,7 @@
import React from 'react';
import { SuperChart } from '@superset-ui/core';
import SunburstChartPlugin from '@superset-ui/legacy-plugin-chart-sunburst';
+import ResizableChartDemo from '../../../shared/components/ResizableChartDemo';
import data from './data';
new SunburstChartPlugin().configure({ key: 'sunburst' }).register();
@@ -23,3 +24,21 @@ export const basic = () => (
}}
/>
);
+
+export const resizable = () => (
+
+ {({ width, height }) => (
+
+ )}
+
+);
diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/ReactSankey.jsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/ReactSankey.jsx
index 228c9ed6331c..b8c6278f069e 100644
--- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/ReactSankey.jsx
+++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/ReactSankey.jsx
@@ -44,6 +44,7 @@ export default styled(SankeyComponent)`
text {
pointer-events: none;
text-shadow: 0 1px 0 #fff;
+ font-size: ${({ fontSize }) => fontSize}em;
}
}
.link {
@@ -54,15 +55,18 @@ export default styled(SankeyComponent)`
stroke-opacity: 0.5;
}
}
+ .opacity-0 {
+ opacity: 0;
+ }
}
- .superset-legacy-chart-sankey-tooltip {
+ .sankey-tooltip {
position: absolute;
width: auto;
background: #ddd;
padding: 10px;
- font-size: ${({ theme }) => theme.typography.sizes.s};
+ font-size: ${({ fontSize }) => fontSize}em;
font-weight: ${({ theme }) => theme.typography.weights.light};
- color: #333;
+ color: #000;
border: 1px solid #fff;
text-align: center;
pointer-events: none;
diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/Sankey.js b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/Sankey.js
index 23cbd31b1cb1..f7a2ee0305e5 100644
--- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/Sankey.js
+++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/Sankey.js
@@ -22,6 +22,7 @@ import d3 from 'd3';
import PropTypes from 'prop-types';
import { sankey as d3Sankey } from 'd3-sankey';
import { getNumberFormatter, NumberFormats, CategoricalColorNamespace } from '@superset-ui/core';
+import { getOverlappingElements } from './utils';
const propTypes = {
data: PropTypes.arrayOf(
@@ -40,9 +41,8 @@ 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);
+ div.classed(`superset-legacy-chart-sankey`, true);
const margin = {
top: 5,
right: 5,
@@ -53,6 +53,7 @@ function Sankey(element, props) {
const innerHeight = height - margin.top - margin.bottom;
div.selectAll('*').remove();
+ const tooltip = div.append('div').attr('class', 'sankey-tooltip').style('opacity', 0);
const svg = div
.append('svg')
.attr('width', innerWidth + margin.left + margin.right)
@@ -60,8 +61,6 @@ function Sankey(element, props) {
.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]);
@@ -119,6 +118,7 @@ function Sankey(element, props) {
.duration(200)
.style('left', `${d3.event.offsetX + 10}px`)
.style('top', `${d3.event.offsetY + 10}px`)
+ .style('position', 'absolute')
.style('opacity', 0.95);
}
@@ -148,6 +148,23 @@ function Sankey(element, props) {
link.attr('d', path);
}
+ function checkVisibility() {
+ const elements = div.selectAll('.node')[0] ?? [];
+ const overlappingElements = getOverlappingElements(elements);
+
+ elements.forEach(el => {
+ const text = el.getElementsByTagName('text')[0];
+
+ if (text) {
+ if (overlappingElements.includes(el)) {
+ text.classList.add('opacity-0');
+ } else {
+ text.classList.remove('opacity-0');
+ }
+ }
+ });
+ }
+
const node = svg
.append('g')
.selectAll('.node')
@@ -163,7 +180,8 @@ function Sankey(element, props) {
.on('dragstart', function dragStart() {
this.parentNode.append(this);
})
- .on('drag', dragmove),
+ .on('drag', dragmove)
+ .on('dragend', checkVisibility),
);
const minRectHeight = 5;
node
@@ -188,9 +206,12 @@ function Sankey(element, props) {
.attr('text-anchor', 'end')
.attr('transform', null)
.text(d => d.name)
+ .attr('class', 'opacity-0')
.filter(d => d.x < innerWidth / 2)
.attr('x', 6 + sankey.nodeWidth())
.attr('text-anchor', 'start');
+
+ checkVisibility();
}
Sankey.displayName = 'Sankey';
diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/tests/utils.test.js b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/tests/utils.test.js
new file mode 100644
index 000000000000..d679d6bbc6c8
--- /dev/null
+++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/tests/utils.test.js
@@ -0,0 +1,93 @@
+/**
+ * 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 { getOverlappingElements, isOverlapping } from '../utils';
+
+const overlapRects = [
+ {
+ x: 10,
+ y: 10,
+ width: 10,
+ height: 10,
+ },
+ {
+ x: 12,
+ y: 12,
+ width: 12,
+ height: 12,
+ },
+ {
+ x: 32,
+ y: 32,
+ width: 32,
+ height: 32,
+ },
+];
+
+const notOverlapRects = [
+ {
+ x: 10,
+ y: 10,
+ width: 10,
+ height: 10,
+ },
+ {
+ x: 24,
+ y: 15,
+ width: 15,
+ height: 15,
+ },
+ {
+ x: 32,
+ y: 32,
+ width: 32,
+ height: 32,
+ },
+];
+
+const createSVGs = objects =>
+ objects.map(data => {
+ const el = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ el.getBoundingClientRect = jest.fn(() => data);
+
+ return el;
+ });
+
+// https://www.khanacademy.org/computer-programming/rectx-y-width-height-radius/839496660
+describe('legacy-plugin-chart-sankey/utils', () => {
+ it('isOverlapping to be truthy', () => {
+ const [rect1, rect2] = overlapRects;
+ expect(isOverlapping(rect1, rect2)).toBeTruthy();
+ });
+
+ it('isOverlapping to be falsy', () => {
+ const [rect1, rect2] = notOverlapRects;
+ expect(isOverlapping(rect1, rect2)).toBeFalsy();
+ });
+
+ it('getOverlappingElements to be truthy', () => {
+ const elements = createSVGs(overlapRects);
+ expect(getOverlappingElements(elements).length).toBe(2);
+ });
+
+ it('getOverlappingElements to be falsy', () => {
+ const elements = createSVGs(notOverlapRects);
+ expect(getOverlappingElements(elements).length).toBe(0);
+ });
+});
diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/transformProps.js b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/transformProps.js
index 938d30f9df77..5297994fb952 100644
--- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/transformProps.js
+++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/transformProps.js
@@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { getLabelFontSize } from './utils';
+
export default function transformProps(chartProps) {
const { width, height, formData, queriesData } = chartProps;
const { colorScheme } = formData;
@@ -25,5 +27,6 @@ export default function transformProps(chartProps) {
height,
data: queriesData[0].data,
colorScheme,
+ fontSize: getLabelFontSize(width),
};
}
diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/utils.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/utils.ts
new file mode 100644
index 000000000000..b84afe89eba2
--- /dev/null
+++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sankey/src/utils.ts
@@ -0,0 +1,74 @@
+/**
+ * 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.
+ */
+type Rect = {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+};
+
+export function getLabelFontSize(width: number): number {
+ if (width > 550) {
+ return 0.8;
+ }
+
+ if (width > 400 && width <= 550) {
+ return 0.55;
+ }
+
+ return 0.45;
+}
+
+export const isOverlapping = (rect1: Rect, rect2: Rect): boolean => {
+ const { x: x1, y: y1, width: width1, height: height1 } = rect1;
+ const { x: x2, y: y2, width: width2, height: height2 } = rect2;
+
+ return !(x1 > x2 + width2 || x1 + width1 < x2 || y1 > y2 + height2 || y1 + height1 < y2);
+};
+
+export const getRectangle = (element: SVGElement, offset = 0): Rect => {
+ const { x, y, width, height } = element.getBoundingClientRect();
+
+ return {
+ x,
+ y: y + offset,
+ width,
+ height: height - offset * 2,
+ };
+};
+
+export const getOverlappingElements = (elements: SVGElement[]): SVGElement[] => {
+ const overlappingElements: SVGElement[] = [];
+
+ elements.forEach((e1, index1) => {
+ const rect1: Rect = getRectangle(e1, 1);
+
+ elements.forEach((e2, index2) => {
+ if (index2 <= index1) return;
+ const rect2: Rect = getRectangle(e2, 1);
+
+ if (isOverlapping(rect1, rect2)) {
+ overlappingElements.push(elements[index2]);
+ overlappingElements.push(elements[index1]);
+ }
+ });
+ });
+
+ return overlappingElements;
+};
diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sunburst/src/Sunburst.css b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sunburst/src/Sunburst.css
index 2f836ac48db0..0afe0a87951c 100644
--- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sunburst/src/Sunburst.css
+++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sunburst/src/Sunburst.css
@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
-
.superset-legacy-chart-sunburst text {
text-rendering: optimizeLegibility;
}
@@ -51,21 +50,21 @@
}
/* dashboard specific */
-/*
-.dashboard .superset-legacy-chart-sunburst text {
- font-size: 1em;
+.dashboard-chart.sunburst {
+ overflow: visible;
}
-.dashboard .superset-legacy-chart-sunburst .path-abs-percent {
- font-size: 2em;
- font-weight: 700;
+.superset-legacy-chart-sunburst svg {
+ overflow: visible;
}
-.dashboard .superset-legacy-chart-sunburst .path-cond-percent {
- font-size: 1.5em;
+.superset-legacy-chart-sunburst.m text {
+ font-size: 0.55em;
}
-.dashboard .superset-legacy-chart-sunburst .path-metrics {
- font-size: 1em;
+.superset-legacy-chart-sunburst.s text {
+ font-size: 0.45em;
}
-.dashboard .superset-legacy-chart-sunburst .path-ratio {
- font-size: 1em;
+.superset-legacy-chart-sunburst.l text {
+ font-size: 0.75em;
+}
+.superset-legacy-chart-sunburst .path-abs-percent {
+ font-weight: 700;
}
-*/
diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sunburst/src/Sunburst.js b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sunburst/src/Sunburst.js
index e5be0f00309d..88dd7464e173 100644
--- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sunburst/src/Sunburst.js
+++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-sunburst/src/Sunburst.js
@@ -131,12 +131,37 @@ function buildHierarchy(rows) {
return root;
}
+function getResponsiveContainerClass(width) {
+ if (width > 500) {
+ return 'l';
+ }
+
+ if (width > 200 && width <= 500) {
+ return 'm';
+ }
+
+ return 's';
+}
+
+function getYOffset(width) {
+ if (width > 500) {
+ return ['0', '20', '40', '60'];
+ }
+
+ if (width > 200 && width <= 500) {
+ return ['0', '15', '30', '45'];
+ }
+
+ return ['0', '10', '20', '30'];
+}
+
// 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, linearColorScheme, metrics, numberFormat } = props;
-
+ const responsiveClass = getResponsiveContainerClass(width);
+ const isSmallWidth = responsiveClass === 's';
+ container.attr('class', `superset-legacy-chart-sunburst ${responsiveClass}`);
// vars with shared scope within this function
const margin = { top: 10, right: 5, bottom: 10, left: 5 };
const containerWidth = width;
@@ -202,22 +227,35 @@ function Sunburst(element, props) {
// 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}`);
+ if (isSmallWidth) {
+ points.push('0,0');
+ points.push(`${width},0`);
+ points.push(`${width},0`);
+ points.push(`${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}`);
+ }
+ } else {
+ 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 breadcrumbWidth = isSmallWidth ? width : breadcrumbDims.width;
const g = breadcrumbs.selectAll('g').data(sequenceArray, d => d.name + d.depth);
// Add breadcrumb and label for entering nodes.
@@ -232,9 +270,9 @@ function Sunburst(element, props) {
entering
.append('svg:text')
- .attr('x', (breadcrumbDims.width + breadcrumbDims.tipTailWidth) / 2)
+ .attr('x', (breadcrumbWidth + breadcrumbDims.tipTailWidth) / 2)
.attr('y', breadcrumbDims.height / 4)
- .attr('dy', '0.85em')
+ .attr('dy', '0.35em')
.style('fill', d => {
// Make text white or black based on the lightness of the background
const col = d3.hsl(
@@ -245,13 +283,15 @@ function Sunburst(element, props) {
})
.attr('class', 'step-label')
.text(d => d.name.replace(/_/g, ' '))
- .call(wrapSvgText, breadcrumbDims.width, breadcrumbDims.height / 2);
+ .call(wrapSvgText, breadcrumbWidth, breadcrumbDims.height / 2);
// Set position for entering and updating nodes.
- g.attr(
- 'transform',
- (d, i) => `translate(${i * (breadcrumbDims.width + breadcrumbDims.spacing)}, 0)`,
- );
+ g.attr('transform', (d, i) => {
+ if (isSmallWidth) {
+ return `translate(0, ${i * (breadcrumbDims.height + breadcrumbDims.spacing)})`;
+ }
+ return `translate(${i * (breadcrumbDims.width + breadcrumbDims.spacing)}, 0)`;
+ });
// Remove exiting nodes.
g.exit().remove();
@@ -259,8 +299,20 @@ function Sunburst(element, props) {
// 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('x', () => {
+ if (isSmallWidth) {
+ return (breadcrumbWidth + breadcrumbDims.tipTailWidth) / 2;
+ }
+
+ return (sequenceArray.length + 0.5) * (breadcrumbDims.width + breadcrumbDims.spacing);
+ })
+ .attr('y', () => {
+ if (isSmallWidth) {
+ return (sequenceArray.length + 1) * breadcrumbDims.height;
+ }
+
+ return breadcrumbDims.height / 2;
+ })
.attr('dy', '0.35em')
.text(percentageString);
@@ -280,7 +332,7 @@ function Sunburst(element, props) {
const conditionalPercString = parentOfD ? formatPerc(conditionalPercentage) : '';
// 3 levels of text if inner-most level, 4 otherwise
- const yOffsets = ['-25', '7', '35', '60'];
+ const yOffsets = getYOffset(width);
let offsetIndex = 0;
// If metrics match, assume we are coloring by category
@@ -365,7 +417,7 @@ function Sunburst(element, props) {
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(rows) {
const root = buildHierarchy(rows);
-
+ maxBreadcrumbs = rows[0].length - 2;
vis = svg
.append('svg:g')
.attr('class', 'sunburst-vis')
@@ -373,7 +425,11 @@ function Sunburst(element, props) {
'transform',
'translate(' +
`${margin.left + visWidth / 2},` +
- `${margin.top + breadcrumbHeight + visHeight / 2}` +
+ `${
+ margin.top +
+ (isSmallWidth ? breadcrumbHeight * maxBreadcrumbs : breadcrumbHeight) +
+ visHeight / 2
+ }` +
')',
)
.on('mouseleave', mouseleave);