D3: Multiple Values on Y-Axis - javascript

I have a graph I need to make but having a hard time figuring out the best approach. Essentially what I need is two different data sets on the y-axis that are separate values but still related. At zero on the y-axis the data set changes to a different value that goes in positive increments.This is an example of the type of graph I am talking about
What would be the best way to go about creating this? While I can certainly find examples of multiple y-axis graphs, they don't seem to account for this use case.

You can indeed create two different scales, which is probably the standard solution, or... you can create only one scale! So, just for the sake of curiosity, here is how to do it:
Create a scale going from -10 to 10...
var yScale = d3.scaleLinear()
.domain([-10, 10])
... changing the negative values to positive ones in the axis...
var yAxis = d3.axisLeft(yScale)
.tickFormat(d => d < 0 ? Math.abs(d) : d);
... and, of course, changing the y values to negative ones in the data for the lines below the x axis (here named dataInspiration):
dataInspiration.forEach(d => d.y = -d.y)
Here is a demo using random numbers:
var width = 600,
height = 200,
padding = 20;
svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
var dataExpiration = d3.range(10).map(d => ({
x: d,
y: Math.random() * 10
}));
var dataInspiration = d3.range(10).map(d => ({
x: d,
y: Math.random() * 10
}));
dataInspiration.forEach(d => d.y = -d.y)
var xScale = d3.scalePoint()
.domain(d3.range(10))
.range([padding, width - padding]);
var yScale = d3.scaleLinear()
.domain([-10, 10])
.range([height - padding, padding])
var line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.curve(d3.curveMonotoneX);
var lineExpiration = svg.append("path")
.attr("fill", "none")
.attr("stroke", "blue")
.attr("d", line(dataExpiration));
var lineInspiration = svg.append("path")
.attr("fill", "none")
.attr("stroke", "red")
.attr("d", line(dataInspiration));
var xAxis = d3.axisBottom(xScale)
.tickFormat(d => d != 0 ? d : null);
var yAxis = d3.axisLeft(yScale)
.tickFormat(d => d < 0 ? Math.abs(d) : d);
var gX = svg.append("g")
.attr("transform", "translate(0," + yScale(0) + ")")
.call(xAxis);
var gY = svg.append("g")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>

Related

Bar graph bleeding below y axis D3.js

