Auto width and height for SVG image - image

I am new to SVG, so sorry if this is a bit of a noob question. I am trying to figure out how to get an image to display with the full width and height of the referenced image. I am using D3.js to build a graph and I want a logo to appear in the corner. The problem is that the image href will be set using javascript, so the image being referenced is never a fixed width or height. What I am wanting is the image to display in it's full width and height, not scaled up or down. What I was hoping for is the ability to automatically set the width and height of the image based on it's content, such as setting the width and height attributes to auto. This however just results in the image not being shown.
d3.select("body")
.append("svg")
.attr('id','mySVG')
.attr({
"width": "100%",
"height": "100%"
})
.attr("viewBox", "0 0 " + width + " " + height )
.attr("preserveAspectRatio", "none");
d3.select('#mySVG')
.append('image')
.attr('image-rendering','optimizeQuality')
.attr('height','auto') <--- This does not work
.attr('width','auto') <--- This does not work
.attr('xlink:href', logoUrl)
.attr('x','0')
.attr('y','0');
How would I go about specifying that the SVG image width and height must be dynamically determined based on the referenced image size?

I don't think 'auto' is a valid value for the 'length' attr of a svg image element. Take a look at the spec here. You might want to use '100%'
You could load the image then inspect the width and height onload.
JSFiddle: http://jsfiddle.net/WQBPC/4/
var logoUrl = 'http://placehold.it/100x50&text=Logo';
//SVG Setup stuff
var svg = d3.select("body").append("svg")
.attr('id','mySVG')
.attr({
"width": '100%',
"height": '100%'
})
var svg_img= svg.append('image')
.attr('image-rendering','optimizeQuality')
.attr('x','0')
.attr('y','0');
//Load image and get size.
var img = new Image();
img.src = logoUrl;
img.onload = function() {
var width = this.width;
var height = this.height;
svg_img .attr('height', height)
.attr('width',width)
.attr('xlink:href', logoUrl)
}

d3.select("svg")
.append("image")
.attr("id", "myImage")
.attr("xlink:href", "myImage.png")
.on("load", function(d) {
var myImage = new Image();
myImage.onload = function() {
d3.select("#myImage")
.attr("width", this.width)
.attr("height", this.height);
}
myImage.src = "myImage.png";
});

It's a couple years since this has been asked but I'm currently trying to determine some things about the use of auto as a value for width or height myself and thought I would share what I've found. According to Amelia Bellamy-Royds' 2015 article on scaling SVGs and the codepen example she provides, if you put auto in as either the width or the height value of your <svg>, along with a value for the viewBox, you should be able to see the content scaling proportionately. Unfortunately, she also points out that at the time this only worked in "Blink/Firefox" browsers. Blink is a fork of part of WebKit, so it's possible that by now Chrome will support this as well. I am seeing behavior in Chrome which would seem to support this possibility, but I'm also getting an error so YMMV.

Related

d3 zoom and drag with SVG axes and canvas chart

