D3.js Multi-Series Chart with Y value tracking - javascript
Working solution: Right now I'm working on styling and on solving some of the issues regarding my problem with creating chart consisting of multiple-data series with values tracking. I will try as soon as I can to give you a sample of working code soo if anybody will came across the same or similar problem as I did, could work on it as a base. For now most of the tips which I used are in the comments below.
This will be my first question on StackOverflow and I'm looking forward to seeing what answers you might have to my problem.
Recently I got project in which I have to write Javascript code for generating charts and in which I would be able to read Y values from every line of the chart at the same time. I very new to D3 framework and by now I'm able to read csv data, create multi-series chart and track and read Y value but only when I'm creating chart from a single data series. I was trying to create multiple similar functions that would track data from diferent series of data but it won't work and in console i see that the Y is showing as null from what I can understand. I was using examples from D3 website to try to learn it and for now code will be very similar to those examples.
Later on I would need to do some other things with it but i think that after solving that problem i will be able to keep going. There will be like:
reduce data from csv by code because I will need to delete header infromation
change visual style of the chart and edit axis scaling
For now I have something like that. Sorry if it is a little bit messy but I'm still learning and trying a lot of different things. I have added also screenshot from what it looks like for me and some console information that i could get. I hope it will help you guys see what I'm doing wrong and what I would need to learn. Also this is not my only approach and it would be too long to show them all.
EDIT: I'm trying a little bit different approach. On the bottom of the page i will show what I have done by now.
EDIT2: Sorry if i was't precise enough about my goal. What I'm trying to do with this is I want to be able to read all Y-axis values of drawn lines (it will be 4 of them) at the same time on one X-axis value. I have added screenshot of the second code in which I'm able to read only one Y-axis value and can't read the over one.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
.overlay {
fill: none;
pointer-events: all;
}
.focus circle {
fill: none;
stroke: steelblue;
}
</style>
<body>
<script src="d3.min.js"></script>
<script>
var margin = {top: 20, right: 80, bottom: 30, left: 200},
//-margin.left
width = 960 - margin.right,
height = 750 - margin.top - margin.bottom;
var parseDate = d3.time.format("%Y-%M-%d %H:%M").parse,
//dodane do sledzenia myszy i rysowania kuleczek
bisectDate = d3.bisector(function(d) { return d.date; }).left,
formatValue = d3.format(",.2f"),
formatCurrency = function(d) { return "$" + formatValue(d); };
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var color = d3.scale.category10();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var line = d3.svg.line()
.interpolate("basis")
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.transfers); });
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("data2.csv", function(error, data) {
color.domain(d3.keys(data[0]).filter(function(key) { return key !== "date"; }));
data.forEach(function(d) {
d.date = parseDate(d.date);
});
var bitrates = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {date: d.date, transfers: +d[name]};
})
};
});
console.log(bitrates);
//data.sort(function(a, b) {
//return a.date - b.date;
//});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([
d3.min(bitrates, function(c) { return d3.min(c.values, function(v) { return v.transfers; }); }),
d3.max(bitrates, function(c) { return d3.max(c.values, function(v) { return v.transfers; }); })
]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Transfers");
var chart = svg.selectAll(".chart")
.data(bitrates)
.enter().append("g")
.attr("class", "chart");
chart.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
//.attr("d", line);
.style("stroke", function(d) { return color(d.name); });
chart.append("text")
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.transfers) + ")"; })
.attr("x", 3)
.attr("dy", ".35em");
//.text(function(d) { return d.name; });
//sledzenie myszy i rysowanie kuleczek
var focus = svg.append("g")
.attr("class", "focus")
.style("display", "none");
focus.append("circle")
.attr("r", 4.5);
focus.append("text")
.attr("x", 9)
.attr("dy", ".35em");
svg.append("g").append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", function() { focus.style("display", null); })
.on("mouseout", function() { focus.style("display", "none"); })
.on("mousemove", mousemove);
function mousemove() {
var x0 = x.invert(d3.mouse(this)[0]),
i = bisectDate(data, x0, 1),
d0 = data[i - 1],
d1 = data[i],
d = x0 - d0.date > d1.date - x0 ? d1 : d0;
focus.attr("transform", "translate(" + x(d.date) + "," + y(d.value) + ")");
focus.select("text").text(formatCurrency(d.value));
}
});
</script>
It looks like for me like this:
Generated chart
CSV data file looks like this:
date,Średni wych.:,Średni wch.:,Maks. wych.:,Maks. wch.:
2014-02-14 15:40,86518717581,101989990772,88304882317,108036052338
2014-02-14 16:00,85739038102,98312113056,87060053514,107154908503
Some over information that I inspected while trying to understand what is wrong:
[Object, Object, Object, Object]
0: Object
name: "Średni wych.:"
values: Array[504]
__proto__: Object
1: Object
2: Object
name: "Maks. wych.:"
values: Array[504]
[0 … 99]
[100 … 199]
100: Object
date: Thu Jan 16 2014 01:00:00 GMT+0100 (Środkowoeuropejski czas stand.)
transfers: 49305177944
__proto__: Object
101: Object
date: Thu Jan 16 2014 01:20:00 GMT+0100 (Środkowoeuropejski czas stand.)
transfers: 42169641572
__proto__: Object
102: Object
date: Thu Jan 16 2014 01:40:00 GMT+0100 (Środkowoeuropejski czas stand.)
transfers: 39400112189
__proto__: Object
103: Object
104: Object
105: Object
106: Object
107: Object
108: Object
109: Object
110: Object
I would really appreciate any help from you. I know some Object Oriented Programming, HTML, CSS, but for now I wasn't really working with any framework and it is fun to learn but on the over hand could be really frustrating while trying to figure out what the heck I'm doing wrong.
EDIT
Now I'm trying drawing two lines separately. It is working great and it could make it easier for me to change lines style later on. Now i need to use mousemove function for each of those lines. Then it would be fairly easy to just pass readed values to some variables and show them in some box or something.
This is the code for the my second try(sorry for post getting long):
Screenshot for the second code is called Chart2.jpg
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
.overlay {
fill: none;
pointer-events: all;
}
.focus circle {
fill: none;
stroke: steelblue;
}
</style>
<body>
<script src="d3.js"></script>
<script>
var margin = {top: 20, right: 50, bottom: 30, left: 100},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var parseDate = d3.time.format("%d-%b-%y").parse,
bisectDate = d3.bisector(function(d) { return d.date; }).left,
formatValue = d3.format(",.2f"),
formatCurrency = function(d) { return "$" + formatValue(d); };
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var line = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.close); });
var valueline2 = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.open); });
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("data.csv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
d.open = +d['open data'];
});
data.sort(function(a, b) {
return a.date - b.date;
});
x.domain([data[0].date, data[data.length - 1].date]);
y.domain([0, d3.max(data, function(d) { return Math.max(d.close, d.open); })]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Price ($)");
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
svg.append("path")
.datum(data)
.attr("class", "line")
.style("stroke", "red")
.attr("d", valueline2);
var focus = svg.append("g")
.attr("class", "focus")
.style("display", "none");
focus.append("circle")
.attr("r", 4.5);
focus.append("text")
.attr("x", 9)
.attr("dy", ".35em");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", function() { focus.style("display", null); })
.on("mouseout", function() { focus.style("display", "none"); })
.on("mousemove", mousemove1)
.on("mousemove", mousemove2);
function mousemove1() {
var x0 = x.invert(d3.mouse(this)[0]),
i = bisectDate(data, x0, 1),
d0 = data[i - 1],
d1 = data[i],
d = x0 - d0.date > d1.date - x0 ? d1 : d0;
focus = focus.attr("transform", "translate(" + x(d.date) + "," + y(d.close) + ")");
focus.select("text").text(formatCurrency(d.close));
}
function mousemove2() {
var x0 = x.invert(d3.mouse(this)[0]),
i = bisectDate(data, x0, 1),
d0 = data[i - 1],
d1 = data[i],
d = x0 - d0.date > d1.date - x0 ? d1 : d0;
focus = focus.attr("transform", "translate(" + x(d.date) + "," + y(d.open) + ")");
focus.select("text").text(formatCurrency(d.open));
}
});
</script>
You've got all the basic code there, you just need to get it all to run at the same time.
The first problem is that you're setting two different "mousemove" event handlers on the same elements. Unless you use namespaces to distinguish them, the second function just replaces the first, so your first function is never getting called. Rather than creating two event handlers with different namespaces, it's much easier to just put all your event-handling code into one function.
The second problem is that you only have one "focus" element, and so even if you did run both functions to set the two different tooltip contents and position, only the second version would be displayed, because it just replaces the first.
So to recap, you need to: create a tooltip/focus element for each path, and then have one event-handling function that sets all the values and positions according to the appropriate column of your data file.
To keep the code concise, and to allow you to quickly switch from two lines to four or more, I'm going to suggest that you create the focus elements as a data-joined selection, where the data is an array of column names:
var columnNames = d3.keys( data[0] ) //grab the key values from your first data row
//these are the same as your column names
.slice(1); //remove the first column name (`date`);
var focus = svg.selectAll("g")
.data(columnNames)
.enter().append("g") //create one <g> for each columnName
.attr("class", "focus")
.style("display", "none");
focus.append("circle") //add a circle to each
.attr("r", 4.5);
focus.append("text") //add a text field to each
.attr("x", 9)
.attr("dy", ".35em");
Now, when you show or hide focus in the mouseover/mouseout events, it will show or hide all the tooltips:
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", function() { focus.style("display", null); })
.on("mouseout", function() { focus.style("display", "none"); })
.on("mousemove", mousemove);
But what should you do in your mousemove function? The first part, figuring out the nearest x-value (date) is the same. But then you have to set the text and position of each focus tooltip according to the values in the correct column. You can do this because each focus element has a column name bound to it as a data object, and d3 will pass that data object as the first parameter to any function you pass in to a d3 method:
function mousemove() {
var x0 = x.invert(d3.mouse(this)[0]),
i = bisectDate(data, x0, 1),
d0 = data[i - 1],
d1 = data[i],
d = x0 - d0.date > d1.date - x0 ? d1 : d0;
//d is now the data row for the date closest to the mouse position
focus.attr("transform", function(columnName){
return "translate(" + x( d.date ) + "," + y( d[ columnName ] ) + ")";
});
focus.select("text").text(function(columnName){
//because you didn't explictly set any data on the <text>
//elements, each one inherits the data from the focus <g>
return formatCurrency(d[ columnName ]);
});
}
By the way, you can use this same structure -- use the column names as data, and then use that name in a function to grab the correct data value -- to create all your lines with the same code, without having to create separate data arrays. Let me know if you have difficulty implementing that.
Related
D3 "pinning" a tick to a bar value in a chart?
I am struggling trying to solve a problem with setting the yAxis tick marks so that I can "pin" a tick to a specific value. Please bear with my explanation, I will likely butcher the d3 terminology... First, I am using this Grouped bar chart example as my reference, so please refer to that also. Here is an image that more or less shows what I am trying to accomplish: I want to make the y scale a percentage, and make "12" (red line) display "100%" (as a label?) and pin that to the value of the gray bar. The other ticks would be labelled as percentages also, with a linear scale (10%, 20%, etc). As the data will change, the y axis values will also change and the "100%" label will need to always align with the data corresponding to the gray bar value. Think of this as a reference value that I can then compare the other bars to, and those bars can exceed 100%. I am not sure of this is helpful, but the code that sets the y domain in question is as follows: y.domain([0, d3.max(data, function(categorie) { return d3.max(categorie.values, function(d) { return d.value; }); })]); How can I accomplish my goal? I hope this makes sense. Thanks!
Currently, the y scale you pass to yAxis maps your data values to svg dimensions. data -> svg dimensions. If you notice the yAxis labels its ticks with the actual data values, or the domain of the scale you give it. So all you need to do is pass it a scale with percentage in its domain that goes to svg dimensions: percentage -> svg dimensions Here are two ways you can do that. 1) First convert the data to percentages and then make the scale. Then you use this scale for plotting and passing to the axis. 2) Leave the plotting alone and create a third scale that maps percentage -> height and just pass this to yAxis. Notice this works because the scales are both linear. Modifying the example you referenced I used the second method. I pasted the code for completeness and highlighted the part I modified. Note I made the "medium value" (grey bar) in the Student set 100%. <!DOCTYPE html> <meta charset="utf-8"> <style> body { font: 10px sans-serif; } .axis path, .axis line { fill: none; stroke: #000; shape-rendering: crispEdges; } .x.axis path { display: none; } </style> <body> <script src="https://d3js.org/d3.v3.min.js"></script> <script> var margin = {top: 20, right: 20, bottom: 30, left: 40}, width = 960 - margin.left - margin.right, height = 500 - margin.top - margin.bottom; var x0 = d3.scale.ordinal() .rangeRoundBands([0, width], .1); var x1 = d3.scale.ordinal(); var y = d3.scale.linear() .range([height, 0]); var xAxis = d3.svg.axis() .scale(x0) .tickSize(0) .orient("bottom"); var yAxis = d3.svg.axis() .scale(y) .orient("left"); var color = d3.scale.ordinal() .range(["#ca0020","#f4a582","#d5d5d5","#92c5de","#0571b0"]); var svg = d3.select('body').append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); d3.json("data.json", function(error, data) { var categoriesNames = data.map(function(d) { return d.categorie; }); var rateNames = data[0].values.map(function(d) { return d.rate; }); //========================================================================= //========================================================================= // Modified code here //------------------------------------------------------------------------- //------------------------------------------------------------------------- //data -> percent - using the Student value of Medium to set to 100% let dataToPercent = d3.scale.linear() .domain( [0, data[0].values.filter( d => d.rate === "Medium" )[0].value] ) .range( [ 0, 100 ] ) // percent -> height // Third scale just to pass to yAxis let percentToHeight = d3.scale.linear() .domain( [0, d3.max(data, function(categorie) { return d3.max(categorie.values, function(d) { return dataToPercent( d.value ); //only change is dataToPercent( d.value ) versus original return of d.value }) })]) .range( [ height, 0] ) // set the yAxis with our new scale - use tickFormat to add percentage sign yAxis .scale( percentToHeight ) .tickFormat( d => d + "%" ) //add the red line svg.append( "line" ) .attr( "x1", 0 ) .attr( "y1", percentToHeight( 100 )) .attr( "x1", width ) .attr( "y2", percentToHeight( 100 )) .attr( "stroke", "red" ) .attr( "stroke-width", "2px" ) //=========================================================================== //=========================================================================== // End modified code //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- x0.domain(categoriesNames); x1.domain(rateNames).rangeRoundBands([0, x0.rangeBand()]); y.domain([0, d3.max(data, function(categorie) { return d3.max(categorie.values, function(d) { return d.value; }); })]); svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); svg.append("g") .attr("class", "y axis") .style('opacity','0') .call(yAxis) .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em") .style("text-anchor", "end") .style('font-weight','bold') .text("Value"); svg.select('.y').transition().duration(500).delay(1300).style('opacity','1'); var slice = svg.selectAll(".slice") .data(data) .enter().append("g") .attr("class", "g") .attr("transform",function(d) { return "translate(" + x0(d.categorie) + ",0)"; }); slice.selectAll("rect") .data(function(d) { return d.values; }) .enter().append("rect") .attr("width", x1.rangeBand()) .attr("x", function(d) { return x1(d.rate); }) .style("fill", function(d) { return color(d.rate) }) .attr("y", function(d) { return y(0); }) .attr("height", function(d) { return height - y(0); }) .on("mouseover", function(d) { d3.select(this).style("fill", d3.rgb(color(d.rate)).darker(2)); }) .on("mouseout", function(d) { d3.select(this).style("fill", color(d.rate)); }); slice.selectAll("rect") .transition() .delay(function (d) {return Math.random()*1000;}) .duration(1000) .attr("y", function(d) { return y(d.value); }) .attr("height", function(d) { return height - y(d.value); }); //Legend var legend = svg.selectAll(".legend") .data(data[0].values.map(function(d) { return d.rate; }).reverse()) .enter().append("g") .attr("class", "legend") .attr("transform", function(d,i) { return "translate(0," + i * 20 + ")"; }) .style("opacity","0"); legend.append("rect") .attr("x", width - 18) .attr("width", 18) .attr("height", 18) .style("fill", function(d) { return color(d); }); legend.append("text") .attr("x", width - 24) .attr("y", 9) .attr("dy", ".35em") .style("text-anchor", "end") .text(function(d) {return d; }); legend.transition().duration(500).delay(function(d,i){ return 1300 + 100 * i; }).style("opacity","1"); }); </script> This sets the 100% to the first grey bar, but you could extend this set each grey bar as 100% for its associated values, and then make the xaxis update on hover or something. As to the red line, just append it to the svg (or any g element) with d3.append and modify its position as appropriate.
Animate position of svg rect on transition
Edit: here is an example Fiddle: https://jsfiddle.net/3c9dtLyh/6/ I have a layout that I am attempting to animate to compare the arrangement on two different dates. What I would like to accomplish is a transition where items whose x,y position is different on the second date smoothly fly to the new position. I have attempted to do this using an updateData function set to trigger onclick. The layout looks like this: I do not neccesarily expect this approach to work because how would the transition know which (x,y) pairs correspond to the correct item name in the new arrangement. What am I missing about how these transitions work and how could I improve my approach? Here is the code I am using. It's a relatively simple sequence of appending and svg element, drawing the rectangles, then (failing) to update their position on click. <!DOCTYPE html> <meta charset="utf-8"> <style> body { font: 10px sans-serif; } .axis path, .axis line { fill: none; stroke: #000; shape-rendering: crispEdges; } .dot { stroke: #000; } </style> <body> <div id = "chart"> </div> <div id = "select_params"> <input name="updateButton" type="button" value="Update" onclick="updateData()" /> </div> </body> <!-- load js libraries --> <script src="https://d3js.org/d3.v4.min.js"></script> <!-- uses v4 of d3 --> <script type="text/javascript" src="http://code.jquery.com/jquery-1.6.2.min.js"></script> <!-- need to use this older version for tipsy --> <script type="text/javascript" src="jquery.tipsy.js"></script> <!-- load from locally hosted source code --> <!-- build the visualization --> <script type='text/javascript'> var item_width = 40, item_height = 60; var margin = {top: 20, right: 50, bottom: 75, left: 40}, width = 700 - margin.left - margin.right, height = 500 - margin.top - margin.bottom; var x = d3.scaleLinear() .range([0, width]); var y = d3.scaleLinear() .range([height, 0]); var color = d3.scaleOrdinal(d3.schemeCategory10); var svg = d3.select("#chart").append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); d3.csv("http://localhost:8080/udacity_test_vis_1/combined_item_pos.csv", function(data) { // cast string to numeric data.forEach(function(d) { d.x_pos = +d.x_pos; d.y_pos = +d.y_pos; d.sales = +d.sales; }); console.log(data); var x_offset = 5, y_offset = 5; x.domain(d3.extent(data, function(d) { return d.x_pos; })); // set the x domain y.domain(d3.extent(data, function(d) { return d.y_pos; })); // set the y domain svg.selectAll("g") .data(data) .enter() .append("rect") .filter(function(d){ return d.date == '1-20-2017'}) .attr("class", "dot") .attr("width", item_width) .attr("height", item_height) .attr("x", function(d) { return x(d.x_pos) + x_offset; }) // x position of dots .attr("y", function(d) { return y(d.y_pos) + y_offset; }) // y position of dots .attr("rx", 5) .attr("ry", 5) .style("fill", "#1f5fc6") // color factor variable .style("fill-opacity", 0.5); svg.selectAll("g") .data(data) .enter() .append("text") .filter(function(d){ return d.date == '1-20-2017'}) .attr("x", function(d) { return x(d.x_pos) + item_width/2 + x_offset; }) .attr("y", function(d) { return y(d.y_pos) + item_height/2 + y_offset; }) .attr("font-size", 10) .attr("text-anchor", "middle") .attr("fill", "black") .text(function(d){ return d.item_name}); }); function updateData() { // grab the data again d3.csv("http://localhost:8080/udacity_test_vis_1/combined_item_pos.csv", function(data) { // cast string to numeric data.forEach(function(d) { d.x_pos = +d.x_pos; d.y_pos = +d.y_pos; d.sales = +d.sales; }); var svg = d3.select("#chart").transition(); svg.selectAll("g") .data(data) .enter() .append("rect") .filter(function(d){ return d.date == '2-10-2017'}) .attr("class", "dot") .attr("width", item_width) .attr("height", item_height) .attr("x", function(d) { return x(d.x_pos) + x_offset; }) // x position of dots .attr("y", function(d) { return y(d.y_pos) + y_offset; }) // y position of dots .attr("rx", 5) .attr("ry", 5) .style("fill", "#1f5fc6") // color factor variable .style("fill-opacity", 0.5); svg.selectAll("g") .data(data) .enter() .append("text") .filter(function(d){ return d.date == '2-10-2017'}) .attr("x", function(d) { return x(d.x_pos) + item_width/2 + x_offset; }) .attr("y", function(d) { return y(d.y_pos) + item_height/2 + y_offset; }) .attr("font-size", 10) .attr("text-anchor", "middle") .attr("fill", "black") .text(function(d){ return d.item_name}); }); } </script> Here is my data: ,x_pos,y_pos,item_name,sales,date 0,1,1,S8221,2022,1-20-2017 1,2,1,NLC11,518,1-20-2017 2,3,1,35UUY,1614,1-20-2017 3,4,1,PPTNV,1059,1-20-2017 4,5,1,G0CWS,2183,1-20-2017 5,6,1,3JHUA,2513,1-20-2017 6,7,1,4HXGA,2251,1-20-2017 7,8,1,RYM9K,2330,1-20-2017 8,9,1,T8PUB,1476,1-20-2017 9,10,1,PLULW,1225,1-20-2017 10,1,2,YJ6S0,2403,1-20-2017 11,2,2,E9RGD,1361,1-20-2017 12,3,2,E2SW4,1131,1-20-2017 13,4,2,BZPGX,698,1-20-2017 14,5,2,0K682,1855,1-20-2017 15,6,2,D8UZW,2371,1-20-2017 16,7,2,USKY7,1851,1-20-2017 17,8,2,D0L0Y,1767,1-20-2017 18,9,2,P1AGP,1025,1-20-2017 19,10,2,9LT7O,1380,1-20-2017 20,1,3,1J184,1108,1-20-2017 21,2,3,RJDEG,2106,1-20-2017 22,3,3,LTSLR,1980,1-20-2017 23,4,3,ET3DF,2700,1-20-2017 24,5,3,42W1W,2194,1-20-2017 25,6,3,5QTJN,958,1-20-2017 26,7,3,O8XKY,2381,1-20-2017 27,8,3,LS9NW,516,1-20-2017 28,9,3,0MPZ7,2198,1-20-2017 29,10,3,R4E3J,2494,1-20-2017 30,1,4,WFPPY,2349,1-20-2017 31,2,4,MT2DB,2525,1-20-2017 32,3,4,6DRYS,600,1-20-2017 33,4,4,NVV0S,1556,1-20-2017 34,5,4,ODGZ2,912,1-20-2017 35,6,4,E3NLS,931,1-20-2017 36,7,4,9FFZ7,722,1-20-2017 37,8,4,UKZGF,2170,1-20-2017 38,9,4,XXORI,896,1-20-2017 39,10,4,QYU9Q,1104,1-20-2017 40,1,5,4KQPU,1562,1-20-2017 41,2,5,S3AYK,2298,1-20-2017 42,3,5,5W3CE,2580,1-20-2017 43,4,5,T0S7H,1677,1-20-2017 44,5,5,02SJG,1972,1-20-2017 45,6,5,GBMNZ,1845,1-20-2017 46,7,5,2Y7KH,982,1-20-2017 47,8,5,3WMOL,1952,1-20-2017 48,9,5,93KLU,2240,1-20-2017 49,10,5,K80OQ,2467,1-20-2017 50,1,6,2SIJS,1788,1-20-2017 51,2,6,5ZJ7V,2277,1-20-2017 52,3,6,HTL99,873,1-20-2017 53,4,6,C06QP,2185,1-20-2017 54,5,6,2S1YI,580,1-20-2017 55,6,6,IQ0L8,2395,1-20-2017 56,7,6,PEE2Y,2299,1-20-2017 57,8,6,6DEWK,2019,1-20-2017 58,9,6,9FY5B,1517,1-20-2017 59,10,6,NZQ54,2624,1-20-2017 60,1,7,C4SVV,1823,1-20-2017 61,2,7,Q4C4I,2339,1-20-2017 62,3,7,996OQ,1621,1-20-2017 63,4,7,PISK6,895,1-20-2017 64,5,7,KOKHE,1315,1-20-2017 65,6,7,6P4FT,1467,1-20-2017 66,7,7,3FY75,2085,1-20-2017 67,8,7,9YCNB,992,1-20-2017 68,9,7,NXXK1,2080,1-20-2017 69,10,7,4RDHV,2031,1-20-2017 0,6,1,9FFZ7,592,2-10-2017 1,1,6,E2SW4,622,2-10-2017 2,6,7,PLULW,1699,2-10-2017 3,8,3,ET3DF,784,2-10-2017 4,9,4,KOKHE,1092,2-10-2017 5,2,6,5ZJ7V,1691,2-10-2017 6,4,5,9FY5B,630,2-10-2017 7,9,4,G0CWS,1523,2-10-2017 8,9,2,PISK6,1778,2-10-2017 9,6,4,35UUY,2107,2-10-2017 10,3,5,5QTJN,1751,2-10-2017 11,6,6,NLC11,526,2-10-2017 12,8,2,C06QP,2308,2-10-2017 13,8,3,XXORI,1453,2-10-2017 14,5,1,E9RGD,1864,2-10-2017 15,7,2,HTL99,1222,2-10-2017 16,3,3,PEE2Y,2050,2-10-2017 17,9,7,GBMNZ,1941,2-10-2017 18,3,1,T8PUB,1440,2-10-2017 19,5,1,3WMOL,2692,2-10-2017 20,7,7,S3AYK,523,2-10-2017 21,1,5,BZPGX,2245,2-10-2017 22,2,1,S8221,2241,2-10-2017 23,9,7,IQ0L8,566,2-10-2017 24,8,5,D8UZW,1769,2-10-2017 25,3,1,RYM9K,1044,2-10-2017 26,4,6,4HXGA,2650,2-10-2017 27,2,2,WFPPY,2203,2-10-2017 28,2,4,93KLU,2289,2-10-2017 29,7,3,P1AGP,1084,2-10-2017 30,4,3,3JHUA,1364,2-10-2017 31,1,4,9LT7O,1198,2-10-2017 32,4,6,4RDHV,771,2-10-2017 33,10,7,T0S7H,873,2-10-2017 34,3,6,NXXK1,2391,2-10-2017 35,8,2,2SIJS,811,2-10-2017 36,8,4,LTSLR,1670,2-10-2017 37,6,7,02SJG,1880,2-10-2017 38,9,3,0MPZ7,2090,2-10-2017 39,2,6,E3NLS,2350,2-10-2017 40,7,6,QYU9Q,1092,2-10-2017 41,6,3,0K682,894,2-10-2017 42,1,5,LS9NW,1928,2-10-2017 43,7,7,NVV0S,951,2-10-2017 44,9,4,996OQ,670,2-10-2017 45,7,6,USKY7,706,2-10-2017 46,10,4,Q4C4I,2270,2-10-2017 47,4,2,UKZGF,1691,2-10-2017 48,10,3,RJDEG,597,2-10-2017 49,10,2,1J184,1921,2-10-2017 50,2,3,5W3CE,2604,2-10-2017 51,5,5,3FY75,1260,2-10-2017 52,1,1,6DEWK,2491,2-10-2017 53,7,5,9YCNB,1743,2-10-2017 54,4,7,6DRYS,2450,2-10-2017 55,5,2,MT2DB,1292,2-10-2017 56,8,5,C4SVV,1395,2-10-2017 57,3,7,ODGZ2,2685,2-10-2017 58,10,4,2S1YI,2617,2-10-2017 59,1,2,YJ6S0,1611,2-10-2017 60,6,3,2Y7KH,2188,2-10-2017 61,5,4,4KQPU,1413,2-10-2017 62,10,1,D0L0Y,2291,2-10-2017 63,5,1,NZQ54,1405,2-10-2017 64,5,2,6P4FT,1885,2-10-2017 65,3,1,PPTNV,1442,2-10-2017 66,1,5,K80OQ,2140,2-10-2017 67,4,5,42W1W,1697,2-10-2017 68,2,7,O8XKY,1007,2-10-2017 69,10,6,R4E3J,887,2-10-2017
So, I took a few minutes to completely refactor your code into proper d3 style. This aims to demonstrate a couple things: The proper use of the enter, update, exit pattern. Removed cut / paste duplicate code. The proper way to use g to group elements and position them together. How to add the transitions. Here is the code running. Commented code: <!DOCTYPE html> <meta charset="utf-8"> <style> body { font: 10px sans-serif; } .axis path, .axis line { fill: none; stroke: #000; shape-rendering: crispEdges; } .dot { stroke: #000; } </style> <body> <div id="chart"> </div> <div id="select_params"> <input name="updateButton" type="button" value="Update" onclick="updateData()" /> </div> </body> <!-- load js libraries --> <script src="https://d3js.org/d3.v4.min.js"></script> <!-- uses v4 of d3 --> <!-- build the visualization --> <script type='text/javascript'> var item_width = 40, item_height = 60; var margin = { top: 20, right: 50, bottom: 75, left: 40 }, width = 700 - margin.left - margin.right, height = 500 - margin.top - margin.bottom; var x = d3.scaleLinear() .range([0, width]); var y = d3.scaleLinear() .range([height, 0]); var color = d3.scaleOrdinal(d3.schemeCategory10); var svg = d3.select("#chart").append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); // a single function to draw function draw(data, someDate) { data.forEach(function(d) { d.x_pos = +d.x_pos; d.y_pos = +d.y_pos; d.sales = +d.sales; }); // pre-filter data data = data.filter(function(d) { return d.date === someDate }); var x_offset = 5, y_offset = 5; x.domain(d3.extent(data, function(d) { return d.x_pos; })); // set the x domain y.domain(d3.extent(data, function(d) { return d.y_pos; })); // set the y domain // create an update selection with a key function var g_sel = svg.selectAll("g") .data(data, function(d) { return d.item_name; }); // get rid of those leaving the update g_sel.exit().remove(); // our entering g var g_ent = g_sel.enter() .append("g"); // add our rects to our g g_ent.append("rect") .attr("class", "dot") .attr("width", item_width) .attr("height", item_height) .attr("rx", 5) .attr("ry", 5) .style("fill", "#1f5fc6") // color factor variable .style("fill-opacity", 0.5); // add our text to our g g_ent.append("text") .attr("font-size", 10) .attr("text-anchor", "middle") .attr("fill", "black") .attr("dx", item_width / 2) .attr("dy", item_height / 2) .text(function(d) { return d.item_name }); // UPDATE + ENTER selection g_sel = g_ent.merge(g_sel); // move them into position with transition g_sel .transition() .attr("transform", function(d) { return "translate(" + (x(d.x_pos) + x_offset) + "," + (y(d.y_pos) + y_offset) + ")"; }); } d3.csv("test.csv", function(data) { draw(data, '1-20-2017'); }); function updateData() { d3.csv("test.csv", function(data) { draw(data, '2-10-2017'); }); } </script>
Heres my attempt: https://jsfiddle.net/guanzo/3c9dtLyh/10/ There are multiple data points that share the same position, which is why some rectangles are overlapping. I made a lot of changes to your code that resulted in less repetition. Your data contains duplicate item_names with different dates/positions, but in your visualization you seem to want to only show items at a single date. Therefore, you only need to pass d3 data for a certain date, versus passing d3 ALL the data and then filtering. Your code: svg.selectAll("g") .data(data) .enter() .append("rect") .filter(function(d){ return d.date == '1-20-2017'}) My code: var firstDateData = data.filter(d=>d.date == '1-20-2017'); var groups = svg.selectAll("g") .data(firstDateData, d=> d.item_name) The difference between these 2 is that in my example, D3 is only aware of a single set of item_names on date 1-20-2017. Therefore when i update the date with item_names on date 2-10-2017, D3 will automatically move all updated rectangles to their new position. How? Here is where your question comes into play: I do not neccesarily expect this approach to work because how would the transition know which (x,y) pairs correspond to the correct item name in the new arrangement This is because i associated each rectangle with an item_name. D3s data function can take an optional 2nd parameter that specifies HOW the data is bound to the rectangles. This is called the key function. svg.selectAll("g").data(firstDateData, d=> d.item_name) In this case, i told d3 that each group (rectangles and their text) is bound to item_name. Therefore, the next time i pass data to D3, it tries to match existing elements (that are associated with an item_name) to the data (which contains item_names). If in my new data, i pass an item_name that corresponds to an existing element, and the data contains a new x and y position, D3 will move to element to that new position. Note that even though i'm talking about rectangles, i bound data to the g element, which contains the rectangle and the text. Feel free to ask any questions, i made a lot of changes that i didn't discuss.
limit number of tick marks in d3.js x axis with ordinal scale
I have been exploring possibilities of limiting the number of tick marks in d3.js so that it doesn't get overloaded with text if data set is relatively large. I did have a look at http://bl.ocks.org/mbostock/3212294 and documentation around the ticks() and tickValues() and this seemed to me like it would work, but it did not draw anything: HTML: <!DOCTYPE html> <body> <script src="//d3js.org/d3.v3.min.js"></script> <script> CSS: <style> body { font: 10px Arial; } .axis path { fill: none; stroke: grey; shape-rendering: crispEdges; } .axis text { font-family: Arial; font-size: 10px; } .axis line { fill: none; stroke: grey; stroke-width: 1; shape-rendering: crispEdges; } </style> JAVASCRIPT: function renderAreaChart() { var data = [{"name":"24-Apr-07","value":93.24},{"name":"25-Apr-07","value":95.35},{"name":"26-Apr-07","value":98.84},{"name":"27-Apr-07","value":99.92},{"name":"30-Apr-07","value":99.8},{"name":"1-May-07","value":99.47},{"name":"2-May-07","value":100.39},{"name":"3-May-07","value":100.4},{"name":"4-May-07","value":100.81},{"name":"7-May-07","value":103.92},{"name":"8-May-07","value":105.06},{"name":"9-May-07","value":106.88},{"name":"10-May-07","value":107.34},{"name":"11-May-07","value":108.74},{"name":"14-May-07","value":109.36},{"name":"15-May-07","value":107.52},{"name":"16-May-07","value":107.34},{"name":"17-May-07","value":109.44},{"name":"18-May-07","value":110.02},{"name":"21-May-07","value":111.98},{"name":"22-May-07","value":113.54},{"name":"23-May-07","value":112.89},{"name":"24-May-07","value":110.69},{"name":"25-May-07","value":113.62},{"name":"29-May-07","value":114.35},{"name":"30-May-07","value":118.77},{"name":"31-May-07","value":121.19},{"name":"1-Jun-07","value":118.4},{"name":"4-Jun-07","value":121.33},{"name":"5-Jun-07","value":122.67},{"name":"6-Jun-07","value":123.64},{"name":"7-Jun-07","value":124.07},{"name":"8-Jun-07","value":124.49},{"name":"11-Jun-07","value":120.19},{"name":"12-Jun-07","value":120.38},{"name":"13-Jun-07","value":117.5},{"name":"14-Jun-07","value":118.75},{"name":"15-Jun-07","value":120.5},{"name":"18-Jun-07","value":125.09},{"name":"19-Jun-07","value":123.66},{"name":"20-Jun-07","value":121.55},{"name":"21-Jun-07","value":123.9},{"name":"22-Jun-07","value":123},{"name":"25-Jun-07","value":122.34},{"name":"26-Jun-07","value":119.65},{"name":"27-Jun-07","value":121.89},{"name":"28-Jun-07","value":120.56},{"name":"29-Jun-07","value":122.04},{"name":"2-Jul-07","value":121.26},{"name":"3-Jul-07","value":127.17},{"name":"5-Jul-07","value":132.75},{"name":"6-Jul-07","value":132.3},{"name":"9-Jul-07","value":130.33},{"name":"10-Jul-07","value":132.35},{"name":"11-Jul-07","value":132.39},{"name":"12-Jul-07","value":134.07},{"name":"13-Jul-07","value":137.73},{"name":"16-Jul-07","value":138.1},{"name":"17-Jul-07","value":138.91},{"name":"18-Jul-07","value":138.12},{"name":"19-Jul-07","value":140},{"name":"20-Jul-07","value":143.75},{"name":"23-Jul-07","value":143.7},{"name":"24-Jul-07","value":134.89}]; var margin = { top: 20, right: 20, bottom: 30, left: 50 }, width = 800 - margin.left - margin.right, height = 600 - margin.top - margin.bottom; var x = d3.scale.ordinal() .domain(data.map(function (d) { return d.name; })) .rangePoints([0, width], 1); var y = d3.scale.linear() .range([height, 0]); var newArray = []; var stepSize = Math.floor(data.length / 4); for (var index in data){ if (index % 3 === stepSize){ newArray.push(data[index]["name"]) } }; var xAxis = d3.svg.axis() .scale(x) .tickValues(newArray) .orient("bottom"); var yAxis = d3.svg.axis() .scale(y) .orient("left"); var line = d3.svg.line() .x(function (d) { return x(d.name); }) .y(function (d) { return y(d.value); }); var area = d3.svg.area() .x(function (d) { return x(d.name); }) .y0(height) .y1(function (d) { return y(d.value); }); var svg = d3.select("body").append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); data.forEach(function (d) { d.name = d.name; d.value = +d.value; }); y.domain([0, 650]); svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); svg.append("g") .attr("class", "y axis") .call(yAxis) .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em") .style("text-anchor", "end") .text("Label"); svg.append("path") .datum(data) .attr("d", area) .attr("fill", "#FF0000"); } renderAreaChart(); RENDER: As you can see for some reason (and I am sure its my faulty understanding of javascript and d3 since i am a noob), it doesn't draw any tick marks. I will appreciate if someone would be able to help me with this.
It looks like your logic is ok, but some specifics might be steering you wrongly. Have you debugged by outputting newArray after you populate it? I got an empty array when I ran your code, because your check: index % 3 === stepSize; doesn't work with this dataset. The LHS (left-hand-side) of the equation will be either 0, 1, 2, depending on the value of indx. The RHS (right-hand-side) is 16, for the specific dataset that you have here. 0, 1, 2 will never equal 16, so you wind up with an empty newArray, and thus no tickValues. You can try, for example: newArray = data .filter(function(d, i) { return !(i % 4); }) .map(function(d) { return d.name; }) to give you every fourth tick.
I also found this example: var xAxis = d3.svg.axis() .scale(x) .orient("bottom") .tickValues(data.map( function(d,i) { if(i % 9 ===0 ) { return d.name; } }) .filter(function (d) { return !!d; } )); it seems to be just what i need to filter out every nth tick value without generating an additional array.
Multiples with x/y axis and mouseover example - line path shows wrong values
I am trying to combine severeal D3.js examples based on example. I managed to get mouseover for each multiples chart in part working (values are not displayed at mouse pointer yet but via console.log). By checking those values I realized that my line paths at the upper two charts are off in relation to the Y-Axis, also causing the mouseover focus to be in the wrong place. I am new to D3, so I am still having trouble to pin down the problem beeing caused by domain/scale/axis etc. You can see the example here This is my code: <!DOCTYPE html> <meta charset="utf-8"> <style> body { font: 10px sans-serif; margin: 0; } .axis path, .axis line { fill: none; stroke: #000; //shape-rendering: crispEdges; } .line { fill: none; stroke: steelblue; stroke-width: 1.5px; } .area { //fill: #e7e7e7; fill: transparent; } .overlay { fill: none; pointer-events: all; } .focus circle { fill: none; stroke: steelblue; } </style> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> <script> var margin = {top: 8, right: 10, bottom: 20, left: 30}, width = 960 - margin.left - margin.right, height = 138 - margin.top - margin.bottom; var parseDate = d3.time.format("%b %Y").parse, bisectDate = d3.bisector(function(d) { return d.date; }).left, formatValue = d3.format(",.2f"), formatCurrency = function(d) { return formatValue(d); }; var x = d3.time.scale() .range([0, width]); var y = d3.scale.linear() .range([height, 0]); var area = d3.svg.area() .x(function(d) { return x(d.date); }) .y0(height) .y1(function(d) { return y(d.price); }); var line = d3.svg.line() .x(function(d) { return x(d.date); }) .y(function(d) { return y(d.price); }); var xAxis = d3.svg.axis() .scale(x) // x is the d3.time.scale() .orient("bottom") // the ticks go below the graph .ticks(4); // specify the number of ticks var yAxis = d3.svg.axis() .scale(y) .orient("left") .ticks(4); d3.csv("stocks_chart2.csv", type, function(error, data) { // Nest data by symbol. var symbols = d3.nest() .key(function(d) { return d.symbol; }) .entries(data); // Compute the maximum price per symbol, needed for the y-domain. symbols.forEach(function(s) { s.maxPrice = d3.max(s.values, function(d) { return d.price; }); }); // Compute the minimum and maximum date across symbols. // We assume values are sorted by date. x.domain([ d3.min(symbols, function(s) { return s.values[0].date; }), d3.max(symbols, function(s) { return s.values[s.values.length - 1].date; }) ]); // Add an SVG element for each symbol, with the desired dimensions and margin. var svg = d3.select("body").selectAll("svg") .data(symbols) .enter().append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); // Add the area path elements. Note: the y-domain is set per element. svg.append("path") .attr("class", "area") .attr("d", function(d) { y.domain([0, d.maxPrice]); return area(d.values); }); // Add the line path elements. Note: the y-domain is set per element. svg.append("path") .attr("class", "line") .attr("d", function(d) { y.domain([0, d.maxPrice]); return line(d.values); }); // Add a small label for the symbol name. svg.append("text") .attr("x", width - 6) .attr("y", height - 6) .style("text-anchor", "end") .text(function(d) { return d.key; }); svg.append('g') // create a <g> element .attr('class', 'x axis') // specify classes .attr("transform", "translate(0," + height + ")") .call(xAxis); // let the axis do its thing svg.append("g") .attr("class", "y axis") .call(yAxis) .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em") .style("text-anchor", "end") .text("Value"); var focus = svg.append("g") .attr("class", "focus") .style("display", "none"); focus.append("circle") .attr("r", 4.5); focus.append("text") .attr("x", 9) .attr("dy", ".35em"); svg.append("rect") .attr("class", "overlay") .attr("width", width) .attr("height", height) .on("mouseover", function() { focus.style("display", null); }) .on("mouseout", function() { focus.style("display", "none"); }) .on("mousemove", mousemove); function mousemove() { var date, index; date = x.invert(d3.mouse(this)[0]); index = 0; var focus = svg.selectAll(".focus"); focus.attr("transform", function(d) { index = bisectDate(d.values, date, 0, d.values.length - 1); console.log(index, d.values[index].symbol, d.values[index].date, d.values[index].price); return "translate(" + x(d.values[index].date) + "," + y(d.values[index].price) + ")" }); focus.selectAll("text", function(d) { return formatCurrency(d.values[index].price); }); } }); function type(d) { d.price = +d.price; d.date = parseDate(d.date); return d; } </script> How do I assign the correct Y-Axis to each individual multiples chart causing the line path and mouseover values to be at the correct position? Any help would be greatly appreciated! Thank you!
This is an interesting problem. The example you link to uses a single y scale and yAxis for all 4 sub-plots. In your situation, though, your data has a very different domain for each sub-plot and when you add the dynamic mouse over a shared scale just won't work. So, my solution would be to create a different y scale and yAxis for each subplot. ... // variable to hold our scales var ys = {}; var area = d3.svg.area() .x(function(d) { return x(d.date); }) .y0(height) .y1(function(d) { return ys[d.symbol](d.price); //<-- call the y function matched to our symbol }); var line = d3.svg.line() .x(function(d) { return x(d.date); }) .y(function(d, i) { return ys[d.symbol](d.price); //<-- call the y scale function matched to our symbol }); ... // for each symbol create our scale symbols.forEach(function(s) { var maxPrice = d3.max(s.values, function(d) { return d.price; }); ys[s.key] = d3.scale.linear() //<-- create a scale for each "symbol" (ie Sensor 1, etc...) .range([height, 0]) .domain([0, maxPrice]); }); ... // build 4 y axis var axisGs = svg.append("g"); //<-- create a collection of axisGs axisGs .attr("class", "y axis") .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em") .style("text-anchor", "end") .text("Value"); axisGs.each(function(d, i) { //<-- for each axisG create an axis with it's scale var self = d3.select(this); self.call( d3.svg.axis() .scale(ys[d.key]) .orient("left") .ticks(4) ); }); ... // adjust mouseover to use appropriate scale focus.attr("transform", function(d) { index = bisectDate(d.values, date, 0, d.values.length - 1); console.log(index, d.values[index].symbol, d.values[index].date, d.values[index].price); return "translate(" + x(d.values[index].date) + "," + ys[d.key](d.values[index].price) + ")"; //<-- finally in our mouse move use the appropriate scale }); Fully working code here.
As far as best practices are concerned when you are dealing with n number of datasets you have to go for n number of y scales and their corresponding y axis. It is good for seperation of concern and keeps the visulization intact. here you can see the example. http://grafitome.github.io/advanced-charts.html#(first chart)
How do I update this data without redrawing datapoints that are already there?
New to D3. I am trying to modify the simple bar chart example shown here. I'm trying to update the data but am missing something fundamental. I'm trying to follow along here, where Mike talks about object constancy. Specifically, I am trying to achieve the following in my code: Key functions can be useful for improving performance independent of transitions. For example, if you filter a large table, you can use a key function to reduce the number of DOM modifications: reorder DOM elements in the update selection rather than regenerating them. We used this technique at Square to improve the performance of merchant analytics, and it’s one of the reasons that D3 is faster than most template frameworks. (In my case, my key function is simply ".data(data)" (which is ok, according to this post) My code below works, but I suspect isn't the most performance friendly. For instance, frequency "70" is in both sets of data, but by "removing" the data, I am effectively redrawing it. (If I don't "remove" the data first, then another chart is drawn, rather than the old chart just getting updated data). How do I modify the code below to adhere to the key function and so that data that exists in both datasets doesn't get redrawn? My code for the bar chart: <!DOCTYPE html> <meta charset="utf-8"> <style> body { font: 10px sans-serif; } .axis path, .axis line { fill: none; stroke: #000; shape-rendering: crispEdges; } .bar { fill: steelblue; } .x.axis path { display: none; } </style> <body> <button id="change" name="change">Update</button> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js" type="text/javascript"></script> <script src="http://d3js.org/d3.v3.min.js"></script> <script> $(document).ready(function () { var old_data = [{"letter": "A","frequency": "50"}, {"letter": "B","frequency": "60"}, {"letter": "C","frequency": "70"}, // this also appears in new_data {"letter": "D","frequency": "80"}, ]; draw_chart(old_data); $("#change").click(function(){ var new_data = [{"letter": "A","frequency": "10"}, {"letter": "B","frequency": "20"}, {"letter": "C","frequency": "70"}, // this appears in old_data {"letter": "D","frequency": "30"}, ]; var bar = d3.select('body').selectAll('svg').remove();// delete this line and you'll get multiple charts rather than just updating the data in the original chart draw_chart(new_data); }); }); </script> <script> function draw_chart(data){ var margin = {top: 20, right: 20, bottom: 30, left: 40}, width = 960 - margin.left - margin.right, height = 500 - margin.top - margin.bottom; var x = d3.scale.ordinal() .rangeRoundBands([0, width], .1); var y = d3.scale.linear() .range([height, 0]); var xAxis = d3.svg.axis() .scale(x) .orient("bottom"); var yAxis = d3.svg.axis() .scale(y) .orient("left"); var svg = d3.select("body").append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); data.forEach(function(d) { d.frequency = +d.frequency; }); x.domain(data.map(function(d) { return d.letter; })); y.domain([0, d3.max(data, function(d) { return d.frequency; })]); svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); svg.append("g") .attr("class", "y axis") .call(yAxis) .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em") .style("text-anchor", "end") .text("Frequency"); svg.selectAll(".bar") .data(data) .enter().append("rect") .attr("class", "bar") .attr("x", function(d) { return x(d.letter); }) .attr("width", x.rangeBand()) .attr("y", height) .attr("height","0") .transition() .delay(function(d, i) { return i*300 }) .duration(1000) .attr("y", function(d) { return y(d.frequency); }) .attr("height", function(d) { return height - y(d.frequency); }); } </script>
First, the reason why you have the line, var bar d3.select('body')...remove() // delete this line and you'll get double... Is beacuse in your draw_chart your always appending to the page when it's called. You need to change this line, var svg = d3.select("body").append("svg") to something that doesn't continually append a new svg If I have more time i'll take a look at the main question.