D3 Grouped Bar Chart From Arrays - javascript

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>

Related

D3.js rect no display on chart

I started the D3.js challenge on FreeCodeCamp, the problem is that I solved it with the chart but it only gives me a display on the rectum, only one with the width and height that it I put, I'll show the code below.
The entire code on
<script>
//set d3
var w = 1000, h = 500;
var padding = 50;
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h)
//title
svg.append('text')
.attr('x', w / 2)
.attr('y', 50)
.text('United States GDP')
fetch('https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json')
.then((result)=>result.json())
.then((data)=>{
var the_data = data['data']
//get vals
var get_max = d3.max(data['data'])
var get_mix = d3.min(data['data'])
//for x
var max_x = Number(get_max[0].split('-')[0])
var min_x = Number(get_mix[0].split('-')[0])
//for y
var max_y = get_max[1]
var min_y = get_mix[1]
var xScale = d3.scaleLinear()
.domain([min_x, max_x])
.range([padding, w-padding])
var yScale = d3.scaleLinear()
.domain([min_y, max_y])
.range([h-padding, padding])
//the_chars
for(var i in the_data){
var get_year = Number(the_data[i][0].split('-')[0])
the_data[i][0] = get_year
}
svg.selectAll('rect')
.data(the_data)
.enter()
.append('rect')
.attr("x", (d) => { xScale(d[0]) })
.attr('y', (d)=>{ yScale(d[1]) })
.attr("width", 200)
.attr("height", 20)
//axis
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
//display axis
svg.append("g")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
svg.append('g')
.attr('transform', 'translate(' + padding + ', 0)')
.call(yAxis)
})
Now, what I need to do to display the charts!
I mention that the script tags are embedded in the body
Problem: Arrow functions without a return value. Solution: Instead use an explicit or an implicit return.
.attr("x", (d) => { xScale(d[0]) }) // returns undefined
.attr("x", (d) => xScale(d[0])) // implicit return
.attr("x", (d) => { return xScale(d[0]) }) // explicit return
Problem: Fixed height value. Solution Evaluate the height of each based on the GDP value (d[1]) instead.
.attr('height', 20) // fixed height
.attr('height', d => yScale(min_y) - yScale(d[1]))
// subtract from min range to account for padding and inverted y coordinates in SVG
Full solution in this codepen

How do I adjust if/else nomenclature for d3 v7 (d3.nest to d3.groups)?

