How to add ascending - descending sort buttons in a bar chart using d3.js? - sorting

I'm trying to figure out how to add interactive features in a bar chart.
Now I'm stuck about program fuctions for a click action using a button to sort ascending and descending.
This is my code:
const width = 800
const height = 400
const margin = {
top: 40,
bottom: 60,
left: 40,
right: 10
}
const svg = d3.select("div#chart").append("svg").attr("width", width).attr("height", height)
const elementGroup = svg.append("g").attr("id", "elementGroup").attr("transform", `translate(${margin.left}, ${margin.top})`)
const axisGroup = svg.append("g").attr("id", "axisGroup")
const xAxisGroup = axisGroup.append("g").attr("id", "xAxisGroup").attr("transform", `translate(${margin.left}, ${height - margin.bottom})`)
const yAxisGroup = axisGroup.append("g").attr("id", "yAxisGroup").attr("transform", `translate(${margin.left}, ${margin.top})`)
const x = d3.scaleBand().range([0, width - margin.left - margin.right]).padding(0.1)
const y = d3.scaleLinear().range([height - margin.bottom - margin.top, 0])
const xAxis = d3.axisBottom().scale(x)
const yAxis = d3.axisLeft().scale(y).ticks(5)
d3.csv("WorldCup.csv").then(data => {
let nest = d3.nest()
.key(d => d.Winner)
.entries(data)
nest.map(d => d.values = d.values.length)
console.log(nest)
x.domain(nest.map(d => d.key))
y.domain([0, d3.max(nest.map(d => d.values))])
xAxisGroup.call(xAxis)
yAxisGroup.call(yAxis)
//Asceding sort
function sortAscending() {nest.sort((a, b) => d3.ascending(a.values, b.values))
let xLabel = elementGroup.append("text").text("Countries")
.attr("transform", `translate(${width - margin.right - 30}, ${height - margin.bottom})`)
.attr("text-anchor", "end").attr("font-weight", 700)
let yLabel = elementGroup.append("text").text("Cups per country")
.attr("transform", `translate(${-20}, ${-10})`).attr("font-weight", 700)
let elements = elementGroup.selectAll("rect").data(nest)
elements.enter().append("rect")
.attr("class", "bar")
.attr("x", d => x(d.key))
.attr("width", x.bandwidth())
.attr("height", d => height - margin.top - margin.bottom - y(d.values))
.attr("y", d => y(d.values))
}
d3.select("#Ascending").on("click", sortAscending)
//Descending sort
function sortDescending() {nest.sort((a, b) => d3.descending(a.values, b.values))
let xLabel = elementGroup.append("text").text("Countries")
.attr("transform", `translate(${width - margin.right - 30}, ${height - margin.bottom})`)
.attr("text-anchor", "end").attr("font-weight", 700)
let yLabel = elementGroup.append("text").text("Cups per country")
.attr("transform", `translate(${-20}, ${-10})`).attr("font-weight", 700)
let elements = elementGroup.selectAll("rect").data(nest)
elements.enter().append("rect")
.attr("class", "bar")
.attr("x", d => x(d.key))
.attr("width", x.bandwidth())
.attr("height", d => height - margin.top - margin.bottom - y(d.values))
.attr("y", d => y(d.values))
}
d3.select("#Ascending").on("click", sortDescending)
})
I tried different examples.
By the moment, I just understood it is about configure a function, and then link the fuction with a button by 'on'.

