I think I'm almost there. I have a working version for a single line that updates dynamically, but am having trouble getting to the multi-line part. I believe it has something to do with the way the data is being filtered at _selection.each. Not sure the best way to proceed from here though. The example found here (Drawing Multiple Lines in D3.js) seems to deal with this without much work.
Here is the jsfiddle for this, but the code is represented below as well:
http://jsfiddle.net/seoulbrother/NhK43/
Thanks in advance. Also, would love to hear best practices regarding this as well.
So if I have a page with a matrix where each row represents a time-series:
<html>
<body>
<div id="container"><div id="viz"></div></div>
</body>
</html>
<script type="text/javascript">
var matrix = [
[23,12,44,22,12,33,14,76,45,55,66,55],
[33,22,11,88,32,63,13,36,35,51,26,25]
];
</script>
I call this using:
mult_line = d3.graph.line(500, 250, "#viz");
d3.select("#container").datum(matrix).call(mult_line);
where d3.graph.line is:
d3.graph.line = function module(w, h, id) {
var svg;
var margin = {top: 10, right: 20, bottom: 20, left: 40},
width = w - margin.left - margin.right,
height = h - margin.top - margin.bottom;
function chart(_selection) {
_selection.each(function(_data) {
console.log(_data);
var x = d3.scale.linear().domain([1, _data.length]).range([0, width]);
var y = d3.scale.linear().domain([0, 100]).range([height, 0]);
var xAxis = d3.svg.axis().scale(x).ticks(5).orient("bottom");
var yAxis = d3.svg.axis().scale(y).ticks(5).orient("left");
var line = d3.svg.line()
.x(function(d,i) { return x(i+1); })
.y(function(d) { return y(d); });
if (!svg){
svg = d3.select(id).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 + ")");
svg.append("path")
.attr("class","line")
.attr("d", line(_data));
svg.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y-axis")
.call(yAxis);
}
var line_m = d3.selectAll("path.line")
.data(_data);
line_m.transition()
.duration(1000)
.attr('d', line(_data));
line_m.enter().append("path")
.attr("d",line(_data));
update_axis();
function update_axis() {
d3.select(".x-axis").call(xAxis);
}
});
}
return chart;
}
I think there may be varying opinions about this, but I wouldn't use selection.datum() as the mechanism of passing data into the chart component. Instead, I would add a .data() setter method as a property of the multiline component. I would do the same for width, height, etc. This is the way d3's components are implemented (for example, check out the source code for the d3.svg.axis). Then your chart creation would look like this:
mult_line = d3.graph.line()
.width(500)
.height(250)
.id("#viz")
.data(matrix);
d3.select("#container").call(mult_line);
You can refine this so that there are 2 data-setting methods of multiline: .data() and .multilineData(). The first would expect data for a single line (an array) and the 2nd would expect an array of arrays (multiline). Internally, they'd always be represented as an array of arrays. That way, the chart drawing code always expects to draw multiline data, except sometimes the data has just one line.
Related
I am really new into d3 and js. I wanted to make a choropleth map of the Us showing the different states. However something is not working since i don't get any path appended to my g object. Also the console.log(states) doesn't work. I'm pretty sure this is a newbie error and due to my lacks in js/d3. However i think i must use Promise.all since i want to add another csv file later on.
Thank you in advance!
Developer Tool
Below is the Code
//Standard Method to start our d3 visualization. For Maps the margin is not that important,but we make best practices
(margin = { top: 0, left: 0, right: 0, right: 0, bottom: 0 }),
(height = 400 - margin.top - margin.bottom),
(width = 800 - margin.left - margin.right);
var svg = d3
.select("#map")
.append("svg")
.attr("height", height + margin.top + margin.bottom)
.attr("width", width + margin.left + margin.right)
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
//Read in our us Data
Promise.all([d3.json("us.json")]).then(([data]) => {
console.log(data)
});
var projection = d3
.geoAlbersUsa()
.translate([width / 2, height / 2])
.scale(100);
//create path of projection so that we can work with latidudes and longitudes
var path = d3.geoPath().projection(projection);
function ready(error, data) {
console.log(data)
var states = topojson.feature(data, data.objects.states).features;
console.log(states);
svg
.selectAll(".state")
.data(states)
.enter()
.append("path")
.attr("class", "state")
.attr("d", path);
}
I am using this example to implement dragging on a graph.
The most relevant part:
/// IMPLEMENT DRAG BEHAVIOR
drag = d3.drag().on("drag", dragged)
function dragged(event,d) {
d3.select(this).attr("transform", 'translate(' + event.x + ',' + 0 + ')')
}
for (line of quantile_horizontal_lines) {
line.call(drag)
}
The function dragged expects an event. But the object passed into dragged is just the coordinates of my line, with nothing about the event. Of course, it has no attribute x, so the code doesn't work.
An event object is supposed to look like this:
I can't figure out what I'm doing differently from the example.
My full code:
/// BASIC LINE GRAPH SETUP
// 2. Use the margin convention practice
var margin = {top: 50, right: 50, bottom: 50, left: 50}
, width = window.innerWidth - margin.left - margin.right // Use the window's width
, height = window.innerHeight - margin.top - margin.bottom; // Use the window's height
// 8. An array of objects of length N. Each object has key -> value pair, the key being "y" and the value is a random number
var dataset = data
// The number of datapoints
var n = data.length
// 5. X scale will use the index of our data
var xScale = d3.scaleLinear()
.domain([metadata.xmin, metadata.xmax]) // input
.range([0, width]); // output
// 6. Y scale will use the randomly generate number
var yScale = d3.scaleLinear()
.domain([metadata.ymin, metadata.ymax]) // input
.range([height, 0]); // output
// 7. d3's line generator
var line = d3.line()
.x(function(d) { return xScale(d.x); }) // set the x values for the line generator
.y(function(d) { return yScale(d.y); }) // set the y values for the line generator
// 1. Add the SVG to the graph div and employ #2
var svg = d3.select("#graph").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 + ")");
// 3. Call the x axis in a group tag
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale)); // Create an axis component with d3.axisBottom
// 4. Call the y axis in a group tag
svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale)); // Create an axis component with d3.axisLeft
// 9. Append the path, bind the data, and call the line generator
plane = svg.append("g").attr('class','plane')
plane.append("path")
.datum(dataset) // 10. Binds data to the line
.attr("class", "line") // Assign a class for styling
.attr("d", line); // 11. Calls the line generator
d3.select('.line') // move this to a CSS file later
.attr('fill','none')
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
/// ADD HORIZONTAL/VERTICAL LINES
plane.on('click',onclick)
onclick = function (event){
x = xScale.invert(event.layerX - margin.left);
y = yScale.invert(event.layerY - margin.right);
console.log(x,y)
}
quantile_horizontal_lines = new Array()
function drawQuantileLines(quantiles) {
console.log("running drawQuantileLines")
for (let i = 0; i < quantiles.length; i++) {
quantile = quantiles[i]
quantile_horizontal_line_0 = {'x': quantile.x, 'y': metadata.ymin}
quantile_horizontal_line_1 = {'x': quantile.x, 'y': quantile.y}
quantile_horizontal_lines.push(
plane.append("path")
.datum([quantile_horizontal_line_0, quantile_horizontal_line_1])
.attr('d', line)
.attr('class', 'line')
.attr('stroke', 'red'))
}
}
drawQuantileLines(quantiles)
/// IMPLEMENT DRAG BEHAVIOR
drag = d3.drag().on("drag", dragged)
function dragged(event,d) {
d3.select(this).attr("transform", 'translate(' + event.x + ',' + 0 + ')')
}
for (line of quantile_horizontal_lines) {
line.call(drag)
}
data, metadata, and quantiles are JSON objects generated from Python using json.dumps(). I doubt the JSONs are invalid in some way; I am able to draw the lines fine, the problem is with the dragging.
The example you are basing your code off of is d3v6. The canonical examples are generally updated fairly consitently with each version. You are using d3v4.
Versions prior to d3v6 used a different signature for functions passed to .on(). In d3v6, the functions take the form of function(event,d) prior to this these functions took the form:
function(d,i,nodes) {
console.log(d3.event) // event information
}
Where d is the bound datum, i is the index, and nodes is the group of nodes in the selection. So you should be able to use:
function dragged(d) {
d3.select(this).attr("transform", 'translate(' + d3.event.x + ',' + 0 + ')')
}
This change is the most notable change in d3v6.
The multiple line chart example at https://www.d3-graph-gallery.com/graph/line_smallmultiple.html quite clearly provides the examples I need for what I'm trying to do...
except...
I need the y-axis scale for each of the charts to be appropriate for the data associated with the individual keys. As is, the example does d3.max on the entire data set, not the filtered data set controlling the individual lines.
I've tried various ways to apply the filter in the y-axis definition and can't get anything to work.
The closest I've been able to get is to make it use the max value from one of the specific keys for all the charts.
var y = d3.scaleLinear()
// .domain([0, d3.max(data, function(d) { return +d.n; })])
.domain([0, d3.max(data.filter(d => d.name === "Helen"), e => +e.n)])
.range([ height, 0 ]);
svg.append("g")
.call(d3.axisLeft(y).ticks(5));
I think I want it to filter d.name against the CURRENT-CHART key (whatever it might be) rather than a specific one (like "Helen" above), but can't figure out how to do it. Is it some feature of nesting that I haven't found yet? Something amazingly simple that I can't see??
Any suggestions?
Thanks in advance
I have built a demo for you, i hope you are looking for something like this. Please let me know if there is any issue.
// set the dimensions and margins of the graph
var margin = {top: 30, right: 0, bottom: 30, left: 50},
width = 210 - margin.left - margin.right,
height = 210 - margin.top - margin.bottom;
//Read the data
d3.csv("https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/5_OneCatSevNumOrdered.csv", function(data) {
// group the data: I want to draw one line per group
var sumstat = d3.nest() // nest function allows to group the calculation per level of a factor
.key(function(d) { return d.name;})
.entries(data);
// What is the list of groups?
allKeys = sumstat.map(function(d){return d.key})
// Add X axis --> it is a date format
var x = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return d.year; }))
.range([ 0, width ]);
// color palette
var color = d3.scaleOrdinal()
.domain(allKeys)
.range(['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33','#a65628','#f781bf','#999999'])
// Add an svg element for each group. The will be one beside each other and will go on the next row when no more room available
var svg = d3.select("#my_dataviz")
.selectAll("uniqueChart")
.data(sumstat)
.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 + ")")
.each(multiple);
svg
.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(3));
// Add titles
svg
.append("text")
.attr("text-anchor", "start")
.attr("y", -5)
.attr("x", 0)
.text(function(d){ return(d.key)})
.style("fill", function(d){ return color(d.key) })
function multiple(item) {
var svg = d3.select(this);
var y = d3.scaleLinear()
.domain([0, d3.max(item.values, function(d) { return +d.n; })])
.range([height, 0]);
svg.append("g")
.call(d3.axisLeft(y).ticks(5));
var line = d3.line()
.x(function(d) { return x(+d.year); })
.y(function(d) { return y(+d.n); });
// Draw the line
svg
.append("path")
.attr("fill", "none")
.attr("stroke", function(d){ return color(d.key) })
.attr("stroke-width", 1.9)
.attr("d", line(item.values))
}
})
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
I am trying to make a simple web app/dashboard that displays a simple line graph for a specific school that is selected from a dropdown list. It currently works, except when you try to switch back to a school that was already selected. When that happens, the line disappears. I am able to print the data to the console.
More generally, I'm unsure that I'm structuring this the best way. For example, should the update() function really be wrapped in the d3.json() method?
If it matters, my data is being returned by a Django view that sends a JsonResponse with all school data (i.e. the data filtering to the specific school happens in d3, not Django. Again, is this the right way of approaching this?)
// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var parseDate = d3.timeParse("%Y-%y"); // for dates like "2016-17"
// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the line
var projection_line = d3.line()
.x(function(d) { return x(d.year_short_format); })
.y(function(d) { return y(d.projection); });
// append the svg object to the graph div
var svg = d3.select("#graph").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 + ")");
// get the json url
var url = "{% url "schoolview:all_schools_enrollments_by_school_and_year" %}";
d3.json(url, function(error, dataInput) {
if (error) throw error;
// when a new school is selected, trigger update
d3.selectAll("#school_select").on("change", update);
var school_index = document.getElementById('school_select').selectedIndex;
var selected_school = document.getElementById('school_select')[school_index].value;
// filter data to the selected school
data = dataInput.filter(function (d) { return d.school_name == selected_school });
// format the data
data.forEach(function(d) {
d.year_short_format = parseDate(d.year_short_format);
});
// scale the domain of the data
x.domain(d3.extent(data, function(d) { return d.year_short_format; }));
y.domain([0, d3.max(data, function(d) { return d.projection; })])
// append the line path
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", projection_line)
// append the x-axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// append the y-axis
svg.append("g")
.call(d3.axisLeft(y));
function update() {
var school_index = document.getElementById('school_select').selectedIndex;
var selected_school = document.getElementById('school_select')[school_index].value;
// filter data to the selected school
data = dataInput.filter(function (d) { return d.school_name == selected_school });
console.log(data);
// format the data
data.forEach(function(d) {
d.year_short_format = parseDate(d.year_short_format);
});
var svg = d3.select("#graph").transition();
// append the line path
svg.select(".line")
.duration(750)
.attr("d", projection_line(data));
// append the x-axis
svg.select(".x.axis") // change the x axis
.duration(750)
.call(d3.axisBottom(x));
// append the y-axis
svg.select(".y.axis") // change the x axis
.duration(750)
.call(d3.axisBottom(y));
}
});
I figured it out, so for anyone who is having a similar problem:
In the update() function, I was again parsing the date field for each data object. However, this field had already been parsed and overwritten the first time the data was visualized -- the error was from passing an already-parsed field to my parseDate() function. Thus the solution: when parsing the data, simply don't overwrite the field.
// format the data
data.forEach(function(d) {
d.parsed_date = parseDate(d.year_short_format);
});
Alternatively, you could parse the data once initially and then don't worry about it again.
How does one make a basic scatter plot like the one below using Plottable.js?
Is there something wrong with my JSON?
How to reveal the minus scales?
Would you have done anything else differently?
Style doesn't matter, the default Plottable.js one is fine.
window.onload = function() {
var coordinates = [
{
x:"-5",
y:"3"
}, {
x:"2",
y:"-1,5"
}, {
x:"5",
y:"2,5"
}
];
var xScale = new Plottable.Scale.Linear();
var yScale = new Plottable.Scale.Linear();
var colorScale = new Plottable.Scale.Color("10");
var xAxis = new Plottable.Axis.Numeric(xScale, "bottom");
var yAxis = new Plottable.Axis.Numeric(yScale, "left");
var plot = new Plottable.Plot.Scatter(xScale, yScale)
.addDataset(coordinates)
.project("x", "", xScale)
.project("y", "", yScale)
.project("fill", "", colorScale);
var chart = new Plottable.Component.Table([
[yAxis, plot],
[null, xAxis]
]);
chart.renderTo("#my_chart");
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test</title>
<link rel="stylesheet" href="https://rawgit.com/palantir/plottable/develop/plottable.css">
</head>
<body>
<svg width="100%" height="600" id="my_chart"></svg>
<script src="https://rawgit.com/mbostock/d3/master/d3.min.js"></script>
<script src="https://rawgit.com/palantir/plottable/develop/plottable.min.js"></script>
</body>
</html>
Mark has the right idea - the table system doesn't natively support this layout, so you need to take some manual control over how they are laid out. However, using somewhat obscure parts of the Plottable API, there is a cleaner and better-supported way to lay out the chart you want, which doesn't have the problem of the axes being slightly offset.
The first change is we are going to stop using the table layout engine entirely, since it isn't able to do what we want. Instead, we will plop all the components together in a Component.Group. A Group just overlays components in the same space without trying to position them at all.
var chart = new Plottable.Component.Group([yAxis, xAxis, plot]);
Then we are going to use the alignment and offset methods that are defined on the base (abstract) component class. We set the x-alignment of the y axis to "center" and the y-alignment of the x axis to "center" This will put the axes in the center of the chart.
var xAxis = new Plottable.Axis.Numeric(xScale, "bottom").yAlign("center");
var yAxis = new Plottable.Axis.Numeric(yScale, "left").xAlign("center");
We're not quite done at this point, since to really center the axes we need to shift them back by one half of their own width. The width is only calculated when the chart is rendered (strictly speaking, in the computeLayout call, but that is an internal detail), so we need to set an offset after the chart is rendered:
chart.renderTo("#plottable");
xAxis.yOffset(xAxis.height()/2);
yAxis.xOffset(-yAxis.width()/2);
You can see the final result here (it's a fork of Mark's plnkr). Note that now the axes are aligned on the center of the chart, as the center dot is perfectly on 0,0.
Here's a couple examples I just put together. The first is the straight d3 way of doing what you are asking. The second is a hacked up plottable.js. With plottable.js I can't find a way to position the axis outside of their table system, I had to resort to manually moving them. The table system they use is designed to relieve the developer of having to manually position things. This is great and easy, of course, until you want to control where to position things.
Here's the hack, after you render your plottable:
// move the axis...
d3.select(".y-axis")
.attr('transform',"translate(" + width / 2 + "," + 0 + ")");
d3.select(".x-axis")
.attr("transform", "translate(" + 48 + "," + height / 2 + ")");
Note, I didn't remove the left side margin (the 48 above) that plottable puts in. This could be hacked in as well, but at that point, what is plottable providing for you anyway...
It should be noted that the different appearance of each plot is entirely controlled through the CSS.
Complete d3 scatter plot:
// D3 EXAMPLE
var margin = {
top: 20,
right: 20,
bottom: 20,
left: 20
},
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
.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 svg = d3.select("#d3").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 + ")");
x.domain([-100, 100]);
y.domain([-100, 100]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + 0 + "," + height / 2 + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + width / 2 + "," + 0 + ")")
.call(yAxis)
.append("text");
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", function(d) {
return d.r;
})
.attr("cx", function(d) {
return x(d.x);
})
.attr("cy", function(d) {
return y(d.y);
})
.style("fill", function(d) {
return d.c;
});
Plottable.js:
// PLOTTABLE.JS
var xScale = new Plottable.Scale.Linear();
var yScale = new Plottable.Scale.Linear();
var xAxis = new Plottable.Axis.Numeric(xScale, "bottom");
var yAxis = new Plottable.Axis.Numeric(yScale, "left");
var plot = new Plottable.Plot.Scatter(xScale, yScale);
plot.addDataset(data);
function getXDataValue(d) {
return d.x;
}
plot.project("x", getXDataValue, xScale);
function getYDataValue(d) {
return d.y;
}
plot.project("y", getYDataValue, yScale);
function getRDataValue(d){
return d.r;
}
plot.project("r", getRDataValue);
function getFillValue(d){
return d.c;
}
plot.project("fill", getFillValue);
var chart = new Plottable.Component.Table([
[yAxis, plot],
[null, xAxis]
]);
chart.renderTo("#plottable");
d3.select(".y-axis")
.attr('transform',"translate(" + width / 2 + "," + 0 + ")");
d3.select(".x-axis")
.attr("transform", "translate(" + 48 + "," + height / 2 + ")");