I'm having trouble adjusting if/else terminology to adjust for changes from d3.nest (v5) to d3.groups (v6 onward).
I've adjusted from
.data(function(d) { return d.values; })
to
.data(([, values ]) => values)
without any issues. However, the code I'm working off for a source has an if/else statement that is causing me some trouble. I need to adjust the following and I'm at a loss (selectLegis is a filtered dataset):
.data(selectLegis, function(d){
return d ? d.key : this.key;
})
What are d and this.key in the new nomenclature and how do I rewrite the code here?
Edit: added sample data below
year, state, wvalue, lvalue,
1980, AL, 0.4, Example1,
1980, AL, 0.3, Example2,
1984, AL, 0.2, Example1,
1984, AL, 0.7, Example2
Edit: reproducible example below
var margin = { top: 50, right: 50, bottom: 50, left: 50 }
var h = 500 - margin.top - margin.bottom
var w = 700 - margin.left - margin.right
var formatDecimal = d3.format('.2')
d3.csv('15/data.csv').then(function (data) {
// format the data
data.forEach(function(d) {
d.year = +d.year;
d.state = d.state;
d.wvalue = +d.wvalue;
d.lvalue = d.lvalue
});
// Scales
var x = d3.scaleLinear()
.range([0,w])
var y = d3.scaleLinear()
.range([h,0])
y.domain([
d3.min([0,d3.min(data,function (d) { return d.wvalue })]),
d3.max([0,d3.max(data,function (d) { return d.wvalue })])
]);
x.domain([1968, 2016])
// Define the line
var valueLine = d3.line()
.x(function(d) { return x(d.year); })
.y(function(d) { return y(d.wvalue); })
// Create the svg canvas in the "d3block" div
var svg = d3.select("#d3block")
.append("svg")
.style("width", w + margin.left + margin.right + "px")
.style("height", h + margin.top + margin.bottom + "px")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform","translate(" + margin.left + "," + margin.top + ")")
.attr("class", "svg");
//nest variable
var nest = d3.groups(data,
d => d.state, d => d.lvalue)
// X-axis
var xAxis = d3.axisBottom()
.scale(x)
.tickFormat(formatDecimal)
.ticks(7)
// Y-axis
var yAxis = d3.axisLeft()
.scale(y)
.tickFormat(formatDecimal)
.ticks(5)
// Create a dropdown
var legisMenu = d3.select("#legisDropdown")
legisMenu
.append("select")
.selectAll("option")
.data(nest)
.enter()
.append("option")
.attr("value", ([key, ]) => key)
.text(([key, ]) => key)
// Function to create the initial graph
var initialGraph = function(legis){
// Filter the data to include only state of interest
var selectLegis = nest.filter(([key, ]) => key == legis)
var selectLegisGroups = svg.selectAll(".legisGroups")
.data(selectLegis, function(d){
return d ? d.key : this.key;
})
.enter()
.append("g")
.attr("class", "legisGroups")
var initialPath = selectLegisGroups.selectAll(".line")
.data(([, values]) => values)
.enter()
.append("path")
initialPath
.attr("d", d => valueLine(d))
.attr("class", "line")
}
// Create initial graph
initialGraph("Alabama")
// Update the data
var updateGraph = function(legis){
// Filter the data to include only state of interest
var selectLegis = nest.filter(([key, ]) => key == legis)
// Select all of the grouped elements and update the data
var selectLegisGroups = svg.selectAll(".legisGroups")
.data(selectLegis)
// Select all the lines and transition to new positions
selectLegisGroups.selectAll("path.line")
.data(([, values]) => values)
.transition()
.duration(1000)
.attr("d", valueLine(([, values ]) => values))
}
// Run update function when dropdown selection changes
legisMenu.on('change', function(){
// Find which state was selected from the dropdown
var selectedLegis = d3.select(this)
.select("select")
.property("value")
// Run update function with the selected state
updateGraph(selectedLegis)
});
// X-axis
svg.append('g')
.attr('class','axis')
.attr('id','xAxis')
.attr('transform', 'translate(0,' + h + ')')
.call(xAxis)
.append('text') // X-axis Label
.attr('id','xAxisLabel')
.attr('fill','black')
.attr('y',-10)
.attr('x',w)
.attr('dy','.71em')
.style('text-anchor','end')
.text('')
// Y-axis
svg.append('g')
.attr('class','axis')
.attr('id','yAxis')
.call(yAxis)
.append('text') // y-axis Label
.attr('id', 'yAxisLabel')
.attr('fill', 'black')
.attr('transform','rotate(-90)')
.attr('x',0)
.attr('y',5)
.attr('dy','.71em')
.style('text-anchor','end')
.text('wvalue')
})
altocumulus was correct that the issue was how the data were being manipulated/called. I did more digging and found the answer here.
I had to replace the initial path attribute .attr("d", valueLine(([, values]) => values)) with .attr('d', (d) => valueLine(Array.from(d.values())[1])). I also had to replace the code further down within the updateGraph function under selectLegisGroups .attr for it to update properly.

D3 stacked bar graph, with each stack a different colour set by the different groups,

