Recreate D3 Multi-Line Chart on Resize (responsive) - javascript

In order to have a responsive D3 multi-line chart I have added a resize function but it doesn't seem to work although the function gets called:
var data = [{
Date: "2016-10-10",
ValueOne: 1,
ValueTwo: 0
}, {
Date: "2016-10-17",
ValueOne: 23,
ValueTwo: 2
}, {
Date: "2016-10-24",
ValueOne: 32,
ValueTwo: 17
}, {
Date: "2016-10-31",
ValueOne: 57,
ValueTwo: 40
}, {
Date: "2016-11-07",
ValueOne: 74,
ValueTwo: 56
}];
var margin = {top: 10, right: 50, bottom: 100, left: 50},
// Set default width and height
widther = (window.innerWidth),
width = widther - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// Determine current size, which determines vars
function set_vars() {
var width = window.innerWidth - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
}
function drawGraphic() {
var svg = d3.select('#charts')
.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 + ")");
//Parses date for correct time format
var formatTime = d3.timeFormat("%Y-%m-%d");
data.forEach(function(d) {
d.Date = new Date(d.Date)
});
var valueOneData = data.map(function(d) {
return {
date: d.Date,
value: d.ValueOne
}
});
var valueTwoData = data.map(function(d) {
return {
date: d.Date,
value: d.ValueTwo
}
});
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(data, function(d) {
return d.Date
}));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(data, function(d) {
return d.ValueOne
}) * 1.05]);
var lineGenerator = d3.line()
.x(function(d) {
return xScale(d.date)
})
.y(function(d) {
return yScale(d.value)
});
var gX = svg.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale).tickFormat(function(d) {
return formatTime(d)
}).tickValues(data.map(function(d) {
return d.Date
})))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = svg.append("g")
.call(d3.axisLeft(yScale));
var valueOneLine = svg.append("path")
.datum(valueOneData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#124");
var valueTwoLine = svg.append("path")
.datum(valueTwoData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#c7003b");
//RESPONSIVENESS ATTEMPT NO1
d3.select(window).on("resize", resized);
}
//Resize function
function resized() {
d3.select("svg").remove();
set_vars();
drawGraphic();
console.log("FUNCTION IS BEING CALLED")
}
set_vars();
drawGraphic();
//RESPONSIVENESS ATTEMPT NO2
window.addEventListener("resize", function(){ d3.select("svg").remove(); set_vars(); drawGraphic(); });
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
In the snippet, I have tried two ways to do that. None of them make the chart recreate from scratch. The same issue applied in this jsfiddle.

The issue was with the parsing of the data every time the window was resized.
As the date within the data is parsed the first time, calling parseDate(d.date) will fail on every other call as it's already been parsed to a valid date. Do you get it?
Hence, moving the parsing code so that it's executed just once:
// parse data just once
data.forEach(function(d) {
d.date = parseDate(d.date);
d.value = +d.value;
});
Fiddle link: https://jsfiddle.net/a5rqt0L1/
Suggestion: I feel this isn't the right way to make a responsive chart i.e. removing SVG and re-appending to the body with all the configuration done multiple times. Here's how I'd do it:
Parse the data, append svg with initial height and width, append X, Y axes just once but move drawBars (to draw the actual bars) to a separate function that will use d3's own enter, exit and update selection logic.
On window resize, just change the SVG's height and width, re-render the axes by .call(xAxis)... and just call the drawBars function.
Hope this helps.

Related

Passing Data to D3.js with Django Backend

I want to build a line chart following the code here. I've made slight change to the data being passed with contains epoch time and a closing price. Following is the code
{% load static %}
<html>
<script src="https://d3js.org/d3.v4.js"></script>
<body>
<h1> Hello! </h1>
<div id="my_dataviz"></div>
</body>
<script>
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 60},
width = 460 - margin.left - margin.right,
height = 400 - 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 the data
var d = {
"Date":{
"0":1641168000000,
"1":1641254400000,
"2":1641340800000
},
"Close":{
"0":182.01,
"1":179.7,
"2":174.92
}
};
// When reading the csv, I must format variables:
d3.json(d,
function(d){
return { date : d3.timeParse("%s")(d.Date), value : d.Close }
},
// Now I can use this dataset:
function(data) {
// Add X axis --> it is a date format
var x = d3.scaleTime()
.domain(d3.extent(data, function(d) { return d.date; }))
.range([ 0, width ]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return +d.value; })])
.range([ height, 0 ]);
svg.append("g")
.call(d3.axisLeft(y));
// Add the line
svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(function(d) { return x(d.date) })
.y(function(d) { return y(d.value) })
)
})
</script>
</html>
I am unable to generate the graph. I've attached the console screenshot below.
There seems to be an error passing data as seen in html file below
I can't figure out how to pass the data, what should be the correct way to do it?
Edit
Applied suggested changes as follows
{% load static %}
<html>
<script src="https://d3js.org/d3.v6.js"></script>
<body>
<h1> Hello! </h1>
<div id="my_dataviz"></div>
<!-- <canvas id="chart" width="100" height="100"></canvas> -->
<!-- <script src={% static "js\linechart.js" %}></script>
<script>
var data = {{ AAPL|safe }};
var chart = LineChart(data, {
x: d => d.date,
y: d => d.close,
yLabel: "↑ Daily close ($)",
width: 400,
height: 400,
color: "steelblue"
});
</script> -->
</body>
<script>
// set the dimensions and margins of the graph
const margin = {top: 10, right: 30, bottom: 30, left: 60},
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
const 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})`);
var d = [
{
"Date": 1641168000000,
"Close": 182.01
},
{
"Date": 1641254400000,
"Close": 179.7
},
{
"Date": 1641168000000,
"Close": 174.92
},
];
d3.json(d,
// When reading the csv, I must format variables:
function(d){
return { date : d3.timeParse("%s")(d.Date), value : d.Close }
}).then(
// Now I can use this dataset:
function(data) {
// Add X axis --> it is a date format
const x = d3.scaleTime()
.domain(d3.extent(data, function(d) { return d.date; }))
.range([ 0, width ]);
svg.append("g")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(x));
// Add Y axis
const y = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return +d.value; })])
.range([ height, 0 ]);
svg.append("g")
.call(d3.axisLeft(y));
// Add the line
svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(function(d) { return x(d.date) })
.y(function(d) { return y(d.value) })
)
})
</script>
</html>
I think there's two things happeningg here:
When you call the funciton d3.json(data, parsingFunction), d3 is expecting the data entry to be an array of elements, where each element contains all the information for that entry in your datum. What the parsingFunction does here, is recievee each element in the array and you have to write the logic of how to treat your data (in your case, parse the epoch and keep the close price as it was).
Therefore, what you should do is change the way your data is being sent so that it's an array where each entry is an object containing thee corresponding time and close price, i.e:
var d = [
{
"Date": 1641168000000,
"Close": 182.01
},
{
"Date": 1641254400000,
"Close": 179.7
},
{
"Date": 1641168000000,
"Close": 174.92
},
];
This is actually not what's causing your mistake, but I'll say it because i saw it in your code: d3.json return a promise. Which means you have to wait for it to arrive. You can do this by either explicitely using var data = await d3.json(.., ..) and then using data to do whatever you want. OR, specify a callback function (which is what you were trying to do, I believe). To do that though, you need too specify that you want to call that function when the promise has resolved, by using a .then(callback) statement.
And by doing that, you can correctly parse the data with the code you sent above:
d3.json(d, function(d){
return { date : d3.timeParse("%s")(d.Date), value : d.Close }
}
).then(function(data){
// Use the data
})

d3.scaleTime dates between 1890 and 1910 appear as :00 (d3.v4)

I am creating a line plot in d3 using an example I found here -> https://www.d3-graph-gallery.com/graph/line_brushZoom.html
My data contains observations from 1890 through to 2018 in the following format:
1880-01-01,1
1890-01-01,3
1890-02-02,1
1890-02-17,1
1890-03-29,1
1890-04-04,1
1890-05-04,1
1890-06-02,1
1890-06-05,1
1890-06-11,1
1890-07-01,1
1890-10-28,1
1890-12-24,1
1890-12-25,1
1891-01-29,1
1891-03-03,1
1891-06-07,1
1892-05-09,1
1893-08-20,1
1893-10-06,1
1894-03-28,1
1895-10-17,1
1896-05-25,1
1897-02-05,1
1897-07-29,1
1897-08-26,1
1898-07-05,1
1900-01-01,1
1900-08-12,1
1901-09-21,1
1903-08-16,1
1903-09-23,1
1904-02-13,1
1904-09-02,1
1904-09-04,1
1905-05-08,1
1905-07-06,1
1905-11-19,1
1906-09-24,1
1908-02-03,1
1909-01-01,1
1910-09-26,1
I noticed that the x axis scale renders dates between 1890 and 1910 with the following ticks :00
rather than 1890, 1900, 1910
The original chart code gives the following line to set the
// Add X axis --> it is a date format
var x = d3.scaleTime()
//.domain(d3.extent(data, function(d) { return d.date; }))// original line
.domain([new Date(1880, 0, 1), new Date(2018, 0, 1)]) // debugline
.range([ 0, width ]);
xAxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
d3.v5 does it. little different
const xScale = d3.scaleTime().range([0,width]);
const yScale = d3.scaleLinear().rangeRound([height, 0]);
xScale.domain(d3.extent(data, function(d){
return timeConv(d.date)}));
yScale.domain([(0), d3.max(slices, function(c) {
return d3.max(c.values, function(d) {
return d.measurement + 4; });
})
]);
I can't figure out what the problem is, I tried the same data in d3.v5 and I can't reproduce the problem. I am wondering if there is some additional parsing that is required for distinct types?
Thanks
Jonathan
PS Full code was requested (copy and pasted from the d3-gallery)
<!-- Code from d3-graph-gallery.com -->
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<script>
// set the dimensions and margins of the graph
var margin = {top: 50, right: 30, bottom: 30, left: 60},
width = 900 - margin.left - margin.right,
height = 600 - 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 the data
d3.csv("all_cases.csv",
// When reading the csv, I must format variables:
function(d){
return { date : d3.timeParse("%Y-%m-%d")(d.date), value : d.value }
},
// Now I can use this dataset:
function(data) {
// Add X axis --> it is a date format
var x = d3.scaleTime()
// .domain(d3.extent(data, function(d) { return d.date; }))
.domain([new Date(1880, 0, 1), new Date(2018, 0, 1)])
.range([ 0, width ]);
xAxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return +d.value; })+5])
.range([ height, 0 ]);
yAxis = svg.append("g")
.call(d3.axisLeft(y));
// Add a clipPath: everything out of this area won't be drawn.
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", width )
.attr("height", height )
.attr("x", 0)
.attr("y", 0);
// Add brushing
var brush = d3.brushX() // Add the brush feature using the d3.brush function
.extent( [ [0,0], [width,height] ] ) // initialise the brush area: start at 0,0 and finishes at width,height: it means I select the whole graph area
.on("end", updateChart) // Each time the brush selection changes, trigger the 'updateChart' function
// Create the line variable: where both the line and the brush take place
var line = svg.append('g')
.attr("clip-path", "url(#clip)")
// Add the line
line.append("path")
.datum(data)
.attr("class", "line") // I add the class line to be able to modify this line later on.
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(function(d) { return x(d.date) })
.y(function(d) { return y(d.value) })
)
// Add the brushing
line
.append("g")
.attr("class", "brush")
.call(brush);
// A function that set idleTimeOut to null
var idleTimeout
function idled() { idleTimeout = null; }
// A function that update the chart for given boundaries
function updateChart() {
// What are the selected boundaries?
extent = d3.event.selection
// If no selection, back to initial coordinate. Otherwise, update X axis domain
if(!extent){
if (!idleTimeout) return idleTimeout = setTimeout(idled, 350); // This allows to wait a little bit
x.domain([ 4,8])
}else{
x.domain([ x.invert(extent[0]), x.invert(extent[1]) ])
line.select(".brush").call(brush.move, null) // This remove the grey brush area as soon as the selection has been done
}
// Update axis and line position
xAxis.transition().duration(1000).call(d3.axisBottom(x))
line
.select('.line')
.transition()
.duration(1000)
.attr("d", d3.line()
.x(function(d) { return x(d.date) })
.y(function(d) { return y(d.value) })
)
}
// If user double click, reinitialize the chart
svg.on("dblclick",function(){
x.domain(d3.extent(data, function(d) { return d.date; }))
xAxis.transition().call(d3.axisBottom(x))
line
.select('.line')
.transition()
.attr("d", d3.line()
.x(function(d) { return x(d.date) })
.y(function(d) { return y(d.value) })
)
});
})
svg.append("text")
.attr("x", 400)
.attr("y", -5)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("text-decoration", "solid")
.text("Outbreaks 1890 - 2018");
</script>
You're using D3 v4, and the axis works with D3 v5:
<script src="https://d3js.org/d3.v4.min.js"></script>
To make your code work with v5, you have to change the syntax of d3.csv slightly, as it uses promises in v5. You don't need to change anything else.
d3.csv("all_cases.csv",
function(d){
return { date : d3.timeParse("%Y-%m-%d")(d.date), value : d.value }
})
.then(function(data) {
// code
})

How to specify a data column as the property used in a line generator?

In d3.js it is possible to access parts of a dataset by using the syntax d.measure with d accessing the data property and "measure" accessing a specific field in our dataset. Based on the code I found on bl.ocks.org I created a line chart. I however wanted to alter the function dsLineChart() in such a way that I can pass the name of the column that I want to use for visualising the values on the y-axis, i.e. how to specify an argument dsLineChart(argument) that determines the column to use e.g. d.measure2 instead of d.measure.
See below for the script. The dataset I have contains the columns "measure", "measure2", "measure3" and "measure4" of which "measure" is visualised by d.measure, but I want to call e.g. dsLineChart("measure2") to use the same function but for another column.
e.g.
Dataset
var data = [
{group:"All",category:2011,measure:28107,measure2:53301,measure3:89015.40,measure4:138394},
{group:"All",category:2012,measure:39400,measure2:7001, measure3:55550.50,measure4:18004},
{group:"All",category:2013,measure:33894,measure2:690597,measure3:68289.50,measure4:17455},
{group:"All",category:2014,measure:55261,measure2:7172,measure3:73380.93,measure:418143} ];
Script
I have created a minimal working script that can be found on the following link Fiddle D3js line chart
Thanks to the of feedback #GerardoFurtado the resulting script is provided in below snippet and allows for calling the function dsLineChart() with different arguments resulting in linecharts using different measures e.g. dsLineChart("measure2") vs. dsLineChart("measure").
// dataset
var lineChartData = [{
category: 2011,
measure: 28107,
measure2: 53301,
measure3: 89015.40,
measure4: 138394
},
{
category: 2012,
measure: 39400,
measure2: 7001,
measure3: 55550.50,
measure4: 18004
},
{
category: 2013,
measure: 33894,
measure2: 690597,
measure3: 68289.50,
measure4: 17455
},
{
category: 2014,
measure: 55261,
measure2: 7172,
measure3: 73380.93,
measure: 418143
}
];
// layout
var margin = {
top: 20,
right: 10,
bottom: 0,
left: 50
},
width = 350 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
// function to draw linechart
function dsLineChart(selMeasure) {
//convert object to array
var data = d3.values(lineChartData);
var property;
var measures = [selMeasure];
var xScale = d3.scaleLinear()
.domain([0, data.length - 1])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return d[selMeasure];
})])
.range([height, 0])
.range([height, 0]);
var line = d3.line()
.x(function(d, i) {
return xScale(i);
})
.y(function(d) {
return yScale(d[property]);
});
var svg = d3.select("#lineChart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("position", "absolute")
.attr("top", "10px")
.attr("left", "410px")
var plot = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("id", "lineChartPlot");
var paths = plot.selectAll(null)
.data(measures)
.enter()
.append("path")
.attr("class", "line")
.attr("d", function(d) {
property = d;
return line(data)
})
.attr("stroke", "lightgrey")
.attr("fill", "none")
.attr("stroke-width", "4px");
}
dsLineChart("measure2");
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="lineChart"></div>
The most "elegant" solution here, and probably the most idiomatic one, is nesting your data, in such a way that the y value property can have the same name for all lines.
However, this doesn't mean that what you're asking is not possible: it certainly is. You can specify what scale you pass to the line generator (for instance, have a look at this answer), and what property you use for each method.
For this to work, we'll first declare a variable:
var property;
That's the variable we'll use in the line generator:
var line = d3.line()
.x(function(d, i) {
return xScale(i);
})
.y(function(d) {
return yScale(d[property]);
});
Now, let's get the real properties. Here I'm hardcoding them, but you can easily extract them from the data:
var measures = ["measure", "measure2", "measure3", "measure4"];
Then, we bind that array as data:
var paths = plot.selectAll(null)
.data(measures)
.enter()
.append("path")
Now comes the important part: in the callback, you simply assign the value of property, which is used by the line generator:
.attr("d", function(d) {
property = d;
return line(data)
})
All together, here is your code with those changes:
// dataset
var data = [{
group: "All",
category: 2011,
measure: 28107,
measure2: 53301,
measure3: 89015.40,
measure4: 138394
},
{
group: "All",
category: 2012,
measure: 39400,
measure2: 7001,
measure3: 55550.50,
measure4: 18004
},
{
group: "All",
category: 2013,
measure: 33894,
measure2: 690597,
measure3: 68289.50,
measure4: 17455
},
{
group: "All",
category: 2014,
measure: 55261,
measure2: 7172,
measure3: 73380.93,
measure: 418143
}
];
var property;
var measures = ["measure", "measure2", "measure3", "measure4"];
// layout
var margin = {
top: 20,
right: 10,
bottom: 0,
left: 50
},
width = 350 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
// function to draw linechart
function dsLineChart() {
var firstDatasetLineChart = data
var xScale = d3.scaleLinear()
.domain([0, firstDatasetLineChart.length - 1])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, d3.max(firstDatasetLineChart, function(d) {
return d.measure;
})])
.range([height, 0])
.range([height, 0]);
var line = d3.line()
.x(function(d, i) {
return xScale(i);
})
.y(function(d) {
return yScale(d[property]);
});
var svg = d3.select("#lineChart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("position", "absolute")
.attr("top", "10px")
.attr("left", "410px")
var plot = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("id", "lineChartPlot");
var paths = plot.selectAll(null)
.data(measures)
.enter()
.append("path")
.attr("class", "line")
.attr("d", function(d) {
property = d;
return line(data)
})
.attr("stroke", "lightgrey")
.attr("fill", "none")
.attr("stroke-width", "4px");
}
dsLineChart();
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="lineChart"></div>

d3 v4 scale returning incorrect values

I am trying to plot a scatter plot with with a variable containing json. I have about 800 points to plot.
This is my of code:
var data = json_games;
var svg = d3.select("svg"),
margin = {top: 30, right: 30, bottom: 30, left: 400},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
data.forEach(function(d) {
d.Component1 = +d.Component1;
d.Component2 = +d.Component2;
});
var x = d3.scaleLinear().range([0, width]).domain([d3.min(data, function(d) { return d.Component1; }),d3.max(data, function(d) { return d.Component1; })]);
var y = d3.scaleLinear().range([0, height]).domain([d3.min(data, function(d) { return d.Component2; }),d3.max(data, function(d) { return d.Component2; })]);
// Add the scatterplot
g.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d) {
return x(d.Component1); })
.attr("cy", function(d) {
return y(d.Component2); });
// Add the X Axis
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add the Y Axis
g.append("g")
.call(d3.axisLeft(y));
As the data is huge in order to find the maximum and minimum values of x and y components I used cgrome console and the values that I am getting are as follows:
>d3.min(data, function(d) { return d.Component1; })
>-0.20829495230931433
>d3.max(data, function(d) { return d.Component1; })
>0.35130959917777926
>d3.min(data, function(d) { return d.Component2; })
>-1.2035701018868445
>d3.max(data, function(d) { return d.Component2; })
>0.7208057297018022
and the scaled values of x and y that I am getting are:
x(0.6)
>1169.967094854204
y(0.6)
>468.6117109457583
Because of this the points in the scatterplot are being drawn in corners.
Can someone please help. I also tried d3.extent but with the same values.
Any help will be highly appreciated.

"NaN" when zooming and selecting by id

I'm probably doing something wrong but the following fiddle is displaying some really strange behavior:
https://jsfiddle.net/pkerpedjiev/42w01t3e/8/
Before I explain it, here's the code:
function skiAreaElevationsPlot() {
var width = 550;
var height = 400;
var margin = {
'top': 30,
'left': 30,
'bottom': 30,
'right': 40
};
function chart(selection) {
selection.each(function(data) {
// Select the svg element, if it exists.
var svg = d3.select(this).selectAll("svg").data([data]);
// Otherwise, create the skeletal chart.
var gEnter = svg.enter().append("svg").append("g");
svg.attr('width', width)
.attr('height', height);
var zoom = d3.behavior.zoom()
.on("zoom", draw);
data = Object.keys(data).map(function(key) {
return data[key];
}).sort(function(a, b) {
return b.max_elev - a.max_elev;
});
svg.insert("rect", "g")
.attr("class", "pane")
.attr("width", width)
.attr("height", height)
.attr('pointer-events', 'all')
.call(zoom);
var yScale = d3.scale.linear()
.domain([0, d3.max(data.map(function(d) {
return d.max_elev;
}))])
.range([height - margin.top - margin.bottom, 0]);
var xScale = d3.scale.linear()
.domain([0, data.length])
.range([0, width - margin.left - margin.right]);
var widthScale = d3.scale.linear()
.domain(d3.extent(data.map(function(d) {
return d.area;
})))
.range([10, 30]);
zoom.x(xScale).scaleExtent([1, data.length / 30]);
var gMain = gEnter.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
gMain.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom);
function skiAreaMouseover(d) {
gMain.select('#n-' + d.uid)
.attr('visibility', 'visible');
}
function skiAreaMouseout(d) {
gMain.select('#n-' + d.uid)
.attr('visibility', 'visible');
}
// the rectangle showing each rect
gMain.selectAll('.resort-rect')
.data(data)
.enter()
.append('rect')
.classed('resort-rect', true)
.attr("clip-path", "url(#clip)")
.attr('id', function(d) {
return 'n-' + d.uid;
})
.on('mouseover', skiAreaMouseover)
.on('mouseout', skiAreaMouseout);
var gYAxis = svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (width - margin.right) + "," + margin.top + ")");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("right")
.tickSize(-(width - margin.left - margin.right))
.tickPadding(6);
gYAxis.call(yAxis);
draw();
function draw() {
function scaledX(d, i) {
console.log('xd', d);
return xScale(i);
}
function rectWidth(d, i) {
return widthScale(d.area);
}
gMain.selectAll('.resort-rect')
.attr('x', scaledX)
.attr('y', function(d) {
console.log('d', d);
return yScale(d.max_elev);
})
.attr('width', 20)
.attr('height', function(d) {
console.log('d:', d)
return yScale(d.min_elev) - yScale(d.max_elev);
})
.classed('resort-rect', true);
}
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
return chart;
};
return chart;
}
var elevationsPlot = skiAreaElevationsPlot()
.width(550)
.height(300);
data = [{
"min_elev": 46,
"max_elev": 54,
"uid": "9809641c-ab03-4dec-8d51-d387c7e4f114",
"num_lifts": 1,
"area": "0.00"
}, {
"min_elev": 1354,
"max_elev": 1475,
"uid": "93eb6ade-8d78-4923-9806-c8522578843f",
"num_lifts": 1,
"area": "0.00"
}, {
"min_elev": 2067,
"max_elev": 2067,
"uid": "214fdca9-ae62-473b-b463-0ba3c5755476",
"num_lifts": 1,
"area": "0.00"
}];
d3.select('#ski-area-elevations')
.datum(data)
.call(elevationsPlot)
So, when the page is first loaded, a rectangle will be visible in the middle. If you try scrolling on the graph, the console.log statements in the draw function will produce output. Notice that the xd: and d: statements all consist of just one object from the data set.
Now, if you mouseover the rectangle and try zooming again (using the scroll wheel). A bunch of NaN errors will be displayed. Now some of the d: and xd: statements will now print lists of objects.
Why is this happening? The underlying bound data never changed.
What puzzles me is that if these statements:
gMain.select('#n-' + d.uid)
Are changed to:
gMain.selectAll('#n-' + d.uid)
The fiddle behaves properly. Why does this make a difference? Is this a bug, or am I missing something?
For googleability, here's the error I get:
Error: Invalid value for <rect> attribute y="NaN"
The simple solution is to replace gMain.select/gMain.selectAll in the mouse event routines with d3.select(this)
The complicated solution seems to be that a single select binds a parents data to whatever is selected if you're acting on an existing selection. gMain is an existing selection and has the 3 data values as an array bound to it - console.log (gMain.datum()) to see - so when you do a gMain.select("#oneoftherects") you replace the single object in #oneoftherects with that array, thus knackering the x,y,width,height etc routines that expect one object. (Using d3.select doesn't do the same as d3 isn't a selection)
http://bost.ocks.org/mike/selection/#non-grouping

Categories