How to match "Y" scale data with "X" scale domain - d3.js

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" )
}

Related

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

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>

D3 - Radio Buttons not Updating Data Visualization

I have created a D3 chart which I would like to update after the user clicks on one of the radio buttons. The value of the button isn't part of the data but an update of the time range along the X axis which should update as well as Y axis data based on the newly selected timeRange value (starting point is 36 hours from "now"). I can tell from console.log statements that the timeRange is updating when the user clicks on a radio button but can't figure out what I need to do to get that chart updated (the change() function is what I am trying to use to make this happen.
Here is a fiddle: https://jsfiddle.net/dtepdc/L1qf0bvk/
Here is my code:
const dataset = [
{
start_date: "2019-01-27T14:30:40",
end_date: "2019-01-27T16:32:25",
elapsed_time: 130,
coNum:"CO19044"
},
{
start_date: "2019-01-27T03:05:40",
end_date: "2019-01-27T03:32:25",
elapsed_date: 189,
coNum:"CO12904"
},
{
start_date: "2019-01-26T22:15:40",
end_date: "2019-01-26T23:32:25",
elapsed_time: 89,
coNum:"CO18345"
},
{
start_date: "2019-01-26T07:00:40",
end_date: "2019-01-26T07: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),
filtered = [],
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 + ")");
let radio = d3.select('input[name="options"]:checked').property("value");
let timeRange = radio;
let start = moment().subtract(timeRange, 'hours').format('LLL');
const end = moment().format('LLL');
timeRange = this.value;
dataset.forEach(function(d, i) {
console.log('forEach timeRange: ', timeRange);
d.start_date = new Date(d.start_date);
d.end_date = new Date(d.end_date);
if (d.start_date >= new Date(start) && d.end_date <= new Date(end)) {
filtered.push(d);
}
});
xSc.domain([new Date(end), new Date(start)])
.range([0, w]);
ySc.domain(filtered.map(d => d.coNum)).padding(0.1);
console.log('xSc & ySc timeRange: ', timeRange)
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(filtered)
.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");
d3.selectAll("input")
.on("change", change);
function change() {
timeRange = this.value;
dataset.forEach(function(d, i) {
console.log('forEach timeRange: ', timeRange);
d.start_date = new Date(d.start_date);
d.end_date = new Date(d.end_date);
if (d.start_date >= new Date(start) && d.end_date <= new Date(end)) {
filtered.push(d);
}
});
xSc.domain([new Date(end), new Date(start)])
.range([0, w]);
ySc.domain(filtered.map(d => d.coNum)).padding(0.1);
console.log('xSc & ySc timeRange: ', timeRange)
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(filtered)
.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))
}
I've done something similar in a project. I think the main thing you are missing is that when you change the data you need to remove the current chart and draw a new one. You may be able to make Angular do this for you, but I'm not sure how.
Personally, I would refactor the logic into something like drawChart(filteredData) and filterData(unfilteredData, HOW TO FILTER). The initial draw on page load would be a call to drawChart with whatever data you want (it could go through filterData(). Your change() function would take the HOW TO FILTER from the radio button, pass it to filterData(), then remove the chart, then call drawChart with the filtered data. This architecture makes every draw of the chart the same and if you need changes to how the chart it drawn you can do it one place.
Example of new change function
change(howToFilter){
var filteredData = filterData(dataset, howToFilter);
CODE TO REMOVE THE CHART
drawChart(filteredData);
}

d3.js appending two labels from a dataset on the Y axis

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>

D3 How to update the chart after selection from drop down menu with new data

I'm building a waterfall chart in D3. When the page will load, it will render the default page but user will have choice to select different
'Company' and 'Year' from the drop down menu. I have been able to create the chart what I want. But when I select any different Company or Year, D3 adds another chart on top of the existing instead of replacing it and thats because I'm targeting a particular div / svg from the HTML. How can I use D3 to update the chart with new data instead add another one of top? And if I can have that movement of chart bars with transition, that will be awesome.
HTML is a simple svg:
<svg class="chart"></svg>
Here is the function to create the chart which I call when Ajax call is successful:
function waterfallChart (dataset) {
var data = [];
for (var key in dataset[0]) {
data.push({
name: key,
value: dataset[0][key]
})
}
var margin = {top: 20, right: 30, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
padding = 0.3;
var x = d3.scaleBand()
.domain(data.map(function(d) {
return d.name
}))
.range([0, width])
.padding(padding);
var y = d3.scaleLinear()
.range([height, 0]);
var xAxis = d3.axisBottom(x)
var yAxis = d3.axisLeft(y)
.tickFormat(function(d) {
return dollarFormatter(d);
});
var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var cumulative = 0;
for (var i = 0; i < data.length; i++) {
data[i].start = cumulative;
cumulative += data[i].value;
data[i].end = cumulative;
data[i].class = (data[i].value >= 0) ? 'positive' : 'negative'
}
data.push({
name: 'Total',
end: cumulative,
start: 0,
class: 'total'
});
x.domain(data.map(function(d) {
return d.name;
}));
y.domain([0, d3.max(data, function(d) {
return d.end;
})]);
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.call(yAxis);
var bar = chart.selectAll(".bar")
.data(data)
.enter().append("g")
.attr("class", function(d) {
return "bar " + d.class
})
.attr("transform", function(d) {
return "translate(" + x(d.name) + ",0)";
});
bar.append("rect")
.attr("y", function(d) {
return y(Math.max(d.start, d.end));
})
.attr("height", function(d) {
return Math.abs(y(d.start) - y(d.end));
})
.attr("width", x.bandwidth());
bar.append("text")
.attr("x", x.bandwidth() / 2)
.attr("y", function(d) {
return y(d.end) + 5;
})
.attr("dy", function(d) {
return ((d.class == 'negative') ? '-' : '') + ".75em"
})
.text(function(d) {
return dollarFormatter(d.end - d.start);
});
bar.filter(function(d) {
return d.class != "total"
}).append("line")
.attr("class", "connector")
.attr("x1", x.bandwidth() + 5)
.attr("y1", function(d) {
return y(d.end)
})
.attr("x2", x.bandwidth() / (1 - padding) - 5)
.attr("y2", function(d) {
return y(d.end)
})
function dollarFormatter(n) {
n = Math.round(n);
var result = n;
if (Math.abs(n) > 1000) {
result = Math.round(n/1000) + 'B';
}
return '$ ' + result;
}
}
Here is code where I have event listener and on selection it will run the above function:
$("#airline-selected, #year-selected").change(function chartsData(event) {
event.preventDefault();
var airlineSelected = $('#airline-selected').find(":selected").val();
var yearSelected = $('#year-selected').find(":selected").val();
$.ajax({
url: "{% url 'airline_specific_filtered' %}",
method: 'GET',
data : {
airline_category: airlineSelected,
year_category: yearSelected
},
success: function(dataset){
waterfallChart(dataset)
},
error: function(error_data){
console.log("error")
console.log(error_data)
}
})
});
You are missing some pretty important things here. If you are going to do updates on your data you need to do a couple things.
Give a key to the data() function. You need to give D3 a way to identify data when you update it so it knows if it should add, remove, or leave existing data. The key does this. For instance you might do something like this:
.data(data, function(d) { return d.name })
Now d3 will be able to tell you data items apart assuming d.name is a unique identifier.
You need an exit() for data that is removed during update. You need to save the data joined selection so you can call enter and exit on it:
var bar = chart.selectAll(".bar")
.data(data, function(d) { return d.name})
now you can call: bar.exit().remove() to get rid of deleted items and bar.enter() to add items.
You need to make a selection that hasn't had enter() called on it to update attributes.
Probably more a matter of style, but you should set up the SVG and margins outside the update function since they state the same. You can still update the axis and scales by calling the appropriate functions in the update.
The code you posted is a little hard for other people to run — you'll always get better faster answers if you post code that has been reduced to the main problem and that others can run without needing access to offsite data or apis.
Here's an example that updates on a setInterval between two data sets based on your code. But you should also look at the General Update Patterns - they are very simple but have almost everything you need to know. (https://bl.ocks.org/mbostock/3808234)
dataset = [
{name: "Albert", start: 0, end:220},
{name: "Mark", start: 0, end:200},
{name: "Søren", start: 0, end:100},
{name: "Immanuel", start: 0, end:60},
{name: "Michel", start: 0, end:90},
{name: "Jean Paul", start: 0, end: 80}
]
dataset2 = [
{name: "Albert", start: 0, end:20},
{name: "Immanuel", start:0, end:220},
{name: "Jaques", start: 0, end:100},
{name: "Gerhard", start:0 , end:50},
{name: "Søren", start: 0, end:150},
{name: "William", start: 0, end: 180}
]
var margin = {
top: 10,
right: 30,
bottom: 30,
left: 40
},
width = 400 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom,
padding = 0.3;
var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleBand()
.range([0, width])
.padding(padding);
var y = d3.scaleLinear()
.range([height, 0])
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
chart.append("g")
.attr("class", "y axis")
var currentData = dataset
waterfallChart(currentData)
setInterval(function() {
currentData = currentData === dataset ? dataset2 : dataset
waterfallChart(currentData)
}, 3000)
function waterfallChart(data) {
var t = d3.transition()
.duration(750)
x.domain(data.map(function(d) {
return d.name
}))
y.domain([0, d3.max(data, function(d) {
return d.end
})])
var xAxis = d3.axisBottom(x)
var yAxis = d3.axisLeft(y)
d3.select('g.x').transition(t).call(xAxis)
d3.select('g.y').call(yAxis)
var bar = chart.selectAll(".bar")
.data(data, function(d) {
return d.name
})
// ENTER -- ADD ITEMS THAT ARE NEW IN DATA
bar.enter().append("g")
.attr("transform", function(d) {
return "translate(" + x(d.name) + ",0)"
})
.attr("class", 'bar')
.append("rect")
.attr("y", function(d) {
return y(Math.max(d.start, d.end));
})
.attr("height", function(d) {
return Math.abs(y(d.start) - y(d.end));
})
.attr("width", x.bandwidth())
// UPDATE EXISTING ITEMS
chart.selectAll(".bar")
.transition(t)
.attr("transform", function(d) {
return "translate(" + x(d.name) + ",0)"
})
.select('rect')
.attr("y", function(d) {
return y(Math.max(d.start, d.end))
})
.attr("height", function(d) {
return Math.abs(y(d.start) - y(d.end))
})
.attr("width", x.bandwidth())
// REMOVE ITEMS DELETED FROM DATA
bar.exit().remove()
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg class="chart"></svg>

getting error with d3 (V4) Stacked Bar Chart

Getting error while creating Stacked Bar Chart using D3 JS in a Angular 2 application,
here is the code,
//data
var data = [
{ month: 'Jan', A: 20, B: 5, C: 10 },
{ month: 'Feb', A: 30, B: 10, C: 20 }
];
var xData = ["A", "B", "C"];
var margin = { top: 20, right: 50, bottom: 30, left: 0 },
width = 350 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
var x = d3.scaleBand()
.range([0, width])
.padding(0.35);
var y = d3.scaleLinear()
.range([height, 0]);
var color = d3.scaleOrdinal(d3.schemeCategory20);
var xAxis = d3.axisBottom(x);
var svg = d3.select("#chart").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 dataIntermediate = xData.map(function (c) {
return data.map(function (d) {
return { x: d.month, y: d[c] };
});
});
var dataStackLayout = d3.stack().keys([dataIntermediate]);
x.domain(dataStackLayout[0].map(function (d) {
return d.x;
}));
y.domain([0,
d3.max(dataStackLayout[dataStackLayout.length - 1],
function (d) { return d.y0 + d.y; })
])
.nice();
var layer = svg.selectAll(".stack")
.data(dataStackLayout)
.enter().append("g")
.attr("class", "stack")
.style("fill", function (d, i) {
return color(i);
});
layer.selectAll("rect")
.data(function (d) {
return d;
})
.enter().append("rect")
.attr("x", function (d) {
return x(d.x);
})
.attr("y", function (d) {
return y(d.y + d.y0);
})
.attr("height", function (d) {
return y(d.y0) - y(d.y + d.y0);
})
.attr("width", x.range());
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
Error are,
(51,41): error TS7017: Index signature of object type implicitly has an 'any' type.
# below line,
return { x: d.month, y: d[c] };
(76,19): error TS2345: Argument of type '(this: BaseType, d: {}) => {}' is not assignable to parameter of type 'ValueFn'.
Type '{}' is not assignable to type '{}[]'.
Property 'find' is missing in type '{}'.
# below line,
var dataStackLayout = d3.stack().keys([dataIntermediate]);
im using same example to implement stacked barchart in angular2.
i think your issue is
var dataStackLayout = d3.stack().keys([dataIntermediate]);
dataStackedLayout should be array instead of function.
were you able to resolve this issue yet?

Resources