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>
I have a codepen here - https://codepen.io/anon/pen/xpaYYw?editors=0010
Its a simple test graph but the date will be formatted like this.
I have dates on the x axis and amounts on the y
How can I use the x scale to set the width and x position of the bars.
layers.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('height', function(d, i) {
return height - y(d.one);
})
.attr('y', function(d, i) {
return y(d.one);
})
.attr('width', function(d, i) {
return 50;
})
.attr('x', function(d, i) {
return 80*i;
})
.style('fill', (d, i) => {
return colors[i];
});
The problem with your question has nothing to do with programming, or JavaScript, or D3... the problem is a basic dataviz concept (that's why I added the data-visualization tag in your question):
What you're trying to do is not correct! You should not use bars with a time scale. Time scales are for time series (in which we use dots, or dots connected by lines).
If you use bars with time in the x axis you'll face problems:
Positioning the bar: the left margin of the bar will be always at the date you set. The whole bar will lie after that date;
Setting the width of the bar: in a real bar chart, which uses categorical variables for the x axis, the width has no meaning. But in a time scale the width represents time.
However, just for the sake of explanation, let's create this bar chart with a time scale (despite the fact that this is a wrong choice)... Here is how to do it:
First, set the "width" of the bars in time. Let's say, each bar will have 10 days of width:
.attr("width", function(d){
return x(d3.timeDay.offset(d.date, 10)) - x(d.date)
})
Then, set the x position of the bar to the current date less half its width (that is, less 5 days in our example):
.attr('x', function(d, i) {
return x(d3.timeDay.offset(d.date, -5));
})
Finally, don't forget to create a "padding" in the time scale:
var x = d3.scaleTime()
.domain([d3.min(data, function(d) {
return d3.timeDay.offset(d.date, -10);
}), d3.max(data, function(d) {
return d3.timeDay.offset(d.date, 10);
})])
.range([0, width]);
Here is your code with those changes:
var keys = [];
var legendKeys = [];
var maxVal = [];
var w = 800;
var h = 450;
var margin = {
top: 30,
bottom: 40,
left: 50,
right: 20,
};
var width = w - margin.left - margin.right;
var height = h - margin.top - margin.bottom;
var colors = ['#FF9A00', '#FFEBB6', '#FFC400', '#B4EDA0', '#FF4436'];
var data = [{
"one": 4306,
"two": 2465,
"three": 2299,
"four": 988,
"five": 554,
"six": 1841,
"date": "2015-05-31T00:00:00"
}, {
"one": 4378,
"two": 2457,
"three": 2348,
"four": 1021,
"five": 498,
"six": 1921,
"date": "2015-06-30T00:00:00"
}, {
"one": 3404,
"two": 2348,
"three": 1655,
"four": 809,
"five": 473,
"six": 1056,
"date": "2015-07-31T00:00:00"
},
];
data.forEach(function(d) {
d.date = new Date(d.date)
})
for (var i = 0; i < data.length; i++) {
for (var key in data[i]) {
if (!data.hasOwnProperty(key) && key !== "date")
maxVal.push(data[i][key]);
}
}
var x = d3.scaleTime()
.domain([d3.min(data, function(d) {
return d3.timeDay.offset(d.date, -10);
}), d3.max(data, function(d) {
return d3.timeDay.offset(d.date, 10);
})])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, d3.max(maxVal, function(d) {
return d;
})])
.range([height, 0]);
var svg = d3.select('body').append('svg')
.attr('class', 'chart')
.attr('width', w)
.attr('height', h);
var chart = svg.append('g')
.classed('graph', true)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var layersArea = chart.append('g')
.attr('class', 'layers');
var layers = layersArea.append('g')
.attr('class', 'layer');
layers.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('height', function(d, i) {
return height - y(d.one);
})
.attr('y', function(d, i) {
return y(d.one);
})
// .attr('width', function(d, i) {
// return 50;
// })
.attr("width", function(d) {
return x(d3.timeDay.offset(d.date, 10)) - x(d.date)
})
.attr('x', function(d, i) {
return x(d3.timeDay.offset(d.date, -5));
})
.style('fill', (d, i) => {
return colors[i];
});
chart.append('g')
.classed('x axis', true)
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x)
.tickFormat(d3.timeFormat("%Y-%m-%d")).tickValues(data.map(function(d) {
return new Date(d.date)
})));
chart.append('g')
.classed('y axis', true)
.call(d3.axisLeft(y)
.ticks(10));
<script src="https://d3js.org/d3.v4.min.js"></script>
I have been searching for a while about how to handle the X axis in a stacked bar chart (since dataset is a little different from a single bar chart).
Basically, I have data for a 24hr period in 15 minute intervals. However, I only want to display the x-axis in 2hr ticks.
Existing Fiddle: [https://jsfiddle.net/lucksp/crwb4v5u/][1]
It currently prints all the intervals.
I have tried various scale options with time but something doesn't translate with the way I have this setup.
var xScale = d3.scale.ordinal()
.domain(dataset[0].map(function(d) {
return d.x;
}))
.rangeRoundBands([0, width - margin.left]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.tickSize(0)
.ticks(12)
.tickFormat(function(d) {
return d;
});
var rect = groups.selectAll('rect')
.data(function(d) {
return d;
})
.enter()
.append('rect')
.attr('class', function(d, i) {
return 'stacks ' + d.type;
})
.classed('stacks', true)
.attr('id', function(d, i) {
return d.type + '_' + i;
})
.attr('x', function(d) {
return xScale(d.x);
})
.attr('y', function(d) {
return yScale(d.y0 + d.y);
})
.attr('height', function(d) {
return yScale(d.y0) - yScale(d.y0 + d.y);
})
.attr('width', xScale.rangeBand());
[1]: https://jsfiddle.net/lucksp/crwb4v5u/
I know it's user error, but after looking at this for the last 2 days, I am resorting to asking this question now. Thanks!
You are currently trying to use .ticks which will only work if the scale you're using has an inbuilt ticks function. Your ordinal scale in this case does not. It will by default use all values in the domain.
To go around it, we can manually set the ticks using xAxis.tickValues(["custom tick values that match domain vals"]). Check the snippet below.
var data = [{"hour":"0:00","inProgress":3,"inQueue":0},{"hour":"0:15","inProgress":5,"inQueue":3},{"hour":"0:30","inProgress":1,"inQueue":1},{"hour":"0:45","inProgress":1,"inQueue":0},{"hour":"1:00","inProgress":2,"inQueue":0},{"hour":"1:15","inProgress":8,"inQueue":2},{"hour":"1:30","inProgress":5,"inQueue":3},{"hour":"1:45","inProgress":5,"inQueue":1},{"hour":"2:00","inProgress":6,"inQueue":0},{"hour":"2:15","inProgress":6,"inQueue":0},{"hour":"2:30","inProgress":7,"inQueue":0},{"hour":"2:45","inProgress":7,"inQueue":0},{"hour":"3:00","inProgress":8,"inQueue":0},{"hour":"3:15","inProgress":8,"inQueue":0},{"hour":"3:30","inProgress":9,"inQueue":1},{"hour":"3:45","inProgress":9,"inQueue":4},{"hour":"4:00","inProgress":10,"inQueue":6},{"hour":"4:15","inProgress":10,"inQueue":2},{"hour":"4:30","inProgress":10,"inQueue":1},{"hour":"4:45","inProgress":11,"inQueue":0},{"hour":"5:00","inProgress":11,"inQueue":0},{"hour":"5:15","inProgress":12,"inQueue":0},{"hour":"5:30","inProgress":12,"inQueue":0},{"hour":"5:45","inProgress":13,"inQueue":0},{"hour":"6:00","inProgress":13,"inQueue":0},{"hour":"6:15","inProgress":14,"inQueue":0},{"hour":"6:30","inProgress":14,"inQueue":0},{"hour":"6:45","inProgress":15,"inQueue":0},{"hour":"7:00","inProgress":15,"inQueue":3},{"hour":"7:15","inProgress":15,"inQueue":1},{"hour":"7:30","inProgress":16,"inQueue":0},{"hour":"7:45","inProgress":16,"inQueue":0},{"hour":"8:00","inProgress":17,"inQueue":2},{"hour":"8:15","inProgress":17,"inQueue":3},{"hour":"8:30","inProgress":18,"inQueue":1},{"hour":"8:45","inProgress":18,"inQueue":0},{"hour":"9:00","inProgress":19,"inQueue":0},{"hour":"9:15","inProgress":19,"inQueue":0},{"hour":"9:30","inProgress":20,"inQueue":0},{"hour":"9:45","inProgress":20,"inQueue":0},{"hour":"10:00","inProgress":20,"inQueue":0},{"hour":"10:15","inProgress":21,"inQueue":1},{"hour":"10:30","inProgress":21,"inQueue":4},{"hour":"10:45","inProgress":22,"inQueue":6},{"hour":"11:00","inProgress":22,"inQueue":2},{"hour":"11:15","inProgress":23,"inQueue":1},{"hour":"11:30","inProgress":23,"inQueue":0},{"hour":"11:45","inProgress":3,"inQueue":0},{"hour":"12:00","inProgress":5,"inQueue":0},{"hour":"12:15","inProgress":1,"inQueue":0},{"hour":"12:30","inProgress":1,"inQueue":0},{"hour":"12:45","inProgress":2,"inQueue":0},{"hour":"13:00","inProgress":8,"inQueue":0},{"hour":"13:15","inProgress":5,"inQueue":0},{"hour":"13:30","inProgress":5,"inQueue":0},{"hour":"13:45","inProgress":6,"inQueue":3},{"hour":"14:00","inProgress":6,"inQueue":1},{"hour":"14:15","inProgress":7,"inQueue":0},{"hour":"14:30","inProgress":7,"inQueue":0},{"hour":"14:45","inProgress":8,"inQueue":2},{"hour":"15:00","inProgress":8,"inQueue":3},{"hour":"15:15","inProgress":9,"inQueue":1},{"hour":"15:30","inProgress":9,"inQueue":0},{"hour":"15:45","inProgress":10,"inQueue":0},{"hour":"16:00","inProgress":10,"inQueue":0},{"hour":"16:15","inProgress":10,"inQueue":0},{"hour":"16:30","inProgress":11,"inQueue":0},{"hour":"16:45","inProgress":11,"inQueue":0},{"hour":"17:00","inProgress":12,"inQueue":1},{"hour":"17:15","inProgress":12,"inQueue":4},{"hour":"17:30","inProgress":13,"inQueue":6},{"hour":"17:45","inProgress":13,"inQueue":2},{"hour":"18:00","inProgress":14,"inQueue":1},{"hour":"18:15","inProgress":14,"inQueue":0},{"hour":"18:30","inProgress":15,"inQueue":0},{"hour":"18:45","inProgress":15,"inQueue":0},{"hour":"19:00","inProgress":15,"inQueue":0},{"hour":"19:15","inProgress":16,"inQueue":0},{"hour":"19:30","inProgress":16,"inQueue":0},{"hour":"19:45","inProgress":17,"inQueue":0},{"hour":"20:00","inProgress":17,"inQueue":0},{"hour":"20:15","inProgress":18,"inQueue":0},{"hour":"20:30","inProgress":18,"inQueue":3},{"hour":"20:45","inProgress":19,"inQueue":1},{"hour":"21:00","inProgress":19,"inQueue":0},{"hour":"21:15","inProgress":20,"inQueue":0},{"hour":"21:30","inProgress":20,"inQueue":2},{"hour":"21:45","inProgress":20,"inQueue":3},{"hour":"22:00","inProgress":21,"inQueue":1},{"hour":"22:15","inProgress":21,"inQueue":0},{"hour":"22:30","inProgress":22,"inQueue":0},{"hour":"22:45","inProgress":22,"inQueue":0},{"hour":"23:00","inProgress":23,"inQueue":0},{"hour":"23:15","inProgress":23,"inQueue":0},{"hour":"23:30","inProgress":1,"inQueue":0},{"hour":"23:45","inProgress":2,"inQueue":1}];
var margin = {top: 20, right: 50, bottom: 30, left: 20},
width = 500,
height = 300;
// Transpose the data into layers
var dataset = d3.layout.stack()(['inProgress', 'inQueue'].map(function(types) {
return data.map(function(d) {
return {
x: d.hour,
y: +d[types],
type: types
};
});
}));
var svg = d3.select('svg'),
margin = {top: 40, right: 10, bottom: 20, left: 10},
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 + ")");
// Set x, y and colors
var xScale = d3.scale.ordinal()
.domain(dataset[0].map(function(d) {
return d.x;
}))
.rangeRoundBands([0, width - margin.left]);
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) {
return d3.max(d, function(d) {
return d.y0 + d.y;
});
})])
.range([height, 0]);
var colors = ['#56a8f8', '#c34434'];
// Define and draw axes
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left')
.ticks(5)
.tickSize(0)
.tickFormat(function(d) {
return d;
});
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.tickSize(0)
.ticks(12) // this
.tickFormat(function(d) {
return d; // and this will not work with an ordinal scale
});
xAxis.tickValues(["0:00", "2:00", "4:00", "6:00", "8:00", "10:00", "12:00", "14:00", "16:00", "18:00", "20:00", "22:00"]);
svg.append('g')
.attr('class', 'y axis')
.call(yAxis);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
// Create groups for each series, rects for each segment
var groups = svg.selectAll('g.bar-stacks')
.data(dataset)
.enter().append('g')
.attr('class', function(d, i) {
return 'bar-stacks ' + d[i].type;
})
.classed('bar-stacks', true)
.style('fill', function(d, i) {
return colors[i];
});
var rect = groups.selectAll('rect')
.data(function(d) {
return d;
})
.enter()
.append('rect')
.attr('class', function(d, i) {
return 'stacks ' + d.type;
})
.classed('stacks', true)
.attr('id', function(d, i) {
return d.type + '_' + i;
})
.attr('x', function(d) {
return xScale(d.x);
})
.attr('y', function(d) {
return yScale(d.y0 + d.y);
})
.attr('height', function(d) {
return yScale(d.y0) - yScale(d.y0 + d.y);
})
.attr('width', xScale.rangeBand());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div>
<svg width="600" height="300"></svg>
</div>