/* eslint no-param-reassign: [2, {"props": false}] */ /* eslint no-use-before-define: ["error", { "functions": false }] */ import d3 from 'd3'; import { d3TimeFormatPreset, } from '../javascripts/modules/utils'; import { getColorFromScheme } from '../javascripts/modules/colors'; import './partition.css'; d3.hierarchy = require('d3-hierarchy').hierarchy; d3.partition = require('d3-hierarchy').partition; function init(root) { // Compute dx, dy, x, y for each node and // return an array of nodes in breadth-first order const flat = []; const dy = 1.0 / (root.height + 1); let prev = null; root.each((n) => { n.y = dy * n.depth; n.dy = dy; if (!n.parent) { n.x = 0; n.dx = 1; } else { n.x = prev.depth === n.parent.depth ? 0 : prev.x + prev.dx; n.dx = n.weight / n.parent.sum * n.parent.dx; } prev = n; flat.push(n); }); return flat; } // This vis is based on // http://mbostock.github.io/d3/talk/20111018/partition.html function partitionVis(slice, payload) { const data = payload.data; const fd = slice.formData; const div = d3.select(slice.selector); const metrics = fd.metrics || []; // Chart options const logScale = fd.log_scale || false; const chartType = fd.time_series_option || 'not_time'; const hasTime = ['adv_anal', 'time_series'].indexOf(chartType) >= 0; const format = d3.format(fd.number_format); const timeFormat = d3TimeFormatPreset(fd.date_time_format); div.selectAll('*').remove(); d3.selectAll('.nvtooltip').remove(); const tooltip = d3 .select('body') .append('div') .attr('class', 'nvtooltip') .style('opacity', 0) .style('top', 0) .style('left', 0) .style('position', 'fixed'); function drawVis(i, dat) { const datum = dat[i]; const w = slice.width(); const h = slice.height() / data.length; const x = d3.scale.linear().range([0, w]); const y = d3.scale.linear().range([0, h]); const viz = div .append('div') .attr('class', 'chart') .style('width', w + 'px') .style('height', h + 'px') .append('svg:svg') .attr('width', w) .attr('height', h); // Add padding between multiple visualizations if (i !== data.length - 1 && data.length > 1) { viz.style('padding-bottom', '3px'); } if (i !== 0 && data.length > 1) { viz.style('padding-top', '3px'); } const root = d3.hierarchy(datum); function hasDateNode(n) { return metrics.indexOf(n.data.name) >= 0 && hasTime; } // node.name is the metric/group name // node.disp is the display value // node.value determines sorting order // node.weight determines partition height // node.sum is the sum of children weights root.eachAfter((n) => { n.disp = n.data.val; n.value = n.disp < 0 ? -n.disp : n.disp; n.weight = n.value; n.name = n.data.name; // If the parent is a metric and we still have // the time column, perform a date-time format if (n.parent && hasDateNode(n.parent)) { // Format timestamp values n.weight = fd.equal_date_size ? 1 : n.value; n.value = n.name; n.name = timeFormat(n.name); } if (logScale) n.weight = Math.log(n.weight + 1); n.disp = n.disp && !isNaN(n.disp) && isFinite(n.disp) ? format(n.disp) : ''; }); // Perform sort by weight root.sort((a, b) => { const v = b.value - a.value; if (v === 0) { return b.name > a.name ? 1 : -1; } return v; }); // Prune data based on partition limit and threshold // both are applied at the same time if (fd.partition_threshold && fd.partition_threshold >= 0) { // Compute weight sums as we go root.each((n) => { n.sum = n.children ? n.children.reduce((a, v) => a + v.weight, 0) || 1 : 1; if (n.children) { // Dates are not ordered by weight if (hasDateNode(n)) { if (fd.equal_date_size) { return; } const removeIndices = []; // Keep at least one child for (let j = 1; j < n.children.length; j++) { if (n.children[j].weight / n.sum < fd.partition_threshold) { removeIndices.push(j); } } for (let j = removeIndices.length - 1; j >= 0; j--) { n.children.splice(removeIndices[j], 1); } } else { // Find first child that falls below the threshold let j; for (j = 1; j < n.children.length; j++) { if (n.children[j].weight / n.sum < fd.partition_threshold) { break; } } n.children = n.children.slice(0, j); } } }); } if (fd.partition_limit && fd.partition_limit >= 0) { root.each((n) => { if (n.children && n.children.length > fd.partition_limit) { if (!hasDateNode(n)) { n.children = n.children.slice(0, fd.partition_limit); } } }); } // Compute final weight sums root.eachAfter((n) => { n.sum = n.children ? n.children.reduce((a, v) => a + v.weight, 0) || 1 : 1; }); const verboseMap = slice.datasource.verbose_map; function getCategory(depth) { if (!depth) { return 'Metric'; } if (hasTime && depth === 1) { return 'Date'; } const col = fd.groupby[depth - (hasTime ? 2 : 1)]; return verboseMap[col] || col; } function getAncestors(d) { const ancestors = [d]; let node = d; while (node.parent) { ancestors.push(node.parent); node = node.parent; } return ancestors; } function positionAndPopulate(tip, d) { let t = ''; if (!fd.rich_tooltip) { t += ( '' ); t += ( '' + '' + `` + `` + '' ); } else { const nodes = getAncestors(d); nodes.forEach((n) => { const atNode = n.depth === d.depth; t += ''; t += ( `` + `' + `` + `` + `` + '' ); }); } t += '
' + `${getCategory(d.depth)}` + '
' + `
' + '
${d.name}${d.disp}
` + '
' + '
${n.name}${n.disp}${getCategory(n.depth)}
'; tip.html(t) .style('left', (d3.event.pageX + 13) + 'px') .style('top', (d3.event.pageY - 10) + 'px'); } const g = viz .selectAll('g') .data(init(root)) .enter() .append('svg:g') .attr('transform', d => `translate(${x(d.y)},${y(d.x)})`) .on('click', click) .on('mouseover', (d) => { tooltip .interrupt() .transition() .duration(100) .style('opacity', 0.9); positionAndPopulate(tooltip, d); }) .on('mousemove', (d) => { positionAndPopulate(tooltip, d); }) .on('mouseout', () => { tooltip .interrupt() .transition() .duration(250) .style('opacity', 0); }); let kx = w / root.dx; let ky = h / 1; g.append('svg:rect') .attr('width', root.dy * kx) .attr('height', d => d.dx * ky); g.append('svg:text') .attr('transform', transform) .attr('dy', '0.35em') .style('opacity', d => d.dx * ky > 12 ? 1 : 0) .text((d) => { if (!d.disp) { return d.name; } return `${d.name}: ${d.disp}`; }); // Apply color scheme g.selectAll('rect') .style('fill', (d) => { d.color = getColorFromScheme(d.name, fd.color_scheme); return d.color; }); // Zoom out when clicking outside vis // d3.select(window) // .on('click', () => click(root)); // Keep text centered in its division function transform(d) { return `translate(8,${d.dx * ky / 2})`; } // When clicking a subdivision, the vis will zoom in to it function click(d) { if (!d.children) { if (d.parent) { // Clicking on the rightmost level should zoom in return click(d.parent); } return false; } kx = (d.y ? w - 40 : w) / (1 - d.y); ky = h / d.dx; x.domain([d.y, 1]).range([d.y ? 40 : 0, w]); y.domain([d.x, d.x + d.dx]); const t = g .transition() .duration(d3.event.altKey ? 7500 : 750) .attr('transform', nd => `translate(${x(nd.y)},${y(nd.x)})`); t.select('rect') .attr('width', d.dy * kx) .attr('height', nd => nd.dx * ky); t.select('text') .attr('transform', transform) .style('opacity', nd => nd.dx * ky > 12 ? 1 : 0); d3.event.stopPropagation(); return true; } } for (let i = 0; i < data.length; i++) { drawVis(i, data); } } module.exports = partitionVis;