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 doing some tests with d3.js regarding zooming. At the moment, I have successfully implemented geometric zoom in my test, but it has a drawback: the elements under the zoomed g are being scaled. As I understood it, this could be solved by using semantic zooming.
The problem is that I need scale in my test, as I'm syncing it with a jQuery.UI slider value.
On the other hand, I would like the text elements being resized to maintain their size after a zoom operation.
I have an example of my current attempt here.
I'm having trouble changing my code to fit this purpose. Can any one share some insight/ideas?
For your solution I have merged 2 examples:
Semantic Zooming
Programmatic Zooming
Code snippets:
function zoom() {
text.attr("transform", transform);
var scale = zoombehavior.scale();
//to make the scale rounded to 2 decimal digits
scale = Math.round(scale * 100) / 100;
//setting the slider to the new value
$("#slider").slider( "option", "value", scale );
//setting the slider text to the new value
$("#scale").val(scale);
}
//note here we are not handling the scale as its Semantic Zoom
function transform(d) {
//translate string
return "translate(" + x(d[0]) + "," + y(d[1]) + ")";
}
function interpolateZoom(translate, scale) {
zoombehavior
.scale(scale)//we are setting this zoom only for detecting the scale for slider..we are not zoooming using scale.
.translate(translate);
zoom();
}
var slider = $(function() {
$("#slider").slider({
value: zoombehavior.scaleExtent()[0],//setting the value
min: zoombehavior.scaleExtent()[0],//setting the min value
max: zoombehavior.scaleExtent()[1],//settinng the ax value
step: 0.01,
slide: function(event, ui) {
var newValue = ui.value;
var center = [centerX, centerY],
extent = zoombehavior.scaleExtent(),
translate = zoombehavior.translate(),
l = [],
view = {
x: translate[0],
y: translate[1],
k: zoombehavior.scale()
};
//translate w.r.t the center
translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
view.k = newValue;//the scale as per the slider
//the translate after the scale(so we are multiplying the translate)
l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];
view.x += center[0] - l[0];
view.y += center[1] - l[1];
interpolateZoom([view.x, view.y], view.k);
}
});
});
I am zooming w.r.t. 250,250 which is the center of the clip circle.
Working code here (have added necessary comments)
Hope this helps!
To do what you want, you need to refactor the code first a little bit. With d3, it is good practice to use data() to append items to a selection, rather than using for loops.
So this :
for(i=0; i<7; i++){
pointsGroup.append("text")
.attr("x", function(){
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randx = Math.random();
return Math.floor(plusOrMinus*randx*75)+centerx;
})
.attr("y", function(){
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randy = Math.random();
return Math.floor(plusOrMinus*randy*75)+centery;
})
.html("star")
.attr("class", "point material-icons")
.on("click", function(){console.log("click!");});
}
Becomes this
var arr = [];
for(i=0; i<7; i++){
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randx = Math.random();
var x = Math.floor(plusOrMinus*randx*75)+centerx;
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randy = Math.random();
var y = Math.floor(plusOrMinus*randy*75)+centery;
arr.push({"x":x,"y":y});
}
pointsGroup.selectAll("text")
.data(arr)
.enter()
.append("text")
.attr("x", function(d,i){
return d.x;// This corresponds to arr[i].x
})
.attr("y", function(d,i){
return d.y;// This corresponds to arr[i].y
})
.html("star")
.attr("class", "point material-icons")
.on("click", function(){console.log("click!");});
This way, you can access individual coordinates using for instance.attr("x",function(d,i){ //d.x is equal to arr.x, i is item index in the selection});
Then, to achieve your goal, rather than changing the scale of your items, you should use a linear scale to change each star position.
First, add linear scales to your fiddle and apply them to the zoom:
var scalex = d3.scale.linear();
var scaley = d3.scale.linear();
var zoom = d3.behavior.zoom().x(scalex).y(scaley).scaleExtent([1, 5]).on('zoom', onZoom);
And finally on zoom event apply the scale to each star's x and y
function onZoom(){
d3.selectAll("text")
.attr("x",function(d,i){
return scalex(d.x);
})
.attr("y",function(d,i){
return scaley(d.y);
});
}
At this point, zoom will work without the slider. To add the slider, simply change manually the zoom behavior scale value during onSlide event, then call onZoom.
function onSlide(scale){
var sc = $("#slider").slider("value");
zoom.scale(sc);
onZoom();
}
Note: I used this config for the slider:
var slider = $(function() {
$( "#slider" ).slider({
value: 1,
min: 1,
max: 5,
step: 0.1,
slide: function(event, ui){
onSlide(5/ui.value);
}
});
});
Please note that at this point the zoom from the ui is performed relative to (0,0) at this point, and not your "circle" window center. To fix it I simplified the following function from the programmatic example, that computes valid translate and scale to feed to the zoom behavior.
// To handle center zooming
var width = 500;
var height = 600;
function zoomClick(sliderValue) {
var center = [width / 2, height / 2],
extent = zoom.scaleExtent(),
translate = zoom.translate();
var view = {
x: zoom.translate()[0],
y: zoom.translate()[1],
k: zoom.scale()
};
var target_zoom = sliderValue;
if (target_zoom < extent[0] || target_zoom > extent[1]) {
return false;
}
var translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
view.k = target_zoom;
var l = [];
l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];
view.x += center[0] - l[0];
view.y += center[1] - l[1];
// [view.x view.y] is valid translate
// view.k is valid scale
// Then, simply feed them to the zoom behavior
zoom
.scale(view.k)
.translate([view.x, view.y]);
// and call onZoom to update points position
onZoom();
}
then just change onSlide to use this new function every time slider moves
function onSlide(scale){
var sc = $("#slider").slider("value");
zoomClick(sc);
}
Full snippet
function onZoom(){
d3.selectAll("text")
.attr("x",function(d,i){
return scalex(d.x);
})
.attr("y",function(d,i){
return scaley(d.y);
});
}
function onSlide(scale){
var sc = $("#slider").slider("value");
zoomClick(sc);
}
var scalex = d3.scale.linear();
var scaley = d3.scale.linear();
var zoom = d3.behavior.zoom().x(scalex).y(scaley).scaleExtent([1, 5]).on('zoom', onZoom);
var svg = d3.select("body").append("svg")
.attr("height", "500px")
.attr("width", "500px")
.call(zoom)
.on("mousedown.zoom", null)
.on("touchstart.zoom", null)
.on("touchmove.zoom", null)
.on("touchend.zoom", null);
var centerx = 250,
centery = 250;
var circleGroup = svg.append("g")
.attr("id", "circleGroup");
var circle = circleGroup.append("circle")
.attr("cx", "50%")
.attr("cy", "50%")
.attr("r", 150)
.attr("class", "circle");
var pointsParent = svg.append("g").attr("clip-path", "url(#clip)").attr("id", "pointsParent");
var pointsGroup = pointsParent.append("g")
.attr("id", "pointsGroup");
var arr = [];
for(i=0; i<7; i++){
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randx = Math.random();
var x = Math.floor(plusOrMinus*randx*75)+centerx;
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randy = Math.random();
var y = Math.floor(plusOrMinus*randy*75)+centery;
arr.push({"x":x,"y":y});
}
pointsGroup.selectAll("text")
.data(arr)
.enter()
.append("text")
.attr("x", function(d,i){
return d.x;// This corresponds to arr[i].x
})
.attr("y", function(d,i){
return d.y;// This corresponds to arr[i].y
})
.html("star")
.attr("class", "point material-icons")
.on("click", function(){console.log("click!");});
zoom(pointsGroup);
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:circle")
.attr("id", "clip-circ")
.attr("cx", centerx)
.attr("cy", centery)
.attr("r", 149);
var slider = $(function() {
$( "#slider" ).slider({
value: 1,
min: 1,
max: 5,
step: 0.1,
slide: function(event, ui){
onSlide(5/ui.value);
}
});
});
// To handle center zooming
var width = 500;
var height = 600;
function zoomClick(sliderValue) {
var target_zoom = 1,
center = [width / 2, height / 2],
extent = zoom.scaleExtent(),
translate = zoom.translate();
var view = {x: zoom.translate()[0], y: zoom.translate()[1], k: zoom.scale()};
target_zoom = sliderValue;
if (target_zoom < extent[0] || target_zoom > extent[1]) { return false; }
var translate0 = [];
translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
view.k = target_zoom;
var l = [];
l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];
view.x += center[0] - l[0];
view.y += center[1] - l[1];
zoom
.scale(view.k)
.translate([view.x, view.y]);
onZoom();
}
body {
font: 10px sans-serif;
}
text {
font: 10px sans-serif;
}
.circle{
stroke: black;
stroke-width: 2px;
fill: white;
}
.point{
fill: goldenrod;
cursor: pointer;
}
.blip{
fill: black;
}
#slider{
width: 200px;
margin: auto;
}
<!DOCTYPE html>
<html>
<head>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
<link href="https://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css" rel="stylesheet" type="text/css" />
<script src="https://code.jquery.com/jquery-1.11.3.js"></script>
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<meta charset=utf-8 />
<title>d3.JS slider zoom</title>
</head>
<body>
<div id="slider"></div>
</body>
</html>
I'm using d3 to animate a route (path) on a map. When the route reaches a point along the route I'd like to popup some information.
Most of my code is based on the following example. http://bl.ocks.org/mbostock/1705868. I'm really just trying to determine if there is a way to detect when the transitioning circle collides or overlaps any of the stationary circles in this example.
You can detect collision in your tween function. Define a collide function to be called from inside the tween function as follows:
function collide(node){
var trans = d3.transform(d3.select(node).attr("transform")).translate,
x1 = trans[0],
x2 = trans[0] + (+d3.select(node).attr("r")),
y1 = trans[1],
y2 = trans[1] + (+d3.select(node).attr("r"));
var colliding = false;
points.each(function(d,i){
var ntrans = d3.transform(d3.select(this).attr("transform")).translate,
nx1 = ntrans[0],
nx2 = ntrans[0] + (+d3.select(this).attr("r")),
ny1 = ntrans[1],
ny2 = ntrans[1] + (+d3.select(this).attr("r"));
if(!(x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1))
colliding=true;
})
return colliding;
}
Where points are the stationary points, and node is the transitioning element. What collide does is check whether node overlaps with any of the points (as shown in collision detection example here).
Because we need the node to be passed to the tween function, we replace attrTween used in Mike's example, with tween:
circle.transition()
.duration(10000)
.tween("attr", translateAlong(path.node()))
.each("end", transition);
Finally, the tween function calling our collide:
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
d3.select(this).attr("transform","translate(" + p.x + "," + p.y + ")");
if(collide(this))
d3.select(this).style("fill", "red")
else
d3.select(this).style("fill", "steelblue")
};
};
}
See the full demo here
The easiest way is to just check how "close" the transitioning circle is to the other points.
var pop = d3.select("body").append("div")
.style("position","absolute")
.style("top",0)
.style("left",0)
.style("display", "none")
.style("background", "yellow")
.style("border", "1px solid black");
// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
var l = path.getTotalLength();
var epsilon = 5;
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
points.forEach(function(d,i){
if ((Math.abs(d[0] - p.x) < epsilon) &&
(Math.abs(d[1] - p.y) < epsilon)){
pop.style("left",d[0]+"px")
.style("top",d[1]+20+"px")
.style("display","block")
.html(d);
return false;
}
})
return "translate(" + p.x + "," + p.y + ")";
};
};
}
The faster the circle moves the greater your epsilon will need to be.
Example here.
I am yet a noob for d3 and javascript,while I was writing some code to deal with drag the dots and zoom the coordinate, something weird happened, the dots call the drag function,an outer group() element call the zoom, but when I dragging the dot, the outer group change its translate attribute.Code comes below:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>zoom test</title>
</head>
<body></body>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js">
</script>
<script type="text/javascript" charset="utf-8">
function addElem(container,elem){
return container.append(elem);
}
function createLinearScale(dx,dy,rx,ry){
return d3.scale.linear()
.domain([dx,dy])
.range([rx,ry])
}
function appendWithData(container,selector,type,id,classed,dataset){
var result = container.selectAll(selector)
.data(dataset)
.enter()
.append(type);
if(id)
result.attr("id",id);
if(classed)
result.classed(classed,true);
return result;
}
function getElem(selector){
return d3.select(selector);
}
function getAxis(){
return d3.svg.axis();
}
function drag(){
return d3.behavior.drag();
}
function getThis(){
return d3.select("this");
}
function zoom(){
return d3.behavior.zoom();
}
function arrayDelete(array,target,condition){
for (var i = 0, l = array.length; i < l; i ++) {
var v = arr[i];
if((target = v) && condition){
array.splice(i,1);
}
}
}
</script>
<script type="text/javascript" charset="utf-8">
/**
* Set frame for page
*
*/
var body = getElem("body");
var svg = addElem(body,"svg");
var outer = addElem(svg,"g");
var target = addElem(outer,"g");
var x_axis = addElem(target,"g");
var y_axis = addElem(target,"g");
var dots = addElem(target,"g");
var data = [
[ 5, 20 ],
[ 480, 90 ],
[ 250, 50 ],
[ 100, 33 ],
[ 330, 95 ],
[ 410, 12 ],
[ 475, 44 ],
[ 25, 67 ],
[ 85, 21 ],
[ 220, 88 ]
];
/**
* Add axis to chart
*
*/
var height = 500;
var width = 960;
var x_scale = createLinearScale(0, d3.max(data,function(d){return d[0];}) + 50, 0, width - 10);
var y_scale = createLinearScale(0, d3.max(data, function(d){return d[1];}) + 50, height - 10, 0);
var ax_scale = createLinearScale(0, width - 10, 0, d3.max(data,function(d){return d[0];}) + 50);
var ay_scale = createLinearScale(height -10, 0, 0, d3.max(data, function(d){ return d[1];}) + 50);
var xaxis = getAxis().scale(x_scale).orient("bottom").ticks(30);
var yaxis = getAxis().scale(y_scale).orient("right").ticks(10);
x_axis.attr("transform",function(d){return "translate(0," + (height - 10) + ")"}).call(xaxis);
y_axis.attr("transform",function(d){return "translate(0,0)"}).call(yaxis);
/**
* Add dots * */
var dot = appendWithData(dots,"circle","circle","","datum",data);
var text = appendWithData(dots,"text","text","","index",data);
dot.data(data).attr({
"id" : function(d,i){return "datum" +i;},
"tag": function(d,i){return i + "";},
"cx" : function(d){return x_scale(d[0]).toFixed(0)},
"cy" : function(d){return y_scale(d[1]).toFixed(0)},
"fill" : function(d){return "rgb(0," + (d[1] * 5) % 255 + ",53)"},
"r" : 10 });
text.data(data).attr({
"id" : function(d,i){return "index" + i;},
"tag": function(d,i){return i + ""},
"y" : function(d){return y_scale(d[1]).toFixed(0)},
"x" : function(d){return x_scale(d[0]).toFixed(0)},
"transform" : "translate(15,-15)"
})
.text(function(d){return d[0] + "," + d[1]});
var flag = 1;
var drag = drag();
function dragstart(){
console.log("dragstart")
var cur = d3.select(this);
console.log("drag");
cur.transition().ease("elastc").attr({
"r" : 15
});
}
function dragging(){
flag = 0;
var cur = d3.select(this);
var tag = cur.attr("tag");
cir = d3.select("circle[tag='" + tag + "']");
txt = d3.select("text[tag='" + tag + "']");
console.log(cur);
console.log(txt);
var cur_x = d3.event.x;
var cur_y = d3.event.y;
//target.attr("transform","translate(0,0)");
cir.attr({
"cx" : function(d){return cur_x.toFixed(0)},
"cy" : function(d){return cur_y.toFixed(0)},
//"fill" : function(d){return "rgb(0," + (y_scale(cur_y) * 5) % 255 + ",53)"},
});
txt.attr({
"x" : function(d){return cur_x.toFixed(0)},
"y" : function(d){return cur_y.toFixed(0)},
})
.text(new Number(ax_scale(cur_x)).toFixed(0) + "," + new Number(ay_scale(cur_y)).toFixed(0));
}
function dragged(){
var cur = d3.select(this);
cur.transition().ease("elastc").attr({
"r" : 10
});
flag = 1;
}
drag.on("dragstart",dragstart)
.on("drag",dragging)
.on("dragend",dragged);
dot.call(drag);;
var zoom = zoom();
function zoomed(){
//if(flag){
console.log("zoomed");
outer.attr("transform","translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
//}
}
zoom.on("zoom",zoomed);
outer.call(zoom);
</script>
</html>
As you see, I set a toggle(the variable called flag) to solve the trigger problem,but it cause a new problem, I can't zoom with wheel when the mouse is aloft a blank space.
If I understand the question correctly, you want the zooming on the outer while you want dragging on the dots. In that case, there were two problems with your program:
Empty g as the outer: A g element is just an empty container and is unable to capture events inside itself. If you want to capture mouse actions anywhere inside the g, you need to include an invisible background rect inside it with pointer-events: all. Also, you want this element to appear before all other elements in the DOM so that it is indeed in the background:
var bgRect = addElem(outer, "rect");
bgRect.attr('fill', 'none')
.attr('stroke', 'none')
.attr('width', width)
.attr('height', height);
var target = addElem(outer, "g");
// ...
Update: See this question as well: d3.js - mouseover event not working properly on svg group
Transitioning on zooming: The "expected" zoom behavior is that the point under the mouse pointer stays static while everything around the pointer zooms in/out. Hence, the container needs to both scale and transition.
function zoomed(){
console.log("zoomed");
outer.attr("transform","translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
}
However, in your case, you do not want the container to transition so that the axis stays firmly rooted at the origin (0, 0). So:
function zoomed(){
console.log("zoomed");
outer.attr("transform","translate(" + [0,0] + ") scale(" + d3.event.scale + ")");
}
Working demo with these changes: http://jsfiddle.net/S3GsC/
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
});