I have asked the below help with respect to creation of Bar chart with showing difference value percentage and line, below is the link:
How to create a % difference arrow line with value in a bar chart using D3.js
I have created this thread to seek help on making the same chart more dynamic and below are my try:
Making it dynamic in a way to fit in any resolution i.e making responsive (any width and height of Div can render the chart)
Tryin to do a reverse difference calculation.(currently it is calculating forward difference calculation and showing forward arrow, but with backward calculation need to show arrow in backward direction)
(Edited:
For example: the calculation is basically will start from last bar and then it will continue till the first bar. for example: (6453-7345)/7345 = -12% and (6453-5388)/6453 = -17%.)
Trying to get the difference by first bar and last bar value i.e:
Also below is the code:
var barData = [{
"Time": "2019",
"Value": 5388
},
{
"Time": "2020",
"Value": 6453
},
{
"Time": "2021",
"Value": 4345
},
{
"Time": "2022",
"Value": 7345
},
{
"Time": "2023",
"Value": 5345
}];
// Consider this width and Height are dynamic for div "graphID" because I am trying to responsive design
const divWidth = 700,
divHeight = 700;
//Adding Margin to Viz Area
var margin = {top: 30, right: 50, bottom: 0, left: 50},
width = parseInt(divWidth,10) - margin.left - margin.right,
height = parseInt(divHeight,10) - margin.top - margin.bottom;
//To add svg in the visualization node i.e Dome node
const svg = d3.select("#graphID").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 + ")");
//Adding x axis width i.e based on Viz Width
const xScale = d3.scaleBand()
.domain(barData.map(d => d.Time))
.range([0, width]);
const xAxis = d3.axisBottom(xScale);
//Adding g attribute to svg for x axis
svg.append('g')
//.attr('transform', 'translate(20,170)')
.attr("transform", "translate(10," + (height - 50) + ")")
.call(xAxis);
/*
//To get the Max value from an json object
const maxVal = barData.reduce((acc, shot) => acc = acc > shot.Value ? acc : shot.Value, 0);
const valMax = Math.max.apply(barData, barData.map(function(o) { return o.Value; }));
alert(valMax);
const yScale = d3.scaleLinear()
.domain([0, maxVal+(maxVal/2)])
.range([150, 0]);
*/
const yAxisMax = Math.max.apply(barData, barData.map(function(o) { return o.Value; }));
const yScale = d3.scaleLinear()
.domain([0, yAxisMax+(yAxisMax/2)])
.range([height, 0]);
const yAxis = d3.axisLeft(yScale).ticks(4);
svg.append('g')
//.attr('transform', 'translate(50,170)')
.attr("transform", "translate(10,-50)")
//.attr("transform", "translate(20," + (height - 50) + ")")
.call(yAxis);
const bars = svg.selectAll('g.bar')
.data(barData)
.enter()
.append('g')
.classed('bar', true)
.attr('transform', d => `translate(${xScale(d.Time) + 50 + xScale.bandwidth() / 20}, 170)`)
bars.append('rect')
.attr('x', -10)
.attr('width', 40)
.attr('y', d => -height + yScale(d.Value))
.attr('height', d => yScale(d.Value) )
.style('fill', 'blue')
bars.append('text')
.text(d => d.Value)
.attr('text-anchor', 'middle')
.attr('y', d => -(height - 55) + yScale(d.Value))
bars.filter((d, i) => i < barData.length - 1)
.append('path')
.attr('d', (d, i) => `M 5,${-170 + yScale(d.Value)} V ${-210 + yScale(d.Value)} H ${xScale.bandwidth() - 5} V ${-180 + yScale(barData[i + 1].Value)}`)
.style('stroke', 'gray')
.style('fill', 'none')
.attr('marker-end', 'url(#arrowhead)')
bars.filter((d, i) => i < barData.length - 1)
.append('rect')
.attr('x', 15)
.attr('y', d => -220 + yScale(d.Value))
.attr('width', xScale.bandwidth() - 30)
.attr('height', 20)
.attr('rx', 10)
.style('fill', 'white')
.style('stroke', 'gray');
bars.filter((d, i) => i < barData.length - 1)
.append('text')
.text((d, i) => `${barData[i + 1].Value > d.Value ? '+' : '-'}${Math.round((barData[i + 1].Value / d.Value * 100) - 100)}%`)
.attr('x', xScale.bandwidth() / 2)
.attr('y', d => -207 + yScale(d.Value))
.attr('text-anchor', 'middle')
.style('fill', 'black');
#graphID
{
width:500px;
height:700px
}
text {
font-size: 12px;
font-family: "Ubuntu";
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="graphID" width=500 height=700>
</div>
Here is a solution for responsive sizing:
Get dimentions of your parent container:
const container = d3.select('#graph');
const divWidth = parseInt(container.style('width'));
const divHeight = parseInt(container.style('height'));
Create SVG with the DIV dimentions:
const svg = container.append("svg")
.attr("width", divWidth)
.attr("height", divHeight);
Calculate the chart size using the margins:
const margin = {top: 30, right: 50, bottom: 50, left: 50};
const width = divWidth - margin.left - margin.right;
const height = divHeight - margin.top - margin.bottom;
Add a parent <g> and position it using the margins. All other elements should be placed under:
const svgG = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
var barData = [{
"Time": "2019",
"Value": 5388
},
{
"Time": "2020",
"Value": 6453
},
{
"Time": "2021",
"Value": 4345
},
{
"Time": "2022",
"Value": 7345
},
{
"Time": "2023",
"Value": 5345
}];
const container = d3.select('#graph');
const divWidth = parseInt(container.style('width'));
const divHeight = parseInt(container.style('height'));
// Consider this width and Height are dynamic for div "graphID" because I am trying to responsive design
const margin = {top: 30, right: 50, bottom: 50, left: 50};
const width = divWidth - margin.left - margin.right;
const height = divHeight - margin.top - margin.bottom;
//To add svg in the visualization node i.e Dome node
const svg = container.append("svg")
.attr("width", divWidth)
.attr("height", divHeight);
const svgG = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const xScale = d3.scaleBand()
.domain(barData.map(d => d.Time))
.range([0, width]);
const xAxis = d3.axisBottom(xScale);
//Adding g attribute to svg for x axis
svgG.append('g')
.attr("transform", `translate(0,${height})`)
.call(xAxis);
const yAxisMax = barData.reduce((max, item) => Math.max(max, item.Value), 0) * 1.5;
const yScale = d3.scaleLinear()
.domain([0, yAxisMax])
.range([height, 0]);
const yAxis = d3.axisLeft(yScale).ticks(4);
svgG.append('g')
.call(yAxis);
const bars = svgG.selectAll('g.bar')
.data(barData)
.enter()
.append('g')
.classed('bar', true)
.attr('transform', d => `translate(${xScale(d.Time) + xScale.bandwidth() / 2}, 0)`)
bars.append('rect')
.attr('x', -20)
.attr('width', 40)
.attr('y', d => yScale(d.Value))
.attr('height', d => height - yScale(d.Value) )
.style('fill', 'blue')
bars.append('text')
.text(d => d.Value)
.attr('text-anchor', 'middle')
.attr('y', d => yScale(d.Value))
.attr('dy', -5)
bars.filter((d, i) => i < barData.length - 1)
.append('path')
.attr('d', (d, i) => `M 5,${yScale(d.Value) - 20} V ${Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 60} H ${xScale.bandwidth() - 5} V ${yScale(barData[i + 1].Value) - 20}`)
.style('stroke', 'gray')
.style('fill', 'none')
.attr('marker-end', 'url(#arrowhead)')
bars.filter((d, i) => i < barData.length - 1)
.append('rect')
.attr('x', 15)
.attr('y', (d, i) => Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 70)
.attr('width', xScale.bandwidth() - 30)
.attr('height', 20)
.attr('rx', 10)
.style('fill', 'white')
.style('stroke', 'gray');
bars.filter((d, i) => i < barData.length - 1)
.append('text')
.text((d, i) => `${barData[i + 1].Value > d.Value ? '+' : '-'}${Math.round((barData[i + 1].Value / d.Value * 100) - 100)}%`)
.attr('x', xScale.bandwidth() / 2)
.attr('y', (d, i) => Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 56)
.attr('text-anchor', 'middle')
.style('fill', 'black');
#graph
{
width:500px;
height:700px
}
text {
font-size: 12px;
font-family: "Ubuntu";
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="graph">
</div>
Related
I am completely new at D3. I have managed to create a basic bar chart but I am having trouble adding the data labels.. I tried adding in some things that I researched but it seems to take away the bar chart when I do this.. I still see the bars but not the drawing.
Can some please assist on where I would start the svg.selectall(text)?
var data = [
{ ScrumTeamName: 'Name1', score: 4},
{ ScrumTeamName: 'Name2', score: 6},
{ ScrumTeamName: 'Name3', score: 0},
];
var width = 800;
var height = 400;
var margin = { top: 50, bottom: 50, left: 50, right: 50 };
var svg = d3.select("#d3-container")
.append('svg')
.attr('height', height - margin.top - margin.bottom)
.attr('width', width - margin.left - margin.right)
.attr('viewBox', [0, 0, width, height]);
var x = d3.scaleBand()
.domain(d3.range(data.length))
.range([margin.left, width - margin.right])
.padding(0.1);
var y = d3.scaleLinear()
.domain([0, 20])
.range([height - margin.bottom, margin.top]);
svg
.append('g')
.attr('fill', 'royalblue')
.selectAll('rect')
.data(data.sort((a, b) => d3.descending(a.score, b.score)))
.join('rect')
.attr('x', (d, i) => x(i))
.attr('y', (d) => y(d.score))
.attr('height', d => y(0) - y(d.score))
.attr('width', x.bandwidth())
.attr('class', 'rectangle')
function xAxis (g){
g.attr('transform', `translate(0, ${height - margin.bottom})`)
g.call(d3.axisBottom(x).tickFormat(i => data[i].ScrumTeamName))
.attr('font-size', '20px')
}
function yAxis (g){
g.attr('transform', `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y).ticks(null, data.format))
.attr('font-size', '20px')
}
svg.append('g').call(yAxis);
svg.append('g').call(xAxis);
svg.node();
Here's how I do a simple bar chart label. You can just add this section:
svg.append('g')
.attr('fill', 'royalblue')
.selectAll('text')
.data(data.sort((a, b) => d3.descending(a.score, b.score)))
.join('text')
.text((d) => d.score)
.attr('x', (d, i) => x(i) - x.bandwidth()/2) // center of the bar
.attr('y', (d) => y(d.score) - 2) //lift off the bar
.style('text-anchor','middle')
I am have a requirement where the bar chart will containing max 4 bars and I am trying to add a filter on a bar chart where I can pick a particular bar from 4 bars and add a path to display the data.
Example
Different Paths:
Bar1 --> Bar3
Bar2 --> Bar4
Bar3 --> Bar2
Bar4 --> Bar1
Please help me to understand how can I pick any specific bar and set a path to another bar like above example?
I have created different path code for the same but not able to get how I can get the target bar to create the proper path.
Below is my code:
var barData = [{
"Time": "Bar1",
"Value": 5388
},
{
"Time": "Bar2",
"Value": 6453
},
{
"Time": "Bar3",
"Value": 3345
},
{
"Time": "Bar4",
"Value": 5345
}];
const container = d3.select('#graph');
const divWidth = parseInt(container.style('width'));
const divHeight = parseInt(container.style('height'));
// Consider this width and Height are dynamic for div "graphID" because I am trying to responsive design
const margin = {top: 30, right: 50, bottom: 50, left: 50};
const width = divWidth - margin.left - margin.right;
const height = divHeight - margin.top - margin.bottom;
//To add svg in the visualization node i.e Dome node
const svg = container.append("svg")
.attr("width", divWidth)
.attr("height", divHeight);
const svgG = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
//To add tooltip for bar
var tooltip = d3.select("body").append("div").attr("class", "toolTip");
const defs = svg.append("defs");
const marker = defs.append("marker")
.attr("id","arrowhead")
.attr("markerWidth","10")
.attr("markerHeight","7")
.attr("refX","0")
.attr("refY","3.5")
.attr("orient","auto")
const polygon = marker.append("polygon")
.attr("fill","gray")
.attr("points","0 0, 10 3.5, 0 7")
const xScale = d3.scaleBand()
.domain(barData.map(d => d.Time))
.range([0, width+margin.right]);
const xAxis = d3.axisBottom(xScale);
//Adding g attribute to svg for x axis
svgG.append('g')
.attr("transform", `translate(0,${height})`)
.call(xAxis);
const yAxisMax = barData.reduce((max, item) => Math.max(max, item.Value), 0) * 1.5;
const yScale = d3.scaleLinear()
.domain([0, yAxisMax])
.range([height, 0]);
const yAxis = d3.axisLeft(yScale).ticks(4);
svgG.append('g')
.call(yAxis);
const bars = svgG.selectAll('g.bar')
.data(barData)
.enter()
.append('g')
.classed('bar', true)
.attr('transform', d => `translate(${xScale(d.Time) + xScale.bandwidth() / 2}, 0)`);
/*
const staticColor = "steelblue",
highlightColor = "orange";
var sheet = document.createElement('style')
sheet.innerHTML = ".bar {fill: "+staticColor+"} .highlight {fill:"+highlightColor+"}";
document.body.appendChild(sheet);
*/
bars.append('rect')
.attr('x', -20)
.attr('width', 40)
.attr('y', d => yScale(d.Value))
.attr('height', d => height - yScale(d.Value) )
.attr('fill', 'blue')
//.attr("class", "bar")
.on("mousemove", onMouseOver)
.on("mouseout", onMouseOut);
function onMouseOver(d,i)
{
tooltip
.style("left", d3.event.pageX - 50 + "px")
.style("top", d3.event.pageY - 70 + "px")
.style("display", "inline-block")
.html("Year: " + (d.Time) + "<br>" + "Value: " + (d.Value));
d3.select(this).attr('fill', "#eec42d");
//d3.select(this).attr('class', 'highlight');
//this.setState({ fillColour: 'green' });
}
function onMouseOut(d,i)
{
tooltip.style("display", "none");
d3.select(this).attr('fill', "blue");
//d3.select(this).attr('class', 'bar');
//this.setState({ fillColour: 'blue' });
}
bars.append('text')
.text(d => d.Value)
.attr('text-anchor', 'middle')
.attr('y', d => yScale(d.Value))
.attr('dy', -5)
;
//path from Bar1 to Bar3
bars.filter((d, i) => i == 0 )
.append('path')
.attr('d', (d, i) => `M 5,${yScale(d.Value) - 20} V ${Math.min(yScale(d.Value), yScale(barData[i+2].Value)) - 60} H ${xScale.bandwidth() - 5} V ${yScale(barData[i+2].Value) - 25}`)
.style('stroke', 'gray')
.style('fill', 'none')
.attr('marker-end', 'url(#arrowhead)')
//path from Bar2 to Bar4
bars.filter((d, i) => i == 1 )
.append('path')
.attr('d', (d, i) => `M 5,${yScale(d.Value) - 20} V ${Math.min(yScale(d.Value), yScale(barData[i+2].Value)) - 60} H ${xScale.bandwidth() - 5} V ${yScale(barData[i+2].Value) - 25}`)
.style('stroke', 'gray')
.style('fill', 'none')
.attr('marker-end', 'url(#arrowhead)')
//path from Bar3 to Bar2
bars.filter((d, i) => i == 2 )
.append('path')
.attr('d', (d, i) => `M 5,${yScale(d.Value) - 20} V ${Math.min(yScale(d.Value), yScale(barData[i-1].Value)) - 60} H ${xScale.bandwidth() - 5} V ${yScale(barData[i-1].Value) - 25}`)
.style('stroke', 'gray')
.style('fill', 'none')
.attr('marker-end', 'url(#arrowhead)')
//path from Bar3 to Bar2
bars.filter((d, i) => i == 3 )
.append('path')
.attr('d', (d, i) => `M 5,${yScale(d.Value) - 20} V ${Math.min(yScale(d.Value), yScale(barData[0].Value)) - 60} H ${xScale.bandwidth() - 5} V ${yScale(barData[0].Value) - 25}`)
.style('stroke', 'gray')
.style('fill', 'none')
.attr('marker-end', 'url(#arrowhead)')
/*
bars.filter((d, i) => i < barData.length - 1)
.append('rect')
.attr('x', 15)
.attr('y', (d, i) => Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 70)
.attr('width', xScale.bandwidth() - 30)
.attr('height', 20)
.attr('rx', 10)
.style('fill', 'white')
.style('stroke', 'gray');
bars.filter((d, i) => i < barData.length - 1)
.append('text')
.text((d, i) => `${barData[i + 1].Value > d.Value ? '+' : ''}${Math.round((barData[i + 1].Value / d.Value * 100) - 100)}%`)
.attr('x', xScale.bandwidth() / 2)
.attr('y', (d, i) => Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 56)
.attr('text-anchor', 'middle')
.style('fill', 'black');
*/
#graph {
width: 600px;
height: 400px;
}
text {
font-size: 12px;
font-family: "Ubuntu";
}
.toolTip {
position: absolute;
display: none;
min-width: 80px;
height: auto;
background: none repeat scroll 0 0 #ffffff;
border: 1px solid #6F257F;
padding: 5px;
text-align: left;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="graph">
</div>
Here is an example with targets:
var barData = [{
"Time": "Bar1",
"Value": 5388,
"Target": 2
},
{
"Time": "Bar2",
"Value": 6453,
"Target": 0,
},
{
"Time": "Bar3",
"Value": 3345,
"Target": 3,
},
{
"Time": "Bar4",
"Value": 5345,
"Target": 1,
}];
const container = d3.select('#graph');
const divWidth = parseInt(container.style('width'));
const divHeight = parseInt(container.style('height'));
const margin = {top: 30, right: 50, bottom: 50, left: 50};
const width = divWidth - margin.left - margin.right;
const height = divHeight - margin.top - margin.bottom;
//To add svg in the visualization node i.e Dome node
const svg = container.append("svg")
.attr("width", divWidth)
.attr("height", divHeight);
const svgG = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
//To add tooltip for bar
var tooltip = d3.select("body").append("div").attr("class", "toolTip");
const defs = svg.append("defs");
const marker = defs.append("marker")
.attr("id","arrowhead")
.attr("markerWidth","10")
.attr("markerHeight","7")
.attr("refX","0")
.attr("refY","3.5")
.attr("orient","auto")
const polygon = marker.append("polygon")
.attr("fill","gray")
.attr("points","0 0, 10 3.5, 0 7")
const xScale = d3.scaleBand()
.domain(barData.map(d => d.Time))
.range([0, width+margin.right]);
const xAxis = d3.axisBottom(xScale);
//Adding g attribute to svg for x axis
svgG.append('g')
.attr("transform", `translate(0,${height})`)
.call(xAxis);
const yAxisMax = barData.reduce((max, item) => Math.max(max, item.Value), 0) * 1.5;
const yScale = d3.scaleLinear()
.domain([0, yAxisMax])
.range([height, 0]);
const yAxis = d3.axisLeft(yScale).ticks(4);
svgG.append('g')
.call(yAxis);
const bars = svgG.selectAll('g.bar')
.data(barData)
.enter()
.append('g')
.classed('bar', true)
.attr('transform', d => `translate(${xScale(d.Time) + xScale.bandwidth() / 2}, 0)`);
/*
const staticColor = "steelblue",
highlightColor = "orange";
var sheet = document.createElement('style')
sheet.innerHTML = ".bar {fill: "+staticColor+"} .highlight {fill:"+highlightColor+"}";
document.body.appendChild(sheet);
*/
bars.append('rect')
.attr('x', -20)
.attr('width', 40)
.attr('y', d => yScale(d.Value))
.attr('height', d => height - yScale(d.Value) )
.attr('fill', 'blue')
.on("mousemove", onMouseOver)
.on("mouseout", onMouseOut);
function onMouseOver(d,i)
{
tooltip
.style("left", d3.event.pageX - 50 + "px")
.style("top", d3.event.pageY - 70 + "px")
.style("display", "inline-block")
.html("Year: " + (d.Time) + "<br>" + "Value: " + (d.Value));
d3.select(this).attr('fill', "#eec42d");
//d3.select(this).attr('class', 'highlight');
//this.setState({ fillColour: 'green' });
}
function onMouseOut(d,i)
{
tooltip.style("display", "none");
d3.select(this).attr('fill', "blue");
}
bars.append('text')
.text(d => d.Value)
.attr('text-anchor', 'middle')
.attr('y', d => yScale(d.Value))
.attr('dy', -5)
;
const topPosition = i => yScale(9000) + i * 15;
const pathBetweenBars = (d, i) => {
const delta = d.Target - i;
const targetValue = barData[d.Target].Value;
const targetX = delta * xScale.bandwidth() - 5;
const sourceY = yScale(d.Value);
const targetY = yScale(targetValue);
const topY = topPosition(i);
return `M 5,${sourceY - 20} V ${topY} H ${targetX} V ${targetY - 25}`;
};
const LABEL_WIDTH = 50;
const midPosition = (d, i) => {
const delta = d.Target - i;
return delta * xScale.bandwidth() / 2;
}
bars.filter(d => d.Target != null)
.append('path')
.attr('d', (d, i) => pathBetweenBars(d, i))
.style('stroke', 'gray')
.style('fill', 'none')
.attr('marker-end', 'url(#arrowhead)')
bars.filter((d) => d.Target != null)
.append('rect')
.attr('x', (d, i) => midPosition(d, i) - LABEL_WIDTH / 2)
.attr('y', (d, i) => topPosition(i) - 10)
.attr('width', LABEL_WIDTH)
.attr('height', 20)
.attr('rx', 10)
.style('fill', 'white')
.style('stroke', 'gray');
bars.filter((d, i) => d.Target != null)
.append('text')
.text((d, i) => `${barData[d.Target].Value > d.Value ? '+' : ''}${Math.round((barData[d.Target].Value / d.Value * 100) - 100)}%`)
.attr('x', (d, i) => midPosition(d, i))
.attr('y', (d, i) => topPosition(i) + 3)
.attr('text-anchor', 'middle')
.style('fill', 'black');
#graph {
width: 600px;
height: 400px;
}
text {
font-size: 12px;
font-family: "Ubuntu";
}
.toolTip {
position: absolute;
display: none;
min-width: 80px;
height: auto;
background: none repeat scroll 0 0 #ffffff;
border: 1px solid #6F257F;
padding: 5px;
text-align: left;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="graph">
</div>
This thread is a continuation of below:
Trying to make dynamic D3 Chart with Bar and Difference line/value
I was trying to test the chart using negative values (i.e -ve) but it is not working properly. I made some changes in the yAxis scale Domain by passing dynamic min value instead of static "0" value.
At "0" there should be a dotted line and the -ve value should display down of that and +ve (positive values) values start from "0" and height till its value.
Please help me to solve the issue.
Below is the Code:
var barData = [{
"Time": "2019",
"Value": 5388
},
{
"Time": "2020",
"Value": 6453
},
{
"Time": "2021",
"Value": -4345
},
{
"Time": "2022",
"Value": 7345
},
{
"Time": "2023",
"Value": 8345
},
{
"Time": "2024",
"Value": 5345
},
{
"Time": "2025",
"Value": 6345
},
{
"Time": "2026",
"Value": 3345
}
];
const container = d3.select('#graph');
const divWidth = parseInt(container.style('width'));
const divHeight = parseInt(container.style('height'));
// Consider this width and Height are dynamic for div "graphID" because I am trying to responsive design
const margin = {
top: 30,
right: 50,
bottom: 50,
left: 50
};
const width = divWidth - margin.left - margin.right;
const height = divHeight - margin.top - margin.bottom;
//To add svg in the visualization node i.e Dome node
const svg = container.append("svg")
.attr("width", divWidth)
.attr("height", divHeight)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
//To add tooltip for bar
var tooltip = d3.select("body").append("div").attr("class", "toolTip");
const defs = svg.append("defs");
const marker = defs.append("marker")
.attr("id", "arrowhead")
.attr("markerWidth", "10")
.attr("markerHeight", "7")
.attr("refX", "0")
.attr("refY", "3.5")
.attr("orient", "auto")
const polygon = marker.append("polygon")
.attr("fill", "gray")
.attr("points", "0 0, 10 3.5, 0 7")
const xScale = d3.scaleBand()
.domain(barData.map(d => d.Time))
.range([0, width + margin.right]);
const xAxis = d3.axisBottom(xScale);
//Adding g attribute to svg for x axis
svg.append('g')
.attr("transform", `translate(0,${height})`)
.call(xAxis);
const yAxisMax = barData.reduce((max, item) => Math.max(max, item.Value), 0) * 1.5;
const yAxisMin = barData.reduce((min, item) => Math.min(min, item.Value), 0) * 1.5;
const yScale = d3.scaleLinear()
.domain([yAxisMin, yAxisMax])
.range([height, 0]);
const yAxis = d3.axisLeft(yScale).ticks(4);
svg.append('g')
.call(yAxis);
const bars = svg.selectAll('g.bar')
.data(barData)
.enter()
.append('g')
.classed('bar', true)
.attr('transform', d => `translate(${xScale(d.Time) + xScale.bandwidth() / 2}, 0)`);
/*
const staticColor = "steelblue",
highlightColor = "orange";
var sheet = document.createElement('style')
sheet.innerHTML = ".bar {fill: "+staticColor+"} .highlight {fill:"+highlightColor+"}";
document.body.appendChild(sheet);
*/
bars.append('rect')
.attr('x', -20)
.attr('width', 40)
.attr('y', d => yScale(d.Value))
.attr('height', d => height - yScale(d.Value))
.attr('fill', 'blue')
//.attr("class", "bar")
.on("mousemove", onMouseOver)
.on("mouseout", onMouseOut);
function onMouseOver(d, i) {
tooltip
.style("left", d3.event.pageX - 50 + "px")
.style("top", d3.event.pageY - 70 + "px")
.style("display", "inline-block")
.html("Year: " + (d.Time) + "<br>" + "Value: " + (d.Value));
d3.select(this).attr('fill', "#eec42d");
//d3.select(this).attr('class', 'highlight');
//this.setState({ fillColour: 'green' });
}
function onMouseOut(d, i) {
tooltip.style("display", "none");
d3.select(this).attr('fill', "blue");
//d3.select(this).attr('class', 'bar');
//this.setState({ fillColour: 'blue' });
}
bars.append('text')
.text(d => d.Value)
.attr('text-anchor', 'middle')
.attr('y', d => yScale(d.Value))
.attr('dy', -5);
bars.filter((d, i) => i < barData.length - 1)
.append('path')
.attr('d', (d, i) => `M 5,${yScale(d.Value) - 20} V ${Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 60} H ${xScale.bandwidth() - 5} V ${yScale(barData[i + 1].Value) - 25}`)
.style('stroke', 'gray')
.style('fill', 'none')
.attr('marker-end', 'url(#arrowhead)')
bars.filter((d, i) => i < barData.length - 1)
.append('rect')
.attr('x', 15)
.attr('y', (d, i) => Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 70)
.attr('width', xScale.bandwidth() - 30)
.attr('height', 20)
.attr('rx', 10)
.style('fill', 'white')
.style('stroke', 'gray');
bars.filter((d, i) => i < barData.length - 1)
.append('text')
.text((d, i) => `${barData[i + 1].Value > d.Value ? '+' : ''}${Math.round((barData[i + 1].Value / d.Value * 100) - 100)}%`)
.attr('x', xScale.bandwidth() / 2)
.attr('y', (d, i) => Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 56)
.attr('text-anchor', 'middle')
.style('fill', 'black');
#graph {
width: 600px;
height: 500px;
}
text {
font-size: 12px;
font-family: "Ubuntu";
}
.toolTip {
position: absolute;
display: none;
min-width: 80px;
height: auto;
background: none repeat scroll 0 0 #ffffff;
border: 1px solid #6F257F;
padding: 5px;
text-align: left;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="graph">
</div>
Try this:
const barData = [{
"Time": "2019",
"Value": 5388
},
{
"Time": "2020",
"Value": 6453
},
{
"Time": "2021",
"Value": -4345
},
{
"Time": "2022",
"Value": 7345
},
{
"Time": "2023",
"Value": 8345
},
{
"Time": "2024",
"Value": 5345
},
{
"Time": "2025",
"Value": 6345
},
{
"Time": "2026",
"Value": 3345
}
];
const container = d3.select('#graph');
const divWidth = parseInt(container.style('width'));
const divHeight = parseInt(container.style('height'));
// Consider this width and Height are dynamic for div "graphID" because I am trying to responsive design
const margin = {
top: 50,
right: 50,
bottom: 50,
left: 50
};
const width = divWidth - margin.left - margin.right;
const height = divHeight - margin.top - margin.bottom;
//To add svg in the visualization node i.e Dome node
const svg = container.append("svg")
.attr("width", divWidth)
.attr("height", divHeight)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
//To add tooltip for bar
const tooltip = d3.select("body").append("div").attr("class", "toolTip");
const defs = svg.append("defs");
const marker = defs.append("marker")
.attr("id", "arrowhead")
.attr("markerWidth", "10")
.attr("markerHeight", "7")
.attr("refX", "0")
.attr("refY", "3.5")
.attr("orient", "auto")
const polygon = marker.append("polygon")
.attr("fill", "gray")
.attr("points", "0 0, 10 3.5, 0 7")
const xScale = d3.scaleBand()
.domain(barData.map(d => d.Time))
.range([0, width + margin.right]);
const xAxis = d3.axisBottom(xScale)
//Adding g attribute to svg for x axis
const yAxisMax = barData.reduce((max, item) => Math.max(max, item.Value), 0) * 1.5;
const yAxisMin = barData.reduce((min, item) => Math.min(min, item.Value), 0) * 1.5;
const yAxisRange = Math.max(yAxisMax, Math.abs(yAxisMin));
const yScale = d3.scaleLinear()
.domain([-yAxisRange, yAxisRange])
.range([height, 0]);
const yAxis = d3.axisLeft(yScale).ticks(4);
svg.append('g')
.call(yAxis);
const bars = svg.selectAll('g.bar')
.data(barData)
.enter()
.append('g')
.classed('bar', true)
.attr('transform', d => `translate(${xScale(d.Time) + xScale.bandwidth() / 2}, 0)`);
bars.append('rect')
.attr('x', -20)
.attr('width', 40)
.attr('y', d => Math.min(yScale(d.Value), height/2))
.attr('height', d => d.Value > 0 ?
(height/2 - yScale(d.Value)) :
(yScale(d.Value) - height/2)
)
.attr('fill', 'blue')
.on("mousemove", onMouseOver)
.on("mouseout", onMouseOut);
function onMouseOver(d, i) {
tooltip
.style("left", d3.event.pageX - 50 + "px")
.style("top", d3.event.pageY - 70 + "px")
.style("display", "inline-block")
.html("Year: " + (d.Time) + "<br>" + "Value: " + (d.Value));
d3.select(this).attr('fill', "#eec42d");
}
function onMouseOut(d, i) {
tooltip.style("display", "none");
d3.select(this).attr('fill', "blue");
}
bars.append('text')
.text(d => d.Value)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', d => d.Value > 0 ? 'baseline' : 'hanging')
.attr('y', d => yScale(d.Value))
.attr('dy', d => d.Value > 0 ? -5 : 5);
bars.filter((d, i) => i < barData.length - 1)
.append('path')
.attr('d', (d, i) => `M 5,${Math.min(yScale(d.Value) - 20, height/2)} V ${Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 60} H ${xScale.bandwidth() - 5} V ${Math.min(yScale(barData[i + 1].Value) - 25, height/2 - 10)}`)
.style('stroke', 'gray')
.style('fill', 'none')
.attr('marker-end', 'url(#arrowhead)')
bars.filter((d, i) => i < barData.length - 1)
.append('rect')
.attr('x', 15)
.attr('y', (d, i) => Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 70)
.attr('width', xScale.bandwidth() - 30)
.attr('height', 20)
.attr('rx', 10)
.style('fill', 'white')
.style('stroke', 'gray');
bars.filter((d, i) => i < barData.length - 1)
.append('text')
.text((d, i) => `${barData[i + 1].Value > d.Value ? '+' : ''}${Math.round((barData[i + 1].Value / d.Value * 100) - 100)}%`)
.attr('x', xScale.bandwidth() / 2)
.attr('y', (d, i) => Math.min(yScale(d.Value), yScale(barData[i + 1].Value)) - 56)
.attr('text-anchor', 'middle')
.style('fill', 'black');
const xAxisG = svg.append('g')
.attr("transform", `translate(0,${height/2})`)
.call(xAxis);
xAxisG.selectAll('.tick').each(function() {
const tick = d3.select(this);
const text = tick.select('text').text();
const data = barData.find(d => d.Time === text);
if (data.Value < 0) {
tick.select('text').style('fill', 'white');
tick.select('line').style('stroke', 'white');
}
})
#graph {
width: 700px;
height: 500px;
}
text {
font-size: 12px;
font-family: "Ubuntu";
}
.toolTip {
position: absolute;
display: none;
min-width: 80px;
height: auto;
background: none repeat scroll 0 0 #ffffff;
border: 1px solid #6F257F;
padding: 5px;
text-align: left;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="graph">
</div>
I need to change the range for yAxis ticks in my d3 chart. Currently, it has a range of 2M and looks like this:
The majority of data is spread between 0 and 4M. I would like to have a smaller range for ticks up until 4M and then a bigger range. So I would have ticks for 1M, 2M, 3M, 4M and then 8M, 12M, 16M. Here is the code:
const width = 1000;
const height = 700;
const padding = 120; // between title and svg borders
const marginTop = 120;
// Min and max for y axis for Revenue values
const maxRevenue = d3.max(dataset, (d) => d[1]);
const minRevenue = d3.min(dataset, (d) => d[1]);
const yScale = d3
.scaleLinear()
.domain([minRevenue, maxRevenue])
.range([height - padding, marginTop]);
const yAxis = d3.axisLeft(yScale);
svg
.append("g")
.attr("transform", `translate(${padding}, 0)`)
.call(yAxis); // if need to move the chart to the right (for yaxis)
My recommendation is to use a "split" y-axis. Something like this:
<html>
<meta charset="utf-8" />
<style>
/* set the CSS */
.bar {
fill: steelblue;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="https://d3js.org/d3.v6.min.js"></script>
<script>
// set the dimensions and margins of the graph
var margin = { top: 20, right: 20, bottom: 30, left: 100 },
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
pbottomsize = 0.66,
ptopsize = 0.25;
// set the ranges
var xd = ['A', 'B', 'C', 'D'];
var x = d3.scaleBand().range([0, width]).padding(0.1).domain(xd);
var y = d3.scaleLinear().range([height * pbottomsize, 0]).domain([0, 20]);
var y1 = d3.scaleLinear().range([height * ptopsize, 0]).domain([1000000, 2000000]);
var svg = d3
.select('body')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
var g1 = svg
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + (margin.top + height * 0.33) + ')');
var g2 = svg
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var data = d3.range(1, 100).map(function (d, i) {
return {
x: xd[Math.floor(Math.random() * xd.length)],
y: Math.random() < 0.75 ? Math.random() * 20 : (Math.random() + 1) * 1000000,
};
});
var data0 = data.filter(function(d){
return d.y < 1000000;
});
var data1 = data.filter(function(d){
return d.y > 1000000;
})
// append the rectangles for the bar chart
g1
.selectAll('.circle')
.data(data0)
.enter()
.append('circle')
.attr('cx', function (d) {
return x(d.x) + (x.bandwidth() / 2);
})
.attr('r', 10)
.attr('cy', function (d) {
return y(d.y);
})
.style('fill', 'steelblue');
g2
.selectAll('.circle')
.data(data1)
.enter()
.append('circle')
.attr('cx', function (d) {
return x(d.x) + (x.bandwidth() / 2);
})
.attr('r', 10)
.attr('cy', function (d) {
return y1(d.y);
})
.style('fill', 'red');
// add the x Axis
g1
.append('g')
.attr('transform', 'translate(0,' + (height * pbottomsize) + ')')
.call(d3.axisBottom(x));
// add the y Axis
g1.append('g').call(d3.axisLeft(y));
g2.append('g').call(d3.axisLeft(y1).ticks(3));
var g3 = svg.append('g')
.attr('transform', 'translate(' + 70.5 + ',' + 136 + ')')
g3.append('path')
.attr('d', 'M 10 10 Q 20 0, 30 10 T 50 10 M 30 10 L 30 -2')
.attr('stroke', 'black')
.attr('fill', 'none');
g3.append('path')
.attr('d', 'M 10 20 Q 20 10, 30 20 T 50 20 M 30 20 L 30 32')
.attr('stroke', 'black')
.attr('fill', 'none');
</script>
</body>
</html>
In my code below I'm appending multiple svg elements and get 3 different charts from it.
The problem I have is that the max value that's evaluated from the y.domain() for each y-axis is the max value from all of my data.
Is there some clever way to get the max value for each svg and set that to be the max value for each y-axis or do I have to make 3 different y scales?
Here's the code:
var data = [
{category: "Apples", total: 10, goal: 8},
{category: "Oranges", total: 20, goal: 18},
{category: "Bananas", total: 20, goal: 25},
];
chart(data);
function chart(result) {
var margin = {bottom: 25, right: 25, top: 25, left: 25},
width = 180 - margin.left - margin.right,
height = 230 - margin.top - margin.bottom;
var svg = d3.select("#chart").selectAll("svg")
.data(result)
.enter().append("svg")
.attr("width", width)
.attr("height", height)
var x = d3.scaleBand()
.range([margin.left, width - margin.right])
.domain(["Total", "Goal"])
.padding(.1)
.paddingOuter(.2)
var y = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, d3.max(result, d => Math.max(d.total, d.goal))]).nice()
var xAxis = g => g
.attr("transform", "translate(0," + (height - margin.bottom) + ")")
.call(d3.axisBottom(x).tickSizeOuter(0))
var yAxis = g => g
.attr("transform", "translate(" + margin.left + ",0)")
.call(d3.axisLeft(y))
svg.append("g")
.attr("class", "x-axis")
.call(xAxis);
svg.append("g")
.attr("class", "y-axis")
.call(yAxis);
var total = svg.append("rect")
.attr("fill", "steelblue")
.attr("width", x.bandwidth())
.attr("x", x("Total"))
.attr("y", function() {
var d = d3.select(this.parentNode).datum()
return y(d.total)
})
.attr("height", function() {
var d = d3.select(this.parentNode).datum()
return y(0) - y(d.total)
})
var goal = svg.append("rect")
.attr("fill", "orange")
.attr("width", x.bandwidth())
.attr("x", x("Goal"))
.attr("y", function() {
var d = d3.select(this.parentNode).datum()
return y(d.goal)
})
.attr("height", function() {
var d = d3.select(this.parentNode).datum()
return y(0) - y(d.goal)
})
var text = svg.append("text")
.attr("dx", width / 2)
.attr("dy", 15)
.attr("text-anchor", "middle")
.text(function() {
return d3.select(this.parentNode).datum().category
})
}
<meta charset ="utf-8">
<script src="https://d3js.org/d3.v5.min.js "></script>
<div id="chart"></div>
I reckon that the best idea here is refactoring your code to create specific SVGs based on the data you pass to it. However, given the code you have right now, a new idiomatic D3 solution is using local variables.
Actually, according to Mike Bostock...
For instance, when rendering small multiples of time-series data, you might want the same x-scale for all charts but distinct y-scales to compare the relative performance of each metric.
... local variables are the solution for your exact case!
So, all you need is to set the local...
var local = d3.local();
svg.each(function(d) {
var y = local.set(this, d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, Math.max(d.total, d.goal)]).nice())
});
... and get it to create the axes and the bars. For instance:
.attr("y", function(d) {
return local.get(this)(d.total);
})
Have in mind that you don't need that var d = d3.select(this.parentNode).datum() to get the datum!
Here is your code with those changes:
var data = [{
category: "Apples",
total: 10,
goal: 8
},
{
category: "Oranges",
total: 20,
goal: 18
},
{
category: "Bananas",
total: 20,
goal: 25
},
];
var local = d3.local();
chart(data);
function chart(result) {
var margin = {
bottom: 25,
right: 25,
top: 25,
left: 25
},
width = 180 - margin.left - margin.right,
height = 230 - margin.top - margin.bottom;
var svg = d3.select("#chart").selectAll("svg")
.data(result)
.enter().append("svg")
.attr("width", width)
.attr("height", height)
var x = d3.scaleBand()
.range([margin.left, width - margin.right])
.domain(["Total", "Goal"])
.padding(.1)
.paddingOuter(.2);
svg.each(function(d) {
var y = local.set(this, d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, Math.max(d.total, d.goal)]).nice())
});
var xAxis = g => g
.attr("transform", "translate(0," + (height - margin.bottom) + ")")
.call(d3.axisBottom(x).tickSizeOuter(0))
svg.append("g")
.attr("class", "x-axis")
.call(xAxis);
svg.each(function() {
var y = local.get(this);
var yAxis = g => g
.attr("transform", "translate(" + margin.left + ",0)")
.call(d3.axisLeft(y));
d3.select(this).append("g")
.attr("class", "y-axis")
.call(yAxis);
})
var total = svg.append("rect")
.attr("fill", "steelblue")
.attr("width", x.bandwidth())
.attr("x", x("Total"))
.attr("y", function(d) {
return local.get(this)(d.total);
})
.attr("height", function(d) {
return local.get(this)(0) - local.get(this)(d.total)
})
var goal = svg.append("rect")
.attr("fill", "orange")
.attr("width", x.bandwidth())
.attr("x", x("Goal"))
.attr("y", function(d) {
return local.get(this)(d.goal)
})
.attr("height", function(d) {
return local.get(this)(0) - local.get(this)(d.goal)
})
var text = svg.append("text")
.attr("dx", width / 2)
.attr("dy", 15)
.attr("text-anchor", "middle")
.text(function() {
return d3.select(this.parentNode).datum().category
})
}
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js "></script>
<div id="chart"></div>
I would restructure the data in the slightly different way. Doing this allows you to be more flexible when names change, more fruit types are added, words like total & goal get changed, etc
Either way, you can loop through the initial array and create a separate SVG (each with their own yScales) for each object in the array.
const data = [{
"category": "Apples",
"bars": [{
"label": "total",
"val": 10
},
{
"label": "goal",
"val": 8
}
]
},
{
"category": "Oranges",
"bars": [{
"label": "total",
"val": 20
},
{
"label": "goal",
"val": 18
}
]
},
{
"category": "Bananas",
"bars": [{
"label": "total",
"val": 20
},
{
"label": "goal",
"val": 25
}
]
}
]
data.forEach((d) => chart(d))
function chart(result) {
const margin = {
bottom: 25,
right: 25,
top: 25,
left: 25
},
width = 180 - margin.left - margin.right,
height = 230 - margin.top - margin.bottom
const svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
const x = d3.scaleBand()
.range([margin.left, width - margin.right])
.domain(result.bars.map((d) => d.label))
.padding(.1)
.paddingOuter(.2)
const y = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, d3.max(result.bars.map(z => z.val))])
const xAxis = g => g
.attr("transform", "translate(0," + (height - margin.bottom) + ")")
.call(d3.axisBottom(x).tickSizeOuter(0))
const yAxis = g => g
.attr("transform", "translate(" + margin.left + ",0)")
.call(d3.axisLeft(y))
svg.append("g")
.attr("class", "x-axis")
.call(xAxis)
svg.append("g")
.attr("class", "y-axis")
.call(yAxis)
const barEnter = svg.selectAll("rect")
.data(result.bars)
.enter()
.append("rect")
.attr("fill", d => (d.label === 'total' ? "steelblue" : "green"))
.attr("width", x.bandwidth())
.attr("x", (d) => x(d.label))
.attr("y", (d) => y(d.val))
.attr("height", (d) => y(0) - y(d.val))
const text = svg.append("text")
.attr("dx", width / 2)
.attr("dy", 15)
.attr("text-anchor", "middle")
.text(result.category)
}
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js "></script>
<div id="chart"></div>
Update. If you can't optimise your data structure, you could do it this way
const data = [{
category: "Apples",
total: 10,
goal: 8
},
{
category: "Oranges",
total: 20,
goal: 18
},
{
category: "Bananas",
total: 20,
goal: 25
}
]
data.forEach((d) => chart(d))
function chart(result) {
const margin = {
bottom: 25,
right: 25,
top: 25,
left: 25
},
width = 180 - margin.left - margin.right,
height = 230 - margin.top - margin.bottom
const svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
const x = d3.scaleBand()
.range([margin.left, width - margin.right])
.domain(["total", "goal"])
.padding(.1)
.paddingOuter(.2)
const y = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, d3.max([result.total, result.goal])])
const xAxis = g => g
.attr("transform", "translate(0," + (height - margin.bottom) + ")")
.call(d3.axisBottom(x).tickSizeOuter(0))
const yAxis = g => g
.attr("transform", "translate(" + margin.left + ",0)")
.call(d3.axisLeft(y))
svg.append("g")
.attr("class", "x-axis")
.call(xAxis)
svg.append("g")
.attr("class", "y-axis")
.call(yAxis)
const totalBarEnter = svg.selectAll(".total")
.data([result.total])
.enter()
.append("rect")
.attr("class", "total")
.attr("fill", "steelblue")
.attr("width", x.bandwidth())
.attr("x", (d) => x("total"))
.attr("y", (d) => y(d))
.attr("height", (d) => y(0) - y(d))
const goalBarEnter = svg.selectAll(".goal")
.data([result.goal])
.enter()
.append("rect")
.attr("class", "goal")
.attr("fill", "green")
.attr("width", x.bandwidth())
.attr("x", (d) => x("goal"))
.attr("y", (d) => y(d))
.attr("height", (d) => y(0) - y(d))
const text = svg.append("text")
.attr("dx", width / 2)
.attr("dy", 15)
.attr("text-anchor", "middle")
.text(result.category)
}
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js "></script>
<div id="chart"></div>
Codepen
I would create different y_scale for each variable:
//build unique categories set
var categories = d3.set(function(d){return d.category}).values();
//define y scale
var y_scales = {};
//loop through categories, filter data and set y_scale on max.
for (c in categories){
var filtered_result = result.filter(function(d){if(d.category == categories[c]){return d});
y_scales[categories[c]] = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, d3.max(filtered_result, d => Math.max(d.total, d.goal))]).nice()
}