Related
I am using D3 to make a stacked bar chart (for more artistic purposes than scientific). I want to design my stacked bar chart to be centered around one group, with half above and half below an invisible line, and have the other two groups be on either side of the line.
Currently, my graph looks like this
But I want it to look more like this
My code is here:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stacked Bar</title>
</head>
<body>
<div class="container">
<div id="chart"></div>
</div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
const width = 860,
height = 400,
margin = { top: 40, right: 30, bottom: 20, left: 20 };
const svg = d3
.select("#chart")
.append("svg")
.attr("viewBox", [0, 0, width, height]);
d3.csv("test.csv").then((data) => {
let x = d3
.scaleBand(
data.map((d) => d.Time),
[margin.left, width - margin.right]
)
.padding([0.2]);
let y = d3.scaleLinear([0, 500], [height - margin.bottom, margin.top]);
svg
.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x));
svg.append("g").attr("transform", `translate(${margin.left},0)`);
// .call(d3.axisLeft(y).tickSize(-width + margin.left + margin.right));
//protein,carbs,fiber
const subgroups = data.columns.slice(1);
const color = d3.scaleOrdinal(subgroups, [
"#e41a1c",
"#377eb8",
"#4daf4a",
]);
const stackedData = d3.stack().keys(subgroups)(data);
console.log(stackedData);
svg
.append("g")
.selectAll("g")
.data(stackedData)
.join("g")
.attr("fill", (d) => color(d.key))
.selectAll("rect")
.data((d) => d)
.join("rect")
.attr("x", (d) => x(d.data.Time))
.attr("y", (d) => y(d[1]))
.attr("height", (d) => y(d[0]) - y(d[1]))
.attr("width", x.bandwidth());
let legendGroup = svg
.selectAll(".legend-group")
.data(subgroups)
.join("g")
.attr("class", "legend-group");
legendGroup
.append("circle")
.attr("cx", (d, i) => 10 + i * 75)
.attr("cy", 10)
.attr("r", 3)
.attr("fill", (d, i) => color(i));
legendGroup
.append("text")
.attr("x", (d, i) => 20 + i * 75)
.attr("y", 15)
.text((d, i) => subgroups[i]);
});
</script>
</body>
</html>
and csv:
Time,team1,team2,middle
0,5,2,70
1,10,13,89
2,4,15,110
3,6,16,145
4,12,2,167
5,42,3,111
6,6,4,108
7,7,5,92
8,8,34,140
9,12,89,190
10,22,90,398
11,42,91,459
12,60,23,256
13,69,13,253
14,43,11,188
15,42,7,167
16,21,9,124
17,16,12,156
18,7,14,167
19,12,13,188
Does anyone know how I could vertically center each line around the middle group? Is this something to do in the data pre-processing or in the graph making itself?
You have to use the correct offset, in this case d3.offsetWiggle:
const stackedData = d3.stack().offset(d3.stackOffsetWiggle)
In this solution I'm flattening the stacked data and getting the extent, which I'll pass to the y scale:
const flatData = stackedData.flat(2);
y.domain(d3.extent(flatData));
Finally, I'm just moving the x axis to the middle of the y range. Also, I'm hardcoding the stack keys, but making the sequence programatically is trivial, as well as some other details you'll have to adjust.
Here's the result:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stacked Bar</title>
</head>
<body>
<div class="container">
<div id="chart"></div>
</div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
const width = 860,
height = 400,
margin = {
top: 40,
right: 30,
bottom: 20,
left: 20
};
const svg = d3
.select("#chart")
.append("svg")
.attr("viewBox", [0, 0, width, height]);
const csv = `Time,team1,team2,middle
0,5,2,70
1,10,13,89
2,4,15,110
3,6,16,145
4,12,2,167
5,42,3,111
6,6,4,108
7,7,5,92
8,8,34,140
9,12,89,190
10,22,90,398
11,42,91,459
12,60,23,256
13,69,13,253
14,43,11,188
15,42,7,167
16,21,9,124
17,16,12,156
18,7,14,167
19,12,13,188`;
const data = d3.csvParse(csv);
let x = d3
.scaleBand(
data.map((d) => d.Time), [margin.left, width - margin.right]
)
.padding([0.2]);
let y = d3.scaleLinear().range([height - margin.bottom, margin.top]);
svg.append("g").attr("transform", `translate(${margin.left},0)`);
// .call(d3.axisLeft(y).tickSize(-width + margin.left + margin.right));
//protein,carbs,fiber
const subgroups = ["team1", "middle", "team2"];
const color = d3.scaleOrdinal(subgroups, [
"#377eb8",
"#4daf4a",
"#e41a1c"
]);
const stackedData = d3.stack().offset(d3.stackOffsetWiggle).order(d3.stackOrderNone).keys(subgroups)(data);
const flatData = stackedData.flat(2);
y.domain(d3.extent(flatData));
svg
.append("g")
.selectAll("g")
.data(stackedData)
.join("g")
.attr("fill", (d) => color(d.key))
.selectAll("rect")
.data((d) => d)
.join("rect")
.attr("x", (d) => x(d.data.Time))
.attr("y", (d) => y(d[1]))
.attr("height", (d) => y(d[0]) - y(d[1]))
.attr("width", x.bandwidth());
svg
.append("g")
.attr("transform", `translate(0,${margin.top + (height - margin.bottom)/2})`)
.call(d3.axisBottom(x));
let legendGroup = svg
.selectAll(".legend-group")
.data(subgroups)
.join("g")
.attr("class", "legend-group");
legendGroup
.append("circle")
.attr("cx", (d, i) => 10 + i * 75)
.attr("cy", 10)
.attr("r", 3)
.attr("fill", (d, i) => color(i));
legendGroup
.append("text")
.attr("x", (d, i) => 20 + i * 75)
.attr("y", 15)
.text((d, i) => subgroups[i]);
</script>
</body>
</html>
I am currently testing out d3's join, enter, update, exit and desiring to produce something like
chart update 1 or chart update 2.
To achieve this, I have built a HTML dropdown with select element from a dataset and I expect to wire up the dropdown to the viz to achieve at least updated values as per the dropdown selection. But it is failing.
It is a hard concept to grasp and I am not sure where the code is failing. If any value is selected from the dropdown, the chart does not update at all.
////////////////////////////////////////////////////////////
//////////////////////// 00 BUILD DATA//////// /////////////
////////////////////////////////////////////////////////////
//desired permutation length
const length = 4;
//build array from the above length
const perm = Array.from(Array(length).keys()).map((d) => d + 1);
//generate corresponding alphabets for name
const name = perm.map((x) => String.fromCharCode(x - 1 + 65));
//permutation function - https://stackoverflow.com/questions/9960908/permutations-in-javascript/24622772#24622772
function permute(permutation) {
var length = permutation.length,
result = [permutation.slice()],
c = new Array(length).fill(0),
i = 1,
k, p;
while (i < length) {
if (c[i] < i) {
k = i % 2 && c[i];
p = permutation[i];
permutation[i] = permutation[k];
permutation[k] = p;
++c[i];
i = 1;
result.push(permutation.slice());
} else {
c[i] = 0;
++i;
}
}
return result;
};
//generate permutations
const permut = permute(perm);
//generate year based on permutation
const year = permut.map((x, i) => i + 2000);
//generate a yearly constant based on year to generate final value as per the rank {year-name}
const constant = year.map(d => Math.round(d * Math.random()));
const src =
year.map((y, i) => {
return name.map((d, j) => {
return {
Name: d,
Year: y,
Rank: permut[i][j],
Const: constant[i],
Value: Math.round(constant[i] / permut[i][j])
};
});
}).flat();
//console.log(src);
////////////////////////////////////////////////////////////
//////////////////////// 0 BUILD HTML DROPDOWN /////////////
////////////////////////////////////////////////////////////
d3.select('body')
.append('div', 'dropdown')
.style('position', 'absolute')
.style('top', '400px')
.append('select')
.attr('name', 'input')
.classed('Year', true)
.selectAll('option')
.data(year)
.enter()
.append('option')
//.join('option')
.text((d) => d)
.attr("value", (d) => d )
//get the dropdown value
const filterYr = parseFloat(d3.select('.Year').node().value);
////////////////////////////////////////////////////////////
//////////////////////// 1 DATA WRANGLING //////////////////
////////////////////////////////////////////////////////////
const xAccessor = (d) => d.Year;
const yAccessor = (d) => d.Value;
////////////////////////////////////////////////////////////
//////////////////////// 2 CREATE SVG //////////////////////
////////////////////////////////////////////////////////////
//namespace
//define dimension
const width = 1536;
const height = 720;
const svgns = "http://www.w3.org/2000/svg";
const svg = d3.select("svg");
svg.attr("xmlns", svgns).attr("viewBox", `0 0 ${width} ${height}`);
svg
.append("rect")
.attr("class", "vBoxRect")
//.style("overflow", "visible")
.attr("width", `${width}`)
.attr("height", `${height}`)
.attr("stroke", "black")
.attr("fill", "white");
////////////////////////////////////////////////////////////
//////////////////////// 3 CREATE BOUND ////////////////////
////////////////////////////////////////////////////////////
const padding = {
top: 70,
bottom: 100,
left: 120,
right: 120
};
const multiplierH = 1; //controls the height of the visual container
const multiplierW = 1; //controls the width of the visual container
const boundHeight = height * multiplierH - padding.top - padding.bottom;
const boundWidth = width * multiplierW - padding.right - padding.left;
//create BOUND rect -- to be deleted later
svg
.append("rect")
.attr("class", "boundRect")
.attr("x", `${padding.left}`)
.attr("y", `${padding.top}`)
.attr("width", `${boundWidth}`)
.attr("height", `${boundHeight}`)
.attr("fill", "white");
//create bound element
const bound = svg
.append("g")
.attr("class", "bound")
.style("transform", `translate(${padding.left}px,${padding.top}px)`);
function draw() {
// filter data as per dropdown
const data = src.filter(a => a.Year == filterYr);
////////////////////////////////////////////////////////////
//////////////////////// 4 CREATE SCALE ////////////////////
////////////////////////////////////////////////////////////
const scaleX = d3
.scaleLinear()
.range([0, boundWidth])
.domain(d3.extent(data, xAccessor));
const scaleY = d3
.scaleLinear()
.range([boundHeight, 0])
.domain(d3.extent(data, yAccessor));
bound.append('g')
.classed('textContainer', true)
.selectAll('text')
.data(data)
.join(
enter => enter.append('text')
.attr('x', (d, i) => scaleX(d.Year))
.attr('y', (d, i) => i)
.attr('dy', (d, i) => i * 30)
.text((d) => d.Year + '-------' + d.Value.toLocaleString())
.style("fill", "blue"),
update =>
update
.transition()
.duration(500)
.attr('x', (d, i) => scaleX(d.Year))
.attr('y', (d, i) => i)
.attr('dy', (d, i) => i * 30)
.text((d) => d.Year + '-------' + d.Value.toLocaleString())
.style("fill", "red")
/*,
(exit) =>
exit
.style("fill", "black")
.transition()
.duration(1000)
.attr("transform", (d, i) => `translate(${300},${30 + i * 30})`)
.remove()*/
)
}
draw();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg>
</svg>
<!--d3 script-->
<script type="text/javascript">
</script>
</body>
</html>
You have a dropdown, but you're not listening to it. For example:
select.on("change", () => {
const filterYr = parseFloat(d3.select('.Year').node().value);
draw(filterYr);
});
Which alternatively can also be:
select.on("change", event => {
const filterYr = +event.currentTarget.value;
draw(filterYr);
});
Note that I'm passing filterYr to the draw() function as an argument. Also, do not append the containing <g> inside draw(), otherwise you'll have only the enter selection.
Here's your code with those changes:
////////////////////////////////////////////////////////////
//////////////////////// 00 BUILD DATA//////// /////////////
////////////////////////////////////////////////////////////
//desired permutation length
const length = 4;
//build array from the above length
const perm = Array.from(Array(length).keys()).map((d) => d + 1);
//generate corresponding alphabets for name
const name = perm.map((x) => String.fromCharCode(x - 1 + 65));
//permutation function - https://stackoverflow.com/questions/9960908/permutations-in-javascript/24622772#24622772
function permute(permutation) {
var length = permutation.length,
result = [permutation.slice()],
c = new Array(length).fill(0),
i = 1,
k, p;
while (i < length) {
if (c[i] < i) {
k = i % 2 && c[i];
p = permutation[i];
permutation[i] = permutation[k];
permutation[k] = p;
++c[i];
i = 1;
result.push(permutation.slice());
} else {
c[i] = 0;
++i;
}
}
return result;
};
//generate permutations
const permut = permute(perm);
//generate year based on permutation
const year = permut.map((x, i) => i + 2000);
//generate a yearly constant based on year to generate final value as per the rank {year-name}
const constant = year.map(d => Math.round(d * Math.random()));
const src =
year.map((y, i) => {
return name.map((d, j) => {
return {
Name: d,
Year: y,
Rank: permut[i][j],
Const: constant[i],
Value: Math.round(constant[i] / permut[i][j])
};
});
}).flat();
//console.log(src);
////////////////////////////////////////////////////////////
//////////////////////// 0 BUILD HTML DROPDOWN /////////////
////////////////////////////////////////////////////////////
const select = d3.select('body')
.append('div', 'dropdown')
.style('position', 'absolute')
.style('top', '400px')
.append('select')
.attr('name', 'input')
.classed('Year', true);
select.selectAll('option')
.data(year)
.enter()
.append('option')
//.join('option')
.text((d) => d)
.attr("value", (d) => d)
//get the dropdown value
const filterYr = parseFloat(d3.select('.Year').node().value);
select.on("change", event => {
const filterYr = +event.currentTarget.value;
draw(filterYr);
});
////////////////////////////////////////////////////////////
//////////////////////// 1 DATA WRANGLING //////////////////
////////////////////////////////////////////////////////////
const xAccessor = (d) => d.Year;
const yAccessor = (d) => d.Value;
////////////////////////////////////////////////////////////
//////////////////////// 2 CREATE SVG //////////////////////
////////////////////////////////////////////////////////////
//namespace
//define dimension
const width = 1536;
const height = 720;
const svgns = "http://www.w3.org/2000/svg";
const svg = d3.select("svg");
svg.attr("xmlns", svgns).attr("viewBox", `0 0 ${width} ${height}`);
svg
.append("rect")
.attr("class", "vBoxRect")
//.style("overflow", "visible")
.attr("width", `${width}`)
.attr("height", `${height}`)
.attr("stroke", "black")
.attr("fill", "white");
////////////////////////////////////////////////////////////
//////////////////////// 3 CREATE BOUND ////////////////////
////////////////////////////////////////////////////////////
const padding = {
top: 70,
bottom: 100,
left: 120,
right: 120
};
const multiplierH = 1; //controls the height of the visual container
const multiplierW = 1; //controls the width of the visual container
const boundHeight = height * multiplierH - padding.top - padding.bottom;
const boundWidth = width * multiplierW - padding.right - padding.left;
//create BOUND rect -- to be deleted later
svg
.append("rect")
.attr("class", "boundRect")
.attr("x", `${padding.left}`)
.attr("y", `${padding.top}`)
.attr("width", `${boundWidth}`)
.attr("height", `${boundHeight}`)
.attr("fill", "white");
//create bound element
const bound = svg
.append("g")
.attr("class", "bound")
.style("transform", `translate(${padding.left}px,${padding.top}px)`);
const g = bound.append('g')
.classed('textContainer', true);
function draw(filterYr) {
// filter data as per dropdown
const data = src.filter(a => a.Year == filterYr);
////////////////////////////////////////////////////////////
//////////////////////// 4 CREATE SCALE ////////////////////
////////////////////////////////////////////////////////////
const scaleX = d3
.scaleLinear()
.range([0, boundWidth])
.domain(d3.extent(data, xAccessor));
const scaleY = d3
.scaleLinear()
.range([boundHeight, 0])
.domain(d3.extent(data, yAccessor));
g.selectAll('text')
.data(data)
.join(
enter => enter.append('text')
.attr('x', (d, i) => scaleX(d.Year))
.attr('y', (d, i) => i)
.attr('dy', (d, i) => i * 30)
.text((d) => d.Year + '-------' + d.Value.toLocaleString())
.style("fill", "blue"),
update =>
update
.transition()
.duration(500)
.attr('x', (d, i) => scaleX(d.Year))
.attr('y', (d, i) => i)
.attr('dy', (d, i) => i * 30)
.text((d) => d.Year + '-------' + d.Value.toLocaleString())
.style("fill", "red")
/*,
(exit) =>
exit
.style("fill", "black")
.transition()
.duration(1000)
.attr("transform", (d, i) => `translate(${300},${30 + i * 30})`)
.remove()*/
)
}
draw(filterYr);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg>
</svg>
<!--d3 script-->
<script type="text/javascript">
</script>
</body>
</html>
Hi i figured out how to add log labels at Y axis axis but cant make chart line scale log, and i dont really know how to make X axis labels plain text instead of converting it to date
If i just change scaleLinear to scaleLog chart line dissapear
Also removing const parseTime = d3.timeParse('%Y/%m/%d'); and change parseTime(val.date) just to val.date, and changing scaleTime to scaleOrdinal does not work as i would
Also i don't understand why Y axis labels are so blured
Sorry for asking but cant findout how this pluggin works.
using D3 7.2.1 and JQ 3.5.1
(function (d3){
const lineChartData = [
{
currency: "data",
values: [
{
date: "2018/01/01",
close: 0
},
{
date: "2018/02/01",
close: 5
},
{
date: "2018/03/01",
close: 10
},
{
date: "2018/04/01",
close: 50
},
{
date: "2018/05/01",
close: 100
},
{
date: "2018/06/01",
close: 500
},
{
date: "2018/07/01",
close: 1000
},
{
date: "2018/08/01",
close: 5000
},
{
date: "2018/09/01",
close: 10000
},
]
}
];
const margin = {
top: 20,
bottom: 20,
left: 50,
right: 20
};
const width = 400 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const createGradient = select => {
const gradient = select
.select('defs')
.append('linearGradient')
.attr('id', 'gradient')
.attr('x1', '0%')
.attr('y1', '100%')
.attr('x2', '0%')
.attr('y2', '0%');
gradient
.append('stop')
.attr('offset', '0%')
.attr('style', 'stop-color:#FF6500; stop-opacity:0');
gradient
.append('stop')
.attr('offset', '100%')
.attr('style', 'stop-color:#FF6500; stop-opacity: 1');
}
const createGlowFilter = select => {
const filter = select
.select('defs')
.append('filter')
.attr('id', 'glow')
//stdDeviation is px count for make blur around main chart line
filter
.append('feGaussianBlur')
.attr('stdDeviation', '0')
.attr('result', 'coloredBlur');
const femerge = filter
.append('feMerge');
femerge
.append('feMergeNode')
.attr('in', 'coloredBlur');
femerge
.append('feMergeNode')
.attr('in', 'SourceGraphic');
}
const svg = d3.select('#line-chart')
.append('svg')
.attr('width', 700 + margin.left + margin.right)
.attr('height', 300 + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
svg.append('defs');
svg.call(createGradient);
svg.call(createGlowFilter);
const parseTime = d3.timeParse('%Y/%m/%d');
const parsedData = lineChartData.map(company => ({
ticker: company.ticker,
values: company.values.map(val => ({
close: val.close,
date: parseTime(val.date)
}))
}));
const xScale = d3.scaleTime()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.date)),
d3.max(parsedData, d => d3.max(d.values, v => v.date))
])
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.close)),
d3.max(parsedData, d => d3.max(d.values, v => v.close))
])
.range([height, 0]);
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.close))
.curve(d3.curveCatmullRom.alpha(0.5));
svg.selectAll('.line')
.data(parsedData)
.enter()
.append('path')
.attr('d', d => {
const lineValues = line(d.values).slice(1);
const splitedValues = lineValues.split(',');
return `M0,${height},${lineValues},l0,${height - splitedValues[splitedValues.length - 1]}`
})
.style('fill', 'url(#gradient)')
svg.selectAll('.line')
.data(parsedData)
.enter()
.append('path')
.attr('d', d => line(d.values))
.attr('stroke-width', '2')
.style('fill', 'none')
.style('filter', 'url(#glow)')
.attr('stroke', '#FF6500');
const tick = svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale).ticks(9))
.selectAll('.tick')
.style('transition', '.2s');
//Y dashes
//stroke color of line in background
//stroke-dasharray
//first paramter is length
//second parameter is space between
tick
.selectAll('line')
.attr('stroke-dasharray', `4, 7`)
.attr('stroke', '#5E779B')
.attr('y2', `-${height}px`)
tick
.append('rect')
.attr('width', `${(width / 12) + 10}px`)
.attr('x', `-${width / 24 + 5}px`)
.attr('y', `-${height}px`)
.attr('height', `${height + 30}px`)
.style('fill', 'transparent');
svg.selectAll('.tick')
.append('circle')
.attr('r', '5px')
.style('fill', '#ffffff')
.style('stroke', '#FF6500')
.attr('cy', (x, i) => - height + yScale(parsedData[0].values[i].close));
svg.select('.domain')
.attr('stroke', '#5E779B')
.attr('stroke-dasharray', `4, 7`)
var yscale = d3.scaleLog()
.domain([1, 100000])
.nice()
.range([height - 10, -10]);
var y_axis = d3.axisLeft(yscale);
y_axis.ticks(5);
svg.append("g")
.call(d3.axisLeft(xScale).ticks(5))
.attr("transform", "translate(0, 10)")
.attr('stroke', '#5E779B')
.attr('stroke-dasharray', `4, 7`)
.call(y_axis)
})
(d3);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<body>
<div id="line-chart"></div>
</body>
I'll start with the log scale issues. You have two y-scales, yScale and yscale. The first is a linear scale that is used in the line generator and to position the circles. The second is a log scale that is only used for the axis. You should only have one y-scale and use it both for positioning the elements and for the axis.
Also, log scales cannot handle the value 0:
As log(0) = -∞, a log scale domain must be strictly-positive or strictly-negative; the domain must not include or cross zero.
The data point that has a close value of 0 cannot be shown on a log scale.
Next, the y-axis labels look wrong because of the "stroke" and "stroke-dasharray" attributes that you set on the y-axis group.
Finally, for the x-scale you are correct to convert the strings to Date objects and use a d3.scaleTime. For the x-axis, you can then do something like this:
const xAxis = d3.axisBottom(xScale)
.ticks(d3.timeMonth.every(1), '%b');
The first argument to ticks says that you want to have one tick for each month. The second argument is a date format specifier that defines how the tick labels should be formatted. %b puts an abbreviated month name at each tick mark. If you want the tick marks to have the same format as the original string, then you can use %Y/%m/%d, but you might find that the labels are too long and overlap each other.
Here is an example that fixes the issues mentioned above.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="line-chart"></div>
<script>
// data
const lineChartData = [
{
ticker: "ABC",
values: [
{
date: "2018/01/01",
close: 1
},
{
date: "2018/02/01",
close: 5
},
{
date: "2018/03/01",
close: 10
},
{
date: "2018/04/01",
close: 50
},
{
date: "2018/05/01",
close: 100
},
{
date: "2018/06/01",
close: 500
},
{
date: "2018/07/01",
close: 1000
},
{
date: "2018/08/01",
close: 5000
},
{
date: "2018/09/01",
close: 10000
},
]
}
];
const parseTime = d3.timeParse('%Y/%m/%d');
const parsedData = lineChartData.map(company => ({
ticker: company.ticker,
values: company.values.map(val => ({
close: val.close,
date: parseTime(val.date)
}))
}));
// gradient
const createGradient = defs => {
const gradient = defs.append('linearGradient')
.attr('id', 'gradient')
.attr('x1', '0%')
.attr('y1', '100%')
.attr('x2', '0%')
.attr('y2', '0%');
gradient.append('stop')
.attr('offset', '0%')
.attr('style', 'stop-color:#FF6500; stop-opacity:0');
gradient.append('stop')
.attr('offset', '100%')
.attr('style', 'stop-color:#FF6500; stop-opacity: 1');
}
// filter
const createGlowFilter = defs => {
const filter = defs.append('filter')
.attr('id', 'glow')
//stdDeviation is px count for make blur around main chart line
filter.append('feGaussianBlur')
.attr('stdDeviation', '2')
.attr('result', 'coloredBlur');
const femerge = filter.append('feMerge');
femerge.append('feMergeNode')
.attr('in', 'coloredBlur');
femerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
}
// set up
const margin = { top: 20, right: 20, bottom: 20, left: 50 };
const width = 400 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const svg = d3.select('#line-chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.call(svg => svg.append('defs')
.call(createGradient)
.call(createGlowFilter))
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// scales
const xScale = d3.scaleTime()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.date)),
d3.max(parsedData, d => d3.max(d.values, v => v.date))
])
.range([0, width]);
const yScale = d3.scaleLog()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.close)),
d3.max(parsedData, d => d3.max(d.values, v => v.close))
])
.range([height, 0]);
// line and area generators
const area = d3.area()
.x(d => xScale(d.date))
.y1(d => yScale(d.close))
.y0(yScale(yScale.domain()[0]))
.curve(d3.curveCatmullRom.alpha(0.5));
const line = area.lineY1();
// axes
const xAxis = d3.axisBottom(xScale)
.ticks(d3.timeMonth.every(1), '%b');
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
// add vertical grid lines
.call(g =>
g.selectAll('.tick>line')
.clone()
.attr('stroke', '#5E779B')
.attr('stroke-dasharray', '4, 7')
.attr('y0', 0)
.attr('y1', -height)
)
const yAxis = d3.axisLeft(yScale)
.ticks(5);
svg.append('g')
.call(yAxis);
// draw area, line, and circles
// create one group for each company
const companies = svg.append('g')
.selectAll('g')
.data(parsedData)
.join('g');
// add the area
companies.append('path')
.attr('d', d => area(d.values))
.attr('fill', 'url(#gradient)');
// add the line
companies.append('path')
.attr('d', d => line(d.values))
.attr('stroke-width', 2)
.attr('fill', 'none')
.attr('stroke', '#FF6500')
.attr('filter', 'url(#glow)');
// add the circles
companies.selectAll('circle')
.data(d => d.values)
.join('circle')
.attr('fill', 'white')
.attr('stroke', '#FF6500')
.attr('r', '3')
.attr('cx', d => xScale(d.date))
.attr('cy', d => yScale(d.close));
</script>
</body>
</html>
Please, have a look. I feel like i'm somewhere near,after searching stackoverflow.
But still can set each label(it's date) vertically so it's readable.
https://codepen.io/DeanWinchester88/pen/XWgjjeW
const svg = d3.select("body")
.append("svg")
.attr("width",w)
.attr("height",h)
.attr("style", "outline: thin solid red;");
svg.selectAll("rect")
.data(dataSet)
.enter()
.append("rect")
.attr("x", (d,i) => i * 10)
.attr("y", (d) => ( h - (d[1]/10 ) ) )
.attr("width", 8)
.attr("height", (d) => d[1] /10 )
.attr("class", 'bar')
.attr("data-date",(d) => d[0])
.attr("data-gdp",(d) => d[1])
.append("title")
.text((d) => d)
svg.selectAll("text")
.data(dataSet)
.enter()
.append("text")
.text((d) => d)
.attr("x", (d,i) => i * 10)
.attr("y", (d) => ( h - (d[1]/10 ) ))
// .attr("transform", "rotate(-5)")
.attr('transform', (d,i)=>{
return 'translate( i * 10, (d) => ( h - (d[1]/10 ) )) rotate(125)';})
});
You can achieve this with just a little tweak. Instead of setting the x and y attribute, we can add that to the transform instead. So we move the object to the position that we want, then rotate it.
That is:
.attr("transform", (d,i) => "translate(" + i*10 + "," + (h-(d[1]/10)) + "),rotate(90)");
Remove the x and y lines before adding that.
That will show the text overlapping with the bars. To have the text above the bar we can just add:
.attr("text-anchor", "end")
You can also change the font-size, to ensure the text doesn't overlap with the other text:
text {
font-size: 12px;
}
The final result is:
let dataSet;
let data;
// let pizda =[1,5,8,15,35,76,36,
function readTextFile(file, callback) {
var rawFile = new XMLHttpRequest();
rawFile.overrideMimeType("application/json");
rawFile.open("GET", file, true);
rawFile.onreadystatechange = function() {
if (rawFile.readyState === 4 && rawFile.status == "200") {
callback(rawFile.responseText);
}
}
rawFile.send(null);
}
//usage:
readTextFile("https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json", function(text){
data = JSON.parse(text);
dataSet = data["data"]
console.log(dataSet)
const w = 3400;
const h = 2000;
const svg = d3.select("body")
.append("svg")
.attr("width",w)
.attr("height",h)
.attr("style", "outline: thin solid red;");
svg.selectAll("rect")
.data(dataSet)
.enter()
.append("rect")
.attr("x", (d,i) => i * 10)
.attr("y", (d) => ( h - (d[1]/10 ) ) )
.attr("width", 8)
.attr("height", (d) => d[1] /10 )
.attr("class", 'bar')
.attr("data-date",(d) => d[0])
.attr("data-gdp",(d) => d[1])
.append("title")
.text((d) => d)
svg.selectAll("text")
.data(dataSet)
.enter()
.append("text")
.text((d) => d)
.attr("text-anchor", "end")
.attr("transform", (d,i) => "translate(" + i*10 + "," + (h-(d[1]/10)) + "),rotate(90)");
});
.bar:hover {
fill: black;
}
.bar {
margin: 6px;
fill: #a87a44;
}
text {
font-size: 12px;
}
<html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id = "title">Document</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
</head>
<body>
</body>
</html>
I am learning d3.js, and I have this problem:
The following code in d3 basically draws a bar chart, with an update button that is sorting the data once in a descending order and once in ascending order. Also, numeric labels appear on the bars.
I would like to transition the numeric label from the current value to the updated value. For example, if the first bar has a numeric label of 20, and the new updated value after sorting is 100, I would like this label to transition from 20 to 100 as (20, 21, ..., 100) during the specific transition time, and vice versa, if the original label is 100 and the updated value is 20 the transition goes as 100, 99, ..., 20.
I know I can just transition the numeric value with the bar, but I would like to know how to do the transition from the current numeric value to the new update value as an exercise.
const data = [
{key: 0, value: 50},
{key: 1, value: 20},
{key: 2, value: 100},
{key: 3, value: 30},
{key: 4, value: 40},
{key: 5, value: 70}
]
// const dataset = [50, 20, 100, 30, 40]
const svgWidth = 800;
const svgHeight = 400;
const xScale = d3.scaleBand()
.domain(d3.range(data.length))
.rangeRound([0, svgWidth])
.paddingInner(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, svgHeight]);
const svg = d3.select('#chart')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
let bars = svg.selectAll('rect').data(data, d => d.key);
let labels = svg.selectAll('text').data(data);
bars.enter()
.append('rect')
.each(function(d){return this._old = d;})
.attr('width', xScale.bandwidth)
.attr('height', d => yScale(d.value))
.attr('fill', d => `rgb(${d.value}, ${d.value * 2}, ${d.value * 3})`)
.attr('x', (d, i) => xScale(i))
.attr('y', d => svgHeight - yScale(d.value))
.attr('stroke', 'black')
.attr('stroke-width', 3)
labels.enter()
.append('text')
.attr('x', (d, i) => xScale(i) + (xScale.bandwidth() / 2))
.attr('y', d => svgHeight - yScale(d.value) + 20)
.attr('font-size', 20)
.attr('text-anchor', 'middle')
.attr('fill', 'white')
.text(d => d.value);
let asc = false;
d3.select('button').on('click', () => {
if(!asc){
data.sort((a,b) => b.value - a.value );
}else{
data.sort((a,b) => a.value - b.value );
};
asc = !asc;
bars = svg.selectAll('rect').data(data, d => d.key);
labels = svg.selectAll('text').data(data);
bars
.transition()
.delay((d, i) => (i * 10))
.duration(3000)
.each(function(d){return this._old = d;})
.attr('x', (d, i) => xScale(i))
.attr('height', d => yScale(d.value))
.attr('y', d => svgHeight - yScale(d.value));
labels
.transition()
.delay((d, i) => (i * 10))
.duration(3000)
.tween("text", function(d) {
var that = this;
var i = d3.interpolate(0, d.value); // Number(d.percentage.slice(0, -1))
return function(t) {
d3.select(that).text(i(t).toFixed(0));
}
})
.attr('y', d => svgHeight - yScale(d.value) + 20);
})
I found the "tween" function included in the above code for a similar but not exactly the same question. I don't know how to make the interpolation start from the current value instead of 0. I know I need somehow to store the old value, and access it in the tween, but not sure how.
Another question regarding the tween function: why do we assign var that = this and select that in the returned function?
Thanks in advance
You can get the current value for each text by different ways.
For instance, with vanilla JavaScript:
var current = +(this.textContent);
Or using a D3 getter:
var current = +(d3.select(this).text());
Here is your code with that change:
const data = [{
key: 0,
value: 50
},
{
key: 1,
value: 20
},
{
key: 2,
value: 100
},
{
key: 3,
value: 30
},
{
key: 4,
value: 40
},
{
key: 5,
value: 70
}
]
// const dataset = [50, 20, 100, 30, 40]
const svgWidth = 800;
const svgHeight = 400;
const xScale = d3.scaleBand()
.domain(d3.range(data.length))
.rangeRound([0, svgWidth])
.paddingInner(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, svgHeight]);
const svg = d3.select('body')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
let bars = svg.selectAll('rect').data(data, d => d.key);
let labels = svg.selectAll('text').data(data);
bars.enter()
.append('rect')
.each(function(d) {
return this._old = d;
})
.attr('width', xScale.bandwidth)
.attr('height', d => yScale(d.value))
.attr('fill', d => `rgb(${d.value}, ${d.value * 2}, ${d.value * 3})`)
.attr('x', (d, i) => xScale(i))
.attr('y', d => svgHeight - yScale(d.value))
.attr('stroke', 'black')
.attr('stroke-width', 3)
labels.enter()
.append('text')
.attr('x', (d, i) => xScale(i) + (xScale.bandwidth() / 2))
.attr('y', d => svgHeight - yScale(d.value) + 20)
.attr('font-size', 20)
.attr('text-anchor', 'middle')
.attr('fill', 'white')
.text(d => d.value);
let asc = false;
d3.select('button').on('click', () => {
if (!asc) {
data.sort((a, b) => b.value - a.value);
} else {
data.sort((a, b) => a.value - b.value);
};
asc = !asc;
bars = svg.selectAll('rect').data(data, d => d.key);
labels = svg.selectAll('text').data(data);
bars
.transition()
.delay((d, i) => (i * 10))
.duration(3000)
.each(function(d) {
return this._old = d;
})
.attr('x', (d, i) => xScale(i))
.attr('height', d => yScale(d.value))
.attr('y', d => svgHeight - yScale(d.value));
labels
.transition()
.delay((d, i) => (i * 10))
.duration(3000)
.tween("text", function(d) {
var current = +(d3.select(this).text());
var that = this;
var i = d3.interpolate(current, d.value); // Number(d.percentage.slice(0, -1))
return function(t) {
d3.select(that).text(i(t).toFixed(0));
}
})
.attr('y', d => svgHeight - yScale(d.value) + 20);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button>Click</button>
<br>