You are close, but you have to separate the code that is used to draw the plot for the first time, and what is needed to update it after the data was sorted. A possible framework that works, based on on your
code, is this
const height = 400, width = 600;
const margin = {
top: 40,
bottom: 60,
left: 40,
right: 10
};
const svg = d3.select("div#chart").append("svg").attr("width", width).attr("height", height)
const elementGroup = svg.append("g").attr("id", "elementGroup").attr("transform", `translate(${margin.left}, ${margin.top})`)
const axisGroup = svg.append("g").attr("id", "axisGroup")
const xAxisGroup = axisGroup.append("g").attr("id", "xAxisGroup").attr("transform", `translate(${margin.left}, ${height - margin.bottom})`)
const yAxisGroup = axisGroup.append("g").attr("id", "yAxisGroup").attr("transform", `translate(${margin.left}, ${margin.top})`)
const x = d3.scaleBand().range([0, width - margin.left - margin.right]).padding(0.1)
const y = d3.scaleLinear().range([height - margin.top - margin.bottom, 0])
const xAxis = d3.axisBottom().scale(x)
const yAxis = d3.axisLeft().scale(y).ticks(5)
function drawBarPlot(nest){
x.domain(nest.map(d => d.key))
y.domain([0, d3.max(nest.map(d => d.values))])
xAxisGroup.call(xAxis)
yAxisGroup.call(yAxis)
elementGroup.append("text").text("Countries")
.attr("transform", `translate(${width - margin.right - 30}, ${height - margin.bottom})`)
.attr("text-anchor", "end").attr("font-weight", 700);
elementGroup.append("text").text("Cups per country")
.attr("transform", `translate(${-20}, ${-10})`).attr("font-weight", 700);
elementGroup.selectAll("rect").data(nest).enter().append("rect")
.attr("class", "bar")
.attr("x", d => x(d.key))
.attr("width", x.bandwidth())
.attr("height", d => height - margin.top - margin.bottom - y(d.values))
.attr("y", d => y(d.values));
}
function updateBarPlot(nest){
x.domain(nest.map(d => d.key));
xAxisGroup.call(xAxis);
elementGroup.selectAll("rect").data(nest).call(update => update
.attr("height", d => height - margin.top - margin.bottom - y(d.values))
.attr("y", d => y(d.values)));
}
//Ascending sort
function sortAscending(nest) {
nest.sort((a, b) => d3.ascending(a.values, b.values));
updateBarPlot(nest);
}
function sortDescending(nest) {
nest.sort((a, b) => d3.descending(a.values, b.values));
updateBarPlot(nest);
}
//d3.csv("WorldCup.csv").then(data => {
new Promise(resolve => resolve(d3.csvParse("Year,Winner\n" + "1930,Uruguay\n" + "1934,Italy\n" + "1938,Italy\n" + "1950,Uruguay\n" +
"1954,Germany\n" + "1958,Brazil\n" + "1962,Brazil\n" + "1966,England\n" + "1970,Brazil\n" +
"1974,Germany\n" + "1978,Argentina\n" + "1982,Italy\n" + "1986,Argentina\n" + "1990,Germany\n" +
"1994,Brazil\n" + "1998,France\n" + "2002,Brazil\n" + "2006,Italy\n" + "2010,Spain\n" +
"2014,Germany\n" + "2018,France\n" + "2022,Argentina\n"))).then(data => {
let nest = d3.nest()
.key(d => d.Winner)
.entries(data)
nest.map(d => d.values = d.values.length)
drawBarPlot(nest);
d3.select("#Ascending").on("click", ()=>sortAscending(nest));
d3.select("#Descending").on("click", ()=>sortDescending(nest));
});
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://d3js.org/d3-collection.v1.min.js"></script>
<div id="chart"></div>
<button id="Ascending">Ascending</button><button id="Descending">Descending</button>

Related

Bars not showing up in grouped bar chart

I'm trying to create a grouped bar chart showing students attenadance(present or absent) but for some reasons my chart is showing only one set of bars(the highest for present and the highest for absent. On inspecting the console I see that the remaining bars are there but they are not showing up on the chart. I don't know if the problem is with my data or my code(I have only included the js part of the code)
const SVG = {
height: 900,
width: 900
}
const margin = {top: 40, bottom: 40, right: 40, left: 40};
const innerWidth = SVG.width - margin.left - margin.right;
const innerHeight = SVG.height - margin.top - margin.bottom;
const bar = d3.select('body').append('svg').attr('width', SVG.width).attr('height', SVG.height);
const grp = bar.append('g').attr('transform', `translate(${margin.left}, ${margin.top})`);
const data = d3.csv('WEEK.csv')
.then(
function(data) {
console.log(data);
const keys = data.columns.slice(1);
console.log(keys)
const group = d3.map(data, function(d){return +d.Week}).keys()
const x0 = d3.scaleBand().domain(group)
.range([0, innerWidth]).padding(0.1)
const x1 = d3.scaleBand().domain(keys)
.range([0, x0.bandwidth()]).padding(0.05)
const y0 = d3.scaleLinear().domain([0, d3.max(data, function(d) {return d3.max(keys, function (key){return +d[key];}
)})])
.range([innerHeight, 0]);
const color = d3.scaleOrdinal().range('#fcba03', '#eb4034')
const yAxis = d3.axisLeft(y0);
const xAxis = d3.axisBottom(x0);
const yAxisG = grp.append('g').call(yAxis)
const xAxisG = grp.append('g').call(xAxis)
.attr('transform', `translate(0, ${innerHeight})`);
grp.append('g')
.selectAll('g')
.data(data)
.enter().append('g')
.selectAll('rect')
.attr("transform", function(d) {return "translate(" + x0(+d.Week) + ",0)"})
.data(function(d){return keys.map(function(key){return{key: key, value: +d[key]} }) })
.enter()
.append('rect')
.attr('width', x1.bandwidth())
.attr('height', d => (innerHeight - y0(d.value)))
.attr('y', d => y0(d.value))
.attr('x', d => x1(d.key))
.attr('fill', d => color(d.key));
})
The dataset:
Week ,Present,Absent
2,10481,4010
3,11277,4551
4,10499,5036
5,9970,5126
6,8901,4909
7,7929,8405
8,7995,5062
9,7785,5447
10,7670,5822
11,7177,6162
12,6258,6499
13,4689,6631

