How do you plot a line using scaleOrdinal in D3.js? - javascript

I'm trying to plot some numbers on the y axis that correspond to strings on the x axis using d3.js version 7. I'm not sure what x scale to use. I'm trying scaleOrdinal but the plot is not correct. I'd like the strings evenly spaced on the x axis with a little padding on the ends.
const data = [{str: 'a', num: 1}, {str: 'b', num: 3}, {str: 'c', num: 2}, {str: 'd', num: 0}];
const 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 + ")");
const xScale = d3.scaleOrdinal()
.domain(d3.extent(data, function (d) {
return d.str;
}))
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, 5])
.range([height, 0]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale))
.style('color', '#80ffffff')
.style('font-size', 20)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em");
svg.append("g")
.call(d3.axisLeft(yScale))
.style('color', '#80ffffff');
const valueline = d3.line()
.x(function (d) {
return xScale(d.str);
})
.y(function (d) {
return yScale(d.num);
});
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline)
.style("fill", "none")
.style("stroke", '#ff0000ff')
.style("stroke-width", 1);

The ordinal scale is not the correct one here, mainly because ordinal scales need to have discrete (i.e. qualitative, categorical) domain and range.
What you want is a point scale. Also, pay attention to the fact that d3.extent only returns 2 values (in your case "a" and "d"), so what you want is a regular Array.prototype.map. That said, your scale is:
const xScale = d3.scalePoint()
.domain(data.map(function (d) {
return d.str;
}))
.range([0, width])
.padding(yourPaddingHere)

Related

Add labels to d3 line graph

I am using D3.js to create a simple line graph.
<div>
<h6>Price Over Time</h6>
<div id="priceOverTimeChart"></div>
</div>
// Set the dimensions of the canvas / graph
var margin = {top: 30, right: 20, bottom: 30, left: 50},
width = 800 - 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(10);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(10);
// 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("#priceOverTimeChart")
.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);
});
The data for the line graph uses the following data format:
26-Apr-12,0.048
25-Apr-12,0.048
24-Apr-12,0.048
I would like to add an optional string to each record so it looks like:
26-Apr-12,0.048, "product 1 launch",
25-Apr-12,0.048, "product 2",
24-Apr-12,0.048, "product 3"
26-Apr-12,0.048, null
25-Apr-12,0.048, null
24-Apr-12,0.048, null
The graph would then look something like this with the labels on it:
Graph with optional labels
How can I accomplish this? Thanks in advance!
Appending texts to the corresponding x, y position will do the trick.
Please refer this working JS Fiddle
svg.append("g").selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", function(d) { return x(d.date) - paddingForText })
.attr("y", function(d) { return y(d.close) + paddingForText })
.attr("fill", "red")
.text(function(d) { return d.notes });

D3 v4 Line Chart: Error: <path> attribute d: Expected number, "MNaN,NaNLNaN,NaNL…"

