Related
I have a piechart with labels and legend which looks fine in SVG but as soon as I use canvg to transform it to canvas some formats are missing / get wrong.
SVG:
Canvas:
I already inline the CSS to apply all CSS settings but still the format does not match.
Any ideas?
Is this a (canvg) bug or am I doing s.th. wrong?
var columns = ['data11', 'data2', 'data347', 'data40098'];
var data = [150, 250, 300, 50];
var colors = ['#0065A3', '#767670', '#D73648', '#7FB2CE', '#00345B'];
var padding = 5;
/**
* global C3Styles object
*/
var C3Styles = null;
var legendData = [];
var sumTotal = 0
//prepare pie data
var columnData = [];
var columnNames = {};
for (i = 0; i < columns.length; i++) {
columnData.push([columns[i]].concat(data[i]));
var val = (Array.isArray(data[i])) ? data[i].reduce(function(pv, cv) {
return pv + cv;
}, 0) : data[i];
sumTotal += val;
legendData.push({
id: columns[i],
value: val,
ratio: 0.0
});
}
legendData.forEach(function(el, i) {
el.ratio = el.value / sumTotal
columnNames[el.id] = [el.id, d3.format(",.0f")(el.value), d3.format(",.1%")(el.ratio)].join(';');
});
var chart = c3.generate({
bindto: d3.select('#chart'),
data: {
columns: [
[columns[0]].concat(data[0])
],
names: columnNames,
type: 'pie',
},
legend: {
position: 'right',
show: true
},
pie: {
label: {
threshold: 0.001,
format: function(value, ratio, id) {
return [id, d3.format(",.0f")(value), "[" + d3.format(",.1%")(ratio) + "]"].join(';');
}
}
},
color: {
pattern: colors
},
onrendered: function() {
redrawLabelBackgrounds();
redrawLegend();
}
});
function addLabelBackground(index) {
/*d3.select('#chart').select("g.c3-target-" + columns[index].replace(/\W+/g, '-')+".c3-chart-arc")
.insert("rect", "text")
.style("fill", colors[index]);*/
var p = d3.select('#chart').select("g.c3-target-" + columns[index].replace(/\W+/g, '-') + ".c3-chart-arc");
var g = p.append("g");
g.append("rect")
.style("fill", colors[index]);
g.append(function() {
return p.select("text").remove().node();
});
}
for (var i = 0; i < columns.length; i++) {
if (i > 0) {
setTimeout(function(column) {
chart.load({
columns: [
columnData[column],
]
});
//chart.data.names(columnNames[column])
addLabelBackground(column);
}, (i * 5000 / columnData.length), i);
} else {
addLabelBackground(i);
}
}
function redrawLegend() {
d3.select('#chart').selectAll(".c3-legend-item > text").each(function() {
// get d3 node
var legendItem = d3.select(this);
var legendItemNode = legendItem.node();
//check if label is drawn
if (legendItemNode) {
if (legendItemNode.childElementCount === 0 && legendItemNode.innerHTML.length > 0) {
//build data
var data = legendItemNode.innerHTML.split(';');
legendItem.text("");
//TODO format legend dynamically depending on text
legendItem.append("tspan")
.text(data[0] + ": ")
.attr("class", "id-row")
.attr("text-anchor", "start");
legendItem.append("tspan")
.text(data[1] + " = ")
.attr("class", "value-row")
.attr("x", 160)
.attr("text-anchor", "end");
legendItem.append("tspan")
.text(data[2])
.attr("class", "ratio-row")
.attr("x", 190)
.attr("text-anchor", "end");
}
}
});
d3.select('#chart').selectAll(".c3-legend-item > rect").each(function() {
var legendItem = d3.select(this);
legendItem.attr("width", 190);
});
}
function redrawLabelBackgrounds() {
//for all label texts drawn yet
//for all label texts drawn yet
d3.select('#chart').selectAll(".c3-chart-arc > g > text").each(function(v) {
// get d3 node
var label = d3.select(this);
var labelNode = label.node();
//check if label is drawn
if (labelNode) {
if (labelNode.childElementCount === 0 && labelNode.innerHTML.length > 0) {
//build data
var data = labelNode.innerHTML.split(';');
label.text("");
data.forEach(function(i, n) {
label.append("tspan")
.text(i)
.attr("dy", (n === 0) ? 0 : "1.2em")
.attr("x", 0)
.attr("text-anchor", "middle");
}, label);
}
//check if element is visible
if (d3.select(labelNode.parentNode).style("display") !== 'none') {
//get pos of the label text
var pos = label.attr("transform").match(/-?\d+(\.\d+)?/g);
if (pos) {
// TODO: mofify the pos of the text
// pos[0] = (pos[0]/h*90000);
// pos[1] = (pos[1]/h*90000);
// remove dy and move label
//d3.select(this).attr("dy", 0);
//d3.select(this).attr("transform", "translate(" + pos[0] + "," + pos[1] + ")");
//get surrounding box of the label
var bbox = labelNode.getBBox();
//now draw and move the rects
d3.select(labelNode.parentNode).select("rect")
.attr("transform", "translate(" + (pos[0] - (bbox.width + padding) / 2) +
"," + (pos[1] - bbox.height / labelNode.childElementCount) + ")")
.attr("width", bbox.width + padding)
.attr("height", bbox.height + padding);
}
}
}
});
}
document.getElementById("exportButton").onclick = function() {
exportChartToImage();
};
function exportChartToImage() {
var createImagePromise = new Promise(function(resolve, reject) {
var images = [];
d3.selectAll('svg').each(function() {
if (this.parentNode) {
images.push(getSvgImage(this.parentNode, true));
}
});
if (images.length > 0)
resolve(images);
else
reject(images);
});
createImagePromise.then(function(images) {
/*images.forEach(function(img, n) {
img.toBlob(function(blob) {
saveAs(blob, "image_" + (n + 1) + ".png");
});
});*/
})
.catch(function(error) {
throw error;
});
};
/**
* Converts a SVG-Chart to a canvas and returns it.
*/
function getSvgImage(svgContainer) {
var svgEl = d3.select(svgContainer).select('svg').node();
var svgCopyEl = svgEl.cloneNode(true);
if (!svgCopyEl)
return;
d3.select("#svgCopyEl").selectAll("*").remove();
d3.select("#svgCopyEl").node().append(svgCopyEl);
//apply all CSS styles to SVG
/* taken from https://gist.github.com/aendrew/1ad2eed6afa29e30d52e#file-exportchart-js
and changed from, angular to D3 functions
*/
/* Take styles from CSS and put as inline SVG attributes so that Canvg
can properly parse them. */
if (!C3Styles) {
var chartStyle;
// Get rules from c3.css
var styleSheets = document.styleSheets;
for (var i = 0; i <= styleSheets.length - 1; i++) {
if (styleSheets[i].href && (styleSheets[i].href.indexOf('c3.min.css') !== -1 || styleSheets[i].href.indexOf('c3.css') !== -1)) {
try {
if (styleSheets[i].rules !== undefined) {
chartStyle = styleSheets[i].rules;
} else {
chartStyle = styleSheets[i].cssRules;
}
break;
}
//Note that SecurityError exception is specific to Firefox.
catch (e) {
if (e.name == 'SecurityError') {
console.log("SecurityError. Cant read: " + styleSheets[i].href);
continue;
}
}
}
if (chartStyle !== null && chartStyle !== undefined) {
C3Styles = {};
var selector;
// Inline apply all the CSS rules as inline
for (i = 0; i < chartStyle.length; i++) {
if (chartStyle[i].type === 1) {
selector = chartStyle[i].selectorText;
var styleDec = chartStyle[i].style;
for (var s = 0; s < styleDec.length; s++) {
C3Styles[styleDec[s]] = styleDec[styleDec[s]];
}
}
}
}
}
}
if (C3Styles) {
d3.select(svgCopyEl).selectAll('.c3:not(.c3-chart):not(path)').style(C3Styles);
}
// SVG doesn't use CSS visibility and opacity is an attribute, not a style property. Change hidden stuff to "display: none"
d3.select(svgCopyEl).selectAll('*')
.filter(function(d) {
return d && d.style && (d.style('visibility') === 'hidden' || d.style('opacity') === '0');
})
.style('display', 'none');
//fix weird back fill
d3.select(svgCopyEl).selectAll("path").attr("fill", "none");
//fix no axes
d3.select(svgCopyEl).selectAll("path.domain").attr("stroke", "black");
//fix no tick
d3.select(svgCopyEl).selectAll(".tick line").attr("stroke", "black");
//apply svg text fill, set color
d3.select(svgCopyEl).selectAll("text:not(.c3-empty):not(.c3-axis)").attr("opacity", 1);
var canvasComputed = d3.select("#canvasComputed").node();
// transform SVG to canvas using external canvg
canvg(canvasComputed, new XMLSerializer().serializeToString(svgCopyEl));
return canvasComputed;
}
.c3-chart-arc.c3-target text {
color: white;
fill: white;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.css" rel="stylesheet" />
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.js"></script>
<!-- Required to convert named colors to RGB -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.4/rgbcolor.min.js"></script>
<!-- Optional if you want blur -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/stackblur-canvas/1.4.1/stackblur.min.js"></script>
<!-- Main canvg code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.5/canvg.js"></script>
<script src="https://fastcdn.org/FileSaver.js/1.1.20151003/FileSaver.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<h4>
SVG
</h4>
<div id="chart" class "c3">
</div>
<h4>
copy SVG
</h4>
<div id ="svgCopyEl">
</div>
<div>
<h4>
canvas
</h4>
<canvas id="canvasComputed"></canvas>
</div>
<button type="button" id="exportButton">
export to Canvas
</button>
Basically it was a combination of lack knowledge and a bug.
First of all, all relevant CSS styles have to be inlined when using canvg. Second, there was a bug in canvg using tspans and text-anchors.
Here is the (more or less) working code:
var columns = ['data11', 'data2', 'data347', 'data40098'];
var data = [150, 250, 300, 50];
var colors = ['#0065A3', '#767670', '#D73648', '#7FB2CE', '#00345B'];
var padding = 5;
/**
* global C3Styles object
*/
var C3Styles = null;
var legendData = [];
var sumTotal = 0
//prepare pie data
var columnData = [];
var columnNames = {};
for (i = 0; i < columns.length; i++) {
columnData.push([columns[i]].concat(data[i]));
var val = (Array.isArray(data[i])) ? data[i].reduce(function(pv, cv) {
return pv + cv;
}, 0) : data[i];
sumTotal += val;
legendData.push({
id: columns[i],
value: val,
ratio: 0.0
});
}
legendData.forEach(function(el, i) {
el.ratio = el.value / sumTotal
columnNames[el.id] = [el.id, d3.format(",.0f")(el.value), d3.format(",.1%")(el.ratio)].join(';');
});
var chart = c3.generate({
bindto: d3.select('#chart'),
data: {
columns: [
[columns[0]].concat(data[0])
],
names: columnNames,
type: 'pie',
},
legend: {
position: 'right',
show: true
},
pie: {
label: {
threshold: 0.001,
format: function(value, ratio, id) {
return [id, d3.format(",.0f")(value), "[" + d3.format(",.1%")(ratio) + "]"].join(';');
}
}
},
color: {
pattern: colors
},
onrendered: function() {
redrawLabelBackgrounds();
redrawLegend();
}
});
function addLabelBackground(index) {
/*d3.select('#chart').select("g.c3-target-" + columns[index].replace(/\W+/g, '-')+".c3-chart-arc")
.insert("rect", "text")
.style("fill", colors[index]);*/
var p = d3.select('#chart').select("g.c3-target-" + columns[index].replace(/\W+/g, '-') + ".c3-chart-arc");
var g = p.append("g");
g.append("rect")
.style("fill", colors[index]);
g.append(function() {
return p.select("text").remove().node();
});
}
for (var i = 0; i < columns.length; i++) {
if (i > 0) {
setTimeout(function(column) {
chart.load({
columns: [
columnData[column],
]
});
//chart.data.names(columnNames[column])
addLabelBackground(column);
}, (i * 5000 / columnData.length), i);
} else {
addLabelBackground(i);
}
}
function redrawLegend() {
d3.select('#chart').selectAll(".c3-legend-item > text").each(function() {
// get d3 node
var legendItem = d3.select(this);
var legendItemNode = legendItem.node();
//check if label is drawn
if (legendItemNode) {
if (legendItemNode.childElementCount === 0 && legendItemNode.innerHTML.length > 0) {
//build data
var data = legendItemNode.innerHTML.split(';');
legendItem.text("");
//TODO format legend dynamically depending on text
legendItem.append("tspan")
.text(data[0] + ": ")
.attr("class", "id-row")
.attr("text-anchor", "start");
legendItem.append("tspan")
.text(data[1] + " = ")
.attr("class", "value-row")
.attr("x", 160)
.attr("text-anchor", "end");
legendItem.append("tspan")
.text(data[2])
.attr("class", "ratio-row")
.attr("x", 190)
.attr("text-anchor", "end");
}
}
});
d3.select('#chart').selectAll(".c3-legend-item > rect").each(function() {
var legendItem = d3.select(this);
legendItem.attr("width", 190);
});
}
function redrawLabelBackgrounds() {
//for all label texts drawn yet
//for all label texts drawn yet
d3.select('#chart').selectAll(".c3-chart-arc > g > text").each(function(v) {
// get d3 node
var label = d3.select(this);
var labelNode = label.node();
//check if label is drawn
if (labelNode) {
var bbox = labelNode.getBBox();
var labelTextHeight = bbox.height;
if (labelNode.childElementCount === 0 && labelNode.innerHTML.length > 0) {
//build data
var data = labelNode.innerHTML.split(';');
label.html('')
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle");
data.forEach(function(i, n) {
label.append("tspan")
.text(i)
.attr("dy", (n === 0) ? 0 : "1.2em")
.attr("x", 0);
}, label);
}
//check if element is visible
if (d3.select(labelNode.parentNode).style("display") !== 'none') {
//get pos of the label text
var pos = label.attr("transform").match(/-?\d+(\.\d+)?/g);
if (pos) {
// TODO: mofify the pos of the text
// pos[0] = (pos[0]/h*90000);
// pos[1] = (pos[1]/h*90000);
// remove dy and move label
//d3.select(this).attr("dy", 0);
//d3.select(this).attr("transform", "translate(" + pos[0] + "," + pos[1] + ")");
//get surrounding box of the label
bbox = labelNode.getBBox();
//now draw and move the rects
d3.select(labelNode.parentNode).select("rect")
.attr("transform", "translate(" + (pos[0] - bbox.width / 2 - padding) +
"," + (pos[1] - labelTextHeight/2 - padding)+")")
.attr("width", bbox.width + 2*padding)
.attr("height", bbox.height + 2*padding);
}
}
}
});
}
document.getElementById("exportButton").onclick = function() {
exportChartToImage();
};
function exportChartToImage() {
var createImagePromise = new Promise(function(resolve, reject) {
var images = [];
d3.selectAll('svg').each(function() {
if (this.parentNode) {
images.push(getSvgImage(this.parentNode, true));
}
});
if (images.length > 0)
resolve(images);
else
reject(images);
});
createImagePromise.then(function(images) {
/*images.forEach(function(img, n) {
img.toBlob(function(blob) {
saveAs(blob, "image_" + (n + 1) + ".png");
});
});*/
})
.catch(function(error) {
throw error;
});
};
/**
* Converts a SVG-Chart to a canvas and returns it.
*/
function getSvgImage(svgContainer) {
var svgEl = d3.select(svgContainer).select('svg').node();
var svgCopyEl = svgEl.cloneNode(true);
if (!svgCopyEl)
return;
d3.select("#svgCopyEl").selectAll("*").remove();
d3.select("#svgCopyEl").node().append(svgCopyEl); //.transition().duration(0);
//apply C3 CSS styles to SVG
// SVG doesn't use CSS visibility and opacity is an attribute, not a style property. Change hidden stuff to "display: none"
d3.select("#svgCopyEl").selectAll('*')
.filter(function(d) {
return d && d.style && (d.style('visibility') === 'hidden' || d.style('opacity') === '0');
})
.style('display', 'none');
d3.select("#svgCopyEl").selectAll('.c3-chart path')
.filter(function(d) {
return d && d.style('fill') === 'none';
})
.attr('fill', 'none');
d3.select("#svgCopyEl").selectAll('.c3-chart path')
.filter(function(d) {
return d && d.style('fill') !== 'none';
})
.attr('fill', function(d) {
return d.style('fill');
});
//set c3 default font
d3.select("#svgCopyEl").selectAll('.c3 svg')
.style('font', 'sans-serif')
.style('font-size', '10px');
//set c3 legend font
d3.select("#svgCopyEl").selectAll('.c3-legend-item > text')
.style('font', 'sans-serif')
.style('font-size', '12px');
d3.select("#svgCopyEl").selectAll('.c3-legend-item > text > tspan')
.style('font', 'sans-serif')
.style('font-size', '12px');
//set c3 arc shapes
d3.select("#svgCopyEl").selectAll('.c3-chart-arc path,rect')
.style('stroke', '#fff');
d3.select("#svgCopyEl").selectAll('.c3-chart-arc text')
.attr('fill', '#fff')
.style('font', 'sans-serif')
.style('font-size', '13px');
//fix weird back fill
d3.select("#svgCopyEl").selectAll("path").attr("fill", "none");
//fix no axes
d3.select("#svgCopyEl").selectAll("path.domain").attr("stroke", "black");
//fix no tick
d3.select("#svgCopyEl").selectAll(".tick line").attr("stroke", "black");
var canvasComputed = d3.select("#canvasComputed").node();
// transform SVG to canvas using external canvg
canvg(canvasComputed, new XMLSerializer().serializeToString(svgCopyEl));
return canvasComputed;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.css" rel="stylesheet" />
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.js"></script> -->
<!-- Required to convert named colors to RGB -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.4/rgbcolor.min.js"></script>
<!-- Optional if you want blur -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/stackblur-canvas/1.4.1/stackblur.min.js"></script>
<!-- Main canvg code -->
<script src="https://cdn.jsdelivr.net/npm/canvg#2.0.0-beta.1/dist/browser/canvg.min.js"></script>
<script src="https://fastcdn.org/FileSaver.js/1.1.20151003/FileSaver.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<h4>
SVG
</h4>
<div id="chart" class "c3">
</div>
<h4>
copy SVG
</h4>
<div id="svgCopyEl">
</div>
<div>
<h4>
canvas
</h4>
<canvas id="canvasComputed"></canvas>
</div>
<button type="button" id="exportButton">
export to Canvas
</button>
I'm trying to put together an animated sunburst diagram that can zoom and change between data representations, based some examples I've found: Vasco, Visual Cinnamon, David Richard etc.
I'm not able to get the animation part to properly work. The arcs are updating, but the text labels aren't. Any advice? Also, I'm pretty new to this so I'd appreciate any other tips or advice on how to structure my code as well.
// Variables
const width = window.innerWidth;
const height = window.innerHeight;
const radius = (Math.min(width, height) / 2) - 5;
const color = d3.scaleOrdinal(d3.schemeCategory20b);
const x = d3.scaleLinear().range([0, 2 * Math.PI]).clamp(true);
const y = d3.scaleLinear().range([0, radius]);
// Create our sunburst data structure and size it.
const partition = d3.partition();
// Size our <svg> element
const svg = d3.select('#chart').append('svg')
.style('width', '100vw')
.style('height', '100vh')
.attr('viewBox', `${-width / 2} ${-height / 2} ${width} ${height}`)
.attr("id", "container");
arc = d3.arc()
.startAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x0)));
})
.endAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x1)));
})
.innerRadius(function(d) {
return Math.max(0, y(d.y0));
})
.outerRadius(function(d) {
return Math.max(0, y(d.y1));
});
// JSON data
var nodeData = {
"name": "TOPICS", "children": [{
"name": "Topic A",
"children": [{"name": "Sub A1", "size": 4}, {"name": "Sub A2", "size": 4}]
}, {
"name": "Topic B",
"children": [{"name": "Sub B1", "size": 3}, {"name": "Sub B2", "size": 3}, {
"name": "Sub B3", "size": 3}]
}, {
"name": "Topic C",
"children": [{"name": "Sub A1", "size": 4}, {"name": "Sub A2", "size": 4}]
}]
};
createVisualization()
// Main function to draw and set up the visualization
function createVisualization() {
// Find the root node, calculate the node.value, and sort our nodes by node.value
root = d3.hierarchy(nodeData)
.sum(function(d) {
return d.size;
})
.sort(function(a, b) {
return b.value - a.value;
});
original = root;
partition(root);
// Add a <g> element for each node in thd data, then append <path> elements and draw lines based on the arc
// variable calculations. Last, color the lines and the slices.
slices = svg.selectAll('path')
.data(root.descendants())
.enter().append('g').attr("class", "node")
.on("click", focusOn);
slices.append('path')
.attr('class', 'main-arc')
.attr("id", function(d, i) {
return "arc_" + i;
})
.attr("d", arc)
.style('stroke', '#fff')
.style("fill", function(d) {
return color((d.children ? d : d.parent).data.name);
})
.each(function(d, i) {
const halfPi = Math.PI / 2;
const angles = [Math.max(0, Math.min(2 * Math.PI, x(d.x0))) - halfPi, Math.max(0, Math.min(2 * Math.PI, x(d.x1))) - halfPi];
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const middleAngle = (angles[1] + angles[0]) / 2;
const invertDirection = middleAngle > 0 && middleAngle < Math.PI; // On lower quadrants write text ccw
if (invertDirection) {
angles.reverse();
}
const path = d3.path();
path.arc(0, 0, r, angles[0], angles[1], invertDirection);
//Create a new invisible arc that the text can flow along
d3.select(this).append("path")
.attr("class", "hiddenArcs")
.attr("id", "hiddenArc_" + i)
.attr("d", path.toString())
.style("fill", "none");
});
text = slices.append("text")
.attr("class", "arcText");
text.append("textPath")
.attr("xlink:href", function(d, i) {
return "#hiddenArc_" + i;
})
.attr('startOffset', '50%')
.style('fill', 'none')
.style('stroke', '#fff')
.style('stroke-width', 5)
.style('stroke-linejoin', 'round');
text.append("textPath")
.attr("xlink:href", function(d, i) {
return "#hiddenArc_" + i;
})
.attr('startOffset', '50%');
slices.selectAll("textPath")
.text(function(d) {
return d.parent ? d.data.name : ""
});
// Redraw the Sunburst Based on User Input
d3.selectAll(".sizeSelect").on("click", build);
}
function build() {
// Determine how to size the slices.
if (this.value === "size") {
root.sum(function(d) {
return d.size;
});
} else {
root.count();
}
// Calculate the sizes of each arc that we'll draw later.
partition(root);
slices.selectAll("path.main-arc").transition().duration(750).attrTween("d", arcTweenData);
slices.selectAll("path.hiddenArcs").transition().duration(750).attrTween("d", hiddenArcTweenData);
slices.selectAll("textPath")
.text(function(d) {
return d.parent ? d.data.name : ""
});
}
// Respond to slice click.
function focusOn(d) {
original = d;
svg.selectAll("path").transition().duration(1000).attrTween("d", arcTweenZoom(d))
}
// When zooming: interpolate the scales.
function arcTweenZoom(d) {
var xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
yd = d3.interpolate(y.domain(), [d.y0, 1]), // [d.y0, 1]
yr = d3.interpolate(y.range(), [d.y0 ? 40 : 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);
};
};
}
// When switching data: interpolate the arcs in data space.
function arcTweenData(a, i) {
// (a.x0s ? a.x0s : 0) -- grab the prev saved x0 or set to 0 (for 1st time through)
// avoids the stash() and allows the sunburst to grow into being
// var oi = d3.interpolate({ x0: (a.x0s ? a.x0s : 0), x1: (a.x1s ? a.x1s : 0) }, a);
var oi = d3.interpolate({
x0: (a.x0s ? a.x0s : 0),
x1: (a.x1s ? a.x1s : 0)
}, a);
function tween(t) {
var b = oi(t);
a.x0s = b.x0;
a.x1s = b.x1;
return arc(b);
}
if (i == 0) {
// If we are on the first arc, adjust the x domain to match the root node
// at the current zoom level. (We only need to do this once.)
var xd = d3.interpolate(x.domain(), [original.x0, original.x1]);
return function(t) {
x.domain(xd(t));
return tween(t);
};
} else {
return tween;
}
}
function hiddenArcTweenData(a, i) {
// (a.x0s ? a.x0s : 0) -- grab the prev saved x0 or set to 0 (for 1st time through)
var oi = d3.interpolate({
x0: (a.x0s ? a.x0s : 0),
x1: (a.x1s ? a.x1s : 0)
}, a);
function tween(t) {
var b = oi(t);
a.x0s = b.x0;
a.x1s = b.x1;
return middleArc(b);
}
if (i == 0) {
// If we are on the first arc, adjust the x domain to match the root node
// at the current zoom level. (We only need to do this once.)
var xd = d3.interpolate(x.domain(), [original.x0, original.x1]);
return function(t) {
x.domain(xd(t));
return tween(t);
};
} else {
return tween;
}
}
function middleArc(d) {
const halfPi = Math.PI / 2;
const angles = [Math.max(0, Math.min(2 * Math.PI, x(d.x0))) - halfPi, Math.max(0, Math.min(2 * Math.PI, x(d.x1))) - halfPi];
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const middleAngle = (angles[1] + angles[0]) / 2;
const invertDirection = middleAngle > 0 && middleAngle < Math.PI; // On lower quadrants write text ccw
if (invertDirection) {
angles.reverse();
}
const path = d3.path();
path.arc(0, 0, r, angles[0], angles[1], invertDirection);
return path;
}
body {
#import "https://fonts.googleapis.com/css?family=Fakt:400,600";
font-family: 'Fakt', fakt;
font-size: 12px;
font-weight: 400;
background-color: #fff;
width: 960px;
height: 700px;
margin-top: 10px;
}
.node {
cursor: pointer;
}
.node .main-arc {
stroke: #fff;
stroke-width: 1px;
}
.node .hidden-arc {
stroke-width: 1px;
stroke: #000;
}
.node text {
pointer-events: none;
dominant-baseline: middle;
text-anchor: middle;
fill: #000
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sunburst</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<link rel="stylesheet" type="text/css" href="sunburst.css" />
</head>
<body>
<div id="main">
<label>
<input class="sizeSelect" type="radio" name="mode" value="count" checked/> Count </label>
<label>
<input class="sizeSelect" type="radio" name="mode" value="size" /> Size </label>
<div id="chart">
<script type="text/javascript" src="sunburst.js"></script>
</div>
</div>
</body>
</html>
Your JS code on line 97 says:
d3.select(this).append("path")
This will add the .hiddenArcs path as a child of the .main-arc <path>, rather than the parent <g>. This makes it invalid SVG, but it also makes the selector slices.selectAll("path.hiddenArcs") return an empty set. So, the hidden arcs never tween, and so the text stays where it is.
Changing line 97 so the hidden arc is added to the path's parent node, rather than the path, fixes this:
d3.select(this.parentNode).append("path")
I would like to make this d3 bar chart (ignore the different colors, that's for grouping which is not what I'm talking about):
Look like this javascript bar chart (specifically, how it has each instance broken up into "links" or "blocks" i.e. there is spacing between each item):
I can do this in javascript fairly easily but I am not sure how to do this in d3.
Can anyone provide any examples of how to break down each item as shown in the graph above?
I have tried this so far:
.append("rect")
.style("margin", "20")
This is the source for the d3 chart: https://jsfiddle.net/mq2jcfz0/
<!DOCTYPE html>
<html>
<head>
<script src="http://mbostock.github.com/d3/d3.v2.js"></script>
<title>barStack</title>
<style>
.axis text {
font: 10px sans-serif;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<script type="text/javascript" >
function barStack(d) {
var l = d[0].length
while (l--) {
var posBase = 0, negBase = 0;
d.forEach(function(d) {
d=d[l]
d.size = Math.abs(d.y)
if (d.y<0) {
d.y0 = negBase
negBase-=d.size
} else
{
d.y0 = posBase = posBase + d.size
}
})
}
d.extent= d3.extent(d3.merge(d3.merge(d.map(function(e) { return e.map(function(f) { return [f.y0,f.y0-f.size]})}))))
return d
}
/* Here is an example */
var data = [[{y:3},{y:6},{y:-3}],
[{y:4},{y:-2},{y:-9}],
[{y:10},{y:-3},{y:4}]]
var h=500
,w=500
,margin=10
,color = d3.scale.category10()
,x = d3.scale.ordinal()
.domain(['abc','abc2','abc3'])
.rangeRoundBands([margin,w-margin], .1)
,y = d3.scale.linear()
.range([h-margin,0+margin])
,xAxis = d3.svg.axis().scale(x).orient("bottom").tickSize(6, 0)
,yAxis = d3.svg.axis().scale(y).orient("left")
barStack(data)
y.domain(data.extent)
svg = d3.select("body")
.append("svg")
.attr("height",h)
.attr("width",w)
svg.selectAll(".series").data(data)
.enter().append("g").classed("series",true).style("fill", function(d,i) { return color(i)})
.selectAll("rect").data(Object)
.enter().append("rect")
.attr("x",function(d,i) { return x(x.domain()[i])})
.attr("y",function(d) { return y(d.y0)})
.attr("height",function(d) { return y(0)-y(d.size)})
.attr("width",x.rangeBand())
svg.append("g").attr("class","axis x").attr("transform","translate (0 "+y(0)+")").call(xAxis)
svg.append("g").attr("class","axis y").attr("transform","translate ("+x(margin)+" 0)").call(yAxis)
</script>
</body>
</html>
You will need to work with the barStack function that deals with arranging the data to stack the bars.
You just have to move rects position like this:
var barPad = 0.5;
function barStack(d) {
var l = d[0].length
while (l--) {
var posBase = 0, negBase = 0;
d.forEach(function(d) {
d=d[l]
d.size = Math.abs(d.y)
if (d.y<0) {
d.y0 = negBase - barPad
negBase-=d.size + barPad
} else
{
d.y0 = posBase = posBase + d.size + barPad
}
})
}
d.extent= d3.extent(d3.merge(d3.merge(d.map(function(e) { return e.map(function(f) { return [f.y0,f.y0-f.size]})}))))
return d
}
I am working on a Chrome Extension that crawls websites and finds certain types of links and then once the list of links has been populated after a predetermined number of hops, the search results are displayed using a D3 visualization (I am using the Forced Layout).
Problem: In about a couple of hops, I have too many links/nodes (more than 500 on most occasions) and so the graph looks intimidating and impossible to read, since all the nodes get cluttered in the limited space.
How can I increase the inter-node distance and make the graph more readable.
How do I give different colors to my nodes depending on their category/class?
Here are a fewof snapshots of the search results:
As is evident from the snapshots, things get pretty ugly. The code for my visualization script is given below, to give an idea of how I am implementing the graph. (I am very, very new to D3)
document.addEventListener('DOMContentLoaded', function () {
document.querySelector('#tab_5_contents').addEventListener('click', drawVisual);
//drawVisual();
});
function drawVisual(){
var w = 960, h = 500;
//var w = 1024, h = 768;
var labelDistance = 0;
var vis = d3.select("#tab_5_contents").append("svg:svg").attr("width", w).attr("height", h);
var nodes = [];
var labelAnchors = [];
var labelAnchorLinks = [];
var links = [];
for(var i = 0; i < QueuedORG.length; i++)
{
var nodeExists = 0;
//check to see if a node for the current url has already been created. If yes, do not create a new node
for(var j = 0; j < nodes.length; j++)
{
if(QueuedORG[i].url == nodes[j].label)
nodeExists = 1;
}
if (nodeExists == 0)
{
var node = {
label : QueuedORG[i].url
};
nodes.push(node);
labelAnchors.push({
node : node
});
labelAnchors.push({
node : node
});
}
};
for(var i=0;i<nodes.length; i++)
{
console.log("node i:"+i+nodes[i]+"\n");
console.log("labelAnchor i:"+i+labelAnchors[i]+"\n");
}
//To create links for connecting nodes
for(var i = 0; i < QueuedORG.length; i++)
{
var srcIndx = 0, tgtIndx = 0;
for(var j = 0; j < nodes.length; j++)
{
if( QueuedORG[i].url == nodes[j].label ) //to find the node number for the current url
{
srcIndx = j;
}
if( QueuedORG[i].parentURL == nodes[j].label ) //to find the node number for the parent url
{
tgtIndx = j;
}
}
//console.log("src:"+srcIndx+" tgt:"+tgtIndx);
//connecting the current url's node to the parent url's node
links.push({
source : srcIndx,
target : tgtIndx,
weight : 1,
});
labelAnchorLinks.push({
source : i * 2,
target : i * 2 + 1,
weight : 1
});
};
var force = d3.layout.force().size([w, h]).nodes(nodes).links(links).gravity(1).linkDistance(50).charge(-3000).linkStrength(function(x) {
return x.weight * 10
});
force.start();
var force2 = d3.layout.force().nodes(labelAnchors).links(labelAnchorLinks).gravity(0).linkDistance(0).linkStrength(8).charge(-100).size([w, h]);
force2.start();
var link = vis.selectAll("line.link").data(links).enter().append("svg:line").attr("class", "link").style("stroke", "#CCC");
var node = vis.selectAll("g.node").data(force.nodes()).enter().append("svg:g").attr("class", "node");
node.append("svg:circle").attr("r", 5).style("fill", "#555").style("stroke", "#FFF").style("stroke-width", 3);
node.call(force.drag);
var anchorLink = vis.selectAll("line.anchorLink").data(labelAnchorLinks)//.enter().append("svg:line").attr("class", "anchorLink").style("stroke", "#999");
var anchorNode = vis.selectAll("g.anchorNode").data(force2.nodes()).enter().append("svg:g").attr("class", "anchorNode");
anchorNode.append("svg:circle").attr("r", 0).style("fill", "#FFF");
anchorNode.append("svg:text").text(function(d, i) {
return i % 2 == 0 ? "" : d.node.label
}).style("fill", "#555").style("font-family", "Arial").style("font-size", 12);
var updateLink = function() {
this.attr("x1", function(d) {
return d.source.x;
}).attr("y1", function(d) {
return d.source.y;
}).attr("x2", function(d) {
return d.target.x;
}).attr("y2", function(d) {
return d.target.y;
});
}
var updateNode = function() {
this.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
force.on("tick", function() {
force2.start();
node.call(updateNode);
anchorNode.each(function(d, i) {
if(i % 2 == 0) {
d.x = d.node.x;
d.y = d.node.y;
} else {
var b = this.childNodes[1].getBBox();
var diffX = d.x - d.node.x;
var diffY = d.y - d.node.y;
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
var shiftX = b.width * (diffX - dist) / (dist * 2);
shiftX = Math.max(-b.width, Math.min(0, shiftX));
var shiftY = 5;
this.childNodes[1].setAttribute("transform", "translate(" + shiftX + "," + shiftY + ")");
}
});
anchorNode.call(updateNode);
link.call(updateLink);
anchorLink.call(updateLink);
});
}
The array QueuedORG in the above code acts as my dataset for all links that are being shown in the graph. Each entry in the array looks like this:
QueuedORG.push({url: currentURL, level: 1, parentURL: sourceURL, visited: 0});
Can someone please guide me to be able to increase the node repulsion and color code them? Thanks :)
You can set the distance between nodes by using the linkDistance() function of the force layout. This can be a function to allow you to set different distances for different links. Note that these distances are only suggestions to the force layout, so you won't get exactly what you ask for.
You could set this in a similar way to how you're setting the linkStrength at the moment:
force.linkDistance(function(d) {
return d.weight * 10;
});
It looks like you're already coloring the circles (node.append("svg:circle").style("fill", "#555")), which should work. Alternatively, you can assign CSS classes to them and then use that for the color.
circle.class1 {
fill: red;
stroke: black;
}
node.append("circle")
.attr("class", function(d) {
if(d.something == 1) { return "class1"; }
// etc
});
I am using tweaked code from http://hughsk.github.io/colony/ but I keep getting the error:
Uncaught TypeError: Cannot read property 'weight' of undefined
This stems from the force.nodes(nodes) function, for which the document says no weight needs to be set as defaults will be initialized by the layout when start() is created.
Can someone see what I'm doing wrong that keeps the weight values from being initialized?
Here is my code:
var colony = {
"nodes":[
{
"pages":123,
"name":"Test",
"id":2
},
{
"pages":456,
"name":"Test2",
"id":3
}
],
"links":[
{
"source":123,
"target":456,
"weight":100
}
]
}
var nodes = colony.nodes
, links = colony.links
, scale = 1
, focus
var width = 960
, height = 960
, link
, node
, text
, textTarget = false
var colors = {
links: 'FAFAFA'
, text: {
subtitle: 'FAFAFA'
}
, nodes: {
method: function(d) {
return groups[d.group].color
}
, hover: 'FAFAFA'
, dep: '252929'
}
}
links.forEach(function(link) {
var source = function(nodes,link){
for (var i = 0; i < nodes.length; i++){
if (nodes[i].id == link.source){
return nodes[i];
}
}
}
, target = function(nodes,link){
for (var i = 0; i < nodes.length; i++){
if (nodes[i].id == link.target){
return nodes[i];
}
}
}
source.children = source.children || []
source.children.push(link.target)
target.parents = target.parents || []
target.parents.push(link.source)
})
var groups = nodes.reduce(function(groups, file) {
var group = file.mgroup || 'none'
, index = groups.indexOf(group)
if (index === -1) {
index = groups.length
groups.push(group)
}
file.group = index
return groups
}, [])
groups = groups.map(function(name, n) {
var color = d3.hsl(n / groups.length * 300, 0.7, 0.725)
return {
name: name
, color: color.toString()
};
})
var force = d3.layout.force()
.size([width, height])
.charge(-50 * scale)
.linkDistance(20 * scale)
.on('tick', function() {
link.attr('x1', function(d) { return d.source.x; })
.attr('y1', function(d) { return d.source.y; })
.attr('x2', function(d) { return d.target.x; })
.attr('y2', function(d) { return d.target.y; })
node.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
if (textTarget) {
text.attr('transform'
, 'translate(' + textTarget.x + ',' + textTarget.y + ')')
}
})
var vis = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
force.nodes(nodes)
.links(links)
.start()
Your main problem is that you have to modify the links in the forEach loop, which gives you something like:
links.forEach(function (link) {
//var source, target;
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].pages == link.source) {
link.source = nodes[i];
}
}
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].pages == link.target) {
link.target = nodes[i];
}
}
link.source.children = link.source.children || []
link.source.children.push(link.target)
link.target.parents = link.target.parents || []
link.target.parents.push(link.source)
return link;
})
Then, in the tick function, you also have to iterate on all links and nodes. You can find a good example in the documentation