I was trying to make a Bar Chart using D3 by calling the data from the JSON API, I was able to make a single bar visible but not able to make other bars visible. I hope there is some issue placing of the bars.
Different rectangles are visible in the developer tool, but all are coming up in a same place.
Any help would be much appreciated.
// javascript
const svg = d3.select('svg');
const width = +svg.attr('width');
const height = +svg.attr('height');
// Get the data from the JSON api
d3.json("https://api.covid19india.org/data.json")
.then( data => {
// Store the data in two variales
var stateNames = [];
var confirmedCases = [];
for (let i=1; i <= 10; i++){ //i <= (data.statewise.length) - 1
stateNames.push(data.statewise[i].state);
confirmedCases.push(+(data.statewise[i].confirmed));
}
//console.log(stateNames);
//console.log(confirmedCases);
// Max number of cases
let sortedData = [...confirmedCases];
let sortedCases = sortedData.sort(function(a,b){
return a-b;
})
// Measurement of the SVG Element
const margin = { top: 20, right: 20, bottom: 20, left: 100};
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// Horizontal Visualization of Bar Graph
// X scale
const xScale = d3.scaleLinear()
.domain([0, sortedCases[sortedCases.length-1]])
.range([0, innerWidth]);
//console.log(xScale.domain());
//console.log(xScale.range());
const yScale = d3.scaleBand()
.domain(stateNames)
.range([0, innerHeight])
.padding(0.2);
//console.log(yScale.domain());
const yAxis = d3.axisLeft(yScale);
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
g.append('g').call(d3.axisLeft(yScale));
g.append('g').call(d3.axisBottom(xScale))
.attr('transform', `translate(0,${innerHeight})`);
g.selectAll('rect').data(confirmedCases).enter()
.append('rect')
.attr('width', xScale)
.attr('height', 30)
})
Reference to the codepen link, where I have tried
https://codepen.io/Jagat_Nayan/pen/mdepwgZ
You need to defin y property to relate the states to Y asix, and for that you need to pass both stateName and cases while creating rect.
I created an array inputData which containes both stateName and their respective cases and used it while creating rectangles. Below is your updated working code.
// javascript
const svg = d3.select("svg");
const width = +svg.attr("width");
const height = +svg.attr("height");
// Get the data from the JSON api
d3.json("https://api.covid19india.org/data.json").then((data) => {
var inputData = [];
for (let i = 1; i <= 10; i++) {
inputData.push({stateName: data.statewise[i].state, cases: +data.statewise[i].confirmed})
}
// Measurement of the SVG Element
const margin = { top: 20, right: 20, bottom: 20, left: 100 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
//const barWidth = innerWidth;
//const barPadding = 5;
// Horizontal Visualization of Bar Graph
// X scale
const xScale = d3
.scaleLinear()
.domain([0, d3.max(inputData, function(d){ return d.cases; })])
.range([0, innerWidth]);
const yScale = d3
.scaleBand()
.domain(inputData.map(function(d) { return d.stateName; }))
.range([0, innerHeight])
.padding(0.2);
const yAxis = d3.axisLeft(yScale);
const g = svg
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
g.append("g").call(d3.axisLeft(yScale));
g.append("g")
.call(d3.axisBottom(xScale))
.attr("transform", `translate(0,${innerHeight})`);
g.selectAll("rect")
.data(inputData)
.enter()
.append("rect")
.attr("width", function(d) {return xScale(d.cases); })
.attr("y", function(d) {return yScale(d.stateName); })
.attr("height", 30);
});
Working graph on codepen - https://codepen.io/puneet2412/pen/LYpeOzd
Related
I am working with d3.js and draw scatter plot chart using the scaleTime to compute the values on x-axis and y-axis
but the scales are not computing the data in right way.
( 1 )The interval of seconds in the y-axis should be only 15 seconds
and that should start at 37:00.
( 2 ) There should be a slight gap at the beginning of the x-axis
and the tick at the end of the x-axis should have a 2016 value.
as shown in the picture below
retriveData();
async function retriveData() {
let response = await fetch(
"https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/cyclist-data.json"
);
let json = await response.json();
chart(json);
}
function chart(dataset) {
const width = 800,
height = 500,
padding = 60;
let svg = d3
.select("body")
.append("svg")
.style("width", width)
.style("height", height);
const parsedDate = d3.timeParse("%Y");
const dates = [];
for (let i = 0; i < dataset.length; i++) {
dates.push(parsedDate(dataset[i].Year));
}
const extentDate = d3.extent(dates);
const xScale = d3
.scaleTime()
.domain(extentDate)
.range([padding, width - padding]);
const parsedTime = d3.timeParse("%M:%S");
const minutes = [];
for (let i = 0; i < dataset.length; i++) {
minutes.push(parsedTime(dataset[i].Time));
}
const extentTime = d3.extent(minutes).sort((a, b) => b - a);
const yScale = d3
.scaleTime()
.domain(extentTime)
.range([height - padding, padding]);
let format = d3.timeFormat("%M:%S");
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale).tickFormat(format);
svg
.append("g")
.attr("transform", `translate(0,${height - padding})`)
.call(xAxis);
svg.append("g").attr("transform", `translate(${padding},0)`).call(yAxis);
}
[solution][1]
[1]: https://i.stack.imgur.com/D40dF.png
I want a ‘mirrored’ bar chart (i.e. one that looks like a sound wave) and have come up with the following using the d3 stack generator and a linear y scale:
import * as d3 from "d3";
const WIDTH = 300;
const HEIGHT = 300;
const LIMIT = 4;
const container = d3.select("svg").append("g");
var data = [];
for (var i = 0; i < 100; i++) {
data.push({
index: i,
value: Math.random() * LIMIT
});
}
var stack = d3
.stack()
.keys(["value"])
.order(d3.stackOrderNone)
.offset(d3.stackOffsetSilhouette);
var series = stack(data);
var xScale = d3
.scaleLinear()
.range([0, WIDTH])
.domain([0, data.length]);
var yScale = d3
.scaleLinear()
.range([HEIGHT, 0])
.domain([0, LIMIT / 2]);
var xAxis = d3.axisBottom().scale(xScale);
var yAxis = d3.axisLeft().scale(yScale);
container
.selectAll(".bar")
.data(series[0])
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", d => {
return xScale(d.data.index);
})
.attr("y", d => {
return yScale(d[0]) / 2 - HEIGHT / 2;
})
.attr("width", WIDTH / series[0].length)
.attr("height", d => yScale(d[1]));
However, I feel like I’ve hacked the calculations for both the y scale domain and for positioning the blocks.
For the domain I currently use 0 to the data's upper limit / 2.
For my y position I use yScale(d[0]) / 2 - HEIGHT / 2; despite the height being directly based off the scale i.e. d => yScale(d[1]).
Is there a better, more idiomatic way to achieve what I want?
It seems the way the stack function calculates values has changed since D3 v2, and therefore I had to do two things to achieve this in a nicer way.
I switched my y scale domain to be the extents of the data and then translated by -0.5 * HEIGHT
I modified my calculation for the y position and height:
.attr('y', d => yScale(d[1]))
.attr('height', d => yScale(d[0]) - yScale(d[1]));
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>
I have the following bar chart.
https://jsfiddle.net/zyjp1abo/
As you can see the values are between 1000 and 1005. Showing all the data from 0 to 1005 does not sense since the differences aren't visible.
I'd like to show the bars from 1000 and 1005 and change the y axis accordingly. Simply using extent and changing the domain does not work since the bars are drawn through the bottom margin. I want them to stop at the lowest value, i.e 1000.
https://jsfiddle.net/zyjp1abo/1/
Any ideas? Thank you!
If you want the domain to go from 1000 to 1005, you should use d3.extent. That's not the problem.
The problem is that you are using d3.extent but you keep using y(0) both for translating your x axis and for calculating the bars heights, which is wrong. You have to use your height and your margins.
Here is your code with those changes:
var defaults = {
target: '#chart',
width: 500,
height: 170,
margin: {
top: 20,
right: 20,
bottom: 20,
left: 50
},
yTicks: 5
}
class Barchart {
constructor(config) {
Object.assign(this, defaults, config)
const {
target,
width,
height,
margin
} = this
const w = width - margin.left - margin.right
const h = height - margin.top - margin.bottom
const {
yTicks
} = this
this.chart = d3.select(target)
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
this.x = d3.scaleBand()
.rangeRound([0, w])
.padding(0.1)
this.y = d3.scaleLinear()
.rangeRound([h, 0])
this.xAxis = d3.axisBottom(this.x)
this.chart.append('g')
.attr('class', 'x axis')
this.yAxis = d3.axisLeft(this.y)
.ticks(yTicks)
this.chart.append('g')
.attr('class', 'y axis')
}
render(data) {
const {
x,
y,
xAxis,
yAxis,
chart
} = this
// y.domain(d3.extent(data, v => v.value))
y.domain(d3.extent(data, v => v.value))
const domain = data.map(d => d.timestamp)
x.domain(domain)
chart.select('.x.axis')
.attr('transform', `translate(0, ${(defaults.height - defaults.margin.bottom - defaults.margin.top)})`)
.call(xAxis)
chart.select('.y.axis')
.call(yAxis)
const bars = chart.selectAll('.bar')
.data(data)
bars
.enter()
.append('rect')
.attr('class', 'bar')
.merge(bars)
.attr('x', d => x(d.timestamp))
.attr('y', d => y(d.value))
.attr('width', x.bandwidth())
.attr('height', d => defaults.height - defaults.margin.bottom - defaults.margin.top - y(d.value))
}
}
const random = (min = -10, max = 10) => (
window.Math.floor(window.Math.random() * (max - min + 1)) + min
)
let bar = []
for (let i = 0; i < 20; i++) {
bar.push({
timestamp: Date.now() - (19 - i) * 500,
value: random(1000, 1005)
})
}
const barchart = new Barchart()
barchart.render(bar)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg id="chart">
</svg>
PS: You could simplify the math for the SVG size and margins.
PPS: There is a lot of debate regarding if a dataviz can have a non-zero baseline. I believe that some kinds of charts, in some situations, can and should have a non-zero baseline, specially line charts, when the change rate is more important than the absolute value, for instance. However, bar charts should always have a zero baseline.
After you change your domain, this line:
.attr('height', d => Math.abs(y(d.value) - y(0)))
Is still calculating the height of the bar based of a 0 value. The conventional way to calculate the height as in this example, is to base it off the inner height of your chart (your variable h).
Here's an updated fiddle.
y.domain([0, d3.max(data, v => v.value)])
change 0 here to 1000, and other places respectively
I have a bar chart see plunker the problem is that I would like to move the y-axis ticks to be at the middle left side of the rects but they appear on the top and end. and I cannot seem to move them without destroying the chart.
my code
var info = [{
name: "Walnuts",
value: 546546
}, {
name: "Almonds",
value: 456455
}
];
/* Set chart dimensions */
var width = 960,
height = 500,
margin = {
top: 10,
right: 10,
bottom: 20,
left: 60
};
//subtract margins
width = width - margin.left - margin.right;
height = height - margin.top - margin.bottom;
//sort data from highest to lowest
info = info.sort(function(a, b) {
return b.value - a.value;
});
//Sets the y scale from 0 to the maximum data element
var max_n = 0;
var category = []
for (var d in info) {
max_n = Math.max(info[d].value, max_n);
category.push(info[d].name)
}
var dx = width / max_n;
var dy = height / info.length;
var y = d3.scale.ordinal()
.domain(category)
.range([0, height]);
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
var svg = d3.select("#chart")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr('preserveAspectRatio', 'xMidYMin')
.attr("viewBox", '0 0 ' + parseInt(width + margin.left + margin.right) + ' ' + parseInt(height + margin.top + margin.bottom))
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.selectAll(".bar")
.data(info)
.enter()
.append("rect")
.attr("class", function(d, i) {
return "bar" + d.name;
})
.attr("x", function(d, i) {
return 0;
})
.attr("y", function(d, i) {
return dy * i;
})
.attr("width", function(d, i) {
return dx * d.value
})
.attr("height", dy)
.attr("fill", function(d, i) {
if (d.name == 'Walnuts') {
return 'red'
} else {
return 'green'
}
});
var y_xis = svg.append('g')
.attr('id', 'yaxis')
.call(yAxis);
You are using range in y axis like this:
var y = d3.scale.ordinal()
.domain(category)
.range([0, height]);
You should be using 'rangeRoundBands' since the y scale is ordinal
var y = d3.scale.ordinal()
.domain(category)
.rangeRoundBands([0, height], .1);
working code here
For d3 versions like v4/v5.
Defining height as the graph/plot height, and max as the maximum value of y.
import { parseSvg } from 'd3-interpolate/src/transform/parse'
const yScale = d3
.scaleLinear()
.domain([0, max])
.rangeRound([height, 0])
const yAxis = d3.axisLeft(yScale)
svg
.append('g')
.call(yAxis)
.selectAll('.tick')
.each(function(data) {
const tick = d3.select(this)
const { translateX, translateY } = parseSvg(tick.attr('transform'))
tick.attr(
'transform',
translate(translateX, translateY + height / (2 * max))
)
})
Recently I needed something very very similar and I solved this with a call with selecting all text elements in the selection and moving their dy upwards. I will give an example with OP's code:
var y_xis = svg.append('g')
.attr('id','yaxis')
.call(yAxis)
.call(selection => selection
.selectAll('text')
.attr('dy', '-110') // this moves the text labels upwards
.attr('x', '110')); // this does the same job but horizontally