d3 embeded image displayed twice - javascript

I'm embedding an image on a d3 diagram as in this example:
var logoCanvas = d3.select("body").append("canvas")
.attr("id", "newCanvas")
.attr("width", width / 2)
.attr("height", height / 2)
.node();
var context = logoCanvas.getContext("2d");
var imageObj = new Image();
imageObj.src = 'myImage.svg';
imageObj.onload = function() {
context.drawImage(imageObj, 0, 0);
var imageData = document.getElementById("newCanvas").toDataURL("image/png")
//Add as SVG image element
d3.select("svg").
append("image")
.datum(imageData)
.attr("height", 100)
.attr("width", 100)
.attr('transform', 'translate(' + [offsetX - 50, offsetY - 50] + ')')
.attr("xlink:href", function(d) {
return d
})
}
This works all as expected but unfortunately I get the image displayed twice. Basically I only need the appended one and not the one which is drawn onto the context. The thing is the I can't get the appended one without drawing it first.
Is there a way to hide the Image which is drawn in context?
Or a smarter approach of only getting one image displayed but embedded?

Here's a code snippet of an XMLHTTPRequest() and FileReader() approach to fetch a image and "embed" into a SVG. You don't need a canvas at all.
// append svg
d3.select("body").append("svg").attr("height", "300px").attr("width", "276px");
var xhr1 = new XMLHttpRequest();
xhr1.open('GET', 'https://s7.postimg.org/40f9flc1n/dummy1.png');
xhr1.responseType = 'blob';
xhr1.onload = function () {
var fs = new FileReader();
fs.onload = function (result) {
d3.select("svg").append("image")
.datum(result.target.result)
.attr("height", 300)
.attr("width", 276)
.attr("xlink:href", function(d) {
return d
})
};
fs.readAsDataURL(xhr1.response);
};
xhr1.send();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.7/d3.min.js"></script>
Hope this helps. :)

Related

Legend not appearing when using document.createElement('canvas')

I followed this Observable post to easily create a legend.
Since the line
DOM.canvas(1, n)
in the ramp works only on Observable, I replaced it with
document.createElement("canvas")
and also modified the SVG so that it's appended to the main div tag. These changed do not cause any errors however the problem is that the legend is not displayed even though the legend SVG is present in the raw HTML.
Here's the link to a JSFiddle. Any guidance would be greatly appreciated.
The canvas is being created, that's not the problem. The problem is that, since you are now missing the width and height in...
const canvas = DOM.canvas(n, 1);
//these are w & h --------^--^
... you now need to set those yourself. For instance:
d3.select(canvas).attr("width", n)
.attr("height", 1);
Here is a simplified version of that JSFiddle, showing that the canvas works:
legend({
color: d3.scaleSequential([1, 10], d3.interpolateReds),
title: "Title"
})
function legend({
color,
title,
tickSize = 6,
width = 320,
height = 44 + tickSize,
marginTop = 18,
marginRight = 0,
marginBottom = 16 + tickSize,
marginLeft = 0,
ticks = width / 64,
tickFormat,
tickValues
} = {}) {
const svg = d3.select(".scatter").append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.style("overflow", "visible")
.style("display", "block");
svg.append("image")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom)
.attr("preserveAspectRatio", "none")
.attr("xlink:href", ramp(color.interpolator()).toDataURL());
}
function ramp(color, n = 256) {
const canvas = document.createElement('canvas');
const context = canvas.getContext("2d");
d3.select(canvas).attr("width", n)
.attr("height", 1);
for (let i = 0; i < n; ++i) {
context.fillStyle = color(i / (n - 1));
context.fillRect(i, 0, 1, 1);
}
return canvas;
}
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
<div class="scatter">
</div>
By the way, there is no such element as <legend-svg>.
PS: This is the second question from you I'm answering on this subject. As you're new to JavaScript and D3, here is an advice: do not try to use that Observable notebook, that's way too complicated for your purposes. Just create the SVG, the canvas and a basic axis yourself, from scratch, it will be way easier.

avoid zoom transform from resetting after svg is clicked d3

