d3.js need repeated values on y axis - d3.js

I have an array:
taskTypes = [slot1, slot2, slot3,slot4,slot1,slot2,slot6];
my code for y-axis is:
var y = d3.scale.ordinal()
.domain(taskTypes)
.rangeRoundBands([ 0, height - margin.top - margin.bottom ], .1);
It is showing only unique values on y-axis, I want to show the values as they are. Can you help me out with the correct code?

This answer gives the basic solution: Using the ordinal scale, domain values uniquely identify corresponding range values. In your case both the '"slot1"' input values will map to the same output value, so will not appear unique.
The solution is to use array indices (0, 1, 2...) instead of array values ("slot1", "slot2"...) as your ordinal scale input domain:
var taskTypes = ["slot1", "slot2", "slot3","slot4","slot1","slot2","slot6"];
var y = d3.scale.ordinal()
.domain(d3.range(0, taskTypes.length))
.rangeRoundBands([ 0, height - margin.top - margin.bottom ], .1);
Fiddle here: http://jsfiddle.net/henbox/fhf3095r/3/

Related

Unable to display x axis categories and make y axis start at 0

In this D3.js version 6.7 bar chart I am trying to align the x axis to show the categories and show the y axis to start at 0. Extending the height of the svg and changing the transforms does not appear to be working. How can I make the x axis categories appear under the bars and make the y axis start at 0? Thank you.
async function barChart() {
const dataset = await d3.csv("https://assets.codepen.io/417105/bodypart-injury-clean.csv");
console.log(dataset);
const width = 400;
const height = 400;
const margin = {top:20, right:30, bottom:30, left:40};
const canvas = d3.select("#viz")
.append("svg")
.attr("width", width)
.attr("height", height);
const wrapper = canvas.append("g").style("transform",`translate(${margin.left}px,${margin.top}px)`);
const xScale = d3.scaleBand()
.domain(["Arm","Eye","Head","Hand","Leg","Other"])
.range([0,width - margin.left])
.padding(0.2);
const yScale = d3.scaleLinear()
.domain(d3.extent(dataset, d => +d.Total))
.range([height,0]);
console.log(xScale("Leg"));
console.log(yScale(1700));
const barRect = wrapper.append("g")
.selectAll('rect')
.data(dataset)
.join('rect')
.attr('x', d => xScale(d.BodyRegion))
.attr('y', d => yScale(+d.Total))
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(+d.Total))
.attr('fill', 'teal');
const yAxis = d3.axisLeft().scale(yScale);
wrapper.append("g").call(yAxis);
const xAxis = d3.axisBottom().scale(xScale);
wrapper.append("g").attr('transform', `translate(0,${height-margin.bottom})`).call(xAxis);
}
barChart();
The Y scale
The scale's domain sets the extent of the scale in your data's units, the scale's range sets the scale's extent in scaled units (pixels here). The first value in the domain is mapped to the first value in the range.
Your domain is set to:
.domain(d3.extent(dataset, d => +d.Total))
d3.extent returns the minimum and maximum matching values, as your minimum value is not zero, your scale's domain does not start at 0. If you want to set the scale's domain's lower bounds to zero, you need to set that, like so:
.domain([0,d3.max(dataset,d=> +d.Total)])
.domain/.range take arrays, these arrays for a linear scale must have the same number of elements
But you also don't want your scale's range to be [height,0] because of margins:
.range([height-margin.bottom,margin.top])
You want the data to be scaled from between within the two margins, height-margin.bottom is the furthest down the page you want to plot data, and margin.top is the furthest to the top of the SVG you want to plot data.
Now your bars are a bit off, that's because you aren't accounting for the margin in the height attribute:
.attr('height', d => height - yScale(+d.Total))
you need:
.attr('height', d => height - margin.bottom - yScale(+d.Total))
Note, a common approach to avoid having to constantly reference the margin is to apply the margin to a parent g and have width height reflect the size of the plot area within that g (not the entire SVG).
The X Axis
Now that the y scale is configured, let's look at the x axis. All you need to do here is boost the bottom margin: the text is appended (you can inspect the page to see it is there, just not visible). Try margin.bottom = 50.

D3 Text label for y axis in version 4

