Related
I have a project where I need to create a stacked bar chart. This bar chart needs to have a rounded border at the top. I have found some documentation on how to round the top border of bar chart. The example I followed can be found here: Rounded top corners for bar chart.
It says to add a attribute with the following:
`
M${x(item.name)},${y(item.value) + ry}
a${rx},${ry} 0 0 1 ${rx},${-ry}
h${x.bandwidth() - 2 * rx}
a${rx},${ry} 0 0 1 ${rx},${ry}
v${height - y(item.value) - ry}
h${-x.bandwidth()}Z
`
You also need to declare two variables rx and ry that define the sharpness of the corner.
The problem I am facing is that I cannot get it to work with my stacked bar chart. The top stack needs to be rounded at the top. Also when the top stack is 0 zero the next stack needs to be rounded. So that the top of the bar is always rounded no matter what data is included.
I have added a trimmed down snippet. It includes a button to transition where I remove(set to zero) the top stacks. This snippet of course also includes the attribute needed to round the corner. But it doesn't get rounded.
this.width = 400;
this.height = 200;
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 40
}
this.index = 0;
this.svg = d3
.select(".canvas")
.classed("svg-container", true)
.append("svg")
.attr("class", "chart")
.attr(
"viewBox",
`0 0 ${this.width} ${this.height}`
)
.attr("preserveAspectRatio", "xMinYMin meet")
.classed("svg-content-responsive", true)
.append("g");
const scale = [0, 1200];
// set the scales
this.xScale = d3
.scaleBand()
.range([0, width])
.padding(0.3);
this.yScale = d3.scaleLinear().range([this.height, 0]);
var bars = this.svg.append("g").attr("class", "bars");
const update = data => {
const scale = [0, 1200];
// Update scales.
this.xScale.domain(data.map(d => d.key));
this.yScale.domain([scale[0], scale[1]]);
const subgroups = ["home", "work", "public"];
var color = d3
.scaleOrdinal()
.domain(subgroups)
.range(["#206BF3", "#171D2C", "#8B0000"]);
var stackData = d3.stack().keys(subgroups)(data);
const rx = 12;
const ry = 12;
// Set up transition.
const dur = 1000;
const t = d3.transition().duration(dur);
bars
.selectAll("g")
.data(stackData)
.join(
enter => enter
.append("g")
.attr("fill", d => color(d.key)),
null, // no update function
exit => {
exit
.transition()
.duration(dur / 2)
.style("fill-opacity", 0)
.remove();
}
).selectAll("rect")
.data(d => d, d => d.data.key)
.join(
enter => enter
.append("rect")
.attr("class", "bar")
.attr("x", d => {
return this.xScale(d.data.key);
})
.attr("y", () => {
return this.yScale(0);
})
.attr("height", () => {
return this.height - this.yScale(0);
})
.attr("width", this.xScale.bandwidth())
.attr(
'd',
item =>
`M${this.xScale(item.name)},${this.yScale(item.value) + ry}
a${rx},${ry} 0 0 1 ${rx},${-ry}
h${this.xScale.bandwidth() - 2 * rx}
a${rx},${ry} 0 0 1 ${rx},${ry}
v${this.height - this.yScale(item.value) - ry}
h${-this.xScale.bandwidth()}Z
`
),
null,
exit => {
exit
.transition()
.duration(dur / 2)
.style("fill-opacity", 0)
.remove();
}
)
.transition(t)
.delay((d, i) => i * 20)
.attr("x", d => this.xScale(d.data.key))
.attr("y", d => {
return this.yScale(d[1]);
})
.attr("width", this.xScale.bandwidth())
.attr("height", d => {
return this.yScale(d[0]) - this.yScale(d[1]);
});
};
const data = [
[{
key: "1",
home: 282,
work: 363,
public: 379
},
{
key: "2",
home: 232,
work: 432,
public: 0
}
],
[{
key: "1",
home: 282,
work: 363,
public: 379
},
{
key: "2",
home: 232,
work: 0,
public: 0
}
]
];
update(data[this.index]);
const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
if (this.index < 1) this.index += 1;
else this.index = 0;
update(data[this.index]);
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>
Change the drawing of rect to path. So .append("rect") becomes .append("path"), and you don't need the attributes x, y, height and width anymore.
To round the top stack only, you can set rx and ry to 12 just before d3 draws the last subgroup (d[1] - d[0] == d.data[subgroups[subgroups.length-1]]), which is "public" in this case, and otherwise set them to 0.
Finally, for your last problem:
when the top stack is 0 zero the next stack needs to be rounded
Stacks are drawn in order. Before each stack is drawn, find out whether the next stack/subgroup is zero, and if so, set rx and ry = 12. To find this out, you need to get the current subgroup being drawn, so you can identify what the next subgroup will be, and get the value of that subgroup.
const current_subgroup = Object.keys(d.data).find(key => d.data[key] === d[1] - d[0]);
const next_subgroup_index = Math.min(subgroups.length - 1, subgroups.indexOf(current_subgroup)+1);
const next_subgroup_data = stackData[next_subgroup_index][i];
if (next_subgroup_data[1] - next_subgroup_data[0] == 0) { rx = 12; ry = 12; }
this.width = 400;
this.height = 200;
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 40
}
this.index = 0;
this.svg = d3
.select(".canvas")
.classed("svg-container", true)
.append("svg")
.attr("class", "chart")
.attr(
"viewBox",
`0 0 ${this.width} ${this.height}`
)
.attr("preserveAspectRatio", "xMinYMin meet")
.classed("svg-content-responsive", true)
.append("g");
const scale = [0, 1200];
// set the scales
this.xScale = d3
.scaleBand()
.range([0, width])
.padding(0.3);
this.yScale = d3.scaleLinear().range([this.height, 0]);
var bars = this.svg.append("g").attr("class", "bars");
const update = data => {
const scale = [0, 1200];
// Update scales.
this.xScale.domain(data.map(d => d.key));
this.yScale.domain([scale[0], scale[1]]);
const subgroups = ["home", "work", "public"];
var color = d3
.scaleOrdinal()
.domain(subgroups)
.range(["#206BF3", "#171D2C", "#8B0000"]);
var stackData = d3.stack().keys(subgroups)(data);
let rx = 12;
let ry = 12;
// Set up transition.
const dur = 1000;
const t = d3.transition().duration(dur);
bars
.selectAll("g")
.data(stackData)
.join(
enter => enter
.append("g")
.attr("fill", d => color(d.key)),
null, // no update function
exit => {
exit
.transition()
.duration(dur / 2)
.style("fill-opacity", 0)
.remove();
}
).selectAll("path")
.data(d => d, d => d.data.key)
.join(
enter => enter
.append("path")
.attr("class", "bar")
.attr(
'd',
d =>
`M${this.xScale(d.data.key)},${this.yScale(0)}
a0,0 0 0 1 0,0
h${this.xScale.bandwidth()}
a0,0 0 0 1 0,0
v${this.height - this.yScale(0)}
h${-this.xScale.bandwidth()}Z
`
),
null,
exit => {
exit
.transition()
.duration(dur / 2)
.style("fill-opacity", 0)
.remove();
}
)
.transition(t)
.delay((d, i) => i * 20)
.attr(
'd',
(d, i) => {
//if last subgroup, round the corners of the stack
if (d[1] - d[0] == d.data[subgroups[subgroups.length-1]]) { rx = 12; ry = 12; }
else { rx = 0; ry = 0; }
//if next subgroup is zero, round the corners of the stack
const current_subgroup = Object.keys(d.data).find(key => d.data[key] === d[1] - d[0]);
const next_subgroup_index = Math.min(subgroups.length - 1, subgroups.indexOf(current_subgroup)+1);
const next_subgroup_data = stackData[next_subgroup_index][i];
if (next_subgroup_data[1] - next_subgroup_data[0] == 0) { rx = 12; ry = 12; }
//draw the stack
if (d[1] - d[0] > 0) {
return `M${this.xScale(d.data.key)},${this.yScale(d[1]) + ry}
a${rx},${ry} 0 0 1 ${rx},${-ry}
h${this.xScale.bandwidth() - 2 * rx}
a${rx},${ry} 0 0 1 ${rx},${ry}
v${this.yScale(d[0]) - this.yScale(d[1]) - ry}
h${-this.xScale.bandwidth()}Z
`
} else {
return `M${this.xScale(d.data.key)},${this.yScale(d[1])}
a0,0 0 0 1 0,0
h${this.xScale.bandwidth()}
a0,0 0 0 1 0,0
v${this.yScale(d[0]) - this.yScale(d[1]) }
h${-this.xScale.bandwidth()}Z
`
}
}
);
};
const data = [
[{
key: "1",
home: 282,
work: 363,
public: 379
},
{
key: "2",
home: 232,
work: 432,
public: 0
}
],
[{
key: "1",
home: 282,
work: 363,
public: 379
},
{
key: "2",
home: 232,
work: 0,
public: 0
}
]
];
update(data[this.index]);
const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
if (this.index < 1) this.index += 1;
else this.index = 0;
update(data[this.index]);
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>
I'm constructing a graph right now which is taking in data from a postgres backend. For the construction of the x-axis, I have the following:
var x = d3.scaleTime()
.domain(d3.extent(data_prices, function(d){
var time = timeParser(d.timestamp);
return time;
}))
.range([0,width])
where timeParser is a function representing d3.timeParse().
I have a data point which is at 16:58 and another at 22:06 and it looks a little ugly having it just stick at the side like that. How would I say, for instance, have there be a slight padding of say, +/- 30 minutes for each and continue the trendline path on each end? (or at least just the first part)
To create a padding in a time scale, use interval.offset. According to the API:
Returns a new date equal to date plus step intervals. If step is not specified it defaults to 1. If step is negative, then the returned date will be before the specified date.
Let's see it working. This is an axis based on a time scale with a min and a max similar to yours:
var svg = d3.select("svg");
var data = ["16:58", "18:00", "20:00", "22:00", "22:06"].map(function(d) {
return d3.timeParse("%H:%M")(d)
});
var scale = d3.scaleTime()
.domain(d3.extent(data))
.range([20, 580]);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="100"></svg>
Now, to create the padding, we just need to subtract and add 30 minutes at the extremes:
var scale = d3.scaleTime()
.domain([d3.timeMinute.offset(d3.min(data), -30),
//subtract 30 minutes here --------------^
d3.timeMinute.offset(d3.max(data), 30)
//add 30 minutes here ------------------^
])
.range([20, 580]);
Here is the result:
var svg = d3.select("svg");
var data = ["16:58", "18:00", "20:00", "22:00", "22:06"].map(function(d) {
return d3.timeParse("%H:%M")(d)
});
var scale = d3.scaleTime()
.domain([d3.timeMinute.offset(d3.min(data), -30), d3.timeMinute.offset(d3.max(data), 30)])
.range([20, 580]);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="100"></svg>
Have in mind that this solution does not round the extreme ticks to the nearest half hour: it adds and subtracts exactly half an hour.
So, to round to the nearest half hour, you can do a simple math using Math.floor and Math.ceil:
.domain([
d3.min(data).setMinutes(Math.floor(d3.min(data).getMinutes() / 30) * 30),
d3.max(data).setMinutes(Math.ceil(d3.max(data).getMinutes() / 30) * 30)
])
Here is the demo:
var svg = d3.select("svg");
var data = ["16:58", "18:00", "20:00", "22:00", "22:06"].map(function(d) {
return d3.timeParse("%H:%M")(d)
});
var scale = d3.scaleTime()
.domain([d3.min(data).setMinutes(Math.floor(d3.min(data).getMinutes() / 30) * 30), d3.max(data).setMinutes(Math.ceil(d3.max(data).getMinutes() / 30) * 30)])
.range([20, 580]);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,50)")
.call(axis)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="100"></svg>
(sorry for my english bad level)
Hi I'm using D3 for the first time with mithril js. The map is ok but I have a problem with colors of provinces and it comes from the 'd' attribute to get the id of provinces.The attribute is undefined and I don't understand what is 'd' exactly. is mithril the problem? is there an other way to get 'd' attribute?
controller.map = function(el){
var width = 1160;
var height = 960;
var scale = 10000;
var offset = [width / 2, height / 2];
var center = [0, 50.64];
var rotate = [-4.668, 0];
var parallels = [51.74, 49.34];
var projection = d3.geo.albers()
.center(center)
.rotate(rotate)
.parallels(parallels)
.scale(scale)
.translate(offset)
;
var path = d3.geo.path()
.projection(projection)
;
var svg = d3.select(el).append("svg")
.attr("width",width)
.attr("height",height)
;
d3.json("belprov.json",function(error,be){
if (error) return console.error(error);
var bounds = path.bounds(topojson.feature(be, be.objects.subunits));
var hscale = scale*width / (bounds[1][0] - bounds[0][0]);
var vscale = scale*height / (bounds[1][1] - bounds[0][1]);
scale = (hscale < vscale) ? hscale : vscale;
offset = [width - (bounds[0][0] + bounds[1][0])/2,
height - (bounds[0][1] + bounds[1][1])/2];
var centroid = d3.geo.centroid(topojson.feature(be, be.objects.subunits));
center = [0, centroid[1]];
rotate = [-centroid[0],0];
projection = d3.geo.albers()
.center(center)
.rotate(rotate)
.parallels(parallels)
.scale(scale)
.translate(offset);
svg.selectAll(".province")
.data(topojson.feature(be, be.objects.provinces).features)
.enter().append("path")
.attr("class", function(d) { return "province " + d.id })
.attr("d", path)
;
})
};
The "d" attribute in a path object defines the successive coordinates of the points through which the path has to go (it also gives indication about whether the path should use bezier curves, straight lines, etc.). See some documentation here.
Be careful: in d3, d is often used as a parameter for anonymous functions representing the data currently binded to the current element. So the two are completely different things.
Here, your line
.attr("d", path)
should probably look more like
.attr("d", function(d){return d.path})
i.e., take the field path within the data elements.
You can do something like this to color diffrent paths:
//make a color scale
var color20 = d3.scale.category20();
//your code as you doing
//on making paths do
svg.selectAll(".province")
.data(topojson.feature(be, be.objects.provinces).features)
.enter().append("path")
.attr("class", function(d) { return "province " + d.id })
.style("fill", function(d){return color(d.id);})//do this to color path based on id.
.attr("d", path)
I have a D3 chart that is supposed to look like this:
But instead it looks like this:
This is the code i'm using:
<!DOCTYPE html>
<meta charset="utf-8">
<head>
</head>
<body>
<svg id="chart"></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script>
var NSW = "NSW";
var QLD = "QLD";
var width = 600;
var height = 400;
var years = [];
var getStat = function(year, volatility, basis) {
return {
d: year,
x: basis,
vol: volatility,
value: 45 * Math.pow(basis, year),
high: 45 * Math.pow(basis+volatility, year),
low: 45 * Math.pow(basis-volatility, year),
}
}
for(i = 0; i < 25; i++) {
years.push(i);
}
var data = years.map(function(year){ return [getStat(year, 0.03, 1.08),getStat(year, 0.02, 1.08), getStat(year, 0.01, 1.08)]; }); // generate bogus data
var set_one = data.map(function(d) { return d[0];});
var set_two = data.map(function(d) { return d[1];});
var set_three = data.map(function(d) { return d[2];});
var chart = d3.select("#chart").attr("width", width).attr("height", height).append("g");
var x = d3.scale.linear().domain([0, years.length]).range([0, width]);
var y = d3.scale.linear().domain([0, d3.max(data, function(d){ return Math.max(d[0].high, d[1].high); })]).range([0, height]);
var area = d3.svg.area().x(function(d,i) { return x(i); })
.y0(function(d, i){ return d.low}) //FUNCTION FOR BASE-Y
.y1(function(d, i){ return d.high * 0.99;}); //FUNCTION FOR TOP-Y
chart
.selectAll("path.area")
.data([set_one,set_two,set_three]) // !!! here i can pass both arrays in.
.enter()
.append("path")
.attr("fill", "rgba(0,0,0,0.5)")
.attr("class", function(d,i) { return [NSW,QLD,"T"][i]; })
.attr("d", area);
</script>
What am I doing wrong?
Actually your doing nothing wrong the y-axis goes downwards starting at 0 from the top down to height. So to flip it you can set the y values to height - yValue:
var area = d3.svg.area().x(function(d,i) { return x(i); })
.y0(function(d, i){ return (height - (d.low))}) //FUNCTION FOR BASE-Y
.y1(function(d, i){ return (height - (d.high * 0.99))}); //FUNCTION FOR TOP-Y
Fiddle Example
the y ordinate in SVG increases downwards. Try this...
var y = d3.scale.linear().domain([d3.max(data, function(d){ return Math.max(d[0].high, d[1].high); }), 0]).range([0, height]);
Like everything in HTML / CSS / Canvas, the Y axis starts with 0 at the top and goes down to height at the bottom.
So according to your setup, the graph behaves correctly.
There are multiple ways to change the graphs direction.
a) You can change the range of your axis var y = d3.scale.linear().domain([...]).range([height, 0]);
b) You can change the domain of your axis var y = d3.scale.linear().domain([d3.max(data, function(d){ return Math.max(d[0].high, d[1].high); }), 0]).range([...]);
or c) change the way the graph gets its y-values with d3.svg.area().y0(...) and d3.svg.area().y1(...)
I would recommend the first option, because this actually specifies the range your domain gets projected on.
I think there was an issue with your y-scaling.I have inverted the range from range([height, 0] which was initially range([0,height]) as this should be the way as per d3 norms otherwise you have to change the logic while calculating the height of plot.
Here I am attaching the fixed code.
<!DOCTYPE html>
<meta charset="utf-8">
<head>
</head>
<body>
<svg id="chart"></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script>
var NSW = "NSW";
var QLD = "QLD";
var width = 600;
var height = 400;
var years = [];
var getStat = function(year, volatility, basis) {
return {
d: year,
x: basis,
vol: volatility,
value: 45 * Math.pow(basis, year),
high: 45 * Math.pow(basis+volatility, year),
low: 45 * Math.pow(basis-volatility, year),
}
}
for(i = 0; i < 25; i++) {
years.push(i);
}
var data = years.map(function(year){ return [getStat(year, 0.03, 1.08),getStat(year, 0.02, 1.08), getStat(year, 0.01, 1.08)]; }); // generate bogus data
var set_one = data.map(function(d) { return d[0];});
var set_two = data.map(function(d) { return d[1];});
var set_three = data.map(function(d) { return d[2];});
var chart = d3.select("#chart").attr("width", width).attr("height", height).append("g");
var x = d3.scale.linear().domain([0, years.length]).range([0, width]);
var y = d3.scale.linear().domain([0, d3.max(data, function(d){ return Math.max(d[0].high, d[1].high); })]).range([height, 0]);
var area = d3.svg.area().x(function(d,i) { return x(i); })
.y0(function(d, i){ return y(d.low)}) //FUNCTION FOR BASE-Y
.y1(function(d, i){ return y(d.high * 0.99);}); //FUNCTION FOR TOP-Y
chart
.selectAll("path.area")
.data([set_one,set_two,set_three]) // !!! here i can pass both arrays in.
.enter()
.append("path")
.attr("fill", "rgba(0,0,0,0.5)")
.attr("class", function(d,i) { return [NSW,QLD,"T"][i]; })
.attr("d", area);
</script>
I have following code to update chart1 with dataset2 but in the result mychart1 values are not updated to the new values in the dataset2. I want the updated values in dataset2 to be reflected in myChart1, but the old data is getting updated when I look at the class assinged in the debugger.
Can anybody point me where I am going wrong
function chart() {
//Width and height
var w = 200;
var h = 100;
var barPadding = 2;
var max = 0;
this.createChart = function(dataset,cls) {
//create svg element
var svg = d3.select("#chartDisplay").append("svg").attr("class",cls).attr(
"width", w).attr("height", h);
//create rect bars
var rect = svg.selectAll("rect").data(dataset,function(d, i) {return d;});
rect.enter()
.append("rect")
.attr("class","original")
.attr("x",function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return h - d * 3; //Height minus data value
})
.attr("width", w / dataset.length - barPadding)
.attr("height", function(d) {
return d * 3;
});
max = d3.max(dataset);
var xScale = d3.scale.linear().domain([ 0, max ]).range(
[ 0, w ]);
}
this.updateChart = function(dataset,cls) {
var svg = d3.select("#chartDisplay").select("svg."+cls);
var rect = svg.selectAll('rect')
.data(dataset,function(d, i) {
return d;
});
rect.attr("y", function(d) {
return h - d * 3; //Height minus data value
})
.attr("height", function(d) {
return d * 3;
});
rect.attr("class","updated");
}
this.getMax = function() {
return max;
}
}
var dataset1 = [ 5, 10, 12, 19, 21, 25, 22, 18, 15, 13 ];
var dataset2 = [ 1, 4, 14, 19, 16, 30, 22, 18, 15, 13 ];
var myChart1 = new chart();
myChart1.createChart(dataset1,"chart1");
myChart1.updateChart(dataset2,"chart1");
var myChart2 = new chart();
myChart2.createChart(dataset2,"chart2");
This is the problem:
var rect = svg.selectAll('rect')
.data(dataset,function(d, i) { return d; });
This is telling d3 to use the data value as a key into your array. Since your values are changing, the different values don't match and so they aren't being updated (you would need to use another append statement to get them to show up). Since you just have an array of values, you want to use the index of the value as the key (this makes it so that element 1 from dataset1 gets updated with the value of the new element 1 in dataset2).
You can either specifically use the index as the key:
var rect = svg.selectAll('rect')
.data(dataset,function(d, i) { return i; });
Or more simply, by doing this since using the index as the key is the default behavior.
var rect = svg.selectAll('rect')
.data(dataset);