I have a svg element ; the nodes, links, labels etc. are appended to it. I got the zoom-to-particular-node-by-name functionality running but the issue is after zooming automatically to the respective node , whenever I try to pan svg (by clicking and dragging it around), it resets the zoom and the coordinates to how it was before I zoomed to a particular node. I think it has to do with the way d3.event.transform works but I am not able to fix it. I want to be able to continue panning and zooming from the node I zoomed to without resetting any values.
(Also, from a bit of debugging , I observed that the cx and cy coordinates for the nodes did not change by zooming and panning from the code, but If I were to zoom and pan to a node manually , then it would. I guess that is the problem)
var svg1 = d3.select("svg");
var width = +screen.width;
var height = +screen.height - 500;
svg1.attr("width", width).attr("height", height);
var zoom = d3.zoom();
var svg = svg1
.call(
zoom.on("zoom", function() {
svg.attr("transform", d3.event.transform);
})
)
.on("dblclick.zoom", null)
.append("g");
function highlightNode() {
var userInput = document.getElementById("targetNode");
theNode = d3.select("#" + userInput.value);
const isEmpty = theNode.empty();
if (isEmpty) {
document.getElementById("output").innerHTML = "Given node doesn't exist";
} else {
document.getElementById("output").innerHTML = "";
}
svg
.transition()
.duration(750)
.attr(
"transform",
"translate(" +
-(theNode.attr("cx") - screen.width / 2) +
"," +
-(theNode.attr("cy") - screen.height / 4) +
")"
// This works correctly
);
}