Hi I'm trying to set an ordinal scale for a line graph in d3 using v4. For some reason the ticks do not scale properly although I have scaled them as such:
var yTicks = d3.scaleOrdinal()
.domain(["apple", "orange", "banana", "grapefruit", "mango"])
.range([0, h])
var xAxis = d3.axisBottom().scale(x).tickSize(-h);
// var xAxis = d3.svg.axis().scale(x).tickSize(-h).tickSubdivide(true);
var yAxisLeft = d3.axisLeft().scale(yTicks);
// Add the x-axis.
graph.append("svg:g").attr("class", "x axis").attr("transform", "translate(0," + h + ")").call(xAxis);
// add lines
// do this AFTER the axes above so that the line is above the tick-lines
for (var i = data.length - 1; i >= 0; i--) {
graph.append("svg:path").attr("d", line(data[i])).attr("class", "data" + (i + 1));
};
graph.append("svg:g").attr("class", "y axis").attr("transform", "translate(0,0)").call(yAxisLeft);
The full version of what I've done can be found at this fiddle here: https://jsfiddle.net/5g1fe6qd/
You cannot set the range of an ordinal scale the way you did here: you have to specify the discrete values.
An easy solution in using a point scale instead:
var yTicks = d3.scalePoint()
Here is your updated fiddle: https://jsfiddle.net/5g1fe6qd/1/
This is expected behavior:
ordinal.range([range])
If range is specified, sets the range of the ordinal scale to the
specified array of values. The first element in the domain will be
mapped to the first element in range, the second domain value to the
second range value, and so on. If there are fewer elements in the
range than in the domain, the scale will reuse values from the start
of the range. If range is not specified, this method returns the
current range.
(emphasis mine, from API documentation)
You've only specified two elements in your range, therefore, the five values in your domain are mapped to these two values in the range (hence the overlapping text). You could use something along these lines:
d3.scaleOrdinal()
.domain(["apple", "orange", "banana", "grapefruit", "mango"])
.range([0, h*0.25, h*0.5, h*0.75, h]
(fiddle: https://jsfiddle.net/bmysrmcd/)
However, Gerardo's answer provides an alternative that doesn't require you set map each element in the domain to the range, and that is a better solution.

D3 Chart - Using tickValues for Time Gives "translate(NaN,0)"

In my d3 line chart, I only want ticks for the plotted data. This proves to be a issue with time stamps though as I get:
d3.js:7651 Error: <g> attribute transform: Expected number, "translate(NaN,0)"..
I thought to convert the strings to numbers in the tickValues array but I can not since it's got a colon. Any ideas?
// Hard coded data
scope.data = [
{date: '12:00', glucoseLevel: 400},
{date: '15:00', glucoseLevel: 200},
{date: '18:00', glucoseLevel: 300},
{date: '23:00', glucoseLevel: 400}
];
var parseDate = d3.timeParse('%I:%M');
scope.data.forEach(function(d) {
d.date = parseDate(d.date);
d.glucoseLevel = +d.glucoseLevel;
});
var x = d3.scaleTime()
.range([0, width]);
var xAxis = d3.axisBottom(x)
.tickValues(['12:00', '15:00', '18:00', '23:00']);
// Add the X Axis
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
You are specifying X values as times, so you must also specify the X-axis tick values as times.
As you already have the X values in the correct format, you can just write
var xAxis = d3.axisBottom(x)
.tickValues(scope.data.map(function (d) { return d.date; }));
.tickValues() isn't for setting the tick labels, it's for setting where on the axis the ticks appear. If you want the tick labels formatted in some way, specify a formatter using tickFormat, for example:
var xAxis = d3.axisBottom(x)
.tickValues(scope.data.map(function (d) { return d.date; }))
.tickFormat(d3.timeFormat("%H:%M"));
I've used the format string %H:%M instead of %I:%M as %I is hours in the range 01-12 whereas %H uses the 24-hour clock. For consistency I'd recommend changing your time parsing function to d3.timeParse('%H:%M'), although parsing a time with the hours greater than 12 using %I seems to work.
Finally, you'll also need to set the domain of your scale object x, for example:
var x = d3.scaleTime()
.domain([parseDate('12:00'), parseDate('23:00')])
.range([0, width]);
The two values passed to domain are the minimum and maximum X values to use for the axis. I've used the minimum and maximum values of your data, but I could have chosen a different time range (e.g. 00:00 to 24:00) as long as it contained all of your data points.

Using d3.js is there a way to zero align two Y Axes with positive and negative values

I am new to d3, learning a lot. I have an issue I cannot find an example for:
I have two y axes with positive and negative values with vastly different domains, one being large dollar amounts the other being percentages.
The resulting graph from cobbling together examples looks really awesome with one slight detail, the zero line for each y axis is in a slightly different position. Does anyone know of a way in d3 to get the zero line to be at the same x position?
I would like these two yScales/axes to share the same zero line
// define yScale
var yScale = d3.scale.linear()
.range([height, 0])
.domain(d3.extent(dataset, function(d) { return d.value_di1; }))
;
// define y2 scale
var yScale2 = d3.scale.linear()
.range([height, 0])
.domain(d3.extent(dataset, function(d) { return d.calc_di1_di2_percent; }))
;
Here is a link to a jsfiddle with sample data:
http://jsfiddle.net/jglover/XvBs3/1/
(the x-axis ticks look horrible in the jsfiddle example)
In general, there's unfortunately no way to do this neatly. D3 doesn't really have a concept of several things lining up and therefore no means of accomplishing it.
In your particular case however, you can fix it quite easily by tweaking the domain of the second y axis:
.domain([d3.min(dataset, function(d) { return d.calc_di1_di2_percent; }), 0.7])
Complete example here.
To make the 0 level the same position, a strategy is to equalize the length/proportion of the y axes.
Here are the concepts to the solution below:
The alignment of baseline depends on the length of the y axes.
To let all value shown in the bar, we need to extend the shorter side of the dimension, which compares to the other, to make the proportion of the two axes equal.
example:
// dummy data
const y1List = [-1000, 120, -130, 1400],
y2List = [-0.1, 0.2, 0.3, -0.4];
// get proportion of the two y axes
const totalY1Length = Math.abs(d3.min(y1List)) + Math.abs(d3.max(y1List)),
totalY2Length = Math.abs(d3.min(y2List)) + Math.abs(d3.max(y2List)),
maxY1ToY2 = totalY2Length * d3.max(y1List) / totalY1Length,
minY1ToY2 = totalY2Length * d3.min(y1List) / totalY1Length,
maxY2ToY1 = totalY1Length * d3.max(y2List) / totalY2Length,
minY2ToY1 = totalY1Length * d3.min(y2List) / totalY2Length;
// extend the shorter side of the upper dimension with corresponding value
let maxY1Domain = d3.max(y1List),
maxY2Domain = d3.max(y2List);
if (maxY1ToY2 > d3.max(y2List)) {
maxY2Domain = d3.max(y2List) + maxY1ToY2 - d3.max(y2List);
} else {
maxY1Domain = d3.max(y1List) + maxY2ToY1 - d3.max(y1List);
}
// extend the shorter side of the lower dimension with corresponding value
let minY1Domain = d3.min(y1List),
minY2Domain = d3.min(y2List);
if (minY1ToY2 < d3.min(y2List)) {
minY2Domain = d3.min(y2List) + minY1ToY2 - d3.min(y2List);
} else {
minY1Domain = d3.min(y1List) + minY2ToY1 - d3.min(y1List);
}
// finally, we get the domains for our two y axes
const y1Domain = [minY1Domain, maxY1Domain],
y2Domain = [minY2Domain, maxY2Domain];

how do you draw linear line in scatter plot with d3.js

I am looking to implement ggplot2 type of graphs using d3.js library for interactivey purpose. I love ggplot2 but users are interested in interactive graphs. I've been exploring d3.js library and there seems to be lots of different graph capability but I really did not see any statistical graphs like linear line, forecast etc. Given a scatter plot, is it possible to also add linear line to the graph.
I have this sample script that draws scatter plot. How would I add linear line to this graph in d3.js?
// data that you want to plot, I've used separate arrays for x and y values
var xdata = [5, 10, 15, 20],
ydata = [3, 17, 4, 6];
// size and margins for the chart
var margin = {top: 20, right: 15, bottom: 60, left: 60}
, width = 960 - margin.left - margin.right
, height = 500 - margin.top - margin.bottom;
// x and y scales, I've used linear here but there are other options
// the scales translate data values to pixel values for you
var x = d3.scale.linear()
.domain([0, d3.max(xdata)]) // the range of the values to plot
.range([ 0, width ]); // the pixel range of the x-axis
var y = d3.scale.linear()
.domain([0, d3.max(ydata)])
.range([ height, 0 ]);
// the chart object, includes all margins
var chart = d3.select('body')
.append('svg:svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart')
// the main object where the chart and axis will be drawn
var main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main')
// draw the x axis
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'main axis date')
.call(xAxis);
// draw the y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'main axis date')
.call(yAxis);
// draw the graph object
var g = main.append("svg:g");
g.selectAll("scatter-dots")
.data(ydata) // using the values in the ydata array
.enter().append("svg:circle") // create a new circle for each value
.attr("cy", function (d) { return y(d); } ) // translate y value to a pixel
.attr("cx", function (d,i) { return x(xdata[i]); } ) // translate x value
.attr("r", 10) // radius of circle
.style("opacity", 0.6); // opacity of circle
To add a line to your plot, all that you need to do is to append some line SVGs to your main SVG (chart) or to the group that contains your SVG elements (main).
Your code would look something like the following:
chart.append('line')
.attr('x1',x(10))
.attr('x2',x(20))
.attr('y1',y(5))
.attr('y2',y(10))
This would draw a line from (10,5) to (20,10). You could similarly create a data set for your lines and append a whole bunch of them.
One thing you might be interested in is the SVG path element. This is more common for lines than drawing one straight segment at a time. The documentation is here.
On another note you may find it easier to work with data in d3 if you create it all as one object. For example, if your data was in the following form:
data = [{x: 5, y:3}, {x: 10, y:17}, {x: 15, y:4}, {x: 20, y:6}]
You could use:
g.selectAll("scatter-dots")
.data(ydata) // using the values in the ydata array
.enter().append("svg:circle") // create a new circle for each value
.attr("cy", function (d) { return y(d.y); } ) //set y
.attr("cx", function (d,i) { return x(d.x); } ) //set x
This would eliminate potentially messy indexing if your data gets more complex.
UPDATE: Here is the relevant block: https://bl.ocks.org/HarryStevens/be559bed98d662f69e68fc8a7e0ad097
I wrote this function to calculate a linear regression from data, formatted as JSON.
It takes 5 parameters:
1) Your data
2) The column name of the data plotted on your x-axis
3) The column name of the data plotted on your y-axis
4) The minimum value of your x-axis
5) The minimum value of your y-axis
I got the formula for calculating a linear regression from http://classroom.synonym.com/calculate-trendline-2709.html
function calcLinear(data, x, y, minX, minY){
/////////
//SLOPE//
/////////
// Let n = the number of data points
var n = data.length;
var pts = [];
data.forEach(function(d,i){
var obj = {};
obj.x = d[x];
obj.y = d[y];
obj.mult = obj.x*obj.y;
pts.push(obj);
});
// Let a equal n times the summation of all x-values multiplied by their corresponding y-values
// Let b equal the sum of all x-values times the sum of all y-values
// Let c equal n times the sum of all squared x-values
// Let d equal the squared sum of all x-values
var sum = 0;
var xSum = 0;
var ySum = 0;
var sumSq = 0;
pts.forEach(function(pt){
sum = sum + pt.mult;
xSum = xSum + pt.x;
ySum = ySum + pt.y;
sumSq = sumSq + (pt.x * pt.x);
});
var a = sum * n;
var b = xSum * ySum;
var c = sumSq * n;
var d = xSum * xSum;
// Plug the values that you calculated for a, b, c, and d into the following equation to calculate the slope
// m = (a - b) / (c - d)
var m = (a - b) / (c - d);
/////////////
//INTERCEPT//
/////////////
// Let e equal the sum of all y-values
var e = ySum;
// Let f equal the slope times the sum of all x-values
var f = m * xSum;
// Plug the values you have calculated for e and f into the following equation for the y-intercept
// y-intercept = b = (e - f) / n = (14.5 - 10.5) / 3 = 1.3
var b = (e - f) / n;
// return an object of two points
// each point is an object with an x and y coordinate
return {
ptA : {
x: minX,
y: m * minX + b
},
ptB : {
y: minY,
x: (minY - b) / m
}
}
}

Resources