I'm working on a real-time visualization of incoming data. I use D3 for the visualization and started based on this example: http://bl.ocks.org/mbostock/3883195
This is the version I'm currently working on:
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 500 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
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 area = d3.svg.area()
.x(function(d) { return x(d.x); })
.y0(height)
.y1(function(d) { return y(d.y); });
var svg = d3.select("div#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 + ")");
var data = [[1,1],[2,3],[3,2],[4,4]];
var dataCallback = function(d) {
d.x = +d[0];
d.y = +d[1];
};
data.forEach(dataCallback);
x.domain(d3.extent(data, function(d) { return d.x; }));
y.domain([0, d3.max(data, function(d) { return d.y; })]);
svg.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area);
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("Number of Messages");
This produces the following graph:
Now, as it should update itself regularly, I wanted to dynamically update the data in the graph using the following code:
var n = svg.selectAll("path").data([5,20])
n.enter();
n.exit().remove();
However, that does not work. I'm new to D3 and still learning the basics. Ideally, the graph visualization shifts to the left and the new data is shown in the graph. But so far I can't even add new data to it. Could someone help me with that? I also searched for examples similar to this, but did not found anything which really helped me so far.
d3 is pretty good at keeping track of what data's been added and what's being removed - it does this with joins. Take a look at http://bl.ocks.org/mbostock/3808218
IF we use the data binding (instead of "datum")
svg.append("path")
.data([data])
.attr("class", "area")
.attr("d", area);
and then on update do something like:
// update the list of coordinates
data.splice(0,1);
data.push([5,20]);
// re-decorate the last, newly added item
dataCallback(data[data.length - 1]);
// You will also need to update your axes
x.domain(d3.extent(data, function(d) { return d.x; }));
y.domain([0, d3.max(data, function(d) { return d.y; })]);
// update the data association with the path and recompute the area
svg.selectAll("path").data([data])
.attr("d", area);
And you should get your area to update.
You can try it out here
With the way your data is organized using the joined data (instead of datum) may not make much difference. Note that here the (single) data element you are associating with the path is the full list of coordinates so to update the area you need to pass in a new full list of coordinates.
If you really want to minimized the amount you need to redraw you could break up the area and draw each line segment. Then essentially you fall back to the bar chart example but with not-flat bars and no spacing.
Related
First time using the d3 library, I am pulling my data from a local object structured below. Attempting to show the data but the 0 index data is not being shown (see in screenshot) despite the data array having content. I have tried many things including reading other stack questions for similar issues (such as D3 bar chart not showing first element in array), but their solutions are not working for me since my d3 is structured differently. I have been stuck on this for over 3 hours now and any help is appreciated.
Data array
roundDataArr = [...roundDataArr, {'round': roundNumber, 'data': finalRoundTime}]
1st object not displayed on graph:
Relevant code:
function displayGraph() {
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 575 - margin.left - margin.right,
height = 350 - margin.top - margin.bottom;
var x = d3.scaleLinear()
.domain([0, d3.max(roundDataArr, function(d) { return d.round; })])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, d3.max(roundDataArr, function(d) { return d.data; })])
.range([height, 0]);
var xAxis = d3.axisBottom()
.scale(x);
var yAxis = d3.axisLeft()
.scale(y);
var area = d3.area()
.x(function(d) { return x(d.round); })
.y0(height)
.y1(function(d) { return y(d.data); });
var valueline = d3.line()
.x(function(d) { return x(d.round); })
.y(function(d) { return y(d.data); });
var svg = d3.select(".tfny-graphWrapper")
.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")
.datum(roundDataArr)
.attr("class", "area")
.attr("d", area);
svg.append("path")
.datum(roundDataArr)
.attr("class", "line")
.attr("d", valueline);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
}
And the wrapper div is appended to the DOM through Javascript with this code:
let graphWrapper = document.createElement("div")
graphWrapper.classList.add("tfny-graphWrapper");
resultPage.appendChild(graphWrapper);
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 have a dataset with 11 different variables (csv file with 12 columns). I want to be able to select a certain column for my scatterplot, but I'm having some difficulties. Please bear with me, as JavaScript is not my strong suit (obviously). Here's what I attempted:
<div class="variables" id="fixedacidity" onclick="drawPlot('fixedacidity');">
<h1>fixed acidity</h1>
</div>
<div class="variables" id="volatileacidity" onclick="drawPlot('volatileacidity');">
<h1>volatile acidity</h1>
</div>
<div class="variables" id="citricacid" onclick="drawPlot('citricacid');">
<h1>citric acid</h1>
</div>
<div class="variables" id="residualsugar" onclick="drawPlot('residualsugar');">
<h1>residual sugar</h1>
</div>
etc ...
I made a simple menu that calls on the drawPlot function, but I'm having trouble trying to get the variable to pass on correctly.
Relevant d3/javascript:
function drawPlot(selectedVar){
$(".visarea").html("");
var wineVar = selectedVar;
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 860 - margin.left - margin.right,
height = 350 - margin.top - margin.bottom;
var x = d3.scale.linear()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0])
.domain([0,10]);
var color = d3.scale.category10();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left").ticks(10);
var chart1 = d3.select(".visarea").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("red.csv", function(error, data) {
if (error) throw error;
data.forEach(function(d) {
d.wineVar = +d.wineVar;
d.quality = +d.quality;
});
x.domain(d3.extent(data, function(d) { return d.wineVar; })).nice();
y.domain([0,10]).nice();
chart1.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text(wineVar);
chart1.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Rated Quality")
chart1.selectAll(".red.dot")
.data(data)
.enter().append("circle")
.attr("class", "red dot")
.attr("r", 3)
.attr("cx", function(d) { return x(d.wineVar); })
.attr("cy", function(d) { return y(d.quality); })
.style("fill", "red");
});
}
Although the variable gets passed on to the function, d.wineVar, as expected, does not return the desired value, and thus the chart does not draw the correct values. Can anyone recommend a workaround for this? It seems so simple, yet I've spent hours failing trying to figure this out.
Sample of red.csv:
fixedacidity,volatileacidity,citricacid,residualsugar,chlorides,freesulfurdioxide,totalsulfurdioxide,density,pH,sulphates,alcohol,quality
7.4,0.7,0,1.9,0.076,11,34,0.9978,3.51,0.56,9.4,5
7.8,0.88,0,2.6,0.098,25,67,0.9968,3.2,0.68,9.8,5
7.8,0.76,0.04,2.3,0.092,15,54,0.997,3.26,0.65,9.8,5
Image of what I'm trying to accomplish. The first dataset, fixedacidity, gets drawn up fine. I'm having difficulties trying to get the menu to correctly show its respective dataset. "Rated Quality" will always be the data for the Y-axis.
You has wrong variable reference, here:
data.forEach(function(d) {
d.wineVar = +d.wineVar; // <---------Here
d.quality = +d.quality;
});
change by:
data.forEach(function(d) {
d.wineVar = +d[wineVar]; // <----------Here
d.quality = +d.quality;
});
There is the obvious issue pointed out by klaujesi about data extraction. But there are more issues with your code.
I would say you need to adapt your approach to the way d3.js works. Currently you will add a new svg on each call to the function, caused by this line in your code: d3.select(".visarea").append("svg")
I usually have some init code wrapped in one function, which creates the svg and sets ups everything static. Then there is an update function which will handle input changes to show different data, use different scales etc.
The nice thing about d3.js is that you can control very easily what's to happen with newly introduced data via .enter() and removed data via .exit().
I'm very new to d3 and in order to learn I'm trying to manipulate the d3.js line example, the code is below. I'm trying to modify this to use model data that I already have on hand. This data is passed down as a json object. The problem is that I don't know how to manipulate the data to fit what d3 expects. Most of the d3 examples use key-value arrays. I want to use a key array + a value array. For example my data is structured per the example below:
// my data. A name property, with array values and a value property with array values.
// data is the json object returned from the server
var tl = new Object;
tl.date = data[0].fields.date;
tl.close = data[0].fields.close;
console.log(tl);
Here is the structure visually (yes it time format for now):
Now this is different from the data.tsv call which results in key-value pairs in the code below.
The goal is to use my data as is, without having to iterate over my array to preprocess it.
Questions:
1) Are there any built in's to d3 to deal with this situation? For example, if key-values are absolutely necessary in python we could use the zip function to quickly generate a key-value list.
2) Can I use my data as is, or does it have to be turned into key-value pairs?
Below is the line example code.
// javascript/d3 (LINE EXAMPLE)
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 640 - margin.left - margin.right,
height = 480 - margin.top - margin.bottom;
var parseDate = d3.time.format("%d-%b-%y").parse;
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 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.tsv("/data.tsv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain(d3.extent(data, function(d) { return d.close; }));
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);
});
Check out d3's array functions, zip is among them.
Here's a commented version of the original gist working with your data: http://bl.ocks.org/patrickberkeley/9162034
The core of it is this:
// 1) Zip the close value with their corresponding date/time
// Results in an array of arrays:
//
// [[582.13, "02:30:00"], [583.98, "02:45:00"], ...]
//
data = d3.zip(data.close, data.date).map(function(d) {
// 2) Format each close and date/time value so d3 understands what each represents.
close = +d[0];
// If your data source can't be changed at all, I'd rename `date` to `time` here.
date = parseTime(d[1]);
// 3) Return an object for each close and date/time pair.
return {close: close, date: date};
});
You can pass one of your arrays to .data() and use the index to get the respective element from the other:
var line = d3.svg.line()
.x(function(d) { return x(d); })
.y(function(d, i) { return y(tl.close[i]); });
svg.selectAll("path")
.data([tl.date])
.enter().append("path")
.attr("d", line);
You just have to remember to set the domains of the scales you're using with the correct arrays.
I'm just getting into using d3, and relatively novice in js still. I'm trying to set up a page of log file visualizations for monitoring some servers. Right now I'm focusing on getting a line chart of CPU utilization, where I can focus on specific time periods (So an x-zoom only). I am able to do a static charts easily, but when it comes to the zooming the examples are going over my head and I can't seem to make them work.
This is the static example I followed to get things up and running, and this is the zoom example I've been trying to follow.
My input csv is from a rolling set of log files (which will not be labled on the first row), each row looks like this:
datetime,server,cpu,memory,disk,network_in,network_out
So far what I've been able to get on the zoom looks like this:
var parseDate = d3.time.format("%Y-%m-%d %H:%M:%S").parse;
// define dimensions of graph
var margin = {top: 20, right: 80, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
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")
.tickSize(-height, 0)
.tickPadding(6);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickSize(-width)
.tickPadding(6);
// Define how we will access the information needed from each row
var line = d3.svg.line()
.interpolate("step-after")
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[2]); });
// Insert an svg element into the document for each chart
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 + ")");
// Declare zoom handler
var zoom = d3.behavior.zoom().on("zoom", draw);
// Open the log and extract the information
d3.text("log.csv", function(text) {
var data = d3.csv.parseRows(text).map(function(row) {
return row.map(function(value, index) {
if (index == 0) {
return parseDate(value);
}
else if (index > 1) {
return +value;
}
else {
return value;
}
});
});
// Set the global minimum and maximums
x.domain(d3.extent(data, function(d) { return d[0]; }));
y.domain(d3.extent(data, function(d) { return d[2]; }));
zoom.x(x);
// Finally, we have the data parsed, and the parameters of the charts set, so now we
// fill in the charts with the lines and the labels
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")
.attr("text-anchor", "end")
.text("Percent (%)");
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
svg.append("text")
.attr("x", margin.left)
.attr("y", 0 - (margin.top / 2))
.attr("text-anchor", "middle")
.text('all');
svg.append("rect")
.attr("class", "pane")
.attr("width", width)
.attr("height", height)
.call(zoom);
svg.select("path.line").data([data]);
draw();
});
function draw() {
svg.select("g.x.axis").call(xAxis);
svg.select("g.y.axis").call(yAxis);
svg.select("path.line").attr("d", line);
}
What this gives me is a very sluggish chart that can be zoomed and panned, but it does not clip off the line at the ends of the chart. I've tried adding in the clipping elements described in the example, but that ends up fully erasing my line every time.
Thanks for any help or direction