dc.js - Rendering two objects (one chart - renders, one shape - doesn't) together in one group?

I have two elements I need to render and a context of the big picture I am trying to achieve (a complete dashboard).
One is a chart that renders fine.
$scope.riskChart = new dc.pieChart('#risk-chart');
$scope.riskChart
.width(width)
.height(height)
.radius(Math.round(height/2.0))
.innerRadius(Math.round(height/4.0))
.dimension($scope.quarter)
.group($scope.quarterGroup)
.transitionDuration(250);
The other is a triangle, to be used for a more complex shape
$scope.openChart = d3.select("#risk-chart svg g")
.enter()
.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", d3.symbol('triangle-up'))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.style("fill", fill);
On invocation of render functions, the dc.js render function is recognized and the chart is seen, but the d3.js render() function is not recognized.
How do I add this shape to my dc.js canvas (an svg element).
$scope.riskChart.render(); <--------------Works!
$scope.openChart.render(); <--------------Doesn't work (d3.js)!
How do I make this work?
EDIT:
I modified dc.js to include my custom chart, it is a work in progress.
dc.starChart = function(parent, fill) {
var _chart = {};
var _count = null, _category = null;
var _width, _height;
var _root = null, _svg = null, _g = null;
var _region;
var _minHeight = 20;
var _dispatch = d3.dispatch('jump');
_chart.count = function(count) {
if(!arguments.length)
return _count;
_count = count;
return _chart;
};
_chart.category = function(category) {
if(!arguments.length)
return _category
_category = category;
return _chart;
};
function count() {
return _count;
}
function category() {
return _category;
}
function y(height) {
return isNaN(height) ? 3 : _y(0) - _y(height);
}
_chart.redraw = function(fill) {
var color = fill;
var triangle = d3.symbol('triangle-up');
this._g.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", triangle)
.attr("transform", function(d) { return "translate(" + 25 + "," + 25 + ")"; })
.style("fill", fill);
return _chart;
};
_chart.render = function() {
_g = _svg
.append('g');
_svg.on('click', function() {
if(_x)
_dispatch.jump(_x.invert(d3.mouse(this)[0]));
});
if (_root.select('svg'))
_chart.redraw();
else{
resetSvg();
generateSvg();
}
return _chart;
};
_chart.on = function(event, callback) {
_dispatch.on(event, callback);
return _chart;
};
_chart.width = function(w) {
if(!arguments.length)
return this._width;
this._width = w;
return _chart;
};
_chart.height = function(h) {
if(!arguments.length)
return this._height;
this._height = h;
return _chart;
};
_chart.select = function(s) {
return this._root.select(s);
};
_chart.selectAll = function(s) {
return this._root.selectAll(s);
};
function resetSvg() {
if (_root.select('svg'))
_chart.select('svg').remove();
generateSvg();
}
function generateSvg() {
this._svg = _root.append('svg')
.attr({width: _chart.width(),
height: _chart.height()});
}
_root = d3.select(parent);
return _chart;
}
I think I confused matters by talking about how to create a new chart, when really you just want to add a symbol to an existing chart.
In order to add things to an existing chart, the easiest thing to do is put an event handler on its pretransition or renderlet event. The pretransition event fires immediately once a chart is rendered or redrawn; the renderlet event fires after its animated transitions are complete.
Adapting your code to D3v4/5 and sticking it in a pretransition handler might look like this:
yearRingChart.on('pretransition', chart => {
let tri = chart.select('svg g') // 1
.selectAll('path.triangle') // 2
.data([0]); // 1
tri = tri.enter()
.append('path')
.attr('class', 'triangle')
.merge(tri);
tri
.attr("d", d3.symbol().type(d3.symbolTriangle).size(200))
.style("fill", 'darkgreen'); // 5
})
Some notes:
Use chart.select to select items within the chart. It's no different from using D3 directly, but it's a little safer. We select the containing <g> here, which is where we want to add the triangle.
Whether or not the triangle is already there, select it.
.data([0]) is a trick to add an element once, only if it doesn't exist - any array of size 1 will do
If there is no triangle, append one and merge it into the selection. Now tri will contain exactly one old or new triangle.
Define any attributes on the triangle, here using d3.symbol to define a triangle of area 200.
Example fiddle.
Because the triangle is not bound to any data array, .enter() should not be called.
Try this way:
$scope.openChart = d3.select("#risk-chart svg g")
.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", d3.symbol('triangle-up'))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.style("fill", fill);

Converting static code into reusable D3.js pie animation

I'm trying to rework a pen (http://codepen.io/anon/pen/JgyCz) by Travis Palmer so that I can use it on multiple elements. We are trying to place several <div class="donut" data-donut="x">'s on a page.
So it would look similar to the html below:
////// HTML
<div class="donut" data-donut="22"></div>
<div class="donut" data-donut="48"></div>
<div class="donut" data-donut="75></div>
The D3.js / jQuery example I'm trying to convert to a reusable compunent is below. (To see full working example go to this link - http://codepen.io/anon/pen/JgyCz)
////// D3.js
var duration = 500,
transition = 200;
drawDonutChart(
'.donut',
$('.donut').data('donut'),
290,
290,
".35em"
);
function drawDonutChart(element, percent, width, height, text_y) {
width = typeof width !== 'undefined' ? width : 290;
height = typeof height !== 'undefined' ? height : 290;
text_y = typeof text_y !== 'undefined' ? text_y : "-.10em";
var dataset = {
lower: calcPercent(0),
upper: calcPercent(percent)
},
radius = Math.min(width, height) / 2,
pie = d3.layout.pie().sort(null),
format = d3.format(".0%");
var arc = d3.svg.arc()
.innerRadius(radius - 20)
.outerRadius(radius);
var svg = d3.select(element).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(pie(dataset.lower))
.enter().append("path")
.attr("class", function(d, i) { return "color" + i })
.attr("d", arc)
.each(function(d) { this._current = d; }); // store the initial values
var text = svg.append("text")
.attr("text-anchor", "middle")
.attr("dy", text_y);
if (typeof(percent) === "string") {
text.text(percent);
}
else {
var progress = 0;
var timeout = setTimeout(function () {
clearTimeout(timeout);
path = path.data(pie(dataset.upper)); // update the data
path.transition().duration(duration).attrTween("d", function (a) {
// Store the displayed angles in _current.
// Then, interpolate from _current to the new angles.
// During the transition, _current is updated in-place by d3.interpolate.
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, percent)
this._current = i(0);
return function(t) {
text.text( format(i2(t) / 100) );
return arc(i(t));
};
}); // redraw the arcs
}, 200);
}
};
function calcPercent(percent) {
return [percent, 100-percent];
};
The best way to do this is to use angular directives. An angular directive basically wraps html inside a custom tag and let's you stamp the directive over and over across multiple pages or multiple times a page. See this video: http://www.youtube.com/watch?v=aqHBLS_6gF8
There is also a library that is out called nvd3.js that contains prebuilt angular directives that can be re-used: http://nvd3.org/
Hope this helps.
ok, I figured it out. I feel a bit dumb in hindsight, but what can I say, I'm a js n00b. All you have to do is make a few more call to the drawDonutChart() method. In short:
drawDonutChart(
'#donut1',
$('#donut1').data('donut'),
220,
220,
".35em"
);
drawDonutChart(
'#donut2',
$('#donut2').data('donut'),
120,
120,
".35em"
);
drawDonutChart(
'#donut3',
$('#donut3').data('donut'),
150,
150,
".2em"
);

SVG to Canvas with d3.js