I've got a chart with lots of points. That's why I'm using canvas to draw the lines. For the x and y axes I'd like to use SVG since it's sharper and drawing text with canvas isn't super fast.
Here is the code (TypeScript)
import { min, max } from "d3-array";
import { scaleLinear, ScaleLinear } from "d3-scale";
import { select, event, Selection } from "d3-selection";
import { line, Line } from "d3-shape";
import { ZoomBehavior, zoom } from "d3-zoom";
import { axisBottom, Axis, axisLeft } from "d3-axis";
interface Margin {
left: number;
right: number;
top: number;
bottom: number;
}
interface Config {
margin: Margin;
target: HTMLCanvasElement;
svg: SVGSVGElement;
}
export default class ScopeChart {
private canvas: Selection<HTMLCanvasElement, unknown, null, undefined>;
private svg: Selection<SVGGElement, unknown, null, undefined>;
private xAxis: Axis<number>;
private xAxisGroup: Selection<SVGGElement, unknown, null, undefined>;
private yAxis: Axis<number>;
private yAxisGroup: Selection<SVGGElement, unknown, null, undefined>;
private context: CanvasRenderingContext2D;
private raw: number[];
private filtered: number[];
private xScale: ScaleLinear<number, number>;
private yScale: ScaleLinear<number, number>;
private line: Line<number>;
public constructor(config: Config) {
this.raw = [];
this.filtered = [];
const behavior = zoom() as ZoomBehavior<SVGGElement, unknown>;
const width = 640;
const height = 480;
const w = width - config.margin.left - config.margin.right;
const h = height - config.margin.top - config.margin.bottom;
this.canvas = select(config.target)
.attr("width", w)
.attr("height", h)
.style(
"transform",
`translate(${config.margin.left}px, ${config.margin.top}px)`
);
this.svg = select(config.svg)
.attr("width", width)
.attr("height", height)
.append("g")
.attr(
"transform",
`translate(${config.margin.left}, ${config.margin.top})`
);
this.svg
.append("rect")
.attr("width", w)
.attr("height", h)
.style("fill", "none")
.style("pointer-events", "all")
.call(behavior);
behavior
// set min to 1 to prevent zooming out of data
.scaleExtent([1, Infinity])
// prevent dragging data out of view
.translateExtent([[0, 0], [width, height]])
.on("zoom", this.zoom);
this.context = (this.canvas.node() as HTMLCanvasElement).getContext(
"2d"
) as CanvasRenderingContext2D;
this.xScale = scaleLinear().range([0, w]);
this.xAxis = axisBottom(this.xScale) as Axis<number>;
this.xAxisGroup = this.svg
.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${h})`)
.call(this.xAxis);
this.yScale = scaleLinear().range([h, 0]);
this.yAxis = axisLeft(this.yScale) as Axis<number>;
this.yAxisGroup = this.svg
.append("g")
.attr("class", "y axis")
.call(this.yAxis);
this.line = line<number>()
.x((_, i): number => this.xScale(i))
.y((d): number => this.yScale(d))
.context(this.context);
}
private drawRaw(context: CanvasRenderingContext2D): void {
context.beginPath();
this.line(this.raw);
context.lineWidth = 1;
context.strokeStyle = "steelblue";
context.stroke();
}
private drawFiltered(context: CanvasRenderingContext2D): void {
context.beginPath();
this.line(this.filtered);
context.lineWidth = 1;
context.strokeStyle = "orange";
context.stroke();
}
private clear(context: CanvasRenderingContext2D): void {
const width = this.canvas.property("width");
const height = this.canvas.property("height");
context.clearRect(0, 0, width, height);
}
public render(raw: number[], filtered: number[]): void {
this.raw = raw;
this.filtered = filtered;
this.xScale.domain([0, raw.length - 1]);
this.yScale.domain([min(raw) as number, max(raw) as number]);
this.clear(this.context);
this.drawRaw(this.context);
this.drawFiltered(this.context);
this.xAxisGroup.call(this.xAxis);
this.yAxisGroup.call(this.yAxis);
}
public zoom = (): void => {
const newXScale = event.transform.rescaleX(this.xScale);
const newYScale = event.transform.rescaleY(this.yScale);
this.line.x((_, i): number => newXScale(i));
this.line.y((d): number => newYScale(d));
this.clear(this.context);
this.drawRaw(this.context);
this.drawFiltered(this.context);
this.xAxisGroup.call(this.xAxis.scale(newXScale));
this.yAxisGroup.call(this.yAxis.scale(newYScale));
};
}
And here is the live example
https://codesandbox.io/s/1pprq
The problem is translateExtent. I'd like to restrict dragging when zoomed in to my available data, i.e. [0, 20000] on the x axis and [-1.2, 1.2] on the y axis.
Somehow I'm currently able to zoom in further. You can see the effect when zooming in and dragging all the way to the bottom. You will see a gap between the lowest value and the x axis.
I think it has something to do with using canvas and svg. The svg is on top of the canvas and the ZoomBehavior is on the svg. Somehow the zoom isn't properly translated to the canvas.
I'd like to keep the svg on top because I need more interface elements later one which are added to the svg.
Any ideas? Thank you!
If I understand the question correctly:
The issue you are running into is that your translate extent is not correct
behavior
// set min to 1 to prevent zooming out of data
.scaleExtent([1, Infinity])
// prevent dragging data out of view
.translateExtent([[0, 0], [width, height]])
.on("zoom", this.zoom);
In the above, width and height refer to the width and height of the SVG, not the canvas. Also, zoom extent is not often specified explicitly, but if zoom extent is not specified with zoom.extent(), the zoom extent defaults to the dimensions of the container it was called on.
If your translate extent is equal in size to your zoom extent - by default the extent of the container (the SVG) - which it is, you can zoom and pan anywhere within that container's coordinate space, but not to coordinates beyond it. Consequently, when zoom scale is 1, we cannot pan anywhere as we would by definition pan beyond the translate extent.
Note: This logically means translate extent must contain and not be smaller than the zoom extent.
But, in this scenario, if we zoom in, we can pan and remain within the translate extent.
We can see if you zoom in you cannot pan up beyond the intended limits. This is because the top of the canvas is at y==0, this is the bounds of the translate extent.
As you note if you zoom in you can pan down beyond the intended limits. The bottom of the canvas is h, which is smaller than height which is the translate extent limit, so as we zoom in, we can pan further and further down as the gap between h and height increases each time we zoom (and as noted above, cannot be panned when k==1).
We could try to change the translate extent to reflect the bounds of the canvas. But, as the canvas is smaller than the SVG this won't work as the translate extent would be smaller than the zoom extent. As noted above and noted here by Mike: "The problem is that the translateExtent you’ve specified is smaller than the zoom extent. So there’s no way to satisfy the requested constraint."
We can modify the translateExtent and the zoom's extent, however:
behavior
// set min to 1 to prevent zooming out of data
.scaleExtent([1, Infinity])
// set the zoom extent to the canvas size:
.extent([[0,0],[w,h]])
// prevent dragging data out of view
.translateExtent([[0, 0], [w, h]])
.on("zoom", this.zoom);
The above creates a zoom behavior that constrains the canvas to its original extent - we would be providing the same parameters if we were calling the zoom on the canvas and wanted to not be able to pan beyond it (except we could rely on the default zoom extent to provide the appropriate values rather than specifying the zoom extent manually).
Here's an updated sandbox.

Making an button-activated animated tour in d3

I'm trying to make a button-activated d3 zoom tour through three Northeastern US points, but am having a hard time getting the data to show up visually (it shows in the console, though). I'm a beginning user and can usually solve things, but this is over my head.
Here are the an example that comes close to what I'm trying to do:
Zooms between different spots in US:
http://bl.ocks.org/mbostock/6242308
The example doesn't style the data, uses TopoJSON and also uses canvas to do the zooming calls. I'm trying to do the zoom with GeoJSON(so I can link to a CartoDB table), and style it.
I've gone through a lot to make both of those things happen and am running out of successes. Right now it comes up blank and has been. I can see the data live, but can't change the styling.
What am I doing wrong here? I'm sure it's something simple, but need a nudge.
<!DOCTYPE html>
<html lang ="en">
<head>
<meta charset="utf-8">
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<style type="text/css">
canvas{
color: 'blue';
}
path.state {
color: 'red';
}
</style>
</head>
<body>
<script type="text/javascript">
var width = 960,
height = 500,
stateMap;
var sf = [-122.417, 37.775],
ny = [-74.0064, 40.7142];
var scale,
translate,
visibleArea, // minimum area threshold for points inside viewport
invisibleArea; // minimum area threshold for points outside viewport
var zoom = d3.behavior.zoom()
.size([width, height])
.on("zoom", zoomed);
var projection = d3.geo.mercator()
.translate([width/2, height/2])
.scale(500);
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
var path = d3.geo.path()
.projection(simplify)
.context(context);
stateMap = d3.json("http://linepointpath.cartodb.com/api/v2/sql?format=GeoJSON&q=SELECT * FROM GRAPHstates", function(error, stateMap) {
console.log(stateMap)
canvas
svg.selectAll("path")
.data(stateMap.feature)
.enter().append("path")
.attr("class", "state")
.attr("d", path)
.call(zoomTo(sf, 4).event)
.transition()
.duration(60 * 1000 / 89 * 2)
.each(jump);
});
var simplify = d3.geo.transform({
point: function(x, y, z) {
if (z < visibleArea) return;
x = x * scale + translate[0];
y = y * scale + translate[1];
if (x >= -10 && x <= width + 10 && y >= -10 && y <= height + 10 || z >= invisibleArea) this.stream.point(x, y);
}
});
function zoomTo(location, scale) {
var point = projection(location);
return zoom
.translate([width / 2 - point[0] * scale, height / 2 - point[1] * scale])
.scale(scale);
}
function zoomed(d) {
translate = zoom.translate();
scale = zoom.scale();
visibleArea = 1 / scale / scale;
invisibleArea = 200 * visibleArea;
context.clearRect(0, 0, width, height);
context.beginPath();
path(d);
context.stroke();
}
function jump() {
var t = d3.select(this);
(function repeat() {
t = t.transition()
.call(zoomTo(ny, 6).event)
.transition()
.call(zoomTo(sf, 4).event)
.each("end", repeat);
})();
}
</script>
</body>
</html>
I suspect the larger issue is that you're using a GeoJSON when the example you're going by is using a TopoJSON. The differences between the two are likely causing problems with how the paths are being rendered.
Another problem you're running into is that with a canvas, the function calls are different. You're using the normal svg function calls to append an SVG, bind the data, and they style it. With canvas, you interact with the elements through the canvas context object. This has a different syntax and usage than the standard SVG object you're trying to use in your code. If you follow this tutorial you'll notice she doesn't call any of the SVG functions but instead uses the context API to draw and style the canvas elements. Be sure to look at the working example of the code.
In your case, this means your code here:
svg.selectAll("path")
.data(stateMap.feature)
.enter().append("path")
.attr("class", "state")
.attr("d", path)
.call(zoomTo(sf, 4).event)
.transition()
.duration(60 * 1000 / 89 * 2)
Is unnecessary and not doing anything for you. The code that generates the path is embedded in the zoomed() function using the context object:
context.clearRect(0, 0, width, height);
context.beginPath();
path(d);
context.stroke();
It's using the context functions to create the objects you're trying to show. There are a chain of function calls that generate this behavior and you'll need to break down the chain to make sure you're getting what you want at each step.
If you want to use the GeoJSON start with just getting the map to display and then applying the zoom functionality. It'll probably make your life a lot easier in the end to iteratively build the visualization you want.
For more information on the difference between canvas and svg with D3, including examples of doing the same operation with each, checkout this blogpost and good luck with the project.

When a d3.behavior.zoom event is triggered programatically, how do you set inital values for translate and scale?

The squares in the example below are part of an SVG group that has an initial translate and scale set.
Clicking on a square initiates a zoom transition. But the intial values set by the transition are different from my defaults, as made obvious by the jarring start to this transition.
How can I set initial values for translate and scale on a zoom transition that I initiate programatically?
var svg = d3.select("#main");
svg.append("rect").attr({"x":0,"y":0,"height":100,"width":100,"fill":"red"})
svg.append("rect").attr({"x":100,"y":100,"height":100,"width":100,"fill":"blue"})
svg.append("rect").attr({"x":0,"y":100,"height":100,"width":100,"fill":"green"})
svg.append("rect").attr({"x":100,"y":0,"height":100,"width":100,"fill":"yellow"})
var zoom = d3.behavior.zoom().on("zoom",function(){
var t = d3.event.translate;
var s = d3.event.scale;
console.log(s)
svg.attr("transform","translate("+t[0]+","+t[1]+") scale("+s+")")
}).scaleExtent([1,10]).scale(1).translate([0,0])
d3.select("svg").call(zoom)
d3.selectAll("rect").on("mousedown",function(){
var scale = Math.random()*3;
var translate = [Math.random()*200,Math.random()*200]
zoom.scale(scale);
zoom.translate(translate);
//new transition
var T = svg.transition().duration(5000)
zoom.event(T);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<p style="font-weight:bold">When a zoom is triggered programatically, how do you set inital values for translate and scale?</p>
<p>Click on one of the squares</p>
<svg height="600px" width="600px">
<g id="main" transform="translate(25,25) scale(0.25)"></g>
</svg>
That is a problem with the zoom function itself. I would suggest zooming the children as opposed to the parent if that would work
var zoom = d3.behavior.zoom().on("zoom",function(){
var t = d3.event.translate;
var s = d3.event.scale;
svg.selectAll("rect").attr("transform","translate("+t[0]+","+t[1]+") scale("+s+")")
}).scaleExtent([1,10]);
EDIT
The problem with the above code is that d3.js does not register the transformation or initial state of the SVG. This problem runs deeper. As d3 does not keep track of the SVG transformations and just executes them. It only keeps track of transformations you've run on the library in a variable called __chart__.
So when the zoom function is run it just interpolates the variables and gives the output. As no functions have been run on this yet the __chart__ variable has not been set and causing the jerky start from (x=0, y=0, k=1).
Solution:
Run this code before the zoom transformation to set the initial chart manually
svg.transition().each(function(){
this.__chart__={x:25,y:25,k:0.25}; //or you can pick those values using attr
});
Zoom the svg programmatically to 25,25,0.25 first before any other function. (this is why your workaround works as the __chart__ variable gets set)
To set the initial value of the zoom, try something like this:
// Init zoom
var zoom = d3.behavior.zoom().on("zoom", function () {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
});
// Get SVG element
var svg = d3.select("svg")
.call(zoom)
.append("g");
// Create circle
svg.append("circle")
.attr("cx",0)
.attr("cy",0)
.attr("r", 5);
// Create init value
var scale = 5;
var translate = [50, 50];
// Set init value
zoom.scale(scale);
zoom.translate(translate);
// Call zoom event
svg.call(zoom.event);
// or svg.transition().call(zoom.event);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg height="100px" width="100px"></svg>
I was looking for the answer to this, but it seems D3 has already evolved a couple of versions.
Although Majkl and cjds's answers helped me solve my problem, I thought it would help to leave more up to date information, since it was hard finding v5.4 examples out there, until I found Observable at least.
// Applies event transformation to the Group element's attribute
const zoom_action = () => g.attr("transform", d3.event.transform)
// Create the zoom handler
const zoom = d3
.zoom()
.on("zoom", zoom_action)
// Get SVG element and apply zoom behaviour
var svg = d3
.select("svg")
.call(zoom)
// Create Group that will be zoomed
var g = svg.append("g")
// Create circle
g.append("circle")
.attr("cx",0)
.attr("cy",0)
.attr("r", 5)
// Set initial scale and translation
zoom.scaleBy(svg, 5)
zoom.translateBy(svg, 50, 50)
<script src="https://d3js.org/d3.v5.js"></script>
<svg height="100px" width="100px"></svg>

How can i add an image in nvd3 piechart legend?

I want to add an image in nvd3 piechart legend instead of text.
I have this code but it changes only the text in the tooltip with the image. I want to change and the text in the legend with the same image.
var testdata = [
{
key: "<img src="+"./imgs/facebook.jpg"+">",
y: 1
},
{
key: ""<img src="+"./imgs/twitter.jpg"+">"",
y: 2
} ];
nv.addGraph(function() {
var chart = nv.models.pieChart()
.x(function(d) { return d.key})
.labelThreshold(.08)
.showLabels(true)
.color(d3.scale.category10().range())
.donut(true);
chart.pie.donutLabelsOutside(true).donut(true);
d3.select("#mypiechart")
.datum(testdata)
.transition().duration(1200)
.call(chart);
return chart;});
Any ideas?
This is not currently possible with nvd3.js. The tooltip works because the img element you have specified is being set into a div that isn't contained within the svg element. It doesn't work for the legend or chart labels because those are built using svg text elements. In order to show an image within the chart svg we'd need to use an svg image element.
We could build the svg image elements if we hack the nvd3.js code. Here's an outline of what you could do to get the legend working. You could then decide if you'd want to try something similar in the nv.models.pie code for the chart labels or if you'd just want to set chart.showLabels to false in your chart configuration.
Add a new key in your data to provide the image path:
var testdata = [
{
key: "<img src="+"./imgs/facebook.jpg"+">",
y: 1,
image_path: "./imgs/facebook.jpg"
},
{
key: "<img src="+"./imgs/twitter.jpg"+">",
y: 2,
image_path: "./imgs/twitter.jpg"
} ];
Update the nv.models.legend code to show the image:
seriesEnter.append('circle')
.style('stroke-width', 2)
.attr('class','nv-legend-symbol')
.attr('r', 5);
// Add an svg image into the legend
seriesEnter.append('image')
.attr('xlink:href', function(d) { return d.image_path} )
.attr('height',20)
.attr('width',20)
.attr('y', '-10')
.attr('x', '8');
Update the nv.models.legend code to not show the key:
// Don't add the key value into the legend text
//series.select('text').text(getKey);
Update the nv.models.legend code to consider the image width when determining the legend layout:
//seriesWidths.push(nodeTextLength + 28); // 28 is ~ the width of the circle plus some padding
//Include image width...
seriesWidths.push(nodeTextLength + 48);

how to set height and width of nvd3 chart

I'm trying to set the width and height of a nvd3 multi bar chart programmatically using
chart.width(600);
chart.height(400);
See the example here:
http://jsfiddle.net/hPgyq/20/
As you can see this really messes up the chart. I know I can do this is CSS with:
#chart svg {
width: 600px;
height: 400px;
}
but I thought this was also possible using the width() and height() functions on the chart. Am I doing something wrong here or am I mis-using the two functions?
Yes it is possible, like you have specified the width & height of the chart, you have to use the d3.select and set its width & height attribute.
Changes to the code are below and there is a version of the code here
function visualizeData(data) {
nv.addGraph(function() {
var width = 600, height = 400;
chart = nv.models.multiBarChart().x(function(d) {
return d.x;
}).y(function(d) {
return d.y;
}).color(['#aec7e8', '#7b94b5', '#486192']).stacked(true)
//.margin({top:150,right:150,bottom:150,left:150})
.width(width).height(height);
chart.multibar.hideable(false);
chart.xAxis.showMaxMin(true).tickFormat(d3.format(',f'));
chart.yAxis.tickFormat(d3.format(',.1f'));
d3.select('#chart svg').datum(data).transition().duration(500).call(chart).style({ 'width': width, 'height': height });
nv.utils.windowResize(chart.update);
return chart;
});
}
For the AngularJs version (angular-nvd3), I had to add the height either in chart options and as an attribute:
chart.html
<nvd3 options='vm.chartOptions' data='vm.chartData' height="250" config="vm.chartConfig">
</nvd3>
chart.controller.js
vm.chartOptions = {
chart : {
height : 250,
type : "pieChart",
donut : true,
showLegend : false,
//The rest of the configuration
};
As it is told in the comments, first seems to control the height of the inner svg and the second does the global chart height.

Resources