I'm trying d3 for the very first time and I'm trying to understand how to create a dynamic d3 line chart that needs to get updated every time I receive a websocket message from the server with new a new data point.
I have the below code within my angular directive link function:
var data = [];
var margin = { top: 30, right: 20, bottom: 30, left: 50 },
width = 600 - margin.left - margin.right,
height = 270 - 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 valueline = d3.svg.line()
.x(function (d) { return x(d['label']); })
.y(function (d) { return y(d['value']); });
var svg = d3.select(elem[0])
.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", valueline(data));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
Statistics.listen('topic', function(message) {
data.push({
label: message.webEvent.creationTimeStamp,
value: +message.webEvent.value
});
x.domain(d3.extent(data, function (d) { return d.label; }));
y.domain([
d3.min(data, function(d) { return d.value; }),
d3.max(data, function(d) { return d.value; })
]);
var svg = d3.select(elem[0]).transition();
svg.select(".line") // change the line
.duration(0)
.attr("d", valueline(data));
svg.select(".x.axis") // change the x axis
.duration(0)
.call(xAxis);
svg.select(".y.axis") // change the y axis
.duration(0)
.call(yAxis);
}
I'm not sure what the purpose of the "duration" is. The messages do not come through the socket at regular intervals. So I'm not sure if I should set a static duration value which I assume refreshes the graph based on the number given. I want the graph to update as and when I get an update. The graph, over time, should look like a running sine wave.
Right now, the first data that come gets rendered. But the graph stays static after that even though I can see incoming messages on the websocket and the data array is growing.
What am I missing? Any help is greatly appreciated.
Related
Im creating a line chart graph using d3 js. I need a solution to change the y scale values when I resize the window instead of scroll bar.
I have added the code below which adds scroll bar when I resize the screen size. I want design dynamic y scale values when we resize for different screen sizes.
`
<!DOCTYPE html>
<meta charset="utf-8">
<style> /* set the CSS */
body { font: 12px Arial;}
path {
stroke: steelblue;
stroke-width: 2;
fill: none;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
stroke-width: 1;
shape-rendering: crispEdges;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
// Set the dimensions of the canvas / graph
var margin = {top: 30, right: 20, bottom: 30, left: 50},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.time.format("%d-%b-%y").parse;
// Set the ranges
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);
// Define the line
var valueline = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.close); });
// Adds the svg canvas
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 + ")");
// Get the data
d3.csv("data.csv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.close; })]);
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("d", valueline(data));
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
</script>
</body>
`
A method I like to use is to wrap the code in a function (lets call it main()), and re-run when the screen size changes.
At the beginning of this new main() function, remove the old (and now redundantly sized) svg.
d3.select("#<id of svg>").remove();
Then, create the new y scale using
var new_width = document.getElementById("<Div ID>").clientWidth;
var new_height = document.getElementById("<Div ID>").clientHeight;
and apply these to the new svg as you create it. D3 should allow you to run the .remove() line before the initial svg has been created. Make sure to then add an id when you create the svg (using attr("id", "<id of svg>")).
After that, you can call the main() function on resizing with
d3.select(window).on( "resize", main() );
The way in which you want to actually size your Div will now rely on your CSS, so you can use something like {height:50vh} or whatever you like.
Hope this helps.
P.S. By the way, why are you using D3 version 3? We're up to v5 :)
I am working on creating a horizontal bar chart using D3 in a ReactJS application. The issue I'm having is that the bars are too long and get cut off. How can I scale the bars down proportionally?
Appreciate any advice.
If you want to fit your chart then you need to change width/height of container where your chart is rendered (see <svg> in code below). Usually bars maximum height or width must not exceed correspondent container's dimension size, so probably you have an error somewhere in code. It would be helpful if you could share your code.
Here I provide an example of horizontal bar chart that scales correctly (original: https://bl.ocks.org/caravinden/eb0e5a2b38c8815919290fa838c6b63b):
var data = [{"salesperson":"Bob","sales":33},{"salesperson":"Robin","sales":12},{"salesperson":"Anne","sales":41},{"salesperson":"Mark","sales":16},{"salesperson":"Joe","sales":159},{"salesperson":"Eve","sales":38},{"salesperson":"Karen","sales":21},{"salesperson":"Kirsty","sales":25},{"salesperson":"Chris","sales":30},{"salesperson":"Lisa","sales":47},{"salesperson":"Tom","sales":5},{"salesperson":"Stacy","sales":20},{"salesperson":"Charles","sales":13},{"salesperson":"Mary","sales":29}];
// set the dimensions and margins of the graph
var
svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, left: 60},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
// set the ranges
var y = d3.scaleBand()
.range([height, 0])
.padding(0.1);
var x = d3.scaleLinear()
.range([0, width]);
// append a 'group' element to 'svg'
// moves the 'group' element to the top left margin
svg = svg
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// format the data
data.forEach(function(d) {
d.sales = +d.sales;
});
// Scale the range of the data in the domains
x.domain([0, d3.max(data, function(d){ return d.sales; })])
y.domain(data.map(function(d) { return d.salesperson; }));
//y.domain([0, d3.max(data, function(d) { return d.sales; })]);
// append the rectangles for the bar chart
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
//.attr("x", function(d) { return x(d.sales); })
.attr("width", function(d) {return x(d.sales); } )
.attr("y", function(d) { return y(d.salesperson); })
.attr("height", y.bandwidth());
// add the x Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// add the y Axis
svg.append("g")
.call(d3.axisLeft(y));
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
.axis--x path {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.5.0/d3.min.js"></script>
<svg width="300" height="500"></svg>
I am using d3 in angular to create a bar chart of feelings from very bad (1) to very good (5) with the feelings as labels on the yAxis. I am running into an error: Argument of type '(d: any, i: any) => any' is not assignable to parameter of type 'string'. I've been able to use "any" to get around similar type errors, but it isn't working in this part: .tickFormat(function(d:any,i:any): any { return tickLabels[i] }) ;
interface Datum {
created_at: string,
decription: string,
feeling: string,
feeling_attachments: any,
feeling_in_number: number,
id: number,
tag_user_ids: string,
tags: any,
visibility: string
}
buildChart2(feels: Array<Datum>){
var feelsData = feels.reverse()
var margin = {top: 20, right: 30, bottom: 30, left: 40},
width = 800 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
var ticks = [0,1,2,3,4,5];
var tickLabels = ['','very bad','bad','neutral','good','very good']
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], 0);
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")
.tickValues(ticks)
.tickFormat(function(d:any,i:any): any { return tickLabels[i] }) ;
var chart = d3.select(".feelsChart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
y.domain([0, d3.max(feelsData, function(d: any): any { return d.feeling_in_number; })]);
var barWidth = width / feelsData.length;
var bar = chart.selectAll("g")
.data(feelsData)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(" + i * barWidth + ",0)"; });
bar.append("rect")
.attr("y", function(d) { return y(d.feeling_in_number); })
.attr("height", function(d) { return height - y(d.feeling_in_number); })
.attr("width", barWidth - 1);
bar.append("text")
.attr("x", barWidth / 2)
.attr("y", function(d) { return y(d.feeling_in_number) + 3; })
.attr("dy", ".75em");
// .text(function(d) { return d.feeling_in_number; });
chart.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Frequency");
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
}
I've been trying to work off Mike Bostock's Let's Make A Bar Chart tutorial and a few other stack overflow questions about d3 in angular.
D3.JS change text in axis ticks to custom strings
It looks like you are trying to follow some guides that uses javascript with the older style of declaring functions. Typescript uses the newer ES6 syntax with arrow functions. So the function should be written like this in typescript:
.tickFormat((d:any, i:any): any => return tickLabels[i]);
You can read more about functions in typescript here
I have an error in d3 bar chart, when load on the web page
the error :
Error: Invalid value for <rect> attribute y="NaN" , Error: Invalid value for <rect> attribute height="NaN"
I tried to solve it by edit this code
nothing worked
var countriesData = data.countries;
var datac=[];
for (var key in countriesData) {
datac.push({key: key, value: countriesData[key]});
};
console.log(datac);
var width = 250;
var height = 250;
//console.log(data4);
//x and y Scales
var xScale = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var yScale = d3.scale.linear()
.range([height, 0]);
xScale.domain(datac.map(function(d) { return d.x; }));
yScale.domain([0, d3.max(datac, function(d) { return d.y; })]);
//x and y Axes
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
//.ticks();
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
//.ticks(function(d) { return d.x; });
//create svg container
var svg = d3.select("#barchart").select("svg").remove();
svg = d3.select("#barchart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
//.transition().duration(2000)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//create bars
svg.selectAll(".bar")
.data(datac)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return xScale(d.x); })
.attr("width", xScale.rangeBand())
.attr("y", function(d) { return yScale(d.y); })
.attr("height", function(d) { return height - yScale(d.y); });
//drawing the x axis on svg
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
//drawing the y axis on svg
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("Reviews Number");
Please help!
Hard to say, but I'm guessing its the definition of the xScale:
xScale.domain(datac.map(function(d) { return d.x; }));
probably should be something like:
xScale.domain(d3.extent(datac, fucntion (d) {return d.x}))
SO community,
I'm making a D3 histogram as an Angular directive and I want it to be able to change/update accordingly as the data it reads in changes. In other words, I am using Angular to watch the changes in data and (hope to) redraw the histogram every time the data is changed.
This might mostly be a question about D3's updating and binding of data because the $watchCollection seems to work fine. Even though I have went through this tutorial on adding elements to a d3 chart, I still cannot apply it on my histogram. I think the way the elements in my histogram are nested is really confusing me...
Context: Ideally this histogram will read from an array to which data returned from several Ajax calls will get stored in. So every time a new set of data arrive, the histogram will grow itself another bar. That's why I would love to know how to update the chart as well as the x-axis properly.
Thank you! :)
The JS fiddle is here: http://jsfiddle.net/santina/wrtenjny/1/
Code for just the d3 part is here, largely taken from mbostock's sortable bar chart.
// Aesthetic settings
var margin = {top: 20, right: 50, bottom: 20, left: 50},
width = document.getElementById('performance').clientWidth - margin.left - margin.right ||
940 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
barColor = "steelblue",
axisColor = "whitesmoke",
axisLabelColor = "grey",
yText = "# QUERIES",
xText = "BEACON IDs";
// Inputs to the d3 graph
var data = scope[attrs.data];
// A formatter for counts.
var formatCount = d3.format(",.0f");
// Set the scale, separate the first bar by a bar width from y-axis
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1, 1);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(formatCount);
// Initialize histogram
var svg = d3.select(".histogram-chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
function drawAxis(){
data.forEach(function(d) {
d.nqueries = +d.nqueries;
});
x.domain(data.map(function(d) { return d.name; }));
y.domain([0, d3.max(data, function(d) { return d.nqueries; })]);
// Draw x-axis
svg.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("y", 6)
.attr("dy", "-0.71em")
.attr("x", width )
.style("text-anchor", "end")
.style("fill", axisLabelColor)
.text(xText);
// Draw y-axis
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")
.style("fill", axisLabelColor)
.text(yText);
// Change axis color
d3.selectAll("path").attr("fill", axisColor);
}
function updateAxis(){
console.log(data);
data.forEach(function(d) {
d.nqueries = +d.nqueries;
});
x.domain(data.map(function(d) { return d.name; }));
y.domain([0, d3.max(data, function(d) { return d.nqueries; })]);
svg.selectAll("g.y_axis").call(yAxis);
svg.selectAll("g.x_axis").call(xAxis);
}
function drawHistogram(){
drawAxis();
var bar = svg.selectAll(".bar")
.data(data)
.enter().append("g")
.attr("class", "barInfo");
bar.append("rect")
.attr("class", "bar")
.attr("x", function(d){ return x(d.name) })
.attr("width", x.rangeBand())
.attr("y", function(d){ return y(d.nqueries) })
.attr("height", function(d) { return height - y(d.nqueries); })
.attr("fill", barColor);
bar.append("text")
.attr("y", function(d){ return y(d.nqueries) })
.attr("x", function(d){ return x(d.name) })
.attr("dy", "-1px")
.attr("dx", x.rangeBand()/2 )
.attr("text-anchor", "middle")
.attr("class", "numberLabel")
.text(function(d) { return formatCount(d.nqueries); });
}
// Doesn't work :(
function updateHistogram(){
console.log("updating");
// Redefine scale and update axis
updateAxis();
// Select
var bar = svg.selectAll(".barInfo").data(data);
// Update - rect
var rects = bar.selectAll("rect")
.attr("class", "bar")
.attr("x", function(d){ return x(d.name) })
.attr("width", x.rangeBand());
// Update
var texts = bar.selectAll("text")
.attr("x", function(d){ return x(d.name) })
.attr("dx", x.rangeBand()/2 );
// Enter
bar.enter().append("g")
.attr("class", "bar").selectAll("rect").append("rect")
.attr("class", "bar")
.attr("x", function(d){ return x(d.name) })
.attr("width", x.rangeBand())
.attr("y", function(d){ return y(d.nqueries) })
.attr("height", function(d) { return height - y(d.nqueries); })
.attr("fill", barColor);
bar.enter().append("g")
.attr("class", "bar").selectAll("text").append("text")
.attr("y", function(d){ return y(d.nqueries) })
.attr("x", function(d){ return x(d.name) })
.attr("dy", "-1px")
.attr("dx", x.rangeBand()/2 )
.attr("text-anchor", "middle")
.attr("class", "numberLabel")
.text(function(d) { return formatCount(d.nqueries); });
}
drawHistogram();
First, you got the wrong class selector when you update your axis:
svg.selectAll("g.y-axis").call(yAxis); //<-- dash not underscore
svg.selectAll("g.x-axis").call(xAxis);
Second, you were close on your update, but we can clean it up a bit:
// select on what you originally binded data to
var bar = svg.selectAll(".barInfo").data(data);
// for data entering
var bEnter = bar.enter().append("g")
.attr("class", "barInfo");
// append a rect
bEnter.append("rect")
.attr("class", "bar");
// and the text elements
bEnter.append("text")
.attr("class","numberLabel");
// now we can update everybody together
bar.select("rect")
.attr("x", function(d){ return x(d.name) })
.attr("width", x.rangeBand())
.attr("y", function(d){ return y(d.nqueries) })
.attr("height", function(d) { return height - y(d.nqueries); })
.attr("fill", barColor);
bar.select("text")
.attr("y", function(d){ return y(d.nqueries) })
.attr("x", function(d){ return x(d.name) })
.attr("dy", "-1px")
.attr("dx", x.rangeBand()/2 )
.attr("text-anchor", "middle")
.attr("class", "numberLabel")
.text(function(d) { return formatCount(d.nqueries); });
Udpated example here.
EDITS
Opps, I'm not selecting correctly on my updates.
bar.selectAll("rect")
Should be:
bar.select("rect")
This fixes both the updates and the sorting...
Updated fiddle.
Also notice that I collapsed your code further. With your angular watch you really don't need a seperate draw and update function, one can do both.