a solution 🎉
[tweet]
specifically, this stackoverflow answer has the workaround to solve this apparent bug in Chromium's implementation of the SVG 1.1 standard
in d3.sankey.js
, we want to alter the return value of the path generator to ensure that we never return perfectly straight paths. inserting this this new moveto
command "M" + -10 + "," + -10
on the first line does just that:
return "M" + -10 + "," + -10
+ "M" + x0 + "," + y0
+ "C" + x2 + "," + y0
+ " " + x3 + "," + y1
+ " " + x1 + "," + y1;
an iteration on by Patient Flow Sankey Particles from @micahstubbs
see also the earlier version with 13
layout iterations that happens to avoid any perfectly straight paths.
and also this earlier bug reproduction example with 14
Sankey layout iterations that does produce a couple of those problematic-for-Chromium perfectly straight SVG paths
inspired by the blog post Data-based and unique gradients for visualizations with d3.js and associated example Data based gradients - Simple - Solar system from @nadiehbremer
forked from micahstubbs's block: Sankey Gradients - Missing Gradient Solution
forked from micahstubbs's block: origin destination map experiment
forked from micahstubbs's block: origin destination map experiment - confluent nodes
xxxxxxxxxx
<meta charset='utf-8'>
<title>Sankey Gradients</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/4.4.0/d3.min.js'></script>
<script src='d3.sankey.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.19.0/babel.min.js'></script>
<style>
g rect {
cursor: move;
fill-opacity: 0.8;
shape-rendering: crispEdges;
}
g path {
stroke-opacity: 0.8
}
g text {
pointer-events: none;
font-family: Helvetica;
font-size: 12px;
}
</style>
<body>
<div id='chart'>
<script lang='babel' type='text/babel'>
const units = '';
const margin = {top: 10, right: 10, bottom: 10, left: 10};
const sankeyMargin = {top: 0, right: 180, bottom: 0, left: 0};
const width = 960 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
const minard = '#E1C074';
// zero decimal places
const formatNumber = d3.format(',.0f');
​
const format = d => `${formatNumber(d)} ${units}`;
​
const color = d3.scaleOrdinal()
.domain([
'All referred patients',
'First consult outpatient clinic',
'OR-receipt',
'Start surgery',
// 'No OR-receipt',
// 'No emergency',
// 'No surgery',
'Emergency'
])
.range([
'#90eb9d',
'#f9d057',
'#f29e2e',
'#00ccbc',
'#d7191c'
]);
​
d3.select('#chart')
.style('visibility', 'visible');
​
// append the svg canvas to the page
const svg = d3.select('#chart').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
​
// set the sankey diagram properties
const sankeyWidth = width - sankeyMargin.right;
const sankey = d3.sankey()
.nodeWidth(12)
.nodePadding(50)
.size([sankeyWidth, height]);
​
const path = sankey.link();
​
// append a defs (for definition) element to your SVG
const defs = svg.append('defs');
​
// load the data
d3.json('data.json', (error, graph) => {
console.log('graph', graph);
sankey
.nodes(graph.nodes)
.links(graph.links)
.layout(14); // any value > 13 breaks the link gradient
// add in the links
const link = svg.append('g').selectAll('.link')
.data(graph.links)
.enter().append('path')
.attr('class', 'link')
.attr('d', path)
.style('stroke-width', d => Math.max(1, d.dy))
.style('fill', 'none')
.sort((a, b) => b.dy - a.dy)
.on('mouseover', function() {
d3.select(this).style('stroke-opacity', 0.5);
d3.selectAll('text').style('fill', 'white');
})
.on('mouseout', function() {
d3.select(this).style('stroke-opacity', 0.8);
d3.selectAll('text').style('fill', 'none');
});
// add the link titles
link.append('title')
.text(d => `${d.source.name} → ${d.target.name}\n${format(d.value)}`);
// add in the nodes
const node = svg.append('g').selectAll('.node')
.data(graph.nodes)
.enter().append('g')
.attr('class', 'node')
.attr('transform', d => `translate(${d.x},${d.y})`)
.attr('class', d => d.name)
.call(d3.drag()
.subject(d => d)
.on('start', function() {
this.parentNode.appendChild(this); })
.on('drag', dragmove));
// add the rectangles for the nodes
node.append('rect')
.attr('height', d => d.dy)
.attr('width', sankey.nodeWidth())
.style('fill', d => {
if(color.domain().indexOf(d.name) > -1){
return d.color = color(d.name);
} else {
return d.color = minard;
}
})
.append('title')
.text(d => `${d.name}\n${format(d.value)}`);
// add in the title for the nodes
node.append('text')
.attr('x', -6)
.attr('y', d => d.dy / 2)
.attr('dy', '.35em')
.attr('text-anchor', 'end')
.attr('transform', null)
//.style('fill', 'white')
.style('fill', 'none')
.text(d => `${d.id} ${d.name}`)
.filter(d => d.x < width / 2)
.attr('x', 6 + sankey.nodeWidth())
.attr('text-anchor', 'start');
d3.selectAll('.destination')
.append('path')
.style('opacity', 0.8)
.style('fill', minard)
.attr('d', d => `M ${d.dy*0.5},${d.dy*0.5} 0,${d.dy} 0,0 z`)
.attr('transform', d => `translate(${sankey.nodeWidth()},0)`)
// try to hide the hairline gap between
// confluent nodes and adjacent paths
d3.selectAll('.confluence rect')
//.style('stroke-width', '2px')
//.style('stroke-opacity', 0.8)
//.style('stroke', minard)
//.attr('transform', 'translate(-1,0)')
//.attr('width', sankey.nodeWidth() + 1)
​
// add gradient to links
link.style('stroke', (d, i) => {
console.log('d from gradient stroke func', d);
​
// make unique gradient ids
const gradientID = `gradient${i}`;
​
const startColor = d.source.color;
const stopColor = d.target.color;
​
console.log('startColor', startColor);
console.log('stopColor', stopColor);
​
const linearGradient = defs.append('linearGradient')
.attr('id', gradientID);
​
linearGradient.selectAll('stop')
.data([
{offset: '10%', color: startColor },
{offset: '90%', color: stopColor }
])
.enter().append('stop')
.attr('offset', d => {
console.log('d.offset', d.offset);
return d.offset;
})
.attr('stop-color', d => {
console.log('d.color', d.color);
return d.color;
});
​
return `url(#${gradientID})`;
})
// the function for moving the nodes
function dragmove(d) {
d3.select(this).attr('transform',
`translate(${d.x = Math.max(0, Math.min(width - d.dx, d3.event.x))},${d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))})`);
sankey.relayout();
link.attr('d', path);
}
});
</script>
</body>
</html>
https://cdnjs.cloudflare.com/ajax/libs/d3/4.4.0/d3.min.js
https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.19.0/babel.min.js