I can't figure out why my elements are bleeding into the bottom padding. It is preventing me from having a elements. I am sure it is something simple, it always is. I just can't figure out what's wrong. I have tried changing the height and the scales, but it just changes things I don't want changed. I keep looking at tutorials on scales and I haven't found anything that seems off. If I get I have the project on codepen, here is the link: https://codepen.io/critchey/pen/YzEXPrP
Here is a screen shot to know what I am talking about:
Here is the javascript:
//Mapping dataset to its x and y axes
const xData = dataset.map(d => {
let date = new Date(d[0]);
return date;
});
const xDates = xData.map(d => {
let dateFormatted = d.toLocaleString("default", {month: "short"}) + ' ' + d.getDate() + ', ' + d.getFullYear()
return dateFormatted;
});
const yData = dataset.map(d => d[1]);
//Variable for use inside D3
const h = 400;
const w = 800;
const pad = 40;
//Scales for the SVG element
const xDateScale = d3.scaleTime()
.domain([0, w])
.range([pad, w - pad]);
const xScale = d3.scaleLinear()
.domain([0, yData.length])
.range([pad, w - pad]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(yData, (d) => d)])
.range([h - pad, pad]);
//Declaring the SVG element
const svg = d3.select('#svg-container')
.append('svg')
.attr('width', w)
.attr('height', h)
//Declaring each bar
svg.selectAll('rect')
.data(yData)
.enter()
.append('rect')
.attr('x', (d, i) => xScale(i))
.attr('y', (d, i) => yScale(d))
.attr("width", w / (yData.length - pad * 2))
.attr("height", (d, i) => d)
.attr("class", "bar")
.append('title')
.text((d, i) => 'GDP: ' + d + ' | Date: ' + xDates[i])
//Axes Declarations
const xAxis = d3.axisBottom(xDateScale);
const yAxis = d3.axisLeft(yScale);
svg.append("g")
.attr("transform", "translate(0," + (h - pad / 2) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(" + pad + ",0)")
.call(yAxis)
The problem is that you're not using the correct value with the height attribute for your rects
Just make the following change and it should work:
.attr("height", (d, i) => yScale(0) - yScale(d))

D3 Grouped Bar Chart From Arrays

I'm really having trouble with D3 and need some help changing my existing barchart to be a grouped barchart The barchart is being used within a tooltip and currently looks like:
Each colour represents a sector of industry (pink = retail, teal = groceries...etc).
I need to change the bar chart so that it compares the percentage change in each industry with the world average percentage change in this industry.
At the moment the bar chart is being created from an array of data. I also have an array with the world percentage values.
So imagine:
countryData = [10,-20,-30,-63,-23,20],
worldData = [23,-40,-23,-42,-23,40]
Where index 0 = retail sector, index 1 = grocery sector, etc.
I need to plot a grouped barchart comparing each sector to the world average (show the world average in red). This is a bit tricky to explain so I drew it for you (...excuse the shoddy drawing).
Please can someone help me change my existing tooltip?
Here's the current code. If you want to simulate the data values changing.
If you want to scrap my existing code that's fine.
.on('mouseover', ({ properties }) => {
// get county data
const mobilityData = covid.data[properties[key]] || {};
const {
retailAverage,
groceryAverage,
parksAverage,
transitAverage,
workplaceAverage,
residentialAverage,
} = getAverage(covid1);
let avgArray = [retailAverage, groceryAverage, parksAverage, transitAverage, workplaceAverage, retailAverage];
let categoriesNames = ["Retail", "Grocery", "Parks", "Transit", "Workplaces", "Residential"];
// create tooltip
div = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
div.html(properties[key]);
div.transition()
.duration(200)
.style('opacity', 0.9);
// calculate bar graph data for tooltip
const barData = [];
Object.keys(mobilityData).forEach((industry) => {
const stringMinusPercentage = mobilityData[industry].slice(0, -1);
barData.push(+stringMinusPercentage); // changing it to an integer value, from string
});
//combine the two lists for the combined bar graph
var combinedList = [];
for(var i = 0; i < barData.length; i++) {
const stringMinusPercentage2 = +(avgArray[i].slice(0, -1));
const object = {category: categoriesNames[i], country: barData[i], world: stringMinusPercentage2}
combinedList.push(object); //Push object into list
}
console.log(combinedList);
// barData = barData.sort(function (a, b) { return a - b; });
// sort into ascending ^ keeping this in case we need it later
const height2 = 220;
const width2 = 250;
const margin = {
left: 50, right: 10, top: 20, bottom: 15,
};
// create bar chart svg
const svgA = div.append('svg')
.attr('height', height2)
.attr('width', width2)
.style('border', '1px solid')
.append('g')
// apply the margins:
.attr('transform', `translate(${[`${margin.left},${margin.top}`]})`);
const barWidth = 30; // Width of the bars
// plot area is height - vertical margins.
const chartHeight = height2 - margin.top - margin.left;
// set the scale:
const yScale = d3.scaleLinear()
.domain([-100, 100])
.range([chartHeight, 0]);
// draw some rectangles:
svgA
.selectAll('rect')
.data(barData)
.enter()
.append('rect')
.attr('x', (d, i) => i * barWidth)
.attr('y', (d) => {
if (d < 0) {
return yScale(0); // if the value is under zero, the top of the bar is at yScale(0);
}
return yScale(d); // otherwise the rectangle top is above yScale(0) at yScale(d);
})
.attr('height', (d) => Math.abs(yScale(0) - yScale(d))) // the height of the rectangle is the difference between the scale value and yScale(0);
.attr('width', barWidth)
.style('fill', (d, i) => colours[i % 6]) // colour the bars depending on index
.style('stroke', 'black')
.style('stroke-width', '1px');
// Labelling the Y axis
const yAxis = d3.axisLeft(yScale);
svgA.append('text')
.attr('class', 'y label')
.attr('text-anchor', 'end')
.attr('x', -15)
.attr('y', -25)
.attr('dy', '-.75em')
.attr('transform', 'rotate(-90)')
.text('Percentage Change (%)');
svgA.append('g')
.call(yAxis);
})
.on('mouseout', () => {
div.style('opacity', 0);
div.remove();
})
.on('mousemove', () => div
.style('top', `${d3.event.pageY - 140}px`)
.style('left', `${d3.event.pageX + 15}px`));
svg.append('g')
.attr('transform', 'translate(25,25)')
.call(colorLegend, {
colorScale,
circleRadius: 10,
spacing: 30,
textOffset: 20,
});
};
drawMap(svg1, geoJson1, geoPath1, covid1, key1, 'impact1');
drawMap(svg2, geoJson2, geoPath2, covid2, key2, 'impact2');
};
In short I would suggest you to use two Band Scales for x axis. I've attached a code snippet showing the solution.
Enjoy ;)
//Assuming the following data final format
var finalData = [
{
"groupKey": "Retail",
"sectorValue": 70,
"worldValue": 60
},
{
"groupKey": "Grocery",
"sectorValue": 90,
"worldValue": 90
},
{
"groupKey": "other",
"sectorValue": -20,
"worldValue": 30
}
];
var colorRange = d3.scaleOrdinal().range(["#00BCD4", "#FFC400", "#ECEFF1"]);
var subGroupKeys = ["sectorValue", "worldValue"];
var svg = d3.select("svg");
var margin = {top: 20, right: 20, bottom: 30, left: 40};
var width = +svg.attr("width") - margin.left - margin.right;
var height = +svg.attr("height") - margin.top - margin.bottom;
var container = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// The scale spacing the groups, your "sectors":
var x0 = d3.scaleBand()
.domain(finalData.map(d => d.groupKey))
.rangeRound([0, width])
.paddingInner(0.1);
// The scale for spacing each group's bar, your "sector bar":
var x1 = d3.scaleBand()
.domain(subGroupKeys)
.rangeRound([0, x0.bandwidth()])
.padding(0.05);
var yScale = d3.scaleLinear()
.domain([-100, 100])
.rangeRound([height, 0]);
//and then you will need to append both, groups and bars
var groups = container.append('g')
.selectAll('g')
.data(finalData, d => d.groupKey)
.join("g")
.attr("transform", (d) => "translate(" + x0(d.groupKey) + ",0)");
//define groups bars, one per sub group
var bars = groups
.selectAll("rect")
.data(d => subGroupKeys.map(key => ({ key, value: d[key], groupKey: d.groupKey })), (d) => "" + d.groupKey + "_" + d.key)
.join("rect")
.attr("fill", d => colorRange(d.key))
.attr("x", d => x1(d.key))
.attr("width", (d) => x1.bandwidth())
.attr('y', (d) => Math.min(yScale(0), yScale(d.value)))
.attr('height', (d) => Math.abs(yScale(0) - yScale(d.value)));
//append x axis
container.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x0));
//append y axis
container.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale))
.append("text")
.attr("x", 2)
.attr("y", yScale(yScale.ticks().pop()) + 0.5)
.attr("dy", "0.32em")
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "start")
.text("Values");
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg width="600" height="400"></svg>

