Related
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.
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>
I am working on this slider effect using React Hooks and Redux. My codes are the following:
const Barchart = ({chartData}) => {
let newArray = []
let len = chartData.length
const [XArray,setXArray]=useState([chartData])
const [Yarray,setYArray]=useState(chartData[len-1].anArray) //so the initial state here should be an empty array
// const d3Container = useRef(null);
useEffect(()=>{
let len = chartData.length
console.log(chartData.length)
newArray = chartData[len-1].anArray
setYArray(newArray)
if(newArray.length!==0){
const height = 70 //height of the actual chart, different than the svg element
const width = 26.5*newArray.length //width of the actual chart, different than the svg element
const svg = d3.select('.svg-canvas')
svg.selectAll("*").remove()
var x = d3.scaleLinear().domain([0,7]).range([0,width])
var y = d3.scaleLinear().domain([0,d3.max(Yarray)]).range([height,0])
var xAxis = d3.axisBottom(x).ticks(8)
var yAxis = d3.axisLeft(y).ticks(5)
//locate the chart in the middle of the svg frame: 800/2 - width/2
var chartGroup = svg.append('g').attr('transform','translate('+(400 - width/2)+',300)')
chartGroup.selectAll("rect").data(Yarray).enter().append("rect")
.attr("height",(d,i)=>d*3)
.attr("width","15")
.attr("fill","blue")
.attr('x',(d,i)=>26.5*i)
.attr('y',(d,i)=>height-d*3)
chartGroup.selectAll('text').data(Yarray).enter().append("text")
.attr('font-size',15)
.attr('x',(d,i)=>26.5*i)
.attr('y',(d,i)=>height-5-d*3+2)
.text((d,i)=>d)
chartGroup.append('g').attr('class','axis y')
// .attr('transform','translate(500,76)')
.call(yAxis)
chartGroup.append('g').attr('class','axis x')
.attr('transform','translate(0,'+height+')')
.call(xAxis)
}
},[chartData])
const newArrayFunc = (a) =>{
setYArray(a)
}
return(
<div id='chart-container'>
<h3>Bar Chart</h3>
<svg className="svg-canvas" width="800px" height="400px"></svg>
</div>
)
}
const mapStateToProps = state => ({
chartData:state.chartChange
});
export default connect(mapStateToProps)(Barchart)
So as you see, even though I have setYArray in the useEffect, its asynchronous features prevent Yarray from being immediately updated. Whenever I have a new array coming from chartData, the d3 bar chart uses the previous array.
The objective I am trying to achieve here is whenever the array from chartData gets updated, the updated array will then be used in the d3 bar chart right after.
What should I do here?
Option 1
Continue using the same newArray value you updated state with
const Barchart = ({ chartData }) => {
let newArray = [];
let len = chartData.length;
const [XArray, setXArray] = useState([chartData]);
const [Yarray, setYArray] = useState(chartData[len - 1].anArray); //so the initial state here should be an empty array
// const d3Container = useRef(null);
useEffect(() => {
let len = chartData.length;
console.log(chartData.length);
newArray = chartData[len - 1].anArray;
setYArray(newArray);
if (newArray.length) { // <-- use in-scope newArray value
const height = 70; //height of the actual chart, different than the svg element
const width = 26.5 * newArray.length; //width of the actual chart, different than the svg element
const svg = d3.select(".svg-canvas");
svg.selectAll("*").remove();
var x = d3.scaleLinear().domain([0, 7]).range([0, width]);
var y = d3
.scaleLinear()
.domain([0, d3.max(newArray)]) // <-- use in-scope newArray value
.range([height, 0]);
var xAxis = d3.axisBottom(x).ticks(8);
var yAxis = d3.axisLeft(y).ticks(5);
//locate the chart in the middle of the svg frame: 800/2 - width/2
var chartGroup = svg
.append("g")
.attr("transform", "translate(" + (400 - width / 2) + ",300)");
chartGroup
.selectAll("rect")
.data(newArray) // <-- use in-scope newArray value
.enter()
.append("rect")
.attr("height", (d, i) => d * 3)
.attr("width", "15")
.attr("fill", "blue")
.attr("x", (d, i) => 26.5 * i)
.attr("y", (d, i) => height - d * 3);
chartGroup
.selectAll("text")
.data(newArray) // <-- use in-scope newArray value
.enter()
.append("text")
.attr("font-size", 15)
.attr("x", (d, i) => 26.5 * i)
.attr("y", (d, i) => height - 5 - d * 3 + 2)
.text((d, i) => d);
chartGroup
.append("g")
.attr("class", "axis y")
// .attr('transform','translate(500,76)')
.call(yAxis);
chartGroup
.append("g")
.attr("class", "axis x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
}
}, [chartData]);
const newArrayFunc = (a) => {
setYArray(a);
};
return (
<div id="chart-container">
<h3>Bar Chart</h3>
<svg className="svg-canvas" width="800px" height="400px"></svg>
</div>
);
};
Option 2
Update the state and use a second effect to update d3
const Barchart = ({ chartData }) => {
let newArray = [];
let len = chartData.length;
const [XArray, setXArray] = useState([chartData]);
const [Yarray, setYArray] = useState(chartData[len - 1].anArray); //so the initial state here should be an empty array
// const d3Container = useRef(null);
useEffect(() => {
let len = chartData.length;
console.log(chartData.length);
newArray = chartData[len - 1].anArray;
setYArray(newArray);
}, [chartData]);
useEffect(() => {
if (Yarray.length) {
const height = 70; //height of the actual chart, different than the svg element
const width = 26.5 * Yarray.length; //width of the actual chart, different than the svg element
const svg = d3.select(".svg-canvas");
svg.selectAll("*").remove();
var x = d3.scaleLinear().domain([0, 7]).range([0, width]);
var y = d3
.scaleLinear()
.domain([0, d3.max(Yarray)])
.range([height, 0]);
var xAxis = d3.axisBottom(x).ticks(8);
var yAxis = d3.axisLeft(y).ticks(5);
//locate the chart in the middle of the svg frame: 800/2 - width/2
var chartGroup = svg
.append("g")
.attr("transform", "translate(" + (400 - width / 2) + ",300)");
chartGroup
.selectAll("rect")
.data(Yarray)
.enter()
.append("rect")
.attr("height", (d, i) => d * 3)
.attr("width", "15")
.attr("fill", "blue")
.attr("x", (d, i) => 26.5 * i)
.attr("y", (d, i) => height - d * 3);
chartGroup
.selectAll("text")
.data(Yarray)
.enter()
.append("text")
.attr("font-size", 15)
.attr("x", (d, i) => 26.5 * i)
.attr("y", (d, i) => height - 5 - d * 3 + 2)
.text((d, i) => d);
chartGroup
.append("g")
.attr("class", "axis y")
// .attr('transform','translate(500,76)')
.call(yAxis);
chartGroup
.append("g")
.attr("class", "axis x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
}
}, [Yarray]);
const newArrayFunc = (a) => {
setYArray(a);
};
return (
<div id="chart-container">
<h3>Bar Chart</h3>
<svg className="svg-canvas" width="800px" height="400px"></svg>
</div>
);
};
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 included a legend to my Heat Map following this example, built with the threshold scale, and a Linear scale for its axis. The axis is created without issue, the problem is, I believe, in the code where I'm appending its rect elements.
Note: heatMap is a svg element.
The D3.js library version I'm using is D3.v5, in case this might be relevant to mention.
function (dataset,xScale,yScale,heatMap,w,h,p,colorScale,colorDomain)
{
let variance = dataset.monthlyVariance.map(function(val){
return val.variance;
});
const legendColors= [0,1.4,2.8,5.2,6.6,8,9.4,10.8,12.2,13.6].map((n)=> colorScale(n));
const minTemp = dataset.baseTemperature + Math.min.apply(null, variance);
const maxTemp = dataset.baseTemperature + Math.max.apply(null, variance);
const legendDomain= ((min,max,len)=>
{
let arr= [];
let step=(max-min)/len;
for (let i=0; i<len; i++)
{
let eq=min+i*step;
arr.push(eq.toFixed(2));
}
return arr;
})(minTemp,maxTemp,legendColors.length);
//The legend scaling threshold
let threshold= d3.scaleThreshold()
.domain(legendDomain)
.range(legendColors);
//Legend's axis scaling
let legendBar= d3.scaleLinear()
.domain([minTemp,maxTemp])
.range([0,w/3]);
**//Legend's axis**
let legendAxis= d3.axisBottom(legendBar)
.tickSize(6)
.tickValues(threshold.domain())
.tickFormat(d3.format(".1f"));
//append legend's element
let legendEle= heatMap.append("g")
.attr("id","legend")
.classed("legend",true)
.attr("transform", `translate(62,${h-p.bottom+60})`);
//Map legend
legendEle.append("g")
.selectAll("rect")
.data(threshold.range().map(function(color){
let d = threshold.invertExtent(color);
if(d[0] == null) d[0] = legendBar.domain()[0];
if(d[1] == null) d[1] = legendBar.domain()[1];
return d;
}))
.enter()
.append('rect')
.attr('x',(d)=> legendAxis(d[0]))
.attr('y', 0)
.attr('width', (d)=> legendAxis(d[1])-legendAxis(d[0]))
.attr('height', 20)
.attr("fill", function(d) { return threshold(d[0]); });
//append legend's axis
legendEle.append("g").call(legendAxis);
//rect grid map
heatMap.selectAll("rect")
.data(dataset.monthlyVariance)
.enter()
.append("rect")
.attr("class","cell")
.attr('data-month',function(d){
return d.month;
})
.attr('data-year',function(d){
return d.year;
})
.attr('data-temp',function(d){
return dataset.baseTemperature + d.variance;
})
.attr('x', (d)=> xScale(d.year))
.attr('y', (d)=> yScale(d.month))
.attr("width", function(d,i){
return 5;
})
.attr("height", function(d,i){
return yScale.bandwidth(d.month);
})
.attr("fill", (d)=> colorScale(dataset.baseTemperature + d.variance))
.attr("transform", `translate(62,0)`);
}