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.
I am trying to change the x-scale of already plotted path in d3.js
Also, i am able to change the x-scale of circles using following code.
var s = d3.event.selection || xTimeScale.range();
xTimeScale.domain(s.map(xDateScale.invert, xDateScale));
d3.selectAll(".dot").transition().duration(10).attr("cx",function(d){ return xTimeScale(d.date); })
As you can see i have changed the domain of xTimeScale with new values. and then select all the circles with class(.dot) and replot the x and y with changed xTimeScale.
Now i also want to change the path with this new scale.
Here is the code when my path first plotted.
var lineFunction = d3.line().x(function(d) { return x(d.date); }).y(function(d) { return y(d.value); })
var lineGraph = svg.append("path").attr("d",lineFunction(data)).attr("stroke", "blue")
.attr("stroke-width", 2).attr("fill", "none").attr("class","dotsLine");
Since you're just changing the scale used by the line generator you don't need to rebind the data (by the way, I see that you are not binding any data), you just need to pass the new scale to the line generator and, then, update the d attribute of the path.
Look at this demo. There is a x scale, named xScale1, used by the line generator:
var line = d3.line()
.x(function(d) {
return xScale1(d)
})
When you click the button, the line generator uses another scale...
line.x(function(d) {
return xScale2(d)
})
... and the path d attribute is updated:
path.transition()
.duration(1000)
.attr("d", line(data));
Here is the demo:
var svg = d3.select("svg");
var data = [12, 130, 45, 60, 110, 21];
var yScale = d3.scaleLinear()
.domain(d3.extent(data))
.range([140, 10]);
var xScale1 = d3.scalePoint()
.domain(data)
.range([10, 290]);
var xScale2 = d3.scalePoint()
.domain(data.concat(d3.range(6)))
.range([10, 290]);
var line = d3.line()
.x(function(d) {
return xScale1(d)
})
.y(function(d) {
return yScale(d)
})
.curve(d3.curveBasis);
var path = svg.append("path")
.attr("d", line(data))
.attr("fill", "none")
.attr("stroke", "black");
d3.select("button").on("click", function() {
line.x(function(d) {
return xScale2(d)
})
path.transition()
.duration(1000)
.attr("d", line(data));
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<button>Click me</button>
<br>
<svg></svg>
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);
});
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.