D3.JS Y-axis label issue

To start, I am fairly new to D3.Js. I have spent the past week or so working on a D3.JS issue-specifically making a graph with a Y-axis label. However, I cannot get the graph exactly how I want. It is almost there but inverted or my data comes out wrong. Now I will briefly show some of my code and images of my main problem before showing all of the code. I have spent time looking at other Stack Overflow posts with a similar issue and I do what is on those posts and still have the same issue.
For example, I thought that this post would have the solution: reversed Y-axis D3
The data is the following:
[0,20,3,8] (It is actually an array of objects but I think this may be all that is needed.
So, to start, when the yScale is like this:
var yScale = d3.scaleLinear()
.domain([0, maxPound]) //Value of maxpound is 20
.range([0, 350]);
The bar chart looks like this:
As one can see the Y chart starts with zero at the top and 20 at the bottom-which at first I thought was an easy fix of flipping the values in the domain around to this:
var yScale = d3.scaleLinear()
.domain([0, maxPound]) //Value of maxpound is 20
.range([0, 350]);
I get this image:
In the second image the y-axis is right-20 is on top-Yay! But the graphs are wrong. 0 now returns a value of 350 pixels-the height of the SVG element. That is the value that 20 should be returning! If I try to switch the image range values, I get the same problem!
Now the code:
var w = 350;
var h = 350;
var barPadding = 1;
var margin = {top: 5, right: 200, bottom: 70, left: 25}
var maxPound = d3.max(poundDataArray,
function(d) {return parseInt(d.Pounds)}
);
//Y-Axis Code
var yScale = d3.scaleLinear()
.domain([maxPound, 0])
.range([0, h]);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Creating SVG element
var svg = d3.select(".pounds")
.append('svg')
.attr("width", w)
.attr('height', h)
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
svg.selectAll("rect")
.data(poundDataArray)
.enter()
.append("rect")
.attr('x', function(d, i){
return i * (w / poundDataArray.length);
})
.attr('y', function(d) {
return 350 - yScale(d.Pounds);
})
.attr('width', (w / 4) - 25)
.attr('height', function(d){
return yScale(d.Pounds);
})
.attr('fill', 'steelblue');
//Create Y axis
svg.append("g")
.attr("class", "axis")
.call(yAxis);
Thank you for any help! I believe that the error may be in the y or height values and have spent time messing around there with no results.
That is not a D3 issue, but an SVG feature: in an SVG, the origin (0,0) is at the top left corner, not the bottom left, as in a common Cartesian plane. That's why using [0, h] as the range makes the axis seem to be inverted... actually, it is not inverted: that's the correct orientation in an SVG. By the way, HTML5 Canvas has the same coordinates system, and you would have the same issue using a canvas.
So, you have to flip the range, not the domain:
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h, 0]);//the range goes from the bottom to the top now
Or, in your case, using the margins:
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h - margin.bottom, margin.top]);
Besides that, the math for the y position and height is wrong. It should be:
.attr('y', function(d) {
return yScale(d.Pounds);
})
.attr('height', function(d) {
return h - margin.bottom - yScale(d.Pounds);
})
Also, as a bonus tip, don't hardcode the x position and the width. Use a band scale instead.
Here is your code with those changes:
var poundDataArray = [{
Pounds: 10
}, {
Pounds: 20
}, {
Pounds: 5
}, {
Pounds: 8
}, {
Pounds: 14
}, {
Pounds: 1
}, {
Pounds: 12
}];
var w = 350;
var h = 350;
var barPadding = 1;
var margin = {
top: 5,
right: 20,
bottom: 70,
left: 25
}
var maxPound = d3.max(poundDataArray,
function(d) {
return parseInt(d.Pounds)
}
);
//Y-Axis Code
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h - margin.bottom, margin.top]);
var xScale = d3.scaleBand()
.domain(d3.range(poundDataArray.length))
.range([margin.left, w - margin.right])
.padding(.2);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Creating SVG element
var svg = d3.select("body")
.append('svg')
.attr("width", w)
.attr('height', h)
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
svg.selectAll("rect")
.data(poundDataArray)
.enter()
.append("rect")
.attr('x', function(d, i) {
return xScale(i);
})
.attr('y', function(d) {
return yScale(d.Pounds);
})
.attr('width', xScale.bandwidth())
.attr('height', function(d) {
return h - margin.bottom - yScale(d.Pounds);
})
.attr('fill', 'steelblue');
//Create Y axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + margin.left + ",0)")
.call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>

