blob: 23cbd31b1cb1a14fe6944d1a3d98f00ac6b3df26 [file] [log] [blame]
/**
* 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 */
/* eslint-disable react/sort-prop-types */
import d3 from 'd3';
import PropTypes from 'prop-types';
import { sankey as d3Sankey } from 'd3-sankey';
import { getNumberFormatter, NumberFormats, CategoricalColorNamespace } from '@superset-ui/core';
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 = { ...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,
'</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.append(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;