I am attempting to create a line graph using d3, but using javascript object instead of a tsv like the bl.ocks example. However, no matter how I try to parse the time data (written as years e.g. "2003"), I receive the error:
Error: <path> attribute d: Expected number, "MNaN,NaNLNaN,NaNL…"
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, left: 50},
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 + ")");
var parseTime = d3.timeParse("%Y");
var x = d3.scaleTime()
.rangeRound([0, width]);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var line = d3.line()
.x(function(d) { return x( parseTime(d.year)); })
.y(function(d) { return y(d.locations); });
x.domain(d3.extent(myData, function(d) { return parseTime(d.year); }));
y.domain(d3.extent(myData, function(d) { return d.locations; }));
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
.select(".domain")
.remove();
g.append("g")
.call(d3.axisLeft(y))
.append("text")
.attr("fill", "#000")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("text-anchor", "end")
.text("Price ($)");
g.append("path")
.datum(myData)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", line);
The data comes from a spreadsheet that is then converted into a javascript object.
myData=[{"year":"2000","locations":1286.0,"}]
I've tried changing %Y to %y and I've tried formatting the year column in the spreadsheet so it results in 2000.0 and 01/01/2000 instead of a string.
myData=[{"year":2000.0,"locations":1286.0,"}]
I've also used new Date() instead of parseTime, but I don't know what the error message is trying to show

D3: Add data value labels to multi line graph

I am using this multiline graph but so far have failed to generate data value labels on every tick (for every day).
<script>
var margin = {top: 30, right: 40, bottom: 30, left: 50},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
var parseDate = d3.time.format("%d-%m-%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").ticks(7);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(7);
var valueline = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.sev3); });
var valueline2 = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.sev4); });
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.tsv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.sev3 = +d.sev3;
d.sev4 = +d.sev4;
});
// 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 Math.max(d.sev3, d.sev4); })]);
svg.append("path") // Add the valueline path.
.attr("class", "line")
.attr("d", valueline(data));
svg.append("path") // Add the valueline2 path.
.attr("class", "line")
.style("stroke", "red")
.attr("d", valueline2(data));
svg.append("g") // Add the X Axis
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g") // Add the Y Axis
.attr("class", "y axis")
.call(yAxis);
svg.append("text")
.attr("transform", "translate(" + (width+3) + "," + y(data[0].sev4) + ")")
.attr("dy", ".35em")
.attr("text-anchor", "start")
.style("fill", "red")
.text("Sev4");
svg.append("text")
.attr("transform", "translate(" + (width+3) + "," + y(data[0].sev3) + ")")
.attr("dy", ".35em")
.attr("text-anchor", "start")
.style("fill", "steelblue")
.text("Sev3");
});
</script>
Data.tsv
date,sev3,sev4
20-02-15,0,0
19-02-15,0,0
18-02-15,0,0
17-02-15,481,200
16-02-15,691,200
15-02-15,296,200
14-02-15,307,200
The code above gives this:
And THIS is what i am trying to accomplish
I understand that i must use .append("text") and position the text at about the same x,y coords as the data point and pull the value from the data to feed into the "text" but i am having difficulties in integrating that concept.
I suspect that the selection would occur with valueline.append ? I have looked at a HEAP of examples, i dont thing a linegraph with data value labels exists, if it does please point me to it :)
Any thoughts ?
Your text will not be visible, as it is located outside the boundaries of your svg: you added a group that is translated of margin.left, and the put your x at width+3, which means located at width+3+margin.left of the left border of your svg.
Try replacing your append text with something like:
svg.append("text")
.attr("transform", "translate(" + (width/2) + "," + y(data[0].sev4) + ")")
.attr("dy", ".35em")
.attr("text-anchor", "start")
.style("fill", "red")
.text("Sev4");
svg.append("text")
.attr("transform", "translate(" + (width/2) + "," + y(data[0].sev3) + ")")
.attr("dy", ".35em")
.attr("text-anchor", "start")
.style("fill", "steelblue")
.text("Sev3");
I did not test it, so I cannot guarantee, but your code seems fine, that's the only thing I see.
Result of this add:
EDIT
After your clarifications, here is a plunk: http://plnkr.co/edit/lDlseqUQQXgoFwTK5Aop?p=preview
The part you will be interested in is:
svg.append('g')
.classed('labels-group', true)
.selectAll('text')
.data(data)
.enter()
.append('text')
.classed('label', true)
.attr({
'x': function(d, i) {
return x(d.date);
},
'y': function(d, i) {
return y(d.sev3);
}
})
.text(function(d, i) {
return d.sev3;
});
This will draw your labels. Is it the result you try to achieve?

X,Y domain data-binding in multiple grouped bar charts in d3.js

