D3 line graph not filling entire svg width - javascript
As the title states, I have created a D3 line/area graph, and I am finding it difficult to get the graph's width to remain constant, depending on the amount of data I have given it to render, it scales the width of the graph accordingly, but I am unsure of how I can get it to remain at a constant width, regardless of the amount of data given, which is what I would like to achieve.
I imagine it has something to do with the scaling of the x and y coordinates, but I am stuck at the moment and can't seem to figure out why it is doing this.
Here is the code I have thus far,
//dimensions and margins
var width = 625,
height = 350,
margin = 5,
// get the svg and set it's width/height
svg = d3.select("#main")
.attr("width", width)
.attr("height", height);
//initialize the graph
init([
[12345,42345,32345,22345,72345,62345,32345,92345,52345,22345],
[1234,4234,3234,2234,7234,6234,3234,9234,5234,2234]
]);
$("button").live('click', function(){
var id = $(this).attr("id");
if(id == "one"){
updateGraph([
[52345,32345,12345,22345,62345,72345,92345,32345,22345,22345,52345,32345,12345,22345,62345,72345,92345,32345,22345,22345,52345,32345,12345,22345,62345,72345,92345,32345,22345,22345],
[4234,12345,2234,32345,6234,7234,9234,3234,2234,2234,4234,1234,2234,3234,6234,7234,9234,3234,2234,2234,4234,1234,2234,3234,6234,7234,9234,3234,2234,2234]
]);
}else if(id == "two"){
updateGraph([
[12345,42345,32345,22345,72345,62345,32345,92345,52345,22345,12345,42345,32345,22345,72345,62345,32345,92345,52345,22345,12345,42345,32345,22345,72345],
[1234,2345,3234,2234,7234,6234,3234,9234,5234,2234,1234,4234,3234,2234,7234,6234,3234,9234,5234,2234,1234,4234,3234,2234,7234]
]);
}
});
function init(data){
var x = d3.scale.linear()
.domain([0,data[0].length])
.range([margin, width-margin]),
y = d3.scale.linear()
.domain([0,d3.max(data[0])])
.range([height-margin, margin]),
/* line path generator */
line = d3.svg.line().interpolate('monotone')
.x(function(d,i) { return x(i); })
.y(function(d) { return y(d); }),
/* area path generator */
area = d3.svg.area().interpolate('monotone')
.x(line.x())
.y1(line.y())
.y0(y(0)),
groups = svg.selectAll("g")
.data(data)
.enter()
.append("g");
svg.select("g")
.selectAll("circle")
.data(data[0])
.enter()
.append("circle")
.attr("class", "dot")
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 4);
/* add the areas */
groups.append("path")
.attr("class", "area")
.attr("d",area)
.style("fill", function(d,i) { return (i == 0 ? "steelblue" : "red" ); });
/* add the lines */
groups.append("path")
.attr("class", "line")
.attr("d", line);
}
function updateGraph(data){
var x = d3.scale.linear()
.domain([0,data[0].length])
.range([margin, width-margin]),
y = d3.scale.linear()
.domain([0,d3.max(data[0])])
.range([height-margin, margin]),
/* line path generator */
line = d3.svg.line().interpolate('monotone')
.x(function(d,i) { return x(i); })
.y(function(d) { return y(d); }),
/* area path generator */
area = d3.svg.area().interpolate('monotone')
.x(line.x())
.y1(line.y())
.y0(y(0));
groups = svg.selectAll("g")
.data(data),
circles = svg.select("g")
.selectAll("circle");
circles.data(data[0])
.exit().remove();
circles.data(data[0])
.enter().append("circle")
.attr("class", "dot")
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 4);
/* animate circles */
circles.data(data[0])
.transition()
.duration(1000)
.attr("cx", line.x())
.attr("cy", line.y());
/* animate the lines */
groups.select('.line')
.transition()
.duration(1000)
.attr("d",line);
/* animate the areas */
groups.select('.area')
.transition()
.duration(1000)
.attr("d",area);
}
As well as a fiddle http://jsfiddle.net/JL33M/
Thank you!
The width of the graph depends on the range() you give it. range([0,100]) will always "stretch" the domain() values to take up 100 units.
That's what your code is currently doing:
var x = d3.scale.linear()
.domain([0,data[0].length])
.range([margin, width-margin]);// <-- always a fixed width
You want the width to depend on the number of data entries. Say you've decided you want each data point to take up 5 units, then range() needs to depend on the size of the dataset:
var x = d3.scale.linear()
.domain([0,data[0].length])
.range([margin, 5 * data[0].length]);// <-- 5 units per data point
Of course, under these conditions, your graph width grows with the dataset; if you give it a really long data array of, say, 500 points, the graph would be 2500 units wide and likely run off screen. But if your data is such that you know the maximum length of it, then you'll be fine.
On an unrelated note, I think your code could use a refactoring to be less repetitive. You should be able to achieve what you're doing with a single update() function, without the need for the init() function.
This tutorial by mbostock describe the "general update pattern" I'm referring to. Parts II and III then go on to explaining how to work transitions into this pattern.
Related
How do I draw gridlines in d3.js with zoom and pan
I have been able to make a scatter plot with zoom and pan functionality where the axes scale properly and everything works well. Now I am trying to figure out how to add gridlines, but running into some issues. I have started with only adding x-axis gridlines to figure things out. I have attached a fiddle with a working example to build from. I commented out the initial gridlines when the graph is generated, because they would remain after zooming causing clutter, and I will add them back later when I get things working. When zooming the gridlines appear to be drawn correctly, but they do not match up with the x-axis labels, and the x-axis labels disappear after zooming or panning. If you comment out line 163 and uncomment line 164 you can see the basic graph without any gridlines. Clicking the plot button will always generate a new graph. I have left behind some commented out code of different things that I have tried from searching through stackoverflow. Example is using d3.js - 5.9.2 JSFiddle: https://jsfiddle.net/eysLvqkh/11/ HTML: <div id="reg_plot"></div> <button id="b" class="myButton">plot</button> Javascript: var theButton = document.getElementById("b"); theButton.onclick = createSvg; function createSvg() { // clear old chart when 'plot' is clicked document.getElementById('reg_plot').innerHTML = "" // dimensions var margin = {top: 20, right: 20, bottom: 30, left: 55}, svg_dx = 1200, svg_dy =600, chart_dx = svg_dx - margin.right - margin.left, chart_dy = svg_dy - margin.top - margin.bottom; // data var y = d3.randomNormal(400, 100); var x_jitter = d3.randomUniform(-100, 1400); var d = d3.range(1000) .map(function() { return [x_jitter(), y()]; }); // fill var colorScale = d3.scaleLinear() .domain(d3.extent(d, function(d) { return d[1]; })) .range([0, 1]); // y position var yScale = d3.scaleLinear() .domain(d3.extent(d, function(d) { return d[1]; })) .range([chart_dy, margin.top]); // x position var xScale = d3.scaleLinear() .domain(d3.extent(d, function(d) { return d[0]; })) .range([margin.right, chart_dx]); // y-axis var yAxis = d3.axisLeft(yScale); // x-axis var xAxis = d3.axisBottom(xScale); // append svg to div element 'reg_plot' and set zoom to our function named 'zoom' var svg = d3.select("#reg_plot") .append("svg") .attr("width", svg_dx) .attr("height", svg_dy); svg.call(d3.zoom().on("zoom", zoom)); // clip path - sets boundaries so points will not show outside of the axes when zooming/panning var clip = svg.append("defs").append("svg:clipPath") .attr("id", "clip") .append("svg:rect") .attr("id", "clip-rect") .attr("x", "0") .attr("y", "0") .attr('width', chart_dx) .attr('height', chart_dy); // plot data var circles = svg.append("g") .attr("id", "circles") .attr("transform", "translate(75, 0)") .attr("clip-path", "url(#clip)") .selectAll("circle") .data(d) .enter() .append("circle") .attr("r", 4) .attr("cx", function(d) { return xScale(d[0]); }) .attr("cy", function(d) { return yScale(d[1]); }) .style("fill", function(d) { var norm_color = colorScale(d[1]); return d3.interpolateInferno(norm_color) }); // add y-axis var y_axis = svg.append("g") .attr("id", "y_axis") .attr("transform", "translate(75,0)") .call(yAxis).style("font-size", "10px") // add x-axis var x_axis = svg.append("g") .attr("id", "x_axis") .attr("transform", `translate(${margin.left}, ${svg_dy - margin.bottom - margin.top})`) .call(xAxis).style("font-size", "10px") // add x and y grid lines x_axis.call(xAxis.scale(xScale).ticks(20).tickSize(-chart_dy)); y_axis.call(yAxis.scale(yScale).ticks(20).tickSize(-chart_dx)); function zoom(e) { // re-scale y axis during zoom y_axis.transition() .duration(50) .call(yAxis.scale(d3.event.transform.rescaleY(yScale))); // re-scale x axis during zoom x_axis.transition() .duration(50) .call(xAxis.scale(d3.event.transform.rescaleX(xScale))); // re-draw circles using new scales var new_xScale = d3.event.transform.rescaleX(xScale); var new_yScale = d3.event.transform.rescaleY(yScale); // re-scale axes and gridlines x_axis.call(xAxis.scale(new_xScale).ticks(20).tickSize(-chart_dy)); y_axis.call(yAxis.scale(new_yScale).ticks(20).tickSize(-chart_dx)); circles.data(d) .attr('cx', function(d) {return new_xScale(d[0])}) .attr('cy', function(d) {return new_yScale(d[1])}); } }
For anyone looking, I have solved this problem. I have updated the javascript in the original post, and updated the jsfiddle. If you are copying this code to your local machine where you are using d3.js 7.4.4 or higher then you need to change the lines that say d3.event.transform.... to just e.transform.
d3v4 inverse bubble chart with animation/screen size adaptation
I am trying to convert a bubble chart from d3v3 to v4. Running into x,y,d missing variables? In this version -- a rect is applied to the svg - and then a circle is cut -- so its like an inverse bubble chart. I am keen to work out a set radius for the chart as a maxium -- if it should act like a score between 0 and 100? What kind of math to apply that a max radius has been reached to signify that the value is very big? I also tried to have the svg mask adapt - if the browser or its container changed size -- ideally would want it to response during the change - rather than resizeEnd //version 3 https://jsfiddle.net/8ag1vf6e/1/ //current version 4 https://jsfiddle.net/d56g9r0y/ // filters go in defs element var defs = innversebubble.append("defs"); var mask = defs.append("mask") .attr("id", "myMask"); mask.append("rect") .attr("x", 0) .attr("y", 0) .attr("width", width) .attr("height", height) .style("fill", "white") .style("opacity", 1); var invisiblebubble = mask.append("circle") .data(data); //create a fixed bubble first invisiblebubble .attr("cx", "50%") .attr("cy", "50%") .attr("r", function(d) { return d.value - 20; }); //now mask the fixed circle var masker = defs.append(function() { return mask.node().cloneNode(true) }) .attr("id", "myMaskForPointer") .select("rect") .style("fill", "white") .style("opacity", 1); invisiblebubble .attr("r", 10); //apply the rest of the chart elements var rect = innversebubble .attr("class", "series") .append("g") .attr("transform", "translate(0,0)") .append("rect") .attr("x", 0) .attr("y", 0) .attr("width", "100%") .attr("height", "100%") .attr("mask", "url(#myMask)") .style("fill", backcolor) .style("opacity", backopacity); //animate this circle invisiblebubble .attr("cx", "50%") .attr("cy", "50%") .transition() .duration(1800) .attr("r", 10) .transition() .duration(900) .attr("r", function(d) { return d.value; }); latest jsfiddle - 15th June -- needs fixing https://jsfiddle.net/xmrtahns/ "I am keen to work out a set radius for the chart as a maxium -- if it should act like a score between 0 and 100? What kind of math to apply that a max radius has been reached to signify that the value is very big? I also tried to have the svg mask adapt - if the browser or its container changed size -- ideally would want it to response during the change - rather than resizeEnd"
I've fixed the conversion and the data source - but still need issues to resolve. var backcolor = $this.data("color"); var backopacity = $this.data("opacity"); var width = $this.data("width"); var height = $this.data("height"); var data = [{ "label": $this.data("label-name"), "centralLabel": $this.data("central-label"), "xPer": $this.data("displace-left"), "yPer": $this.data("displace-top"), "value": $this.data("bubble-value") }]; http://jsfiddle.net/hLymw8et/2/ --I am keen to work out a set radius for the chart as a maximum -- if it should act like a score between 0 and 100? --What kind of math to apply that a max radius has been reached to signify that the value is very big? --I also tried to have the svg mask adapt - if the browser or its container changed size -- ideally would want it to response during the change - rather than resizeEnd –
Increasing size of d3 scatterplot
I have a scatter plot made in d3.v3 and no matter how large i increase the width and height variables it does not take up more screen space. var w = 700; var h = 700; var dataset = [ "Over 50 pairs of coordinates that look like [0,1][0,43], ]; //Create SVG element var svg = d3.select("body") .append("svg") .attr("width", w) .attr("height", h); svg.selectAll("rect") .data(dataset) .enter() .append("rect") .attr("x", function(d) { return d[0]; }) .attr("y", function(d) { return d[1]; }) .attr("width",5) .attr("height",5); There are more than 50 coordinates in my dataset and i want to be able to display them well so that is why i want this to take up more screen space. Currently there is nothing in my html, and no css for this. How can i adjust this so that the scatter plot takes more screen space?
The code you show doesn't place data points with any consideration of width or height, it places data points based on the values in the data: .attr("x", function(d) { return d[0]; }) .attr("y", function(d) { return d[1]; }) The x and y attributes, without any SVG transformation, expect pixel values. If a point has the datum [25,25] it will be placed 25 pixels from the top and left. Height and width of the svg do not matter - you are stating the pixel position based on the data, not based on the svg dimensions in combination with the data. Scales are a fundamental part of d3 - you can scale the x and y values of your data points across a range of pixel values. To do this we need to know the domain of the input data - the extent of input values - and the range of the output values - the extent of output values. There are a number of scales built into D3, including power, logarithmic and linear. I'll demonstrate a linear scale below, but the form is very similar for other continuous scales, such as those noted above. var xScale = d3.scaleLinear() // create a new linear scale .domain([0,50]) // map values from 0 to 50 to: .range([0,width]) // to pixel values of 0 through width var yScale = d3.scaleLinear() // create a new linear scale .domain([0,50]) // map values from 0 to 50 to: .range([height,0]) // to pixel values of height through 0 Since in SVG coordinate space y=0 is the top of the SVG, and normally we want data with y=0 to be displayed at the bottom of the graph, we use a range of [height,0] rather than [0,height] With the scales set up, to return the pixel value for a given data value we use: xScale(d[0]); // assuming d[0] holds the x value or yScale(d[1]); // assuming d[1] holds the y value Together this gives us: var w = 700; var h = 700; var dataset = d3.range(50).map(function(d) { return [Math.random()*50,Math.random()*50]; }) var x = d3.scaleLinear() .domain([0,50]) // input extent .range([0,w]) // output extent var y = d3.scaleLinear() .domain([0,50]) .range([h,0]) //Create SVG element var svg = d3.select("body") .append("svg") .attr("width", w) .attr("height", h); svg.selectAll("rect") .data(dataset) .enter() .append("rect") .attr("x", function(d) { return x(d[0]); }) .attr("y", function(d) { return y(d[1]); }) .attr("width",5) .attr("height",5); <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script> Of course we might not know the domain of the data if its dynamic, so d3 has a few built in helpers including d3.min, d3.max, d3.extent. d3.min and d3.max iterate through an array to find the minimum and maximum values of some property of each item in the data array: d3.min(dataArray, function(d) { return d.property; }) d3.max(dataArray, function(d) { return d.property; }) D3.extent does both at the same time returning an array containing min and max: d3.extent(dataArray, function(d) { return d.property; }) We can plug those into the scales too: var w = 700; var h = 700; var dataset = d3.range(50).map(function(d) { return [Math.random()*50,Math.random()*50]; }) var x = d3.scaleLinear() .domain([0,d3.max(dataset, function(d) { return d[0]; }) ]) // input extent .range([0,w]) // output extent var y = d3.scaleLinear() .domain( d3.extent(dataset, function(d) { return d[1]; }) ) .range([h,0]) //Create SVG element var svg = d3.select("body") .append("svg") .attr("width", w) .attr("height", h); svg.selectAll("rect") .data(dataset) .enter() .append("rect") .attr("x", function(d) { return x(d[0]); }) .attr("y", function(d) { return y(d[1]); }) .attr("width",5) .attr("height",5); <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
The below works perfectly fine for me. The things you might need to check is : The coordinates range is more than 5. (or you might need to use scale and axis) Is there a overriding styles to your g var w = 700; var h = 700; var dataset = [[10,10],[10,30],[30,50],[30,70]]; //Create SVG element var svg = d3.select("#scatterplot") .append("svg") .attr("width", w) .attr("height", h); svg.selectAll("rect") .data(dataset) .enter() .append("rect") .attr("x", function(d) { return d[0]; }) .attr("y", function(d) { return d[1]; }) .attr("width",5) .attr("height",5); <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script> <div id='scatterplot'></div>
Color coding Parallel Coordinates
I have already a code parallel coordinates graph, everything works fine. Now i'm trying to use colors to color-code the parallel coordinates visualization, but something is wrong. In dataset (http://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data) i've got different names of wine species (1st column is class identifier (1-3)), but in graph draws only one color. Could anybody help me? Graph : enter code here // CREATE A COLOR SCALE var color = d3.scale.ordinal() .domain(['1','2','3']) .range(['red','blue','green']) d3.csv("wine.csv", function(error, wine) { // Extract the list of dimensions and create a scale for each. x.domain(dimensions = d3.keys(wine[0]).filter(function(d) { return d != "name" && (y[d] = d3.scale.linear() .domain(d3.extent(wine, function(p) { return +p[d]; })) .range([h, 0])); })); // Add grey background lines for context. background = svg.append("g") .attr("class", "background") .selectAll("path") .data(wine) .enter().append("path") .attr("d", path); // USE THE COLOR SCALE TO SET THE STROKE BASED ON THE DATA foreground = svg.append("g") .attr("class", "foreground") .selectAll("path") .data(wine) .enter().append("path") .attr("d", path) .attr("stroke", function(d) { var species = d.name.slice(0,d.name.indexOf(' ')); return color(species); })
Once you already have your ordinal scale for the colors with the domain and range defined, you only need to color your lines according to d.name: .attr("stroke", function(d) { return color(d.name); });
d3.js chart area filling with different colors
I'm attempting to fill the area under the graph with different colors, depending on x value ranges, say for example, for x values 0 to 10 yellow, from 10 to 20 red and so on. Is there a way to do that? My javascript for single fill color is var m = 80; var w = 900 - 3*m; var h = 600- 3*m; var x = d3.scale.linear().range([0, w]); var y = d3.scale.linear().range([h, 0]); x.domain(d3.extent(data, function(d) { return d.time; })); y.domain(d3.extent(data, function(d) { return d.points; })); var line = d3.svg.line() .x(function(d) { return x(d.time); }) .y(function(d) { return y(d.points); }) var graph = d3.select("#graph").append("svg:svg") .attr("width", w+3*m) .attr("height", h+3*m) .append("svg:g") .attr("transform", "translate(" + 1.5*m + "," + 1.5*m + ")"); var area = d3.svg.area() .x(function(d) { return x(d.time); }) .y0(h) .y1(function(d) { return y(d.points); }); graph.append("path") .datum(data) .attr("class", "area") .attr("d", area) .style("fill","steelblue"); Thanks in advance!
You basically have two options for doing this. You can define separate areas for the different colours. You can define a single area and use a gradient to simulate different colours. The second one is probably easier, as you don't need to draw any separate paths, you can simply fill the one like you're currently doing. For the gradient, you would need to define the stops (i.e. colour changes) to correspond to the values. In particular, you would need to introduce two stops at the same place to make it appear like the colour is changing suddenly. More information on gradients here. The code would look something like this. var grad = graph.append("defs") .append("linearGradient") .attr("id", "grad"); grad.append("stop").attr("offset", "0%").attr("stop-color", "yellow"); grad.append("stop").attr("offset", "10%").attr("stop-color", "yellow"); grad.append("stop").attr("offset", "10%").attr("stop-color", "red"); grad.append("stop").attr("offset", "20%").attr("stop-color", "red"); // etc graph.append("path") .style("fill", "url(#grad)"); The positions of the stops would be determined by your scale.