d3 animating multiple lines the line can't be completed

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>

data bars on the xaxis should be aligned properly

I am building my first bar chart using d3.js v5 what I want is that the bars should be aligned properly on the xaxis
I have almost done building the chart but can't figure out the problem
var headingOne;
var headingTwo;
var dataset;
var description;
var frequency;
var barPadding = 20;
document.addEventListener("DOMContentLoaded", function() {
var req = new XMLHttpRequest(); // Next time will use d3.json();
req.open(
"GET",
"https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json",
true
);
req.send();
req.onload = function() {
let json = JSON.parse(req.responseText);
headingOne = json.source_name;
headingTwo = `From ${json.from_date.substring(0,4)} to ${json.to_date.substring(0,4)}`;
dataset = json.data;
descripton = json.description;
d3
.select("body")
.append("h1")
.text(headingOne)
.attr("class", "headings")
.attr("id", "title");
d3
.select("body")
.append("h2")
.text(headingTwo)
.attr("class", "headings");
var margin = { top: 20, right: 20, bottom: 50, left: 40 },
height = 600 - margin.top - margin.bottom,
width = 1100 - margin.left - margin.right;
var minDate = new Date(dataset[0][0]);
var maxDate = new Date(dataset[dataset.length - 1][0]);
var xScale = d3.scaleTime().domain([minDate, maxDate]).range([barPadding, width - barPadding]);
var yScale = d3.scaleLinear().domain([0, d3.max(dataset, d => d[1])]).range([height, barPadding]);
var xAxis = d3.axisBottom().scale(xScale);
var yAxis = d3.axisLeft().scale(yScale);
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("rect")
.data(dataset)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", (d, i) => i * (width / dataset.length))
.attr("data-date", (d) => d[0])
.attr("y", (d) => yScale(d[1]))
.attr("data-gdp", (d) => d[1])
.attr("width", width / dataset.length)
.attr("height", d => height - yScale(d[1]))
svg.append("g").attr("transform", "translate("+barPadding+"," + (height) + ")").attr("id", "x-axis").call(xAxis);
svg.append("g").attr("transform", "translate("+margin.left+", 0)").attr("id", "y-axis").call(yAxis);
};
});
I expect the bars properly aligned on the xaxis.
Now the bars started before the xaxis (start of the xaxis towards left) starting point which is wrong but finished in the right position (end of the xaxis towards right)
the data is exceeding the limit.

How to match "Y" scale data with "X" scale domain

I'm successfully displaying change orders for the start/end domain (last 36 hours) on my X axis and would like to display the respecting change order numbers on my Y axis. I know that this is the line that needs to be updated and after many different iterations, I'm stumped:
ySc.domain(yScDomArr).padding(.1);
Currently, three bars are displayed and I only want the three corresponding change orders displayed on the Y axis. Here is my code and a fiddle: https://jsfiddle.net/dtepdc/5pkx42yf/
const dataset = [
{
start_date: "2019-01-16T08:30:40",
end_date: "2019-01-16T09:32:25",
elapsed_date: 130,
coNum:"CO19044"
},
{
start_date: "2019-01-15T04:30:40",
end_date: "2019-01-15T05:32:25",
elapsed_date: 189,
coNum:"CO12904"
},
{
start_date: "2019-01-15T22:05:40",
end_date: "2019-01-15T22:32:25",
elapsed_time: 89,
coNum:"CO18345"
},
{
start_date: "2019-01-12T22:00:40",
end_date: "2019-01-12T22:40:25",
elapsed_time: 89,
coNum:"CO12005"
}
];
const coNumW = window.innerWidth,
coNumH = window.innerHeight,
margin = {top: coNumH * 0.15, right: coNumW * 0.05, bottom: coNumH * 0.12, left: coNumW * 0.12},
w = coNumW - margin.left - margin.right,
h = coNumH - margin.top - margin.bottom;
const xSc = d3.scaleTime().range([0, w]),
ySc = d3.scaleBand().range([h, 0])
xAxis = d3.axisBottom(xSc),
yAxis = d3.axisLeft(ySc),
yScDomArr = [],
dateFormat = d3.timeFormat("%Y-%m-%d %I:%M %p");
const svg = d3.select("body")
.append("svg")
.attr("width", coNumW)
.attr("height", coNumH)
.append("g").classed("no-select", true)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
dataset.forEach(function(d, i) {
yScDomArr.push(d.coNum);
d.start_date = new Date(d.start_date);
d.end_date = new Date(d.end_date);
});
const start = moment().format('LLL');
const end = moment().subtract(60, 'hours').format('LLL');
xSc.domain([new Date(start), new Date(end)])
.range([0, w]);
ySc.domain(yScDomArr).padding(.1);
svg.append("g")
.attr("class", "x Axis")
.attr("transform", "translate(0, " + h + ")")
.call(xAxis)
svg.append("g")
.attr("class", "y Axis")
.call(yAxis);
const tasks = svg.append("g").attr("class", "dataCont")
.selectAll("g")
.data(dataset)
.enter()
.append("g")
.on("mouseenter", showData);
tasks.append("rect")
.attr("x", function(d) {
return xSc(d.start_date ) + 2; // + 2 is for padding
})
.attr("y", function(d) {
return ySc(d.coNum);
})
.attr("width", function(d) {
return xSc(d.start_date) - xSc(d.end_date) - 2;
})
.attr("height", function(d) {
return ySc.bandwidth();
})
.attr("fill", "green");
function showData(d) {
const dur = (d.end_date - d.start_date)/3600000;
console.log("-" + d.coNum + "- start_date: " + dateFormat(d.start_date) + " || end_date: " + dateFormat(d.end_date) + " || duration: " + dur + " hours" )
}