Has anyone tried using a svg to canvas library when creating d3.js visualizations? I've tried to use canvg.js and d3.js to convert svg to canvas from within an android 2.3 application webview, but when I call:
svg.selectAll(".axis")
.data(d3.range(angle.domain()[1]))
.enter().append("g")
.attr("class", "axis")
.attr("transform", function(d) { return "rotate(" + angle(d) * 180 / Math.PI + ")"; })
.call(d3.svg.axis()
.scale(radius.copy().range([-5, -outerRadius]))
.ticks(5)
.orient("left"))
.append("text")
.attr("y",
function (d) {
if (window.innerWidth < 455){
console.log("innerWidth less than 455: ",window.innerWidth);
return -(window.innerHeight * .33);
}
else {
console.log("innerWidth greater than 455: ",window.innerWidth);
return -(window.innerHeight * .33);
}
})
.attr("dy", ".71em")
.attr("text-anchor", "middle")
.text(function(d, i) { return capitalMeta[i]; })
.attr("style","font-size:12px;");
I get the error: Uncaught TypeError: Cannot call method setProperty of null http://mbostock.github.com/d3/d3.js?2.5.0:1707
Would some sort of headless browser application, or a server side js parser work? Has anyone encountered this before?
Here's one way you could write your svg to canvas (and then save the result as a png or whatever):
// Create an export button
d3.select("body")
.append("button")
.html("Export")
.on("click",svgToCanvas);
var w = 100, // or whatever your svg width is
h = 100;
// Create the export function - this will just export
// the first svg element it finds
function svgToCanvas(){
// Select the first svg element
var svg = d3.select("svg")[0][0],
img = new Image(),
serializer = new XMLSerializer(),
svgStr = serializer.serializeToString(svg);
img.src = 'data:image/svg+xml;base64,'+window.btoa(svgStr);
// You could also use the actual string without base64 encoding it:
//img.src = "data:image/svg+xml;utf8," + svgStr;
var canvas = document.createElement("canvas");
document.body.appendChild(canvas);
canvas.width = w;
canvas.height = h;
canvas.getContext("2d").drawImage(img,0,0,w,h);
// Now save as png or whatever
};
The answer by #ace is very good, however it doesn't handle the case of external CSS stylesheets. My example below will automatically style the generated image exactly how the original SVG looks, even if it's pulling styles form separate stylesheets.
// when called, will open a new tab with the SVG
// which can then be right-clicked and 'save as...'
function saveSVG(){
// get styles from all required stylesheets
// http://www.coffeegnome.net/converting-svg-to-png-with-canvg/
var style = "\n";
var requiredSheets = ['phylogram_d3.css', 'open_sans.css']; // list of required CSS
for (var i=0; i<document.styleSheets.length; i++) {
var sheet = document.styleSheets[i];
if (sheet.href) {
var sheetName = sheet.href.split('/').pop();
if (requiredSheets.indexOf(sheetName) != -1) {
var rules = sheet.rules;
if (rules) {
for (var j=0; j<rules.length; j++) {
style += (rules[j].cssText + '\n');
}
}
}
}
}
var svg = d3.select("svg"),
img = new Image(),
serializer = new XMLSerializer(),
// prepend style to svg
svg.insert('defs',":first-child")
d3.select("svg defs")
.append('style')
.attr('type','text/css')
.html(style);
// generate IMG in new tab
var svgStr = serializer.serializeToString(svg.node());
img.src = 'data:image/svg+xml;base64,'+window.btoa(unescape(encodeURIComponent(svgStr)));
window.open().document.write('<img src="' + img.src + '"/>');
};
And, to be complete, the button that calls the function:
// save button
d3.select('body')
.append("button")
.on("click",saveSVG)
.attr('class', 'btn btn-success')
Have you tried the same code on a browser supporting SVG to see if it's a problem with webview? Then try this example using canvg or this one using DOM serialization. For server-side rendering, you could start with this example for how to render it to canvas server-side using Node.js.
I've not tried a library, but have rendered a d3-produced SVG to a canvas following this post on MDN.
This code is a quick mish-mash of the MDN and some jQuery, that'll you'll need to tidy up, and it has no error- or platfrom-checking, but it works, and I hope it helps.
$(document.body).append(
'<canvas id="canvas" width="'+diameter+'" height="'+diameter+'"></canvas>'
);
// https://developer.mozilla.org/en/docs/HTML/Canvas/Drawing_DOM_objects_into_a_canvas
var el = $($('svg')[0]);
var svgMarkup = '<svg xmlns="http://www.w3.org/2000/svg"'
+ ' class="' + el.attr('class') +'"'
+ ' width="' + el.attr('width') +'"'
+ ' height="' + el.attr('height') +'"'
+ '>'
+ $('svg')[0].innerHTML.toString()+'</svg>';
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var DOMURL = this.URL || this.webkitURL || this;
var img = new Image();
var svg = new Blob([svgMarkup], {type: "image/svg+xml;charset=utf-8"});
var url = DOMURL.createObjectURL(svg);
img.onload = function() {
ctx.drawImage(img, 0, 0);
alert('ok');
DOMURL.revokeObjectURL(url);
};
img.src = url;

Categories

Resources