Fiddle Example
I have been following these two examples (1)(2) to create small multiple grouped bar charts on the same page. Here's a JSON data example:
var data = [
{"name":"AA","sales_price":20,"retail_price":25},
{"name":"BB","sales_price":30,"retail_price":45},
{"name":"CC","sales_price":10,"retail_price":55},
{"name":"DD","sales_price":10,"retail_price":25},
{"name":"EE","sales_price":13,"retail_price":20},
{"name":"GG","sales_price":13,"retail_price":15},
];
I've managed to get the bar values to show up correctly in each chart, but the X domain and Y domain values aren't right. I couldn't figure out how to bind each data row's sales_price and retail_price to the axises instead of the entire JSON data. I guess there's a problem with this block of code:
data.forEach(function(d) {
d.compare = field_name.map(function(name) {
return {name: name, value: +d[name]};
});
});
x0.domain(data.map(function(d) { console.log(d); return d.name; }));
x1.domain(field_name).rangeRoundBands([0, x0.rangeBand()]);
y.domain([0, d3.max(data, function(d) {
return d3.max(d.compare, function(d) {
return d.value; });
})]);
How can I make the domains return each row's values for each grouped bar charts?
Full Code:
function multi_bars(el){
var margin = {top: 45, right:20, bottom: 20, left: 50},
width = 350 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
var x0 = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var x1 = d3.scale.ordinal();
var color = d3.scale.ordinal()
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x0)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var field_name = ['retail_price','sales_price'];
data.forEach(function(d) {
d.compare = field_name.map(function(name) {
return {name: name, value: +d[name]};
});
});
x0.domain(data.map(function(d) { console.log(d); return d.name; }));
x1.domain(field_name).rangeRoundBands([0, x0.rangeBand()]);
y.domain([0, d3.max(data, function(d) {
return d3.max(d.compare, function(d) {
return d.value; });
})]);
var svg = d3.select(el).selectAll("svg")
.data(data)
.enter().append("svg: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("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");
// Accessing nested data: https://groups.google.com/forum/#!topic/d3-js/kummm9mS4EA
// data(function(d) {return d.values;})
// this will dereference the values for nested data for each group
svg.selectAll(".bar")
.data(function(d) {return d.compare;})
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x1(d.name); })
.attr("width", x1.rangeBand())
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.attr("fill", color)
var legend = svg.selectAll(".legend")
.data(field_name.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
function type(d) {
d.percent = +d.percent;
return d;
}
}
multi_bars(".container");
Your setting up of x0, x1 and y is fine.
Later when you manipulate the DOM is where your references to the data don't work.
I have done two things: First I change your first block, so you create just one svg instead of
var svg = d3.select(el).selectAll("svg")
.data(data)
.enter().append("svg:svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
Later I just followed the example of http://bl.ocks.org/mbostock/3887051 and made the changes accordingly.
The result is here:
http://jsfiddle.net/ee2todev/g61f93gx/
If you want to have separate charts for each group as in your original fiddle you just have to translate each bar with the x0 scale. Just two adjustments have to be made:
a) you have to add the group name to the d.compare so it is accessible from the corresponding data in the bar selection
data.forEach(function(d) {
d.compare = field_name.map(function(name) {
return {group: d.name, name: name, value: +d[name]};
});
});
b) In the bar selection you have to translate each group accordingly:
svg.selectAll(".bar")
.data(function(d) {return d.compare;})
.enter()
.append("rect")
.attr("class", "bar")
.attr("transform", function(d) { return "translate(" + x0(d.group) + ",0)"; })
.attr("x", function(d) { console.log("x: "+d.value); return x1(d.name); })
.attr("width", x1.rangeBand())
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.attr("fill", color);
The complete fiddle is here: http://jsfiddle.net/ee2todev/en8sr5m4/
Two more notes:
1) I just slightly changed your code. I highly recommend using meaningful and intuitive variable/object names. This is to me the most effective way to minimize errors. This might have been the reason you got confused. So I would rename the d.compare properties, e.g. {groupName: d.name, priceType: name, value: +d[name]}. As of now, you switched the meaning of name since name suddenly refers to the price type not the grouping name as in the original data!
2) This is a nice example of selection of selections. See also http://bost.ocks.org/mike/nest/
The first selectAll selection (the svg variable) contains an Array[6] with the objects. The second selection:
svg.selectAll(".bar").data(function(d) {return d.compare;})
iterates for each element of the svg data over an Array[2] containing an object with the price type and the value. There I added the group name.

A better way for a stacked area chart toggle between a percent scale and absolute scale?