Draw vertical line after n data points in d3

I have a graph drawn with d3 with the x and y scales defined as follows:
x = d3.scaleLinear().domain([30, 150]).range([0, width])
y = d3.scaleLinear().domain([0, 100]).range([height, 0])
I need to draw a vertical line after 25% of data points. So I have my code like this:
svg.append('line')
.attr('x1', lowerLimit)
.attr('y1', 0)
.attr('x2', lowerLimit)
.attr('y2', height)
.style('stroke', 'red')
My problem is I am not sure how to set the lowerLimit x value to be 25% of all the x-values in the scale. Can someone help please? Thanks in advance!
Well, your question is not clear. When you say:
I need to draw a vertical line after 25% of data points
You're talking about the first quartile, which is impossible to calculate without the data, and not only that, but which changes for every different data set.
If you are indeed talking about the first quartile, this is what you have to do:
Given an data array called data, you can use d3.quantile:
var firstQuartile = d3.quantile(data, 0.25);
In the following demo, I'm plotting 100 dots, and calculating the 25 percentile (first quartile) regarding the property x:
var lowerLimit = d3.quantile(data, 0.25, function(d) {
return d.x
});
The console shows the value. Check the demo:
var data = d3.range(100).map(() => ({
x: Math.random() * 120 + 30,
y: Math.random() * 100
}));
var w = 500,
h = 160;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
x = d3.scaleLinear().domain([30, 150]).range([20, w - 20]);
y = d3.scaleLinear().domain([0, 100]).range([h - 20, 20]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
var circles = svg.selectAll("circles")
.data(data)
.enter()
.append("circle")
.attr("r", 2)
.attr("fill", "teal")
.attr("cx", d => x(d.x))
.attr("cy", d => y(d.y));
var data = data.sort((a, b) => d3.ascending(a.x, b.x))
var lowerLimit = d3.quantile(data, 0.25, function(d) {
return d.x
});
console.log(lowerLimit);
svg.append('line')
.attr('x1', x(lowerLimit))
.attr('y1', 20)
.attr('x2', x(lowerLimit))
.attr('y2', h - 20)
.style('stroke', 'red')
svg.append("g")
.attr("transform", "translate(0," + (h - 20) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(20,0)")
.call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
However, if you're talking about "getting the value that is 25% of the domain", the answer is easy. You can, for instance, create a function:
function findLimit(percentage) {
return x(x.domain()[0] + (x.domain()[1] - x.domain()[0]) * percentage / 100);
};
And pass the value of that function to lowerLimit:
var lowerLimit = findLimit(25);
Check this demo, drawing a line at 25% of the x axis:
var data = d3.range(100).map(() => ({
x: Math.random() * 120 + 30,
y: Math.random() * 100
}));
var w = 500,
h = 200;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
x = d3.scaleLinear().domain([30, 150]).range([20, w - 20]);
y = d3.scaleLinear().domain([0, 100]).range([h - 20, 20]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
var circles = svg.selectAll("circles")
.data(data)
.enter()
.append("circle")
.attr("r", 2)
.attr("fill", "teal")
.attr("cx", d => x(d.x))
.attr("cy", d => y(d.y));
var data = data.sort((a, b) => d3.ascending(a.x, b.x))
var lowerLimit = d3.quantile(data, 0.25, function(d) {
return d.x
});
console.log(lowerLimit);
svg.append('line')
.attr('x1', x(lowerLimit))
.attr('y1', 20)
.attr('x2', x(lowerLimit))
.attr('y2', h - 20)
.style('stroke', 'red')
svg.append("g")
.attr("transform", "translate(0," + (h - 20) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(20,0)")
.call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>

d3 hexbin scaling issues

I'm trying to learn how to use the d3.js hexbin plugin.
I started with the example: http://bl.ocks.org/mbostock/4248145 , and I'm adapting it.
I have a data set of points between [0,0] and [600,600]. I want to output them to a 300,300 graph.
My graph doesn't look right. It looks like the data isn't being scaled properly and the graph is only showing 1/4 of the data. Can someone tell me what's wrong? I've read a book about using d3, but I don't have very much experience using it.
Jsfiddle of my hexbin
var graph_width = 300;
var graph_height = 300;
var data_width = 600;
var data_height = 600;
var randomX = d3.random.normal(data_width / 2, 80),
randomY = d3.random.normal(data_height / 2, 80),
points = d3.range(2000).map(function() { return [randomX(), randomY()]; });
var color = d3.scale.linear()
.domain([0, 20])
.range(["white", "steelblue"])
.interpolate(d3.interpolateLab);
var hexbin = d3.hexbin()
.size([graph_width, graph_height])
.radius(20);
var x = d3.scale.linear()
.domain([0, data_width])
.range([0, graph_width]);
var y = d3.scale.linear()
.domain([0, data_height])
.range([0, graph_height]);
var svg = d3.select("body").append("svg")
.attr("width", graph_width)
.attr("height", graph_height)
.append("g");
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("class", "mesh")
.attr("width", graph_width)
.attr("height", graph_height);
svg.append("g")
.attr("clip-path", "url(#clip)")
.selectAll(".hexagon")
.data(hexbin(points))
.enter().append("path")
.attr("class", "hexagon")
.attr("d", hexbin.hexagon())
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.style("fill", function(d) { return color(d.length); });
I think I understand. You have data values in in the range of 0 to 600 but want those mapped to x/y positions in the range of 0 to 300.
If that's it then scale the points:
var x = d3.scale.linear()
.domain([0, data_width])
.range([0, graph_width]);
var y = d3.scale.linear()
.domain([0, data_height])
.range([0, graph_height]);
var randomX = d3.random.normal(data_width / 2, 80),
randomY = d3.random.normal(data_height / 2, 80),
points = d3.range(2000).map(function() { return [x(randomX()), y(randomY())]; });
Updated fiddle.

Categories

Resources