Animate position of svg rect on transition - javascript
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.
Related
D3 Text Slice based on the width of rect
I am building a tree map with D3 v4 and all good so far. However, some of the text within their respective rectangles goes out over the edge of the rectangle. I want to use text slice to cut off the text if it does this, and instead put in three dots. As a test, I have been able to get the slice function to truncate text that goes beyond let's say 5 characters, but when I try to specify that I want the slice function to truncate based on the width of the corresponding rectangle, it doesn't work on all except one (which I think is because it goes out over the edge of the whole tree map. I can't seem to find a way to pull in the width of the rectangles to the slice function in order to compare it to the width of the text. // set the dimensions and margins of the graph var margin = {top: 10, right: 10, bottom: 10, left: 10}, width = 945 - margin.left - margin.right, height = 1145 - margin.top - margin.bottom; // append the svg object to the body of the page var svg = d3.select("#my_dataviz") .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 + ")"); // Read data d3.csv('https://raw.githubusercontent.com/rootseire/survey/main/treemap-data.csv', function(data) { // stratify the data: reformatting for d3.js var root = d3.stratify() .id(function(d) { return d.name; }) // Name of the entity (column name is name in csv) .parentId(function(d) { return d.parent; }) // Name of the parent (column name is parent in csv) (data); root.sum(function(d) { return +d.value }) // Compute the numeric value for each entity // Then d3.treemap computes the position of each element of the hierarchy // The coordinates are added to the root object above d3.treemap() .size([width, height]) .padding(4) (root) // use this information to add rectangles: svg .selectAll("rect") .data(root.leaves()) .enter() .append("rect") .attr('x', function (d) { return d.x0; }) .attr('y', function (d) { return d.y0; }) .attr('width', function (d) { return d.x1 - d.x0; }) .attr('height', function (d) { return d.y1 - d.y0; }) .style("stroke", "black") .style("fill", "#94C162") .attr("class", "label") .on("mouseover", function(d) { tip.style("opacity", 1) .html("Genre: " + d.data.name + "<br/> Number: " + d.value + "<br/>") .style("left", (d3.event.pageX-25) + "px") .style("top", (d3.event.pageY-25) + "px") }) .on("mouseout", function(d) { tip.style("opacity", 0) }); svg .selectAll("text") .data(root.leaves()) .enter() .append("text") .attr("x", function(d){ return d.x0+6}) // +10 to adjust position (more right) .attr("y", function(d){ return d.y0+15}) // +20 to adjust position (lower) .attr('dy', 0) // here .text(function(d){ return d.data.name + ' (' + d.data.value +')' }) .attr("font-size", "15px") .attr("fill", "black") .each(slice); }) // Define the div for the tooltip var tip = d3.select("#my_dataviz").append("div") .attr("class", "tooltip") .style("opacity", 0) // Add events to circles d3.selectAll('.label') .attr("x", function(t) { return Math.max(0, 100-this.textLength.baseVal.value); }); function slice(d) { var self = d3.select(this), textLength = self.node().getComputedTextLength(), text = self.text(); while (textLength > text.getBoundingClientRect().width && text.length > 0) { text = text.slice(0, 5); self.text(text + '...'); textLength = self.node().getComputedTextLength(); } } .tooltip { position: absolute; pointer-events: none; background: #000; color: #fff; } <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script> <!DOCTYPE html> <head> <script type="text/javascript" src="https://raw.githubusercontent.com/rootseire/survey/main/word_wrap.js"></script> </head> <meta charset="utf-8"> <body> <div id="my_dataviz"></div> </body> </html> Any help greatly appreciated.
D3 Charting Tool: How to add label at right of target line (additional horizontal line) in column chart
I have drawn the following chart with D3 Charting tool v4. I have attached the full code at the bottom of this post. The red line is the target goal to be achieved. The following code block is drawing this line: var targetGoalArr = [7]; svg.selectAll(".targetgoal") .data(targetGoalArr) .enter().append("line") .attr("class", "targetgoal") .attr("x1", 0) .attr("x2", width) .attr("y1", y) .attr("y2", y) .style("stroke", "#cc0000"); Now I need to label this line with the text Growth Target (7) to the right of it and in two lines. The label has to be broken in two lines as well! The following screenshot shows the desired output. How can I achieve the above? One more thing I am not able to draw is the Y-Axis baseline. In my chart (with red line) I am creating the horizontal lines using a custom tick array. Here is the code: function draw_yAxis_gridlines() { return d3.axisLeft(y) .tickValues(yTicks); } svg.append("g") .attr("class", "grid axis") .call(draw_yAxis_gridlines() .tickSize(-width) ); However, if I do not use custom ticks for Y-Axis, the baseline appears but I am missing the horizontal grid lines. I have to display both at the same time. Here is my full code: public function evd_unitary_growth_plan_chart( $data_str ){ ob_start(); ?> <style> /* set the CSS */ .line { fill: none; stroke: steelblue; stroke-width: 2px; } .grid line { stroke: lightgrey; stroke-opacity: 0.5; shape-rendering: crispEdges; } .grid path { stroke-width: 0; } .axis { font-size: 13px; font-family: 'Roboto'; color: #808888; } </style> <script type="text/javascript"> var h = 300; var w = 750; var barPadding = 2; function barColor(data_month, current_month) { if( parseInt(data_month) >= current_month) return "#008600"; else return "#c4c4c4"; } // set the dimensions and margins of the graph var margin = {top: 30, right: 20, bottom: 30, left: 40}, width = w - margin.left - margin.right, height = h - margin.top - margin.bottom; var data = <?php echo $data_str ?>; // set the ranges var x = d3.scaleBand().range([0, width]).padding(0.2); var y = d3.scaleLinear().range([height, 0]); var svg = d3.select("#ecbg_unitary").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 + ")"); // Scale the range of the data in the domains x.domain(data.map(function(d) { return d.month; })); var y_domain_upperBound = d3.max(data, function(d) { return d.points; }); y_domain_upperBound = Math.round(y_domain_upperBound / 10) * 10 + 10; y.domain([0, y_domain_upperBound]); // Create Y-Axis tick array to draw grid lines var yTicks = []; var tickInterval = 5; for(var i = 0; i <= y_domain_upperBound; i = i + tickInterval) { yTicks.push(i); } console.log(yTicks); // gridlines in y axis function function draw_yAxis_gridlines() { return d3.axisLeft(y) .tickValues(yTicks); } // Reference line - The red line var targetGoalArr = [7]; svg.selectAll(".targetgoal") .data(targetGoalArr) .enter().append("line") .attr("class", "targetgoal") .attr("x1", 0) .attr("x2", width) .attr("y1", y) .attr("y2", y) .style("stroke", "#cc0000"); // append the rectangles for the bar chart svg.selectAll("rect") .data(data) .enter().append("rect") .attr("x", function(d) { return x(d.month); }) .attr("width", x.bandwidth()) .attr("y", function(d) { return y(d.points); }) .attr("height", function(d) { return height - y(d.points); }) .attr("fill", function(d){return barColor(d.data_month_number, d.current_month_number)}); // column labels svg.selectAll("text") .data(data) .enter() .append("text") .text(function(d) { return d.points; }) .attr("text-anchor", "middle") .attr("x", function(d, i) { return x(d.month) + x.bandwidth() / 2; }) .attr("y", function(d) { return y(d.points) - 10; }) .attr("font-family", "Roboto") .attr("font-size", "13px") .attr("font-weight", "bold") .attr("fill", "#606668"); // add the x Axis svg.append("g") .attr("class", "axis") .attr("transform", "translate(0," + height + ")") .call(d3.axisBottom(x)); // add the Y gridlines svg.append("g") .attr("class", "grid axis") .call(draw_yAxis_gridlines() .tickSize(-width) ); </script> <?php return ob_get_clean(); }
To add a label to your target line, you are best to create group (g) element, and then append a line and text element to it. The g element can be translated to the correct y position, so that the line and text can be positioned relatively to the g. var targetGoalArr = [7]; var target = g.selectAll(".targetgoal") .data(targetGoalArr) .enter() .append("g") .attr("transform", function(d){ return "translate(0, " + y(d) +")" }) target.append("line") .attr("class", "targetgoal") .attr("x1", 0) .attr("x2", width) .attr("y1", 0) //these can be omitted .attr("y2", 0) .style("stroke", "#cc0000"); target.append("text") .text(function(d){ return "Target growth: " + d }) .attr("x", width) .attr("y", "0.35em")
Horizontal bar chart exit method not working
I have a D3 horizontal bar chart that updates the data off a selection filter. When the chart first renders everything works as expected. When the filter selects any of the dropdowns the chart looks as expected, however when I select the "All" option again, the y-axis has the correct domain names and the x-axis resets to the correct scale. However, the problem occurs that the whole svg chart element is a grey block. If you hover over the bar the right size of the bar is selected, so I believe old data isn't exiting correctly, as can be seen in the image below. I'm stuck as I thought my bars.exit() call was removing elements, I'm missing a key point in my understanding of what is happening. I think i'm very close but not sure what I'm missing? ****** UPDATE ********* Selecting all appears to insert an additional "rect" element that isn't present when it is first run. Looking at the console log I can't understand where it is generated from as the array length is 3 not 4. var topicData = [{ "name": "Vehicle - Poor", "value": 5 }, { "name": "Problems - Unresolved", "value": 3 }, { "name": "Reliability - Poor", "value": 2 } ] var margin = { top: 10, right: 10, bottom: 100, left: 200 } var width = 1000 - margin.left - margin.right, height = 700 - margin.top - margin.bottom; var svg = d3.select("#graphic_two").append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) var g = svg.append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); var time = 0; var data; // Set the X Label var xLabel = g.append("text") .attr("class", "x label") .attr("y", height + 50) .attr("x", width / 2) .attr("font-size", "15px") .attr("text-anchor", "middle") .text("Topic Count"); // Set the Y Label var yLabel = g.append("text") .attr("class", "y axisLabel") .attr("transform", "rotate(-90)") .attr("y", -(120)) .attr("x", -350) .attr("font-size", "15px") .attr("text-anchor", "middle") .text("Topic Names") // Scales var x = d3.scaleLinear().range([0, width]); var y = d3.scaleBand() .range([height, 0]); // X-axis var xAxisCall = d3.axisBottom() var xAxis = g.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")"); // Y-axis var yAxisCall = d3.axisLeft() var yAxis = g.append("g") .attr("class", "y axis"); //console.log(data); formattedData = topicData; update(formattedData) $("#survey") .on("change", function() { update(formattedData); }) function step() { // At the end of our data, loop back time = (time < 214) ? time + 1 : 0 update(formattedData); } function update(data) { console.log("I'm in the update function") var survey = $("#survey").val(); var data = data.filter(function(d) { if (survey == "all") { return true; } else { return d.name == survey; } }) console.log(data) // X Scale x.domain([0, d3.max(data, function(d) { return d.value; })]); y.domain(data.map(function(d) { return d.name; })); // Update axes xAxisCall.scale(x); xAxis.transition().call(xAxisCall); yAxisCall.scale(y); yAxis.transition().call(yAxisCall); // JOIN new data with old elements var bars = g.selectAll(".bars").data(data, function(d) { return d.name; }); // EXIT old elements not present in new data. bars.exit() .attr("class", "exit") .remove(); // ENTER new elements present in new data. bars.enter() .append("rect") .attr("width", function(d) { return x(d.value); }) // Width corresponds with the value from the data .attr("y", function(d) { return y(d.name); }) // Maps the name to the bar .attr("height", y.bandwidth()) .attr("fill", "grey") .on("mouseover", function() { d3.select(this) .attr("fill", "blue"); }) .on("mouseout", function(d, i) { d3.select(this) .attr("fill", "grey"); }) .on('click', function click(element) { selection = element.name; url = "http://127.0.0.1:5000/table" window.location = url; window.localStorage.setItem("topic", selection) }); } <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css"> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script> <link href="https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.7/chosen.min.css" rel="stylesheet" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.7/chosen.jquery.min.js"></script> <select id="survey" data-placeholder="Select..." class="chosen-select" style="width:350px;" tabindex="3"> <option selected value="all">All</option> <option value="Vehicle - Poor">Vehicle - Poor</option> <option value="Problems - Unresolved">Problems - Unresolved</option> <option value="Reliability - Poor">Reliability - Poor</option> </select> <div class="col-md-12"> <div id="graphic_two"></div> </div>
The accepted answer is not correct (removing elements before updating a D3 dataviz is definitely not the idiomatic way to do it) and, what's more, it doesn't explain why your exit selection was not working. Your problem is just that you're selecting a class in your update selection... var bars = g.selectAll(".bars") //etc... ... but you never set that class in the enter selection. Therefore, just do: bars = bars.enter() .append("rect") .attr("class", "bars") Also, you have other problems: You never change the update selection. Pay attention to the merge() in my code below; You're changing the domain of the band scale. So, the width of each bar depends on the number of bars. I have a feeling that this is not what you want. If that's correct, refactor that part of the code. In my snippet below I'm using a simple paddingOuter math to change the width of the bars. Here is your code with those changes: var topicData = [{ "name": "Vehicle - Poor", "value": 5 }, { "name": "Problems - Unresolved", "value": 3 }, { "name": "Reliability - Poor", "value": 2 } ]; var margin = { top: 10, right: 10, bottom: 100, left: 200 } var width = 1000 - margin.left - margin.right, height = 700 - margin.top - margin.bottom; var svg = d3.select("#graphic_two").append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) var g = svg.append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); var time = 0; var data; // Set the X Label var xLabel = g.append("text") .attr("class", "x label") .attr("y", height + 50) .attr("x", width / 2) .attr("font-size", "15px") .attr("text-anchor", "middle") .text("Topic Count"); // Set the Y Label var yLabel = g.append("text") .attr("class", "y axisLabel") .attr("transform", "rotate(-90)") .attr("y", -(120)) .attr("x", -350) .attr("font-size", "15px") .attr("text-anchor", "middle") .text("Topic Names") // Scales var x = d3.scaleLinear().range([0, width]); var y = d3.scaleBand() .range([height, 0]); // X-axis var xAxisCall = d3.axisBottom() var xAxis = g.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")"); // Y-axis var yAxisCall = d3.axisLeft() var yAxis = g.append("g") .attr("class", "y axis"); //console.log(data); formattedData = topicData; update(formattedData) $("#survey") .on("change", function() { update(formattedData); }) function step() { // At the end of our data, loop back time = (time < 214) ? time + 1 : 0 update(formattedData); } function update(data) { var survey = $("#survey").val(); var data = data.filter(function(d) { if (survey == "all") { return true; } else { return d.name == survey; } }) // X Scale x.domain([0, d3.max(data, function(d) { return d.value; })]); y.domain(data.map(function(d) { return d.name; })) .paddingOuter(1 / data.length); // Update axes xAxisCall.scale(x); xAxis.transition().call(xAxisCall); yAxisCall.scale(y); yAxis.transition().call(yAxisCall); // JOIN new data with old elements var bars = g.selectAll(".bars").data(data, function(d) { return d.name; }); // EXIT old elements not present in new data. bars.exit() .attr("class", "exit") .remove(); // ENTER new elements present in new data. bars = bars.enter() .append("rect") .attr("class", "bars") .merge(bars) .attr("width", function(d) { return x(d.value); }) // Width corresponds with the value from the data .attr("y", function(d) { return y(d.name); }) // Maps the name to the bar .attr("height", y.bandwidth()) .attr("fill", "grey") .on("mouseover", function() { d3.select(this) .attr("fill", "blue"); }) .on("mouseout", function(d, i) { d3.select(this) .attr("fill", "grey"); }) .on('click', function click(element) { selection = element.name; url = "http://127.0.0.1:5000/table" window.location = url; window.localStorage.setItem("topic", selection) }); } <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css"> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script> <link href="https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.7/chosen.min.css" rel="stylesheet" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.7/chosen.jquery.min.js"></script> <select id="survey" data-placeholder="Select..." class="chosen-select" style="width:350px;" tabindex="3"> <option selected value="all">All</option> <option value="Vehicle - Poor">Vehicle - Poor</option> <option value="Problems - Unresolved">Problems - Unresolved</option> <option value="Reliability - Poor">Reliability - Poor</option> </select> <div class="col-md-12"> <div id="graphic_two"></div> </div> PS: do not mix jQuery and D3. That's not only unnecessary but also harmful sometimes.
Error: <rect> attribute y: Expected length, "NaN"
I am trying to follow this example here for a D3 stacked chart. I've tested it locally and it works fine. I have adapted the code to match my csv dataset, but unfortunately I get issues with the calculation of y and height attributes: Error: attribute y: Expected length, "NaN". Error: attribute height: Expected length, "NaN". Here is my adapted source code: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Enterprise Elements Analysis - In/Out of Scope</title> <script src="http://d3js.org/d3.v4.min.js" charset="utf-8"></script> <style type="text/css"> svg { font: 10px sans-serif; shape-rendering: crispEdges; } .axis path, .axis line { fill: none; stroke: #000; } path.domain { stroke: none; } .y .tick line { stroke: #ddd; } </style> </head> <body> <script type="text/javascript"> // Our D3 code will go here var ratData = []; d3.csv("./etcounts.csv", function(d) { return { type: d.type, in_scope: +d.in_scope, out_scope: +d.out_scope }; }, function(error, rows) { data = rows; console.log(data); createVisualization(); }); function createVisualization() { // Setup svg using with margins var margin = {bottom: 75, left: 15, right: 85}; var w = 200 - margin.left - margin.right; var h = 175 - margin.bottom; // get length of Array var arrayLength = data.length; // length of dataset var x_axisLength = 100; // length of x-axis in our layout var y_axisLength = 100; // length of y-axis in our layout var svg = d3.select("body") .append("svg") .attr("width", w + margin.left + margin.right) .attr("height", h + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + ",10)"); // set up the properties for stack var stack = d3.stack() .keys(["In Scope", "Out Scope"]) .order(d3.stackOrderDescending) .offset(d3.stackOffsetNone); // transpose your data using stack var series = stack(data); // view the stack console.log(series); // setup the Y scale var yScale = d3.scaleLinear() .domain([0, d3.max(series, function(d) { return d3.max(d, function(d) { return d[1]; }); })]) .range([h, 0]); // Set some colors into an array var colors = ["#dfd6d6", "#d85f41"]; // choose colors // Create groups for each series, rect elements for each segment var groups = svg.selectAll("g.type") .data(series) .enter().append("g") .attr("class", "type") .style("fill", function(d, i) { return colors[i]; // color the rectangles }); // Create the rectangles var rect = groups.selectAll("rect") .data(function(d) { return d; }) .enter() .append("rect") .attr("x", function(d,i) { return i * (x_axisLength/arrayLength) + 30; // Set x coordinate of rectangle to index of data value (i) *25 }) .attr("y", function(d) { return yScale(d[1]); // set base of rectangle }) .attr("height", function(d) { return yScale(d[0]) - yScale(d[1]); // set height of rectangle }) .attr("width", (x_axisLength/arrayLength) - 1) // set width of rectangle .on("mouseover", function() { tooltip.style("display", null); // hide tooltip }) .on("mousemove", function(d) { var xPosition = d3.mouse(this)[0] - 15; var yPosition = d3.mouse(this)[1] - 25; tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")"); tooltip.select("text").text(d.data.city + ": " + (d[1] - d[0])); // populate tooltip }) .on("mouseout", function() { tooltip.style("display", "none"); }); // Draw legend var legend = svg.selectAll(".legend") .data(colors) .enter().append("g") .attr("class", "legend") .attr("transform", function(d, i) { return "translate(" + i * 50 + ", 110)"; }); legend.append("rect") .attr("x", w - 70) .attr("width", 18) .attr("height", 18) .style("fill", function(d, i) {return colors.slice().reverse()[i];}); legend.append("text") .attr("x", w - 49) .attr("y", 9) .attr("dy", ".35em") .style("text-anchor", "start") .text(function(d, i) { switch (i) { case 0: return "In"; case 1: return "Out"; } }); // Prep the tooltip bits, initial display is hidden var tooltip = svg.append("g") .attr("class", "tooltip") .style("display", "none"); tooltip.append("text") .attr("x", 15) .attr("dy", "1.2em") .style("text-anchor", "middle") .attr("font-size", "12px"); // Create y-axis svg.append("line") .attr("x1", 30) .attr("y1", 0) .attr("x2", 30) .attr("y2", 100) .attr("stroke-width", 2) .attr("stroke", "black"); // y-axis label svg.append("text") .attr("class", "y label") .attr("text-anchor", "middle") .text("Elements") .attr("transform", "translate(20, 50) rotate(-90)") .attr("font-size", "14px") .attr("font-family", "'Open Sans', sans-serif"); // Create x-axis svg.append("line") .attr("x1", 30) .attr("y1", 100) .attr("x2", 130) .attr("y2", 100) .attr("stroke-width", 2) .attr("stroke", "black"); } </script> </body> </html> My Dataset (etcounts.csv) is here: type,in_scope,out_scope ERKRS,1,1 KKBER,6,5 KOKRS,1,31 BUKRS,78,143 VKORG,23,13 BWKEY,51,6 EKORG,5,6 WERKS,51,65 LGORT,9,180 SPART,9,3 VTWEG,2,0 PERSA,47,73 Unfortunately my D3/JS skills are not quite up to par, but I would appreciate any help. Thanks - John
Instead of var stack = d3.stack() .keys(["In Scope", "Out Scope"]) <-- there is no key as such .order(d3.stackOrderDescending) .offset(d3.stackOffsetNone); it should have been: var stack = d3.stack() .keys(["in_scope", "out_scope"]) .order(d3.stackOrderDescending) .offset(d3.stackOffsetNone); Reason: there is no keys in your CSV "In Scope", "Out Scope" It should have been "in_scope", "out_scope" EDIT For tool tip : tooltip.select("text").text(d.data.city + ": " + (d[1] - d[0])); should have been tooltip.select("text").text(d.data.type + ": " + (d[1] - d[0])); Reason: There is no data.city in your CSV. working code here
D3.js Multi-Series Chart with Y value tracking
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.