I am a newcomer to javascript and D3.
I am trying to create a stacked area chart that toggles between showing the data on an absolute scale and a percent scale. I realize that this involves playing around with
d3.layout.stack().offset("zero") and
d3.layout.stack().offset("expand").
I've succeeded in getting the chart to do what I want:
http://jsfiddle.net/dSJ4E/
...but I'm not proud of my approach and am sure there is a better way to do it. Any ideas? Are there simple examples you might be aware of?
I am not happy with my code because upon setting up my if/else statement, I simply redeclare everything I've written prior to then, changing only the offset variable. That seems like a clunky approach. Also, I don't think this will allow me to set up transitions.
data = [{"type": "Group1",
"values": [
{"x":0, "y": 2.5},
{"x":1, "y": 2.4},
{"x":2, "y": 0.3}]},
{"type": "Group2",
"values": [
{"x":0, "y": 1.5},
{"x":1, "y": 1.3},
{"x":2, "y": 1.1}]}
];
var stackZero = d3.layout.stack()
.values(function(d){return d.values;})
.offset("zero");
stackZero(data);
var xScale = d3.scale.linear()
.domain([0,2])
.range([0, width]);
var yScale = d3.scale.linear()
.range([height,0])
.domain([0, d3.max(data, function(d){return d3.max(d.values, function(d){return d.y0 + d.y;});})]);
var area = d3.svg.area()
.x(function(d){return xScale(d.x);})
.y0(function(d){return yScale(d.y0);})
.y1(function(d){return yScale(d.y0 + d.y);});
var svg = d3.selectAll("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 +")");
svg.selectAll(".layers")
.data(data)
.enter()
.append("path")
.attr("class", "layer")
.attr("d", function(d){return area(d.values);})
.style("fill", function (d,i){return colors(i)});
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0,0)")
.call(yAxis);
d3.select("p") //now we start to interact with the chart
.on("click", function() {
console.log("entering variable is " + stackType);
svg.selectAll("path").data([]).exit().remove();
svg.selectAll(".y.axis").data([]).exit().remove();
if(stackType){ //enter true, or expanded data
var stackExpand = d3.layout.stack()
.values(function(d){return d.values;})
.offset("expand");
stackExpand(data);
console.log(data);
var yScale = d3.scale.linear()
.range([height,0])
.domain([0, d3.max(data, function(d){return d3.max(d.values, function(d){return d.y0 + d.y;});})]);
var area = d3.svg.area()
.x(function(d){return xScale(d.x);})
.y0(function(d){return yScale(d.y0);})
.y1(function(d){return yScale(d.y0 + d.y);});
svg.selectAll(".layers")
//.data(stackZero(data))
.data(stackExpand(data))
.enter()
.append("path")
.attr("class", "layer")
.attr("d", function(d){return area(d.values);})
.style("fill", function (d,i){return colors(i)});
formatter = d3.format(".0%");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.tickFormat(formatter);
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0,0)")
.call(yAxis);
stackType = false;
console.log("exiting variable is " + stackType);
} else { //enter false, or zero data
data = [{"type": "Group1",
"values": [
{"x":0, "y": 2.5},
{"x":1, "y": 2.4},
{"x":2, "y": 0.3}]},
{"type": "Group2",
"values": [
{"x":0, "y": 1.5},
{"x":1, "y": 1.3},
{"x":2, "y": 1.1}]}
];
var stackZero = d3.layout.stack()
.values(function(d){return d.values;})
.offset("zero");
stackZero(data);
console.log(data);
var yScale = d3.scale.linear()
.range([height,0])
.domain([0, d3.max(data, function(d){return d3.max(d.values, function(d){return d.y0 + d.y;});})]);
var area = d3.svg.area()
.x(function(d){return xScale(d.x);})
.y0(function(d){return yScale(d.y0);})
.y1(function(d){return yScale(d.y0 + d.y);});
svg.selectAll(".layers")
.data(stackZero(data))
//.data(stackExpand(data))
.enter()
.append("path")
.attr("class", "layer")
.attr("d", function(d){return area(d.values);})
.style("fill", function (d,i){return colors(i)});
stackType = true;
console.log("exiting variable is" + stackType);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0,0)")
.call(yAxis);
};
});
</script>
So there are a lot of things you can do to avoid repeating yourself in the code. First of all a lot of the D3 functions you are using can be reused - they are general functions. This together with the concept of javascript closures means you only have to declare them once and initialize the parts that won't change.
The stack layout does overwrite the y and y0 values, so to get the toggle to work I renamed your initial data to raw_x and raw_y and adjusted the accessors appropriately.
Then the work of figuring out the yScale domain and actually updating the paths I wrapped into a function:
function drawChart() {
yScale.domain([0, d3.max(data, function (d) {
return d3.max(d.values, function (d) {
return d.y0 + d.y;
});
})]);
// new data items need to get added
var areas = svg.selectAll(".layer")
.data(data, function(d) { return d.type; });
areas.enter()
.append("path")
.attr("class", "layer")
.style("fill", function (d, i) {
return colors(i)
});
// Added and updated items need to be updated
areas.attr("d", function (d) {
return area(d.values);
})
// Old items need to be removed - we should not actually need this with the data as it is
areas.exit().remove();
svg.selectAll("g.y.axis").call(yAxis);
}
It's really just a start and you will want to reorganize based on your needs, but take a look at an updated fiddle

Categories

Resources