I'm trying to learn how to use d3.js in react, but something is wrong in my code.
I'm doing a bar chart, but the value of bar are "inverted", for example, a bar has a value of 30% but in the chart, the bar appears with 70% (like, 100% - 30% = 70%).
How can I fix that?
Here is my code: codeSandBox.
Other question that I have is: how can I change the height of bars? I want to add some margin-top to show everything of the y Axis, but if I do that, the bars still with the same height and don't match with yAxis value
The issue here is that you're swapping the y and height logic, it should be:
.attr("y", d => yScale(d.percent))
.attr("height", d => height - margin.bottom - margin.top - yScale(d.percent))
Or, if you set the working height as...
height = totalHeight - margin.bottom - margin.top
... it can be just:
.attr("y", d => yScale(d.percent))
.attr("height", d => height - yScale(d.percent))
On top of that (and this addresses your second question), you are using Bostock's margin convention wrong. You should translate the g group according to the margins, and then appending all the bars to that translated group, without translating them again. Also, append the axes' groups to that g group.
All that being said, this is the code with those changes:
const data = [{
year: 2012,
percent: 50
},
{
year: 2013,
percent: 30
},
{
year: 2014,
percent: 90
},
{
year: 2015,
percent: 60
},
{
year: 2016,
percent: 75
},
{
year: 2017,
percent: 20
}
];
const height = 300;
const width = 370;
const margin = {
top: 20,
right: 10,
bottom: 20,
left: 25
};
const xScale = d3.scaleBand()
.domain(data.map(d => d.year))
.padding(0.2)
.range([0, width - margin.right - margin.left]);
const yScale = d3
.scaleLinear()
.domain([0, 100])
.range([height - margin.bottom - margin.top, 0]);
const svg = d3
.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.style("margin-left", 10);
const g = svg
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
g
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d => xScale(d.year))
.attr("y", d => yScale(d.percent))
.attr("width", xScale.bandwidth())
.attr("height", d => height - margin.bottom - margin.top - yScale(d.percent))
.attr("fill", "steelblue")
g.append("g")
.call(d3.axisLeft(yScale));
g.append("g")
.call(d3.axisBottom(xScale))
.attr(
"transform",
`translate(0, ${height - margin.bottom - margin.top})`
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
svg
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d => xScale(d.year))
.attr("y", d => height - yScale(100-d.percent))
.attr("width", xScale.bandwidth())
.attr("height", d => yScale(100-d.percent))
.attr("fill", "steelblue")
.attr("transform", `translate(${margin.left}, -${margin.bottom})`);
You need to minus the percentage from 100
Working: https://codesandbox.io/s/crimson-microservice-8kjqd
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 have a bar chart with zoom function. The issue is, the zooming isn't actually centered. If, I place the cursor, on a bar and zoom, the bar underneath the cursor moves away as opposed to staying there, However, if I set the MARGIN.LEFT = 0, then the issue is rectified and No matter what bar I have my cursor on, when I zoom the bar stays there, right underneath. Could anyone help me with this?
Working Code Here: https://codesandbox.io/s/d3-zoom-not-centered-sfziyk
D3 Code:
const MARGIN = {
LEFT: 60,
RIGHT: 40,
TOP: 10,
BOTTOM: 130
};
// total width incl margin
const VIEWPORT_WIDTH = 1140;
// total height incl margin
const VIEWPORT_HEIGHT = 400;
const WIDTH = VIEWPORT_WIDTH - MARGIN.LEFT - MARGIN.RIGHT;
const HEIGHT = VIEWPORT_HEIGHT - MARGIN.TOP - MARGIN.BOTTOM;
const svg = d3
.select(".chart-container")
.append("svg")
.attr("width", WIDTH + MARGIN.LEFT + MARGIN.RIGHT)
.attr("height", HEIGHT + MARGIN.TOP + MARGIN.BOTTOM);
const g = svg
.append("g")
.attr("transform", `translate(${MARGIN.LEFT}, ${MARGIN.TOP})`);
g.append("text")
.attr("class", "x axis-label")
.attr("x", WIDTH / 2)
.attr("y", HEIGHT + 110)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.text("Month");
g.append("text")
.attr("class", "y axis-label")
.attr("x", -(HEIGHT / 2))
.attr("y", -60)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.text("");
const zoom = d3.zoom().scaleExtent([0.5, 10]).on("zoom", zoomed);
svg.call(zoom);
function zoomed(event) {
x.range([0, WIDTH].map((d) => event.transform.applyX(d)));
barsGroup
.selectAll("rect.profit")
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth());
barsGroup
.selectAll("rect.revenue")
.attr("x", (d) => x(d.month) + 0.5 * x.bandwidth())
.attr("width", 0.5 * x.bandwidth());
xAxisGroup.call(xAxisCall);
}
const x = d3.scaleBand().range([0, WIDTH]).paddingInner(0.3).paddingOuter(0.2);
const y = d3.scaleLinear().range([HEIGHT, 0]);
const xAxisGroup = g
.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${HEIGHT})`);
const yAxisGroup = g.append("g").attr("class", "y axis");
const xAxisCall = d3.axisBottom(x);
const yAxisCall = d3
.axisLeft(y)
.ticks(3)
.tickFormat((d) => "$" + d);
const defs = svg.append("defs");
const barsClipPath = defs
.append("clipPath")
.attr("id", "bars-clip-path")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", WIDTH)
.attr("height", 400);
const barsGroup = g.append("g");
const zoomGroup = barsGroup.append("g");
barsGroup.attr("class", "bars");
zoomGroup.attr("class", "zoom");
barsGroup.attr("clip-path", "url(#bars-clip-path)");
xAxisGroup.attr("clip-path", "url(#bars-clip-path)");
d3.csv("data.csv").then((data) => {
data.forEach((d) => {
d.profit = Number(d.profit);
d.revenue = Number(d.revenue);
d.month = d.month;
});
var y0 = d3.max(data, (d) => d.profit);
var y1 = d3.max(data, (d) => d.revenue);
var maxdomain = y1;
if (y0 > y1) var maxdomain = y0;
x.domain(data.map((d) => d.month));
y.domain([0, maxdomain]);
xAxisGroup
.call(xAxisCall)
.selectAll("text")
.attr("y", "10")
.attr("x", "-5")
.attr("text-anchor", "end")
.attr("transform", "rotate(-40)");
yAxisGroup.call(yAxisCall);
const rects = zoomGroup.selectAll("rect").data(data);
rects.exit().remove();
rects
.attr("y", (d) => y(d.profit))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.profit));
rects
.enter()
.append("rect")
.attr("class", "profit")
.attr("y", (d) => y(d.profit))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.profit))
.attr("fill", "grey");
const rects_revenue = zoomGroup.selectAll("rect.revenue").data(data);
rects_revenue.exit().remove();
rects_revenue
.attr("y", (d) => y(d.revenue))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.revenue));
rects_revenue
.enter()
.append("rect")
.attr("class", "revenue")
.style("fill", "red")
.attr("y", (d) => y(d.revenue))
.attr("x", (d) => x(d.month) + 0.5 * x.bandwidth())
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.revenue))
.attr("fill", "grey");
});
When you call the zoom on the svg, all zoom behaviour is relative to the svg.
Imagine that your x-axis is at initial zoom level of length 100 representing the domain [0, 100]. So the x-scale has range([0, 100]) and domain([0, 100]). Add a left margin of 10.
If you zoom by scale 2 at the midpoint of your axis at x=50 you would expect to get the following behaviour after the zoom:
The midpoint does not move.
The interval [25, 75] is visible.
However, since the zoom is called on the svg you have to account for the left margin of 10. The zoom does not occur at the midpoint but at x = 10 + 50 = 60. The transform is thus x -> x * k + t with k = 2 and t = -60. This results in
x = 50 -> 2 * 50 - 60 = 40,
x = 80 -> 2 * 80 - 60 = 100,
x = 30 -> 2 * 30 - 60 = 0.
Visible after the zoom is the interval [30, 80] and the point x = 50 is shifted to the left.
This is what you observe in your chart.
In order to get the expected behaviour, you can do two things:
a. Follow the bar chart example where the range of the x-scale does not start at 0 but at the left margin. The g which is translated by margin.left and margin.top is also omitted here. Instead, the ranges of the axes incorporate the margins directly.
b. Add a rect with fill: none; pointer-events: all; to the svg that is of the size of the chart without the margins. Then call the zoom on that rectangle, as done in this example.
Note that all the new examples on ObservableHQ follow the pattern "a" that needs fewer markup.
I have made a bar chart in d3 but I am struggeling with 2 things.
First of all I want to add a legend because I have 2 different bars so I want people to know what each bar is.
The second I want to load data from a json file instead of this:
var data = [
{
"year":1970,
"count1":1,
"count2":0
},
{
"year":1975,
"count1":1,
"count2":0
},
{
"year":1980,
"count1":1,
"count2":1
}]
But when I try to use a json file my map function gives an error.
My code :
var svgSelect = d3.select("svg")
var width = 1020
var height = 900
var margin = {top: 35, right: 25, bottom: 35, left: 50}
var paddingBars = .2
var axisTicks = {amount: 25, outerSize: 0}
var svg = svgSelect
.append("svg")
.attr("widht", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
var xScale0 = d3.scaleBand().range([0, width - margin.left - margin.right]).padding(paddingBars)
var xScale1 = d3.scaleBand()
var yScale = d3.scaleLinear().range([height - margin.top - margin.bottom, 0])
var xAxis = d3.axisBottom(xScale0).tickSizeOuter(axisTicks.outerSize)
var yAxis = d3.axisLeft(yScale).ticks(axisTicks.amount).tickSizeOuter(axisTicks.outerSize)
xScale0.domain(data.map(d => d.year))
xScale1.domain(['count1', 'count2']).range([0, xScale0.bandwidth()])
yScale.domain([0, d3.max(data, d => d.count1 > d.count2 ? d.count1 : d.count2)])
var year = svg.selectAll(".year")
.data(data)
.enter().append("g")
.attr("class", "year")
.attr("transform", d => `translate(${xScale0(d.year)},0)`)
/* Add count1 bars */
year.selectAll(".bar.count1")
.data(d => [d])
.enter()
.append("rect")
.attr("class", "bar count1")
.style("fill","blue")
.attr("x", d => xScale1('count1'))
.attr("y", d => yScale(d.count1))
.attr("width", xScale1.bandwidth())
.attr("height", d => {
return height - margin.top - margin.bottom - yScale(d.count1)
})
/* Add coun2 bars */
year.selectAll(".bar.count2")
.data(d => [d])
.enter()
.append("rect")
.attr("class", "bar count2")
.style("fill","red")
.attr("x", d => xScale1('count2'))
.attr("y", d => yScale(d.count2))
.attr("width", xScale1.bandwidth())
.attr("height", d => {
return height - margin.top - margin.bottom - yScale(d.count2)
})
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0,${height - margin.top - margin.bottom})`)
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
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()
}
I can't figure out how to properly create a histogram where there are both positive and negative values in the data array.
I've used the histogram example here http://bl.ocks.org/mbostock/3048450 as a base, and while the x axis values and ticks are correct, the bars are out to lunch.
Data
var values = [-15, -20, -22, -18, 2, 6, -26, -18, -15, -20, -22, -18, 2, 6, -26, -18];
X Scale
var x0 = Math.max(-d3.min(values), d3.max(values));
var x = d3.scale.linear()
.domain([-x0, x0])
.range([0, width])
.nice();
Check the jfiddle here: http://jsfiddle.net/tNdJj/2/
I assume it's something missing from the "rect" creations but I am not seeing it.
Using the example of histogram from the following question: Bar chart with negative values
I inversed x and y and adapted the display. Now you have a nice basis.
Here is the corresponding jsFiddle: http://jsfiddle.net/chrisJamesC/tNdJj/4/
Here is the relevant code:
var data = [-15, -20, -22, -18, 2, 6, -26, -18];
var margin = {top: 30, right: 10, bottom: 10, left: 30},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var y0 = Math.max(Math.abs(d3.min(data)), Math.abs(d3.max(data)));
var y = d3.scale.linear()
.domain([-y0, y0])
.range([height,0])
.nice();
var x = d3.scale.ordinal()
.domain(d3.range(data.length))
.rangeRoundBands([0, width], .2);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
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 + ")");
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", function(d) { return d < 0 ? "bar negative" : "bar positive"; })
.attr("y", function(d) { return y(Math.max(0, d)); })
.attr("x", function(d, i) { return x(i); })
.attr("height", function(d) { return Math.abs(y(d) - y(0)); })
.attr("width", x.rangeBand());
svg.append("g")
.attr("class", "x axis")
.call(yAxis);
svg.append("g")
.attr("class", "y axis")
.append("line")
.attr("y1", y(0))
.attr("y2", y(0))
.attr("x1", 0)
.attr("x2", width);
Note: For simple visualizations like this, I would recommand using nvd3.js
The trick is that the demo code is overly optimistic, assuming that its input is positive:
bar.append("rect")
.attr("x", 1)
// .attr("width", x(data[0].dx) - 1) // Does the wrong thing for negative buckets.
.attr("width", x(data[0].x + data[0].dx) - 1)
.attr("height", function(d) { return height - y(d.y); });
http://jsfiddle.net/tNdJj/46/