in src/lambda_functions/cw_custom_widget_nodejs/widgets/conversationPath.js [14:128]
function displaySankey({ data, widgetContext } = {}) {
// based on:
// https://bl.ocks.org/tomshanley/6f3fcf68c0dbc401548733dd0c64e3c3
const { document } = new JSDOM().window;
// set the dimensions and margins
const margin = {
top: 20,
right: 20,
bottom: 20,
left: 20,
};
const width = widgetContext.width - margin.left - margin.right;
const height = widgetContext.height - margin.top - margin.bottom;
// append the svg object to the body of the page
const svg = d3
.select(document.body)
.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})`);
const sankey = d3SankeyCircular()
.nodeId((d) => d.name)
.nodeWidth(30)
.nodePaddingRatio(0.8)
.nodeAlign(sankeyJustify)
.circularLinkGap(20)
.extent([
[5, 5],
[width - 5, height - 5],
]);
const { nodes, links } = sankey(data);
const color = d3.scaleSequential(d3.interpolateCool).domain([0, width]);
function getTitle(d) {
const totalValue = d3.sum(
nodes.filter((node) => node.name !== START_NODE_NAME),
(i) => i.value // eslint-disable-line comma-dangle
);
const nodePercent = d3.format('.0%')(d.value / totalValue);
if (d.name === START_NODE_NAME) {
return `${d.name}`;
}
return `${d.name}\n${d.value} (${nodePercent})`;
}
svg
.append('g')
.selectAll('rect')
.data(nodes)
.join('rect')
.attr('x', (d) => d.x0)
.attr('y', (d) => d.y0)
.attr('height', (d) => Math.max(20, d.y1 - d.y0))
.attr('width', (d) => Math.max(20, d.x1 - d.x0))
.style('fill', (d) => color(d.x0))
.style('opacity', 0.5)
.append('title')
.text((d) => getTitle(d));
svg
.append('g')
.attr('font-family', 'sans-serif')
.attr('font-size', 14)
.attr('fill', '#202630')
.selectAll('text')
.data(nodes)
.join('text')
.attr('x', (d) => (d.x0 < width / 2 ? d.x0 : d.x1))
.attr('y', (d) => (d.y1 < width / 2 ? d.y1 + 12 : d.y0 - 12))
.attr('dy', '0.35em')
.attr('text-anchor', (d) => (d.x0 < width / 2 ? 'start' : 'end'))
.text((d) => `${d.name}`)
.append('title')
.text((d) => getTitle(d));
svg
.append('defs')
.append('marker')
.attr('id', 'arrow')
.attr('viewBox', [0, -5, 10, 10])
.attr('refX', 8)
.attr('refY', 0)
.attr('markerWidth', 2)
.attr('markerHeight', 2)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill-opacity', 0.05)
.attr('stroke-opacity', 0.65)
.attr('stroke', 'black');
svg
.append('g')
.attr('class', 'links')
.attr('fill', 'none')
.attr('stroke-opacity', 0.2)
.selectAll('path')
.data(links)
.enter()
.append('path')
.attr('d', (d) => d.path)
.attr('marker-end', 'url(#arrow)')
.style('stroke-width', (d) => Math.max(4, d.width / 10))
.style('stroke', (link) => (link.circular ? 'red' : 'black'))
.append('title')
.text((d) => `${d.source.name} → ${d.target.name}`);
return document.body.innerHTML;
}