Center Pie Chart in SVG Element

I've already read:
https://bl.ocks.org/mbostock/3887235
http://zeroviscosity.com/d3-js-step-by-step/step-1-a-basic-pie-chart
Center align a pie chart on svg
Consider the following:
var dataAsCsv = `Col1,Col2
Type1,123456
Type2,789012
Type3,34567`;
var data = d3.csvParse(dataAsCsv);
var margin = {top: 50, right: 20, bottom: 50, left: 80},
width = 1400 - margin.left - margin.right,
height = 700 - margin.top - margin.bottom;
var svgPie = d3.select('body').append('svg')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var gPie = svgPie.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var radius = Math.min(width, height) / 2;
var color = d3.scaleOrdinal(d3.schemeCategory20b);
var label = d3.arc()
.outerRadius(radius - 40)
.innerRadius(radius - 40);
var path = d3.arc()
.outerRadius(radius - 10)
.innerRadius(0);
var pie = d3.pie()
.value(function(d) { return d.Col2; })
.sort(null);
var arc = gPie.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
arc.append("path")
.attr("d", path)
.attr("fill", function(d) { return color(d.data.Col1); });
arc.append("text")
.attr("transform", function(d) { return "translate(" + label.centroid(d) + ")"; })
.attr("dy", "0.35em")
.text(function(d) { return d.data.Col1; });
<script src="https://d3js.org/d3.v4.js"></script>
I am trying to center the pie chart vertically and horizontally with respect to the entire svg element that it is in. I tried modifying my code to the examples above to no avail.
You just have to translate the parent g element at half width horizontally and at half height vertically:
Instead of:
var gPie = svgPie.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
write:
var gPie = svgPie.append("g")
.attr("transform", "translate(" + width/2 + "," + height/2 + ")");
Check the demo:
var dataAsCsv = `Col1,Col2
Type1,123456
Type2,789012
Type3,34567`;
var data = d3.csvParse(dataAsCsv);
var margin = {top: 50, right: 20, bottom: 50, left: 80},
width = 1400 - margin.left - margin.right,
height = 700 - margin.top - margin.bottom;
var svgPie = d3.select('body').append('svg')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var gPie = svgPie.append("g")
.attr("transform", "translate(" + width/2 + "," + height/2 + ")");
var radius = Math.min(width, height) / 2;
var color = d3.scaleOrdinal(d3.schemeCategory20b);
var label = d3.arc()
.outerRadius(radius - 40)
.innerRadius(radius - 40);
var path = d3.arc()
.outerRadius(radius - 10)
.innerRadius(0);
var pie = d3.pie()
.value(function(d) { return d.Col2; })
.sort(null);
var arc = gPie.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
arc.append("path")
.attr("d", path)
.attr("fill", function(d) { return color(d.data.Col1); });
arc.append("text")
.attr("transform", function(d) { return "translate(" + label.centroid(d) + ")"; })
.attr("dy", "0.35em")
.text(function(d) { return d.data.Col1; });
<script src="https://d3js.org/d3.v4.js"></script>

Resources