The problem
I am trying to get a stacked bar graph in D3 (v5) to have individually coloured bar for different groups (that I can do, Fig 1), with each stack a different colour (depending on the Fig 2).
I can't find a way to get the stack colouring (i.e. I want different shades of the Group colour to vary with the different stack height) example in Fig 3 (except I'd like the different groups to be different colours i.e. not repeating as they are here).
In the code examples I have provided there are two sets of data. A simple set, to help play with the data:
Animal,Group,A,B,C,D
Dog,Domestic,10,10,20,5
Cat,Domestic,20,5,10,10
Mouse,Pest,75,5,35,0
Lion,Africa,5,5,30,25
Elephant,Africa,15,15,20,20
Whale,Marine,35,20,10,45
Shark,Marine,45,55,0, 60
Fish,Marine,20, 5,30,10
And a bigger set that I am actually trying to use.
Here is the bl.ocks.org code that I'm trying to develop:
// set the dimensions and margins of the graph
const margin = {
top: 90,
right: 20,
bottom: 30,
left: 40
},
width = 960 - margin.left - margin.right,
height = 960 - margin.top - margin.bottom;
const svg = d3.select("#my_dataviz")
.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 y = d3.scaleBand()
.range([0, height])
.padding(0.1);
const x = d3.scaleLinear()
.range([0, width]);
const z = d3.scaleOrdinal()
.range(["none", "lightsteelblue", "steelblue", "darksteelblue"]);
d3.csv("https://gist.githubusercontent.com/JimMaltby/844ca313589e488b249b86ead0d621a9/raw/f328ad6291ffd3c9767a2dbdba5ce8ade59a5dfa/TimeBarDummyFormat.csv", d3.autoType, (d, i, columns) => {
var i = 3;
var t = 0;
for (; i < columns.length; ++i)
t += d[columns[i]] = +d[columns[i]];
d.total = t;
return d;
}
).then(function(data) {
const keys = data.columns.slice(3); // takes the column names, ignoring the first 3 items = ["EarlyMin","EarlyAll", "LateAll", "LateMax"]
// List of groups = species here = value of the first column called group -> I show them on the X axis
const Groups = d3.map(data, d => d.Group);
y.domain(data.map(d => d.Ser));
x.domain([2000, d3.max(data, d => d.total)]).nice();
z.domain(keys);
svg.append("g")
.selectAll("g")
.data(d3.stack().keys(keys)(data))
.enter().append("g")
.attr("fill", d => z(d.key)) //Color is assigned here because you want everyone for the series to be the same color
.selectAll("rect")
.data(d => d)
.enter()
.append("rect")
.attr("y", d => y(d.data.Ser))
.attr("x", d => x(d[0]))
.attr("height", y.bandwidth())
.attr("width", d => x(d[1]) - x(d[0]));
svg.append("g")
.attr("transform", "translate(0,0)")
.call(d3.axisTop(x));
svg.append("g")
.call(d3.axisLeft(y));
});
.bar {
fill: rgb(70, 131, 180);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="my_dataviz"></div>
JSFiddle:
https://jsfiddle.net/yq7bkvdL/
What I’ve tried
I feel like I am just missing something simple, but I am a coding noob and my coding is pretty rudimentary so I can't work it out.
I think I am either placing the fill attr in the wrong place. Or it's that I don't understand how to select the key in the nested/hierarchical data of d3.stack.
I have tried various things, all with no success:
1. Array of colours
I have tried writing a function to create an array of the colours, by iterating (with forEach) over the "key" and the "Group" values/names and concatenate them to create an array that I can use with the d3 Scale (ordinal) to select the correct colour. For example with the first dataset it would create an array ColoursID [DomesticA, DomesticB, DomesticC, DomesticD,PestA, PestB...] which then matches up to the colours in ColourS ["grey", "lightgreen", "green", "darkgreen", "yellow", ...]
Below is the attempt to do this, plus various other explorations commented out.
// List of groups = species here = value of the first column called group -> I show them on the X axis
const stack = d3.stack().keys(stackKeys)(data);
//const Groups = d3.map(data, d => d.Group);
//const ColourID = d3.map(data, d => d.Group && d.key);
// const stackID = stack.data // //stack[3].key = "D" // [2][6].data.Group = "Marine"
// const Test1 = Object.entries(stack).forEach(d => d.key);
const stackB = stack.forEach(function(d,i,j){
//var a = Object.keys(d)//= list of 3rd Level keys "0,..7,key,index"
//var a = Object.keys(d).forEach((key) => key) "undefined"
//var a = d.key //= "D" for all
d.forEach(function(d,i){
//var a = d.keys // = "function keys{} ([native code])"
//var a = Object.keys(d)
//var a = Object.keys(d) //= list of 2nd Level keys "0,1,data"
var b = data[i]["Group"]
d.forEach(function(d){
//var a = [d]["key"] // = "undefined"
//var a = Object.keys(d).forEach((key) => d[key]) // = "undefined"
var a = Object.keys(d) //= ""
// var a = d.keys //= "undefined"
data[i]["colourID"] = b + " a" + "-b " + a //d.key
})
})
});
console.log(stack)
svg.append("g")
.selectAll("g")
.data(stack)
.enter().append("g")
//.attr("fill", d => z(d.data.Group) ) //Color is assigned here because you want everyone for the series to be the same color
.selectAll("rect")
.data(d => d)
.enter().append("rect")
.attr("fill", d => colourZ(d.data.colourID)) //Color is assigned here because you want each Group to be a different colour **how do you vary the blocks?**
.attr("y", d => y(d.data.Animal) ) //uses the Column of data Animal to seperate bars
.attr("x", d=> x(d[0]) ) //
.attr("height", y.bandwidth() ) //
.attr("width", d=> x(d[1]) - x(d[0])); //
VizHub code: https://vizhub.com/JimMaltby/373f1dbb42ad453787dc0055dee7db81?file=index.js
2. Create a second colour scale:
I used the advice in here (d3.js-adding different colors to one bar in stacked bar chart), adding an if function to select a different colour scale, by adding this code:
//-----------------------------BARS------------------------------//
// append the rectangles for the bar chart
svg.append("g")
.selectAll("g")
.data(stack)
.enter().append("g")
//Color is assigned here because you want everyone for the series to be the same color
.selectAll("rect")
.data(d => d)
.enter().append("rect")
.attr("fill", d =>
d.data.Group == "Infrastructure"
? z2(d.key)
: z(d.key))
//.attr("class", "bar")
.attr("y", d => y(d.data.Ser) ) //Vert2Hor **x to y** **x(d.data.Ser) to y(d.data.Ser)**
.attr("x", d=> x(d[0]) ) //Vert2Hor **y to x** **y(d[1]) to x(d[0])**
.attr("height", y.bandwidth() ) //Vert2Hor **"width" to "height"** **x.bandwidth to y.bandwidth**
.attr("width", d=> x(d[1]) - x(d[0])); //Vert2Hor **"height" to "width"** **y(d[0]) - y(d[1]) to x(d[1]) - x(d[0])**
VizHub code
3. A big IF function in fill.
If this is the solution I would appreciate some advice on
a. making it work, then b. having a more efficient way of doing it
Here again it seems I am struggling to select the "key" of the "stack" data array. You'll note that I have been trying different ways to select the key in the code here, with no success :(.
.attr("fill", function(d,i, j) {
if (d.data.Group === "Domestic") {
if (d.key === "A") { return "none"}
else if (d.key === "B") { return "lightblue"}
else if (d.key === "C") { return "blue"}
else if (d.key === "D") { return "darkblue"}
else { return "forestgreen"}
}
else if (d.data.Group === "Pest") {
if (d.key === "A") { return "yellow"}
else if (d.key === "B") { return "lightorange"}
else if (d.key === "C") { return "orange"}
else if (d.key === "D") { return "darkorange"}
else { return "Brown"} //"yellow", "lightorange", "orange", ""
}
else if (d.data.Group === "Africa") {
if (Object.keys(root.data) === 1) { return "grey"}
else if (d.key === "B") { return "lightred"}
else if (d.key === "C") { return "red"}
else if (d.key === "D") { return "darkred"}
else { return "pink"}
}
else if (d.data.Group == "Marine") {
if (stackKeys == "A") { return "lightgrey"}
else if (stackKeys[d] == "B") { return "lightblue"}
else if (stackKeys[i] == "C") { return "blue"}
else if (stackKeys[3] == "D") { return "darkblue"}
else { return "steelblue"}
}
else { return "black" }
;})
Code in Viz Hub
If you want to vary the bar colours slightly if the bars are of smaller length, you can use fill-opacity and keep the fill the same! This way, the colours are less pronounced and lighter if the value is lighter.
Just create a new scale opacity with range [0.3, 1]. I chose 0.3 because 0 opacity means the bar is invisible, and you generally don't want that. I added a separate value d.height to denote the entire visible height of the bar, which is separate from start (but equivalent to d.Min + d.All + d.Max). Now, just apply the attribute to every bar and you're done.
You can choose to set the range to [0, 1] and not use d3.extent for the domain - that will probably lead to similar results, though there are some differences which you can spot with a thought experiment.
Right now the fill-opacity attribute is set on every bar. So the bars in the same stack have the same fill-opacity value. Note that this is entirely optional, though, and you can also apply distinct values.
// set the dimensions and margins of the graph
const margin = {
top: 90,
right: 20,
bottom: 30,
left: 40
},
width = 960 - margin.left - margin.right,
height = 960 - margin.top - margin.bottom;
const svg = d3.select("#my_dataviz")
.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 y = d3.scaleBand()
.range([0, height])
.padding(0.1);
const x = d3.scaleLinear()
.range([0, width]);
const z = d3.scaleOrdinal()
.range(["none", "lightsteelblue", "steelblue", "darksteelblue"]);
const opacity = d3.scaleLinear()
.range([0.3, 1]);
d3.csv("https://gist.githubusercontent.com/JimMaltby/844ca313589e488b249b86ead0d621a9/raw/f328ad6291ffd3c9767a2dbdba5ce8ade59a5dfa/TimeBarDummyFormat.csv", d3.autoType, (d, i, columns) => {
var i = 3;
var t = 0;
for (; i < columns.length; ++i)
t += d[columns[i]] = +d[columns[i]];
d.total = t;
d.height = d.total - d.Start;
return d;
}
).then(function(data) {
const keys = data.columns.slice(3); // takes the column names, ignoring the first 3 items = ["EarlyMin","EarlyAll", "LateAll", "LateMax"]
// List of groups = species here = value of the first column called group -> I show them on the X axis
const Groups = d3.map(data, d => d.Group);
y.domain(data.map(d => d.Ser));
x.domain([2000, d3.max(data, d => d.total)]).nice();
z.domain(keys);
opacity.domain(d3.extent(data, d => d.height));
console.log(opacity.domain());
svg.append("g")
.selectAll("g")
.data(d3.stack().keys(keys)(data))
.enter().append("g")
.attr("fill", d => z(d.key))
.selectAll("rect")
.data(d => d)
.enter()
.append("rect")
.attr("y", d => y(d.data.Ser))
.attr("x", d => x(d[0]))
.attr("height", y.bandwidth())
.attr("width", d => x(d[1]) - x(d[0]))
.attr("fill-opacity", d => opacity(d.data.height));
svg.append("g")
.attr("transform", "translate(0,0)")
.call(d3.axisTop(x));
svg.append("g")
.call(d3.axisLeft(y));
});
.bar {
fill: rgb(70, 131, 180);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="my_dataviz"></div>
Edit: knowing that you want to colour the bars by group, I would use the same logic, but make a few adjustments:
For one, I switched z to deal with fill-opacity (which I still use to accentuate the different groups), and use a new ordinal scale group for the colours. The key to that scale is simply the pre-existing field d.Group.
// set the dimensions and margins of the graph
const margin = {
top: 90,
right: 20,
bottom: 30,
left: 40
},
width = 960 - margin.left - margin.right,
height = 960 - margin.top - margin.bottom;
const svg = d3.select("#my_dataviz")
.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 y = d3.scaleBand()
.range([0, height])
.padding(0.1);
const x = d3.scaleLinear()
.range([0, width]);
const z = d3.scaleOrdinal()
.range([0.25, 0.5, 0.75, 1]);
const group = d3.scaleOrdinal()
.range(["darkgreen", "darkred", "steelblue", "purple"]);
d3.csv("https://gist.githubusercontent.com/JimMaltby/844ca313589e488b249b86ead0d621a9/raw/f328ad6291ffd3c9767a2dbdba5ce8ade59a5dfa/TimeBarDummyFormat.csv", d3.autoType, (d, i, columns) => {
var i = 3;
var t = 0;
for (; i < columns.length; ++i)
t += d[columns[i]] = +d[columns[i]];
d.total = t;
return d;
}
).then(function(data) {
const keys = data.columns.slice(3); // takes the column names, ignoring the first 3 items = ["EarlyMin","EarlyAll", "LateAll", "LateMax"]
y.domain(data.map(d => d.Ser));
x.domain([2000, d3.max(data, d => d.total)]).nice();
z.domain(keys);
group.domain(data.map(d => d.Group));
svg.append("g")
.selectAll("g")
.data(d3.stack().keys(keys)(data))
.enter().append("g")
.attr("fill-opacity", d => z(d.key))
.selectAll("rect")
.data(d => d)
.enter()
.append("rect")
.attr("y", d => y(d.data.Ser))
.attr("x", d => x(d[0]))
.attr("height", y.bandwidth())
.attr("width", d => x(d[1]) - x(d[0]))
.attr("fill", d => group(d.data.Group));
svg.append("g")
.attr("transform", "translate(0,0)")
.call(d3.axisTop(x));
svg.append("g")
.call(d3.axisLeft(y));
});
.bar {
fill: rgb(70, 131, 180);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="my_dataviz"></div>
Edit 2: if you want to specify the colours yourself, I would use a map of keys to colours:
// set the dimensions and margins of the graph
const margin = {
top: 90,
right: 20,
bottom: 30,
left: 40
},
width = 960 - margin.left - margin.right,
height = 960 - margin.top - margin.bottom;
const svg = d3.select("#my_dataviz")
.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 y = d3.scaleBand()
.range([0, height])
.padding(0.1);
const x = d3.scaleLinear()
.range([0, width]);
const z = d3.scaleOrdinal()
.range([0.25, 0.5, 0.75, 1]);
const group = d3.scaleOrdinal()
.range([
{ Start: "none", Min: "lightgreen", All: "green", Max: "darkgreen" },
{ Start: "none", Min: "indianred", All: "red", Max: "darkred" },
{ Start: "none", Min: "lightsteelblue", All: "steelblue", Max: "darksteelblue" }
]);
d3.csv("https://gist.githubusercontent.com/JimMaltby/844ca313589e488b249b86ead0d621a9/raw/f328ad6291ffd3c9767a2dbdba5ce8ade59a5dfa/TimeBarDummyFormat.csv", d3.autoType, (d, i, columns) => {
var i = 3;
var t = 0;
for (; i < columns.length; ++i)
t += d[columns[i]] = +d[columns[i]];
d.total = t;
return d;
}
).then(function(data) {
const keys = data.columns.slice(3); // takes the column names, ignoring the first 3 items = ["EarlyMin","EarlyAll", "LateAll", "LateMax"]
y.domain(data.map(d => d.Ser));
x.domain([2000, d3.max(data, d => d.total)]).nice();
z.domain(keys);
group.domain(data.map(d => d.Group));
svg.append("g")
.selectAll("g")
.data(d3.stack().keys(keys)(data))
.enter().append("g")
.each(function(e) {
d3.select(this)
.selectAll("rect")
.data(d => d)
.enter()
.append("rect")
.attr("y", d => y(d.data.Ser))
.attr("x", d => x(d[0]))
.attr("height", y.bandwidth())
.attr("width", d => x(d[1]) - x(d[0]))
.attr("fill", d => group(d.data.Group)[e.key]);
});
svg.append("g")
.attr("transform", "translate(0,0)")
.call(d3.axisTop(x));
svg.append("g")
.call(d3.axisLeft(y));
});
.bar {
fill: rgb(70, 131, 180);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="my_dataviz"></div>

Putting custom labels on D3 X axis graph

My making progress with my graph, it seems to mostly work. However I'm struggling to put my data values on the x axis. In this case, there should be 3 x axis labels, (test1, test2,test3).
// Data
var dataset = [{
name: "test1",
y: 0.1
},
{
name: "test2",
y: 0.6
},
{
name: "test3",
y: 0.9
}
];
It seems to just label it by how many entries there are (0,1,2) rather than using the name. What I tried was changing this:
var line = d3.line()
.x(function(d, i) {
return xScale(i);
To this (which I must admit was a bit of a guess).
var line = d3.line()
.x(function(d, i) {
return xScale(d.name);
Unfortunately that didn't work and I'm not sure what I can try next. Here is the full code if that helps.
http://jsfiddle.net/spadez/cfz3g4w2/
You are using the wrong scale for your x data. You have discrete data and want an ordinal scale.
var xScale = d3.scalePoint()
.domain(dataset.map(d => d.name)) // input is an array of names
.range([0, width]); // output
Running code:
// Data
var dataset = [{
name: "test1",
y: 0.1
},
{
name: "test2",
y: 0.6
},
{
name: "test3",
y: 0.9
}
];
// Count number of datapoints
var n = Object.keys(dataset).length;
// Find max of the data points for Y axis
var mymax = Math.max.apply(Math, dataset.map(function(o) {
return o.y;
}));
// 2. Use the margin convention practice
var margin = {
top: 50,
right: 50,
bottom: 50,
left: 50
},
width = window.innerWidth - margin.left - margin.right;
height = window.innerHeight - margin.top - margin.bottom;
// 5. X scale will use the index of our data
var xScale = d3.scalePoint()
.domain(dataset.map(d => d.name)) // input
.range([0, width]); // output
// 6. Y scale will use the randomly generate number
var yScale = d3.scaleLinear()
.domain([0, mymax]) // input
.range([height, 0]); // output
// 7. d3's line generator
var line = d3.line()
.x(function(d, i) {
return xScale(d.name);
}) // set the x values for the line generator
.y(function(d) {
return yScale(d.y);
}) // set the y values for the line generator
.curve(d3.curveMonotoneX) // apply smoothing to the line
// 1. Add the SVG to the page and employ #2
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 + ")");
// 3. Call the x axis in a group tag
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale)); // Create an axis component with d3.axisBottom
// 4. Call the y axis in a group tag
svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale)); // Create an axis component with d3.axisLeft
// 9. Append the path, bind the data, and call the line generator
svg.append("path")
.datum(dataset) // 10. Binds data to the line
.attr("class", "line") // Assign a class for styling
.attr("d", line); // 11. Calls the line generator
// 12. Appends a circle for each datapoint
svg.selectAll(".dot")
.data(dataset)
.enter().append("circle") // Uses the enter().append() method
.attr("class", "dot") // Assign a class for styling
.attr("cx", function(d, i) {
return xScale(d.name)
})
.attr("cy", function(d) {
return yScale(d.y)
})
.attr("r", 5)
.on("mouseover", function(a, b, c) {
console.log(a)
this.attr('class', 'focus')
})
.on("mouseout", function() {})
.on("mousemove", mousemove);
var focus = svg.append("g")
.attr("class", "focus")
.style("display", "none");
focus.append("circle")
.attr("r", 4.5);
focus.append("text")
.attr("x", 9)
.attr("dy", ".35em");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", function() {
focus.style("display", null);
})
.on("mouseout", function() {
focus.style("display", "none");
})
.on("mousemove", mousemove);
function mousemove() {
var x0 = x.invert(d3.mouse(this)[0]),
i = bisectDate(data, x0, 1),
d0 = data[i - 1],
d1 = data[i],
d = x0 - d0.date > d1.date - x0 ? d1 : d0;
focus.attr("transform", "translate(" + x(d.date) + "," + y(d.close) + ")");
focus.select("text").text(d);
}
.line {
fill: none;
stroke: #ffab00;
stroke-width: 3;
}
.overlay {
fill: none;
pointer-events: all;
}
/* Style the dots by assigning a fill and stroke */
.dot {
fill: #ffab00;
stroke: #fff;
}
.focus circle {
fill: none;
stroke: steelblue;
}
<!-- Load in the d3 library -->
<script src="https://d3js.org/d3.v5.min.js"></script>

How to move d3 ticks on y-axis

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

Categories

Resources