draw: function (options, width) { var data = options.data; var clickHandler = options.clickHandler; var mouseOverHandler = options.mouseOverHandler; var mouseLeaveHandler = options.mouseLeaveHandler; var height; var color; if (width > AppConstants.SUNBURST_MAX_WIDTH) { width = AppConstants.SUNBURST_MAX_WIDTH; } height = width; if (!data) { return; } radius = Math.min(width, height) / 2.2; color = d3.scale.category20c(); x = d3.scale.linear().range([0, 2 * Math.PI]); y = d3.scale.sqrt().range([0, radius]); partition = d3.layout.partition().value(function (d) { return d.size; }); arc = d3.svg.arc() .startAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); }) .endAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); }) .innerRadius(function (d) { return Math.max(0, y(d.y)); }) .outerRadius(function (d) { return Math.max(0, y(d.y + d.dy)); }); d3.select('#sunburst-chart svg').remove(); svg = d3.select('#sunburst-chart') .append('svg') .attr('width', width) .attr('height', height) .append('g') .attr('id', 'sunburstd3-chart-container') .attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')'); // fill data into svg svg.selectAll('path') .data(partition.nodes(data)) .enter().append('path') .attr('d', arc) .style('fill', function (d) { if (!AppConstants.SUNBURST_ARC_COLORS[d.name]) { AppConstants.SUNBURST_ARC_COLORS[d.name] = color(sum(d)); } return AppConstants.SUNBURST_ARC_COLORS[d.name]; }) .on('click', clickHandler) .on('touchstart', clickHandler) .on('mouseover', mouseOverHandler); d3.select('#sunburstd3-chart-container') .on('mouseleave', mouseLeaveHandler); },
/** * Represents CPU flame chart. * @constructor * @param {Object} parent - Parent element for flame chart. * @param {Object} data - Data for flame chart rendering. */ function FlameChart(parent, data) { this.PAD_SIZE = 10; this.HEIGHT = parent.node().scrollHeight - this.PAD_SIZE; this.WIDTH = parent.node().scrollWidth - this.PAD_SIZE; this.TEXT_OFFSET_X = 5; this.TEXT_OFFSET_Y= 14; this.TEXT_CUTOFF = 0.075 * this.WIDTH; this.LEGEND_X = this.WIDTH - 400; this.LEGEND_Y = 100; this.data_ = data; this.parent_ = parent; this.xScale_ = d3.scale.linear().domain([0, 1]).range([0, this.WIDTH]); this.yScale_ = d3.scale.linear().range([0, this.HEIGHT]); this.color_ = d3.scale.category10(); this.flameChart_ = d3.layout.partition() .sort(null) .value(function(d) { return d.cumTime; }); }
// Modified from http://bl.ocks.org/kerryrodden/7090426 function sunburstVis(slice, payload) { const container = d3.select(slice.selector); // vars with shared scope within this function const margin = { top: 10, right: 5, bottom: 10, left: 5 }; const containerWidth = slice.width(); const containerHeight = slice.height(); const breadcrumbHeight = containerHeight * 0.085; const visWidth = containerWidth - margin.left - margin.right; const visHeight = containerHeight - margin.top - margin.bottom - breadcrumbHeight; const radius = Math.min(visWidth, visHeight) / 2; let colorByCategory = true; // color by category if primary/secondary metrics match let maxBreadcrumbs; let breadcrumbDims; // set based on data let totalSize; // total size of all segments; set after loading the data. let colorScale; let breadcrumbs; let vis; let arcs; let gMiddleText; // dom handles // Helper + path gen functions const partition = d3.layout.partition() .size([2 * Math.PI, radius * radius]) .value(function (d) { return d.m1; }); const arc = d3.svg.arc() .startAngle(d => d.x) .endAngle(d => d.x + d.dx) .innerRadius(function (d) { return Math.sqrt(d.y); }) .outerRadius(function (d) { return Math.sqrt(d.y + d.dy); }); const formatNum = d3.format('.3s'); const formatPerc = d3.format('.3p'); container.select('svg').remove(); const svg = container.append('svg:svg') .attr('width', containerWidth) .attr('height', containerHeight); function createBreadcrumbs(rawData) { const firstRowData = rawData.data[0]; // -2 bc row contains 2x metrics, +extra for %label and buffer maxBreadcrumbs = (firstRowData.length - 2) + 1; breadcrumbDims = { width: visWidth / maxBreadcrumbs, height: breadcrumbHeight * 0.8, // more margin spacing: 3, tipTailWidth: 10, }; breadcrumbs = svg.append('svg:g') .attr('class', 'breadcrumbs') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); breadcrumbs.append('svg:text') .attr('class', 'end-label'); } // Given a node in a partition layout, return an array of all of its ancestor // nodes, highest first, but excluding the root. function getAncestors(node) { const path = []; let current = node; while (current.parent) { path.unshift(current); current = current.parent; } return path; } // 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)); } return points.join(' '); } function updateBreadcrumbs(sequenceArray, percentageString) { const g = breadcrumbs.selectAll('g') .data(sequenceArray, function (d) { return d.name + d.depth; }); // Add breadcrumb and label for entering nodes. const entering = g.enter().append('svg:g'); entering.append('svg:polygon') .attr('points', breadcrumbPoints) .style('fill', function (d) { return colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1); }); entering.append('svg:text') .attr('x', (breadcrumbDims.width + breadcrumbDims.tipTailWidth) / 2) .attr('y', breadcrumbDims.height / 4) .attr('dy', '0.35em') .style('fill', function (d) { // Make text white or black based on the lightness of the background const col = d3.hsl(colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1)); return col.l < 0.5 ? 'white' : 'black'; }) .attr('class', 'step-label') .text(function (d) { return d.name.replace(/_/g, ' '); }) .call(wrapSvgText, breadcrumbDims.width, breadcrumbDims.height / 2); // Set position for entering and updating nodes. g.attr('transform', function (d, i) { return 'translate(' + i * (breadcrumbDims.width + breadcrumbDims.spacing) + ', 0)'; }); // Remove exiting nodes. g.exit().remove(); // 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('dy', '0.35em') .text(percentageString); // Make the breadcrumb trail visible, if it's hidden. breadcrumbs.style('visibility', null); } // Fade all but the current sequence, and show it in the breadcrumb trail. function mouseenter(d) { const sequenceArray = getAncestors(d); const parentOfD = sequenceArray[sequenceArray.length - 2] || null; const absolutePercentage = (d.m1 / totalSize).toPrecision(3); const conditionalPercentage = parentOfD ? (d.m1 / parentOfD.m1).toPrecision(3) : null; const absolutePercString = formatPerc(absolutePercentage); const conditionalPercString = parentOfD ? formatPerc(conditionalPercentage) : ''; // 3 levels of text if inner-most level, 4 otherwise const yOffsets = ['-25', '7', '35', '60']; let offsetIndex = 0; // If metrics match, assume we are coloring by category const metricsMatch = Math.abs(d.m1 - d.m2) < 0.00001; gMiddleText.selectAll('*').remove(); gMiddleText.append('text') .attr('class', 'path-abs-percent') .attr('y', yOffsets[offsetIndex++]) .text(absolutePercString + ' of total'); if (conditionalPercString) { gMiddleText.append('text') .attr('class', 'path-cond-percent') .attr('y', yOffsets[offsetIndex++]) .text(conditionalPercString + ' of parent'); } gMiddleText.append('text') .attr('class', 'path-metrics') .attr('y', yOffsets[offsetIndex++]) .text('m1: ' + formatNum(d.m1) + (metricsMatch ? '' : ', m2: ' + formatNum(d.m2))); gMiddleText.append('text') .attr('class', 'path-ratio') .attr('y', yOffsets[offsetIndex++]) .text((metricsMatch ? '' : ('m2/m1: ' + formatPerc(d.m2 / d.m1)))); // Reset and fade all the segments. arcs.selectAll('path') .style('stroke-width', null) .style('stroke', null) .style('opacity', 0.7); // Then highlight only those that are an ancestor of the current segment. arcs.selectAll('path') .filter(function (node) { return (sequenceArray.indexOf(node) >= 0); }) .style('opacity', 1) .style('stroke-width', '2px') .style('stroke', '#000'); updateBreadcrumbs(sequenceArray, absolutePercString); } // Restore everything to full opacity when moving off the visualization. function mouseleave() { // Hide the breadcrumb trail breadcrumbs.style('visibility', 'hidden'); gMiddleText.selectAll('*').remove(); // Deactivate all segments during transition. arcs.selectAll('path').on('mouseenter', null); // Transition each segment to full opacity and then reactivate it. arcs.selectAll('path') .transition() .duration(200) .style('opacity', 1) .style('stroke', null) .style('stroke-width', null) .each('end', function () { d3.select(this).on('mouseenter', mouseenter); }); } function buildHierarchy(rows) { const root = { name: 'root', children: [], }; // each record [groupby1val, groupby2val, (<string> or 0)n, m1, m2] for (let i = 0; i < rows.length; i++) { const row = rows[i]; const m1 = Number(row[row.length - 2]); const m2 = Number(row[row.length - 1]); const levels = row.slice(0, row.length - 2); if (isNaN(m1)) { // e.g. if this is a header row continue; } let currentNode = root; for (let level = 0; level < levels.length; level++) { const children = currentNode.children || []; const nodeName = levels[level]; // If the next node has the name '0', it will const isLeafNode = (level >= levels.length - 1) || levels[level + 1] === 0; let childNode; let currChild; if (!isLeafNode) { // Not yet at the end of the sequence; move down the tree. let foundChild = false; for (let k = 0; k < children.length; k++) { currChild = children[k]; if (currChild.name === nodeName && currChild.level === level) { // must match name AND level childNode = currChild; foundChild = true; break; } } // If we don't already have a child node for this branch, create it. if (!foundChild) { childNode = { name: nodeName, children: [], level, }; children.push(childNode); } currentNode = childNode; } else if (nodeName !== 0) { // Reached the end of the sequence; create a leaf node. childNode = { name: nodeName, m1, m2, }; children.push(childNode); } } } function recurse(node) { if (node.children) { let sums; let m1 = 0; let m2 = 0; for (let i = 0; i < node.children.length; i++) { sums = recurse(node.children[i]); m1 += sums[0]; m2 += sums[1]; } node.m1 = m1; node.m2 = m2; } return [node.m1, node.m2]; } recurse(root); return root; } // Main function to draw and set up the visualization, once we have the data. function createVisualization(rawData) { const tree = buildHierarchy(rawData.data); vis = svg.append('svg:g') .attr('class', 'sunburst-vis') .attr('transform', ( 'translate(' + `${(margin.left + (visWidth / 2))},` + `${(margin.top + breadcrumbHeight + (visHeight / 2))}` + ')' )) .on('mouseleave', mouseleave); arcs = vis.append('svg:g') .attr('id', 'arcs'); gMiddleText = vis.append('svg:g') .attr('class', 'center-label'); // Bounding circle underneath the sunburst, to make it easier to detect // when the mouse leaves the parent g. arcs.append('svg:circle') .attr('r', radius) .style('opacity', 0); // For efficiency, filter nodes to keep only those large enough to see. const nodes = partition.nodes(tree) .filter(function (d) { return (d.dx > 0.005); // 0.005 radians = 0.29 degrees }); let ext; const fd = slice.formData; if (fd.metric !== fd.secondary_metric) { colorByCategory = false; ext = d3.extent(nodes, d => d.m2 / d.m1); colorScale = d3.scale.linear() .domain([ext[0], ext[0] + ((ext[1] - ext[0]) / 2), ext[1]]) .range(['#00D1C1', 'white', '#FFB400']); } const path = arcs.data([tree]).selectAll('path') .data(nodes) .enter() .append('svg:path') .attr('display', function (d) { return d.depth ? null : 'none'; }) .attr('d', arc) .attr('fill-rule', 'evenodd') .style('fill', d => colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1)) .style('opacity', 1) .on('mouseenter', mouseenter); // Get total size of the tree = value of root node from partition. totalSize = path.node().__data__.value; } createBreadcrumbs(payload); createVisualization(payload); }
PieChart.prototype.addPath = function (width, height, svg, slices) { var self = this; var marginFactor = 0.95; var isDonut = self._attr.isDonut; var radius = (Math.min(width, height) / 2) * marginFactor; var color = self.handler.data.getPieColorFunc(); var tooltip = self.tooltip; var isTooltip = self._attr.addTooltip; var partition = d3.layout.partition() .sort(null) .value(function (d) { return d.percentOfParent * 100; }); var x = d3.scale.linear() .range([0, 2 * Math.PI]); var y = d3.scale.sqrt() .range([0, radius]); var arc = d3.svg.arc() .startAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); }) .endAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); }) .innerRadius(function (d) { // option for a single layer, i.e pie chart if (d.depth === 1 && !isDonut) { // return no inner radius return 0; } return Math.max(0, y(d.y)); }) .outerRadius(function (d) { return Math.max(0, y(d.y + d.dy)); }); var path = svg .datum(slices) .selectAll('path') .data(partition.nodes) .enter() .append('path') .attr('d', arc) .attr('class', function (d) { if (d.depth === 0) { return; } return 'slice'; }) .call(self._addIdentifier, 'name') .style('stroke', '#fff') .style('fill', function (d) { if (d.depth === 0) { return 'none'; } return color(d.name); }); if (isTooltip) { path.call(tooltip.render()); } return path; };
domready(function() { var root = window.disc , width = window.innerWidth , height = Math.max(window.innerHeight - 100, 100) , radius = Math.min(width, height) * 0.45 , deg = 120 var svg = d3.select('.chart').append('svg') .attr('width', width) .attr('height', height) .append('g') .attr('transform', 'translate(' + width / 2 + ',' + height * .52 + ')') var paletteDiv = d3.select('.palette-wrap') .style('top', String(window.innerHeight - (schemes.length - 1) * 56 - 16) + 'px') .selectAll('.palette') .data(schemes) .enter() .append('div') .classed('scheme-icon', true) paletteDiv.append('span') .classed('scheme-text', true) .text(function(d) { return d.name }) var palettes = paletteDiv .append('svg') .style('display', 'inline-block') .classed('palette', true) .on('click', function(d, i) { useScheme(i, path.transition() .duration(600) .ease(bounce_high, 1000) .delay(function(d, i) { return d.x * 100 + d.y / maxdepth * 0.06125 }) ) }) palettes.append('rect') .attr('width', 23) .attr('height', 48) .style('fill', function(d) { return d.background }) palettes.selectAll('.color') .data(function(d) { return d.all }) .enter() .append('rect') .style('fill', function(d) { return d }) .attr('x', 25) .attr('y', function(d, i, j) { return 48 * i / schemes[j].all.length - 1 }) .attr('width', 22) .attr('height', function(d, i, j) { return 48 / schemes[j].all.length - 1 }) var partition = d3.layout.partition() .sort(null) .size([2 * Math.PI, radius * radius]) .value(modeFns[modeInitial]) // // Creates the title text in // the center of the rings. // var title = svg.append('text') .text(root.name) .attr('x', 0) .attr('y', -5) .style('font-size', '12px') .style('fill', 'white') .style('font-weight', 500) .style('alignment-baseline', 'middle') .style('text-anchor', 'middle') // // Likewise, this is the file // size stat below the title // var size = svg.append('text') .text(pretty(root.size)) .attr('x', 0) .attr('y', 15) .style('fill', 'white') .style('font-size', '10px') .style('alignment-baseline', 'middle') .style('text-anchor', 'middle') // // Each arc is wrapped in a group element, // to apply rotation transforms while // changing size and shape. // var groups = svg.datum(root).selectAll('g') .data(partition.nodes) .enter() .append('g') .attr('transform', 'rotate(' + deg + ')') var maxdepth = groups[0].reduce(function(max, el) { return Math.max(max, el.__data__.depth) }, 0) // // Actually create the arcs for each // file. // var path = groups.append('path') .attr('d', initArc) .attr('display', function(d) { return d.depth ? null : 'none' }) .style('stroke', '#2B2B2B') .style('stroke-width', '0') .style('fill-rule', 'evenodd') .each(function(d) { d.x0 = d.x d.dx0 = d.dx d.el = this }) // // Colour scheme functionality. // // Triggered immediately with the default // scheme, must be passed a d3 selection. // var background , scheme = 0 , specials , color useScheme(scheme, path) function useScheme(n, path) { background = schemes[n].background specials = schemes[n].specials palettes.each(function(d, i) { d3.select(this.parentNode) .classed('selected', function() { return i === n }) }) palettes .transition() .ease('bounce') .duration(500) .attr('height', function(d, i) { return i === n ? 0 : 48 }) ;[d3.select('body') , d3.select('html')].forEach(function(el) { el.transition() .ease('sin-in-out') .duration(600) .style('background', background) }) var colors = schemes[n].main Object.keys(specials).forEach(function(key) { var idx = colors.indexOf(specials[key].toLowerCase()) if (idx === -1) return colors.splice(idx, 1) }) color = d3.scale .ordinal() .range(colors) path.style('fill', function(d) { var name = d.children ? d.name : d.parent.name d.c = schemes[n].modifier.call(d , specials[name] || color(name) , root ) return d.c }) } path.transition() .duration(1000) .ease('elastic', 2, 1) .delay(function(d, i) { return d.x * 100 + (i % 4) * 250 + d.y / maxdepth * 0.25 }) .attr('d', arc) // // Rotates the newly created // arcs back towards their original // position. // groups.transition() .duration(3250) .delay(function(d, i) { return d.x * 100 + (i % 4) * 250 + d.y / maxdepth * 0.25 + 250 }) .attrTween('transform', rotateTween(deg)) groups.on('mouseover', function(d) { highlight(d) title.text(d.name) size.text(pretty(d.size)) }).on('mouseout', function(d) { unhighlight(d) title.text(root.name) size.text(pretty(root.size)) }) highlight.tween = hoverTween(1) function highlight(d) { if (d.el) d3.select(d.el) .transition() .delay(function(d) { return (d.depth - 1) * 300 / maxdepth }) .ease('back-out', 10) .duration(500) .attrTween('d', highlight.tween) .style('fill', function(d) { return d.c }) if (d.children) { var i = d.children.length while (i--) highlight(d.children[i]) } } unhighlight.tween = hoverTween(0) function unhighlight(d) { if (d.el) d3.select(d.el) .transition() .delay(function(d) { return (d.depth - 1) * 300 / maxdepth }) .ease('back-out', 4) .duration(500) .attrTween('d', unhighlight.tween) .style('fill', function(d) { return d.c }) if (d.children) { var i = d.children.length while (i--) unhighlight(d.children[i]) } } var modes = d3.selectAll('[data-mode]') updateMode(modeInitial) modes.on('click', function() { updateMode( this.getAttribute('data-mode') , true) }) function updateMode(mode, update) { value = modeFns[mode] || value modes.style('opacity', function(d) { return mode === ( this.mode = this.mode || this.getAttribute('data-mode') ) ? 1 : null }) if (!update) return groups .data(partition.value(value).nodes) .select('path') .transition() .duration(1500) .attrTween('d', arcTween) } })
"account": "#6ab975", "other": "#a173d1", "end": "#bbbbbb" }; // Total size of all segments; we set this later, after loading the data. var totalSize = 0; var vis = d3.select("#chart").append("svg:svg") .attr("width", width) .attr("height", height) .append("svg:g") .attr("id", "container") .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); var partition = d3.layout.partition() .size([2 * Math.PI, radius * radius]) .value(function(d) { return d.size; }); var arc = d3.svg.arc() .startAngle(function(d) { return d.x; }) .endAngle(function(d) { return d.x + d.dx; }) .innerRadius(function(d) { return Math.sqrt(d.y); }) .outerRadius(function(d) { return Math.sqrt(d.y + d.dy); }); // Use d3.text and d3.csv.parseRows so that we do not need to have a header // row, and can receive the csv as an array of arrays. d3.text("visit-sequences.csv", function(text) { var csv = d3.csv.parseRows(text); var json = buildHierarchy(csv); createVisualization(json);
constructor(selection) { super(selection); var _Chart = this; // load configs configs.forEach(function (item) { _Chart.configs.set(item.name, item); }); // accessors this.accessor('name', function (d) { return d[d.length-2]; }); // formatters var percent = d3.format('%'); var overUnder = d3.format('+%'); // scales this.x = d3.scale.linear(); this.y = d3.scale.linear(); this.color = d3.scale.ordinal(); // layouts this.partition = d3.layout.partition() .value(function(d) { return d[d.length - 1]; }); // setup main group this._group = this.base.append('g'); // Set up tool tip; this._tooltip = new KotoTooltip(this.base.append('g')); this._tooltip.config({ opacity: 1, format: 'textRectText' }); this._tooltipValue = this._tooltip._appends.append('text') .style('font-family', 'Open Sans'); this._tooltipRect = this._tooltip._appends.append('rect'); this._tooltipLabel = this._tooltip._appends.append('text') .style('font-family', 'Open Sans'); this._tooltip.trigger('draw'); this._tooltip.trigger('remove'); this.on('external:click', function (d) { click(d); }); // Setup Layers var icicle = this.layer('icicle', this._group, { dataBind: function (data) { var root = _Chart.partition(data[0]); if (_Chart._targetData) { var target = _Chart.partition(_Chart._targetData[0]); for (var i=0; i < root.length; i++) { root[i].target = target[i].value; } } return this.selectAll('g').data(root); }, insert: function () { return this.append('g') .on('click', click) .on('mouseover', function (d) { _Chart._group.selectAll('rect') .filter(function (datum) { var thingName = d.name ? d.name : d[d.length - 2]; return !isRelated(d, datum); }) .style('fill', '#e3e3e3'); _Chart._tooltipValue .text(d.name ? d.name : _Chart.accessor('name')(d)); _Chart._tooltipRect.style('fill', '#000'); _Chart._tooltipLabel .text(percent(d.value)); if (_Chart._targetData) { _Chart._tooltipRect2.style('fill', '#000'); _Chart._tooltipLabel2 .text(overUnder(d.value - d.target)); } _Chart._tooltip.trigger('draw'); }) .on('mousemove', function () { var coordinates = d3.mouse(_Chart.base.node()); _Chart._tooltip.trigger('moveTo', {x: coordinates[0], y: coordinates[1]}, null, _Chart.config('width'), _Chart.config('height')); }) .on('mouseout', function (d) { _Chart._group.selectAll('rect') .style('fill', function(d) { return _Chart.color(d.base); }); _Chart._tooltip.trigger('remove'); }); } }); // layer life-cycle events icicle.on('enter', function () { this.append('rect') .style('stroke', '#fff'); if (_Chart._targetData) { this.append('path'); } this.append('text') .style('pointer-events', 'none'); return this; }) .on('merge', function () { // boxes this.select('rect') .attr('x', function(d) { return _Chart.x(d.x); }) .attr('y', function(d) { return _Chart.y(d.y); }) .attr('width', function(d) { return _Chart.x(d.dx); }) .attr('height', 0) .style('fill', function(d) { d.base = getParent(d, _Chart.rootName); return _Chart.color(d.base); }); // over-under indicator if (_Chart._targetData) { this.select('path') .style('fill', '#555') .style('opacity', 0) .attr({ d: function (d) { var symbol = d3.svg.symbol(); var gen = d.value > d.target ? symbol.type('triangle-up') : d.value < d.target ? symbol.type('triangle-down') : symbol.type('triangle-down'); return gen(); }, transform: function (d) { var left = _Chart.x(d.x) + 15; var top = _Chart.y(d.y) + 15; return `translate(${left},${top})`; } }); } // labels this.select('text') .text(function (d) { return d.name ? d.name : d[d.length - 2]; }) .attr('x', function(d) { return _Chart.x(d.x); }) .attr('dx', _Chart._targetData ? 25 : 5) .attr('y', function(d) { return _Chart.y(d.y); }) .attr('dy', 16) .style('alignment-baseline', 'middle') .style('fill', '#555') .style('font-family', 'Open Sans') .style('opacity', 0); }) .on('merge:transition', function () { var totalDuration = _Chart.config('introDuration'); var duration = totalDuration/(_Chart.max-1); // rects this .duration(duration) .ease('linear') .delay(function (d) { return d.depth * duration; }) .select('rect') .attr('height', function(d) { return _Chart.y(d.dy); }); // paths if (_Chart._targetData) { this .delay(totalDuration) .selectAll('path') .style('opacity', function (d) { var shouldShow = d.value > d.target || d.value < d.target; var canFit = _Chart.x(d.dx) > 25; return shouldShow && canFit ? 1 : 0; }); } // linear this .duration(duration) .ease('linear') .delay(function (d) { return d.depth * duration; }) .select('text') .style('opacity', function (d) { d.textLength = this.getComputedTextLength(); return (d.textLength + 30) < _Chart.x(d.dx) ? 1 : 0; }); return this; }); // click event handler function click(d) { _Chart.x.domain([d.x, d.x + d.dx]); _Chart.y.domain([d.y, 1]).range([d.y ? 20 : 0, _Chart.config('height')]); // transition Rects _Chart._group.selectAll('rect') .transition() .duration(_Chart.config('transitionDuration')) .attr('x', function(d) { return _Chart.x(d.x); }) .attr('y', function(d) { return _Chart.y(d.y); }) .attr('width', function(d) { return _Chart.x(d.x + d.dx) - _Chart.x(d.x); }) .attr('height', function(d) { return _Chart.y(d.y + d.dy) - _Chart.y(d.y); }); // transition paths if (_Chart._targetData) { _Chart._group.selectAll('path') .transition() .duration(_Chart.config('transitionDuration')) .attr('transform', function (d) { var left = _Chart.x(d.x) + 15; var top = _Chart.y(d.y) + 15; return `translate(${left},${top})`; }); } // transition text _Chart._group.selectAll('text') .transition() .duration(_Chart.config('transitionDuration')) .attr('x', function(d) { return _Chart.x(d.x); }) .attr('y', function(d) { return _Chart.y(d.y); }) .style('opacity', function (d) { return (d.textLength + 12) < (_Chart.x(d.x + d.dx) - _Chart.x(d.x)) ? 1 : 0; }); } // check to see if nodes are related function isRelated(thing, relative) { return (isChild(thing, relative) || isParent(thing, relative)); } // check to see if node is child function isChild(thing, relative) { if (thing === relative) { return true; } if (!thing.parent) { return false; } return isChild(thing.parent, relative); } // check to see if node is parent function isParent (thing, relative) { var i; var temp; if (thing === relative) { return true; } if (!thing.children) { return false; } for (i=0; i < thing.children.length; i++) { if (isParent(thing.children[i], relative)) { return true; } } return false; } // get parents name - for color function getParent (d, rootName) { if (!d.parent) { return d.name; } if (d.parent.name === rootName) { return d.name; } return getParent(d.parent, rootName); } }
, render: function () { document.querySelector('#viz-style').href = '/who.css' document.querySelector('main').innerHTML = '' var width = 300, height = 300, radius = 50 * Math.max(width, height) / 100, x = d3.scale.linear().range([0, 2 * Math.PI]), y = d3.scale.pow().exponent(1.3).domain([0, 1]).range([0, radius]), padding = 5, duration = 1500; var color = d3.scale.ordinal().domain([0, 15000000]).range(["#64BCBB", "#49A14C", "#10716F", "#A3D3D2", "#49A14C", "#72AE69", "#B5D0AB", "#D8E4D1", "#67576A"]); var div = d3.select("#vis"); //format currency var format = d3.format(",f"); var format1 = d3.format("%"); var content = d3.select("#col2"); //var content1 = d3.select("#annotation"); div.select("img").remove(); var svg = d3.select("body").append("svg").attr("width", width + padding * 2).attr("height", height + padding * 2).append("g").attr("transform", "translate(" + [radius + padding, radius + padding] + ")"); //div.append("p") // .attr("id", "intro") //.text("Click to zoom!"); var partition = d3.layout.partition().value(function (d) { return d.amount; }); var arc = d3.svg.arc().startAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); }) .endAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); }) .innerRadius(function (d) { return Math.max(0, y(d.y)); }) .outerRadius(function (d) { return Math.max(0, y(d.y + d.dy)); }); d3.json("who.json", function (error, root) { var path = svg.selectAll("path").data(partition.nodes(root)).enter().append("path").attr("id", function (d, i) { return "path-" + i; }) .attr("d", arc).attr("fill-rule", "evenodd").style("fill", function (d) { return color((d.children ? d : d.parent).name); }) .on("click", click).on("mouseover", mouseover).on("mouseout", mouseout); //add text var text = svg.selectAll("text").data(partition.nodes(root)); var textEnter = text.enter().append("text") //initial opacity //hides all those but the inner ring .style("fill-opacity", function (d) { //if the depth is 1, innermost, then it's seen if (d.depth === 1) { return 1; } //else the depth is not one, then it's hidden else { return 0; } }) //color fill //#000000 is black .style("fill", "#000000").attr("text-anchor", function (d) { return x(d.x + d.dx / 2) > Math.PI ? "end" : "start"; }) .attr("dy", ".2em") //checks for multiline names .attr("transform", function (d) { var multiline = (d.name || "").split(" ").length > 1.5, angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90, rotate = angle + (multiline ? -.5 : 0); return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")"; }) .on("click", click) //added mouseover and mouseout for the text as well. .on("mouseover", mouseover).on("mouseout", mouseout); //1st row of text textEnter.append("tspan").attr("x", 0).text(function (d) { return d.depth ? d.name.split(" ")[0] : ""; }); //2nd row of text textEnter.append("tspan").attr("x", 0).attr("dy", ".9em").text(function (d) { return d.depth ? d.name.split(" ")[1] || "" : ""; }); //3rd row textEnter.append("tspan").attr("x", 0).attr("dy", ".9em").text(function (d) { return d.depth ? d.name.split(" ")[2] || "" : ""; }); //fourth row (if necessary) textEnter.append("tspan").attr("x", 0).attr("dy", ".9em").text(function (d) { return d.depth ? d.name.split(" ")[3] || "" : ""; }); //click function function click(d) { path.transition() //duration is predefined above at 1500 (1.75 seconds) .duration(duration).attrTween("d", arcTween(d)); // Somewhat of a hack as it relies on arcTween updating the scales. text.style("visibility", function (e) { return isParentOf(d, e) ? null : d3.select(this).style("visibility"); }) .transition().duration(duration).attrTween("text-anchor", function (d) { return function () { return x(d.x + d.dx / 2) > Math.PI ? "end" : "start"; }; }) .attrTween("transform", function (d) { var multiline = (d.name || "").split(" ").length > 1.5; return function () { var angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90, rotate = angle + (multiline ? -.5 : 0); return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")"; }; }) .each("end", function (e) { d3.select(this).style("visibility", isParentOf(d, e) ? null : "hidden"); }); } //mouseover function which will send the values to the legend function mouseover(d) { content.append("p").attr("id", "current").text(d.name) // + " - 2013 amounts: " + d.amount + " - Which was a " + d.percentChange13 + "% change of the previous year.") content.append("p") //.attr("id", "name") .text("Money exchanged: $" + format(d.amount)) } //mouseout function which removes the values and replaces them with a blank space function mouseout(d) { content.html(' '); } }); d3.selectAll("input").on("change", function change() { var value = this.value === "show" ? 1 : 0; d3.selectAll("text").style("fill-opacity", function (d) { if (value === 1) { return 1; } else { //if the depth is 1, innermost, then it's seen if (d.depth === 1) { return 1; } //else the depth is not one, then it's hidden else { return 0; } } }); }); function isParentOf(p, c) { if (p === c) return true; if (p.children) { return p.children.some(function (d) { return isParentOf(d, c); }); } return false; } // Interpolate the scales! function arcTween(d) { var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]), yd = d3.interpolate(y.domain(), [d.y, 1]), yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]); return function (d, i) { return i ? function (t) { return arc(d); } : function (t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return arc(d); }; }; } function maxY(d) { return d.children ? Math.max.apply(Math, d.children.map(maxY)) : d.y + d.dy; } // http://www.w3.org/WAI/ER/WD-AERT/#color-contrast function brightness(rgb) { return rgb.r * .299 + rgb.g * .587 + rgb.b * .114; } }
function link (scope, element, attrs) { var width, height, radius, color_scale, $breadcrumbs, svg, arc, partition, textContainer scope.max_depth = 0 width = attrs.width ? parseInt(attrs.width, 10) : 2000 height = attrs.height ? parseInt(attrs.height, 10) : width + 50 /* breadcrumbs */ radius = Math.min(width, height) / 2 color_scale = d3.scale.category10() scope.breadcrumbsNodes = [] $breadcrumbs = $compile('<breadcrumbs-d3 nodes="breadcrumbsNodes" max-depth="maxDepth"></breadcrumbs-d3>')(scope) element.append($breadcrumbs) textContainer = d3 .select(element[0]) .append('div') .attr('style', 'position:absolute;top:50%;margin-top:-15px;height:50%;left:0;right:0; ') .append('div') .attr('class', 'text-value') .style('font-size', '30px') .style('text-align', 'center') // took responsive svg trick from http://demosthenes.info/blog/744/Make-SVG-Responsive svg = d3.select(element[0]) .append('div') .attr('style', 'position: relative;width: 90%;padding-bottom: 100%;margin-left:5%; vertical-align: middle;overflow: hidden;') .append('svg') .attr('style', 'display: block;position: absolute; top: 0;left: 0;') .attr('width', '100%') .attr('height', '100%') .attr('preserveAspectRatio', 'xMinYMin meet') .attr('viewBox', '0 0 2000 2000') .append('g') .attr('transform', 'translate(' + width / 2 + ',' + height * 0.52 + ')') arc = d3.svg.arc() .startAngle((d) => d.x) .endAngle((d) => d.x + d.dx) .innerRadius((d) => Math.sqrt(d.y)) .outerRadius((d) => Math.sqrt(d.y + d.dy)) scope.$watch('selected', function (newVal) { if (newVal && newVal.id) { highlight(newVal.id) } }) partition = d3.layout.partition() .sort(null) .size([2 * Math.PI, radius * radius]) .value((d) => scope.log ? Math.log(d.size) : d.size) scope.$watch(() => scope.chartData, function (newVal) { var g, chart_data if (!scope.chartData || !scope.chartData.children || !scope.chartData.children.length) { svg .selectAll('path') .remove() scope.breadcrumbsNodes = [] return } // update breadcrumbs and stuff highlight() // d3 add a lot of data to nodes. It triggers watch without rest chart_data = clone(scope.chartData) g = svg .datum(chart_data) .selectAll('path') .data(partition.nodes) g .enter() .append('path') .each(function (d) { this._current = { x: 0, dx: 0, y: 0, dy: 0 } }) // store the initial angles var existing_and_current = g .attr('display', (d) => d.depth ? null : 'none') // hide inner ring .attr('d', arc) .style('stroke', '#fff') .style('stroke-width', width / 200) .style('fill', color) .style('fill-rule', 'evenodd') .on('mouseover', mouseover) .on('click', click) // avoid locking the loop for animation setTimeout(function () { g .transition() .duration(750) .attrTween('d', arcTween) .each('end', function (node) { this._current = { x: node.x, dx: node.dx, y: node.y, dy: node.dy } }) }, 0) existing_and_current .each(function (d) { if (d.depth > scope.max_depth) { scope.max_depth = d.depth } }) .filter((d, i) => !!scope.click && !d.virtual) .style('cursor', 'pointer') g.exit().remove() }, true) function color (d) { if (d.color) { return d.color } return color_scale((d.children ? d : d.parent).name) } function arcTween (node) { if (!this._current) { this._current = { x: node.x, dx: 0, y: node.y, dy: 0 } } var i = d3.interpolate(this._current, node) return function (t) { return arc(i(t)) } } function mouseover (d) { highlight(d.id) if (scope.over) { scope.over.apply(null, [{ d: d }]) } } function click (d) { if (scope.click) { scope.click.apply(null, [{ d: d }]) } } function highlight (id) { const sequenceArray = getAncestors(id) if (sequenceArray && sequenceArray.length) { textContainer.text(sequenceArray[sequenceArray.length - 1].textSize) } updateBreadcrumbs(sequenceArray) // Fade all the segments. svg.selectAll('path') .style('opacity', 0.6) // Then highlight only those that are an ancestor of the current segment. svg.selectAll('path') .filter(function (node) { return (sequenceArray.indexOf(node) >= 0) }) .style('opacity', 1) } // Update the breadcrumb trail to show the current sequence . function updateBreadcrumbs (nodeArray) { const toBreadCrumbs = map(nodeArray, function (node) { return {name: node.name, fill: color(node)} }) $timeout(function () { scope.breadcrumbsNodes = toBreadCrumbs }, 0) } function getAncestors (id) { var path = [] svg.selectAll('path') .each(function (node) { if (node.id === id) { var current = node while (current.parent) { path.unshift(current) current = current.parent } } }) return path } }
update: function (element, valueAccessor, viewModel, bindingContext) { var height = bindingContext.height, width = $(element).parents('.resizable.container').innerWidth(); bindingContext.width(width); radius = ko.unwrap(bindingContext.radius); // To avoid a floating point bug in d3 partition layout, // the range of the x scale needs to be rounded to integers. // For greater precision, 10 * 360 (degrees) is used. // Later, startAngle and endAngle functions will reconvert // those values to the expected [0, 2* Math.PI] range. x = d3.scale.linear().rangeRound([0, 3600]).clamp(true); y = d3.scale.sqrt().range([0, radius]); var d2r = d3.scale.linear().domain([0, 3600]).range([0, 2 * Math.PI]); var el = d3.select(element) .attr('width', width) .attr('height', height), partition = d3.layout.partition() .value(function(d) { return d.size; }), arc = d3.svg.arc() .startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, d2r(x(d.x)))); }).endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, d2r(x(d.x + d.dx)))); }).innerRadius(function(d) { return Math.max(0, y(d.y - d.dy)); }).outerRadius(function(d) { return Math.max(0, y(d.y)); }); // clean up after the old rendering el.select('g').remove(); var container = el.append('g') .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); // not sure about this pattern, saving references in DOM elements... hm element.sunburst = { container: container, partition: partition, arc: arc, totalSize: 0, bindingContext: bindingContext, }; var val = ko.unwrap(valueAccessor()), data = ko.unwrap(val.hierarchy), nodes = element.sunburst.partition.nodes(data); colorizeHierarchy(data); var path = element.sunburst.container.data([data]).selectAll('path') .data(nodes).enter().append('g').append('path') .attr('d', element.sunburst.arc) .attr('fill-rule', 'evenodd') .style('fill', function (d) { return d.color; }) .style('cursor', function (d) { return d.children ? 'pointer' : 'normal'; }) .on('mouseover', hoverPath.bind(element.sunburst)) .on('click', click.bind(element.sunburst)); addLabels.bind(element.sunburst)(1, 2); // Get total size of the tree = value of root node from partition. element.sunburst.totalSize = path.node().__data__.value; // Highlight greatest leaf. var greatestLeaf = element.sunburst.container.select('path').datum(); while (greatestLeaf.children) { greatestLeaf = greatestLeaf.children[0]; } setTimeout(function () { hoverPath.bind(element.sunburst)(greatestLeaf); }, 50); // To avoid race condition that breaks zoom. }