I have a codepen here - https://codepen.io/anon/pen/GyeEpJ?editors=0010#0
Its a stacked bar chart with a line chart on top
The line chart points appear at the left side of the bar below.
How can I position the points in the line so they appear above the ticks in the x axis.
let dataline = d3.line()
.x((d) => {
return x(d.date);
})
.y((d) =>{
return y(d.total);
});
let layersLineArea = chart.append('g')
.attr('class', 'layers-lines');
let layersLine = layersLineArea.append('path')
.data([totalData])
.attr("class", "line")
.attr('d', dataline);
You're using a band scale, which is not suitable for a line chart.
The simplest solution is adding half the bandwidth in the line generator:
let dataline = d3.line()
.x((d) => {
return x(d.date) + x.bandwidth()/2;
})
.y((d) =>{
return y(d.total);
});
Here is your code with that change:
let keys = [];
let maxVal = [];
let dataToStack = [];
let totalData = [];
let legendKeys = ['usedInf', 'newInf'];
let w = 800;
let h = 450;
let margin = {
top: 60,
bottom: 40,
left: 50,
right: 20,
};
let width = w - margin.left - margin.right;
let height = h - margin.top - margin.bottom;
let colors = ['#FFC400', '#FF4436', '#FFEBB6', '#FFC400', '#B4EDA0'];
let data = [{
"one": 10,
"two": 12,
"three": 18,
"four": 22,
"five": 30,
"six": 44,
"seven": 125,
"date": "2015-05-31T00:00:00"
}, {
"one": 30,
"two": 42,
"three": 38,
"four": 62,
"five": 90,
"six": 144,
"seven": 295,
"date": "2015-06-30T00:00:00"
}, {
"one": 30,
"two": 92,
"three": 18,
"four": 100,
"five": 120,
"six": 10,
"seven": 310,
"date": "2015-07-31T00:00:00"
}, ];
for (let i = 0; i < data.length; i++) {
dataToStack.push({
date: data[i]['date'].toString(),
usedInf: data[i]['one'] + data[i]['two'] + data[i]['three'],
newInf: data[i]['four'] + data[i]['five'] + data[i]['six']
});
totalData.push({
date: data[i]['date'].toString(),
total: data[i]['seven']
});
}
//------------------------- Stack ------------------------//
let stack = d3.stack()
.keys(legendKeys);
let stackedSeries = stack(dataToStack);
//------------------------- Stack ------------------------//
let x = d3.scaleBand()
.domain(dataToStack.map(function(d) {
//let date = new Date(d.date);
return d.date;
}))
.rangeRound([0, width])
.padding(0.05);
let y = d3.scaleLinear()
.domain([0, d3.max(stackedSeries, function(d) {
return d3.max(d, (d) => {
return d[1];
})
})])
.range([height, 0]);
let svg = d3.select('.chart').append('svg')
.attr('class', 'chart')
.attr('width', w)
.attr('height', h);
let chart = svg.append('g')
.classed('graph', true)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
//------------------------- Bar Chart ------------------------//
let layersBarArea = chart.append('g')
.attr('class', 'layers-bars');
let layersBar = layersBarArea.selectAll('.layer-bar').data(stackedSeries)
.enter()
.append('g')
.attr('class', 'layer-bar')
.style('fill', (d, i) => {
return colors[i];
});
layersBar.selectAll('rect')
.data((d) => {
return d
})
.enter()
.append('rect')
.attr('height', (d, i) => {
return y(d[0]) - y(d[1]);
})
.attr('y', (d) => {
return y(d[1]);
})
.attr('x', (d, i) => {
return x(d.data.date)
})
.attr('width', x.bandwidth());
//------------------------- Bar Chart ------------------------//
//------------------------- Line Chart ------------------------//
let dataline = d3.line()
.x((d) => {
return x(d.date) + x.bandwidth() / 2;
})
.y((d) => {
return y(d.total);
});
let layersLineArea = chart.append('g')
.attr('class', 'layers-lines');
let layersLine = layersLineArea.append('path')
.data([totalData])
.attr("class", "line")
.attr('d', dataline);
//------------------------- Line Chart ------------------------//
chart.append('g')
.classed('x axis', true)
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
chart.append('g')
.classed('y axis', true)
.call(d3.axisLeft(y)
.ticks(10));
.line {
fill: none;
stroke: #00D7D2;
stroke-width: 5px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div class="chart"></div>
Related
I want to do an animation on the lines, but the second line will draw from two parts, one from begining, and the other from close to the second last point and disappear, so I got a result like this
I was following others'code
const pathLength = path.node().getTotalLength();
const transitionPath = d3.transition().ease(d3.easeQuad).duration(3000);
path
.attrs({
"stroke-dashoffset": pathLength,
"stroke-dasharray": pathLength,
})
.transition(transitionPath)
.attr("stroke-dashoffset", 0);
if you need all the code, I can paste, but it is really just this part that works with the animation, thank you!
I think you're accidentally using the same path length twice - namely that of the first path. path.node() returns the first node, even if there are multiple nodes in the selection.
const data = [{
category: "series_1",
values: [{
name: "A",
value: 10
},
{
name: "B",
value: 21
},
{
name: "C",
value: 19
},
{
name: "D",
value: 23
},
{
name: "E",
value: 20
},
],
}, ];
let counter = 1;
const add_set = (arr) => {
let copy = JSON.parse(JSON.stringify(arr[0]));
const random = () => Math.floor(Math.random() * 20 + 1);
const add = (arr) => {
counter++;
copy.values.map((i) => (i.value = random()));
copy.category = `series_${counter}`;
arr.push(copy);
};
add(arr);
};
add_set(data);
//No.1 define the svg
let graphWidth = 600,
graphHeight = 300;
let margin = {
top: 60,
right: 10,
bottom: 30,
left: 45
};
let totalWidth = graphWidth + margin.left + margin.right,
totalHeight = graphHeight + margin.top + margin.bottom;
let svg = d3
.select("body")
.append("svg")
.attr("width", totalWidth)
.attr("height", totalHeight);
//No.2 define mainGraph
let mainGraph = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//No.3 define axises
let categoriesNames = data[0].values.map((d) => d.name);
let xScale = d3
.scalePoint()
.domain(categoriesNames)
.range([0, graphWidth]); // scalepoint make the axis starts with value compared with scaleBand
let colorScale = d3.scaleOrdinal(d3.schemeCategory10);
colorScale.domain(data.map((d) => d.category));
let yScale = d3
.scaleLinear()
.range([graphHeight, 0])
.domain([
d3.min(data, (i) => d3.min(i.values, (x) => x.value)),
d3.max(data, (i) => d3.max(i.values, (x) => x.value)),
]); //* If an arrow function is simply returning a single line of code, you can omit the statement brackets and the return keyword
//No.4 set axises
mainGraph
.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + graphHeight + ")")
.call(d3.axisBottom(xScale));
mainGraph.append("g").attr("class", "y axis").call(d3.axisLeft(yScale));
//No.5 make lines
let lineGenerator = d3
.line()
.x((d) => xScale(d.name))
.y((d) => yScale(d.value))
.curve(d3.curveMonotoneX);
var lines = mainGraph
.selectAll(".path")
.data(data.map((i) => i.values))
.enter()
.append("path")
.attr("d", lineGenerator)
.attr("fill", "none")
.attr("stroke-width", 3)
.attr("stroke", (d, i) => colorScale(i));
//No.6 append circles
let circleData = data.map((i) => i.values);
mainGraph
.selectAll(".circle-container")
.data(circleData)
.enter()
.append("g")
.attr("class", "circle-container")
.attr("fill", (d, i) => console.log(d) || colorScale(i))
.selectAll("circle")
.data((d) => d)
.enter()
.append("circle")
.attrs({
cx: (d) => xScale(d.name),
cy: (d) => yScale(d.value),
r: 3,
opacity: 1,
});
// HERE we let the lines grow
lines
.attr("stroke-dasharray", function(d) {
// Get the path length of the current element
const pathLength = this.getTotalLength();
return `0 ${pathLength}`
})
.transition()
.duration(2500)
.attr("stroke-dasharray", function(d) {
// Get the path length of the current element
const pathLength = this.getTotalLength();
return `${pathLength} ${pathLength}`
});
.line {
stroke: blue;
fill: none;
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
d3.json("data2.json", function(error, data) {
data.forEach(function(d) {
d.Total = +d.Total;
});
var width = 200,
height = 50;
var margin = {
top: 10,
right: 10,
bottom: 30,
left: 10
};
var svg = d3.select('body')
.append('svg')
.attr('width', '20%')
.attr('height', '20%')
.attr('viewBox', '0 0 ' + width + ' ' + height)
.append('g');
width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;
svg.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var xScale = d3.scaleLinear()
.range([0, width])
var yScale = d3.scaleBand()
.range([0, height])
.padding(0.1);
xScale.domain([0, d3.sum(data, function(d) {
return d.Total;
})]);
var x_axis = svg.append('g')
.attr('class', 'axis')
.attr('padding', 1)
.attr('transform', 'translate(' + 0 + ',' + height + ')');
var keys = data.map(function(d) {
return d.Type;
});
var newData = [{}];
data.forEach(function(d) {
newData[0][d.Type] = d.Total
});
var stack = d3.stack()
.keys(keys);
var series = stack(newData);
var colorScale = d3.scaleOrdinal()
.domain([0, 12])
.range(d3.schemeCategory10);
var bars = svg.selectAll()
.data(series)
.enter()
.append('g')
.attr('fill', function(d) {
return colorScale(d.key);
})
.selectAll('rect')
.data(function(d) {
return d;
})
.enter()
.append('rect')
.attr('x', function(d, i) {
return xScale(d[0]) ;
})
.attr('width', function(d, i) {
return xScale(d[1]) - xScale(d[0]);
})
.attr("height", yScale.bandwidth());
});
This is the code I tried. I tried adding padding to g as well as x. It does not seem to be working. I just need a horizontal single stacked bar chart along with a tooltip. I can add the tooltip later but I just need somebody to help me figure this out. I have been struggling for far too long on this. Below is the data I am using.
[
{
"Type": "Pending Review",
"Total": 3209,
"Percent": "23.90%"
},
{
"Type": "Audit Finding",
"Total": 2715,
"Percent": "20.22%"
},
{
"Type": "No Issues",
"Total": 1675,
"Percent": "12.50%"
}
]
The easiest way that I found is to subtract few pixels from the width of the bar obtained with the x-axis mapping object (xScale in your case). You can keep the positioning (x and y values) as they are. So you can just change this line:
.attr('width', function(d, i) {
return xScale(d[1]) - xScale(d[0]);
})
like this:
.attr('width', function(d, i) {
return xScale(d[1]) - xScale(d[0]) - 2;
})
The -2 do the trick.
https://codepen.io/bemipefe/pen/ExyWeYm
I have a plunker here https://plnkr.co/edit/hBWoIIyzcHELGyewOyZE?p=preview
I'm trying to create a simple stacked bar chart.
The bars go above the top of the chart which I think is a problem with the domain
I also need a scale on the y axis which I think is to do with the y domain.
Is it the y domain that controls the height of the bars and scales shown on the y axis
y.domain([0, d3.max(data, (d)=>{
return d
})])
This is a list of the issues so far:
First, your y domain is not correctly set. It should use the stacked data:
y.domain([0, d3.max(stackedSeries, function(d) {
return d3.max(d, function(d) {
return d[0] + d[1];
});
})])
Second, the math for the y and height of the rectangles is wrong. It should be:
.attr('height', (d) => {
return y(d[0]) - y(d[0] + d[1]);
})
.attr('y', (d) => {
return y(d[0] + d[1]);
})
Finally, use the x scale for the x position:
.attr('x', (d, i) => {
return x(d.data.day)
})
Here is the code with those changes:
var margin = {
top: 40,
right: 20,
bottom: 40,
left: 40
}
var width = 400 - margin.left - margin.right
var height = 500 - margin.top - margin.bottom
var data = [{
day: 'Mon',
apricots: 120,
blueberries: 180,
cherries: 100
},
{
day: 'Tue',
apricots: 60,
blueberries: 185,
cherries: 105
},
{
day: 'Wed',
apricots: 100,
blueberries: 215,
cherries: 110
},
{
day: 'Thu',
apricots: 150,
blueberries: 330,
cherries: 105
},
{
day: 'Fri',
apricots: 120,
blueberries: 240,
cherries: 105
}
];
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 + ')')
var colors = ['#FBB65B', '#513551', '#de3163'];
var stack = d3.stack()
.keys(['apricots', 'blueberries', 'cherries']);
var stackedSeries = stack(data);
// Create a g element for each series
var g = d3.select('g')
.selectAll('g.series')
.data(stackedSeries)
.enter()
.append('g')
.classed('series', true)
.style('fill', (d, i) => {
return colors[i];
});
var x = d3.scaleBand()
.rangeRound([0, width])
.padding(0.1)
var y = d3.scaleLinear()
.range([height, 0])
x.domain(data.map((d) => {
return d.day
}))
y.domain([0, d3.max(stackedSeries, function(d) {
return d3.max(d, function(d) {
return d[0] + d[1];
});
})])
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(d3.axisBottom(x))
svg.append('g')
.attr('class', 'y axis')
.call(d3.axisLeft(y))
// For each series create a rect element for each day
g.selectAll('rect')
.data((d) => {
return d;
})
.enter()
.append('rect')
.attr('height', (d) => {
return y(d[0]) - y(d[0] + d[1]);
})
.attr('y', (d) => {
return y(d[0] + d[1]);
})
.attr('x', (d, i) => {
return x(d.data.day)
})
.attr('width', x.bandwidth())
.style("stroke", "#ccc");
<script src="https://d3js.org/d3.v4.min.js"></script>
My data for a horizontal bar graph is an array of objects that look like this:
{value: -10, dataset:"Corvette", year: "1975"}. The "dataset" labels are on the y axis. I would like to append the "year" label to the "dataset" label, so the labels on the y axis would look like this:
Corvette 1975
So far I can add one or the other to the Y axis but not both. Here is the code I have:
var margin = {top: 30, right: 10, bottom: 50, left: 50},
width = 500,
height = 300;
var data = [{value: -10, dataset:"Corvette", year: "1975"},
{value: 40, dataset:"Lumina", year: "1975"},
{value: -10, dataset:"Gran Torino", year: "1971"},
{value: -50, dataset:"Pomtiac GTO", year: "1964"},
{value: 30, dataset:"Mustang", year: "19655"},
{value: -20, dataset:"Camaro", year: "1973"},
{value: -70, dataset:"Firebird", year: "1975"}];
// Add svg to
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 + ')');
// set the ranges
var y = d3.scaleBand()
.range([height, 0])
.padding(0.1);
var x = d3.scaleLinear()
.range([0, width]);
// Scale the range of the data in the domains
x.domain(d3.extent(data, function (d) {
return d.value;
}));
y.domain(data.map(function (d) {
return d.dataset;
}));
// append the rectangles for the bar chart
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", function (d) {
return "bar bar--" + (d.value < 0 ? "negative" : "positive");
})
.attr("x", function (d) {
return x(Math.min(0, d.value));
})
.attr("y", function (d) {
return y(d.dataset);
})
.attr("width", function (d) {
return Math.abs(x(d.value) - x(0));
})
.attr("height", y.bandwidth());
// add the x Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// add the y Axis
let yAxisGroup = svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + x(0) + ",0)")
.call(d3.axisRight(y));
yAxisGroup.selectAll('.tick')
.data(data)
.select('text')
.attr('x', function(d,i){return d.value<0?9:-9})
.style('text-anchor', function(d,i){return d.value<0?'start':'end'})
Here is the fiddle:
https://jsfiddle.net/Kavitha_2817/2e1xLxLc/
You could map a concatenated string of your d.dataset and d.year to the y scale, and then use the same concatenated string when positioning your rects using that y scale.
The y axis will then use that concatenated string.
Example:
https://jsfiddle.net/2e1xLxLc/4/
Relevant code:
//create a reusable function to concatenate the values you want to use
function yValue(d) { return d.dataset + " " + d.year }
// Scale the range of the data in the domains
x.domain(d3.extent(data, function (d) {
return d.value;
}));
y.domain(data.map(function(d){ return yValue(d) }));
// append the rectangles for the bar chart
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", function (d) {
return "bar bar--" + (d.value < 0 ? "negative" : "positive");
})
.attr("x", function (d) {
return x(Math.min(0, d.value));
})
.attr("y", function (d) {
return y(yValue(d));
})
.attr("width", function (d) {
return Math.abs(x(d.value) - x(0));
})
.attr("height", y.bandwidth());
If you (for any reason) want to keep the same domain, get the year using tickFormat:
.call(d3.axisRight(y)
.tickFormat(function(d) {
//filter the data array according to 'd', which is 'dataset'
var filtered = data.filter(function(e) {
return e.dataset === d;
})[0];
//get the year in the 'filtered' object using 'filtered.year'
return d + " " + filtered.year
})
);
Here is your code with that change:
var margin = {
top: 30,
right: 10,
bottom: 50,
left: 50
},
width = 500,
height = 300;
var data = [{
value: -10,
dataset: "Corvette",
year: "1975"
}, {
value: 40,
dataset: "Lumina",
year: "1975"
}, {
value: -10,
dataset: "Gran Torino",
year: "1971"
}, {
value: -50,
dataset: "Pomtiac GTO",
year: "1964"
}, {
value: 30,
dataset: "Mustang",
year: "19655"
}, {
value: -20,
dataset: "Camaro",
year: "1973"
}, {
value: -70,
dataset: "Firebird",
year: "1975"
}];
// Add svg to
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 + ')');
// set the ranges
var y = d3.scaleBand()
.range([height, 0])
.padding(0.1);
var x = d3.scaleLinear()
.range([0, width]);
// Scale the range of the data in the domains
x.domain(d3.extent(data, function(d) {
return d.value;
}));
y.domain(data.map(function(d) {
return d.dataset;
}));
// append the rectangles for the bar chart
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", function(d) {
return "bar bar--" + (d.value < 0 ? "negative" : "positive");
})
.attr("x", function(d) {
return x(Math.min(0, d.value));
})
.attr("y", function(d) {
return y(d.dataset);
})
.attr("width", function(d) {
return Math.abs(x(d.value) - x(0));
})
.attr("height", y.bandwidth());
// add the x Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// add the y Axis
let yAxisGroup = svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + x(0) + ",0)")
.call(d3.axisRight(y)
.tickFormat(function(d) {
var filtered = data.filter(function(e) {
return e.dataset === d;
})[0];
return d + " " + filtered.year
})
);
yAxisGroup.selectAll('.tick')
.data(data)
.select('text')
.attr('x', function(d, i) {
return d.value < 0 ? 9 : -9
})
.style('text-anchor', function(d, i) {
return d.value < 0 ? 'start' : 'end'
})
<style> .bar--positive {
fill: steelblue;
}
.bar--negative {
fill: darkorange;
}
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
What would be causing this chart to have a value appear below the x-axis (0:00)?
My data looks like this (no values less than zero):
[{"x":1341806400,"y":4},
{"x":1342411200,"y":0},
{"x":1343016000,"y":0},
{"x":1343620800,"y":7},
{"x":1344225600,"y":6},
{"x":1344830400,"y":73},
{"x":1345435200,"y":328},
{"x":1346040000,"y":0},
{"x":1346644800,"y":0},
{"x":1347249600,"y":0},
{"x":1347854400,"y":0},
{"x":1348459200,"y":11},
{"x":1349064000,"y":17},
{"x":1349668800,"y":0},
{"x":1350273600,"y":0}]
Rendered Chart
The above chart is rendered via:
var margin = {top: 20, right: 30, bottom: 30, left: 40};
var width = max_width - margin.left - margin.right;
var height = 300; // + margin.top + margin.bottom;
var height_offset = 100;
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var z = d3.scale.category20c();
var ticks_interval;
switch(this.period_type){
case "day":
ticks_interval = d3.time.days;
break;
case "week":
ticks_interval = d3.time.weeks;
break;
case "month":
ticks_interval = d3.time.months;
break;
case "year":
ticks_interval = d3.time.years;
break;
}
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(ticks_interval);
var yAxis = d3.svg.axis()
.scale(y)
.tickFormat(function(d){
return numeral(d).format("00:00").replace(/^0:/,""); })
.orient("left");
var stack = d3.layout.stack()
.offset("zero")
.values(function(d) { return d.values; })
.x(function(d) { return d.date; })
.y(function(d) { return d.value; });
var nest = d3.nest()
.key(function(d) { return d.key; });
var area = d3.svg.area()
.interpolate("cardinal")
.x(function(d) { return x(d.date); })
.y0(function(d) {return y(d.y0);})
.y1(function(d) { return y(d.y0 + d.y); });
var svg = d3.select(chart_dom_el)
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + height_offset)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var layers = stack(nest.entries(data));
x.domain(d3.extent(data, function(d){ return d.date; }));
y.domain([0,d3.max(data,function(d){
return d.y0+ d.y;
})]);
// re-map for formatted date
data = _.map(data,function(d){
return {date: d.date.format("MM/D"),value:d.value};
});
svg.selectAll(".layer")
.data(layers)
.enter().append("path")
.attr("class", "layer")
.attr("d", function(d) { return area(d.values); })
.style("fill", function(d, i) { return z(i); });
var x_axis_options = {x: 4, y: 9, rotate: 0};
if(data.length > 20){
x_axis_options = {x: -27, y: 8, rotate: -45};
}else if(data.length > 13){
x_axis_options = {y: -5, x: 27, rotate: 90};
}
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.attr("y", x_axis_options.y)
.attr("x", x_axis_options.x)
.attr("transform", "rotate("+(x_axis_options.rotate)+")");
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
This appears to have to do with the interpolation mode you used on the area generator - try changing from cardinal to linear, or some other area interpolation mode