I'm having issues using the function provided at:
https://observablehq.com/@d3/stacked-normalized-horizontal-bar
The data i'm passing into the function is in the format that is used as an example
{Airline: 'Virgin America', Sentiment: 'positive', Count: 11},
{Airline: 'Virgin America', Sentiment: 'neutral', Count: 8},
{Airline: 'Virgin America', Sentiment: 'negative', Count: 3},
{Airline: 'Delta', Sentiment: 'neutral', Count: 10}.....
The data was not already in this format so I use the following code to process to this format, here is the original dataset
for (object of data){
if (processed.length === 0){
processed.push({Airline: object.airline, Sentiment: object.airline_sentiment, Count: 1})
} else {
objIndex = processed.findIndex((obj => obj.Airline === object.airline && obj.Sentiment === object.airline_sentiment))
if (objIndex === -1){
processed.push({Airline: object.airline, Sentiment: object.airline_sentiment, Count: 1})
} else {
processed[objIndex].Count = 1
}
}
}
I'm also passing in a sentiment array as follows for zDomain values
sentiment = ['positive', 'neutral', 'negative']
Here is the parameters i'm using for my function, basically the same as the example
chart = StackedBarChart(processed, {
x: d => d.Count,
y: d => d.Airline,
z: d => d.Sentiment,
yDomain: d3.groupSort(
processed,
D) => D[0].Count / d3.sum(D, d => d.Count),
d => d.Airline
),
colors: d3.schemeSpectral[sentiment.length],
zDomain: sentiment
)
In the StackedBarChar function i've noticed that the variable series is becoming undefined. Here is the code that defines this which I don't fully understand.
// Compute a nested array of series where each series is [[x1, x2], [x1, x2],
// [x1, x2], …] representing the x-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique y- and z-value.
const series = d3.stack()
.keys(zDomain)
.value(([, I], z) => X[I.get(z)])
.order(order)
.offset(offset)
(d3.rollup(I, ([i]) => i, i => Y[i], i => Z[i]))
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));
Also the error message is
Uncaught TypeError: svg.append(...).selectAll(...).data(...).join is not a
function
at StackedBarChart (chart.js:132:8)
which I believe is caused by series being undefined.
What could be causing this? could the format of my data must be wrong somehow?
CodePudding user response:
I am not able to reproduce your issue. The following code successfully draws the normalized stacked bar chart using the twitter airline dataset that you linked. What version of D3 are you using? Perhaps you are using an older version that does not have the selection.join function.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
d3.csv('Tweets.csv').then(drawChart);
function drawChart(data){
const processed = [];
for (object of data) {
if (processed.length === 0){
processed.push({Airline: object.airline, Sentiment: object.airline_sentiment, Count: 1})
} else {
objIndex = processed.findIndex((obj => obj.Airline === object.airline && obj.Sentiment === object.airline_sentiment))
if (objIndex === -1){
processed.push({Airline: object.airline, Sentiment: object.airline_sentiment, Count: 1})
} else {
processed[objIndex].Count = 1
}
}
}
const sentiment = ['positive', 'neutral', 'negative'];
const yDomain = d3.groupSort(
processed,
D => D[0].Count / d3.sum(D, d => d.Count),
d => d.Airline
);
const sbc = StackedBarChart(processed, {
x: d => d.Count,
y: d => d.Airline,
z: d => d.Sentiment,
yDomain: yDomain,
colors: d3.schemeSpectral[sentiment.length],
zDomain: sentiment
});
const div = document.getElementById('chart');
div.append(sbc);
}
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/stacked-normalized-horizontal-bar
function StackedBarChart(data, {
x = d => d, // given d in data, returns the (quantitative) x-value
y = (d, i) => i, // given d in data, returns the (ordinal) y-value
z = () => true, // given d in data, returns the (categorical) z-value
title, // given d in data, returns the title text
marginTop = 30, // top margin, in pixels
marginRight = 20, // right margin, in pixels
marginBottom = 0, // bottom margin, in pixels
marginLeft = 40, // left margin, in pixels
width = 640, // outer width, in pixels
height, // outer height, in pixels
xType = d3.scaleLinear, // type of x-scale
xDomain, // [xmin, xmax]
xRange = [marginLeft, width - marginRight], // [left, right]
yDomain, // array of y-values
yRange, // [bottom, top]
yPadding = 0.1, // amount of y-range to reserve to separate bars
zDomain, // array of z-values
offset = d3.stackOffsetExpand, // stack offset method
order = d3.stackOrderNone, // stack order method
xFormat = "%", // a format specifier string for the x-axis
xLabel, // a label for the x-axis
colors = d3.schemeTableau10, // array of colors
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
const Z = d3.map(data, z);
// Compute default y- and z-domains, and unique them.
if (yDomain === undefined) yDomain = Y;
if (zDomain === undefined) zDomain = Z;
yDomain = new d3.InternSet(yDomain);
zDomain = new d3.InternSet(zDomain);
// Omit any data not present in the y- and z-domains.
const I = d3.range(X.length).filter(i => yDomain.has(Y[i]) && zDomain.has(Z[i]));
// If the height is not specified, derive it from the y-domain.
if (height === undefined) height = yDomain.size * 25 marginTop marginBottom;
if (yRange === undefined) yRange = [height - marginBottom, marginTop];
// Compute a nested array of series where each series is [[x1, x2], [x1, x2],
// [x1, x2], …] representing the x-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique y- and z-value.
const series = d3.stack()
.keys(zDomain)
.value(([, I], z) => X[I.get(z)])
.order(order)
.offset(offset)
(d3.rollup(I, ([i]) => i, i => Y[i], i => Z[i]))
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));
// Compute the default y-domain. Note: diverging stacks can be negative.
if (xDomain === undefined) xDomain = d3.extent(series.flat(2));
// Construct scales, axes, and formats.
const xScale = xType(xDomain, xRange);
const yScale = d3.scaleBand(yDomain, yRange).paddingInner(yPadding);
const color = d3.scaleOrdinal(zDomain, colors);
const xAxis = d3.axisTop(xScale).ticks(width / 80, xFormat);
const yAxis = d3.axisLeft(yScale).tickSizeOuter(0);
// Compute titles.
if (title === undefined) {
title = i => `${Y[i]}\n${Z[i]}\n${X[i].toLocaleString()}`;
} else {
const O = d3.map(data, d => d);
const T = title;
title = i => T(O[i], i, data);
}
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
const bar = svg.append("g")
.selectAll("g")
.data(series)
.join("g")
.attr("fill", ([{i}]) => color(Z[i]))
.selectAll("rect")
.data(d => d)
.join("rect")
.attr("x", ([x1, x2]) => Math.min(xScale(x1), xScale(x2)))
.attr("y", ({i}) => yScale(Y[i]))
.attr("width", ([x1, x2]) => Math.abs(xScale(x1) - xScale(x2)))
.attr("height", yScale.bandwidth());
if (title) bar.append("title")
.text(({i}) => title(i));
svg.append("g")
.attr("transform", `translate(0,${marginTop})`)
.call(xAxis)
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", width - marginRight)
.attr("y", -22)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text(xLabel));
svg.append("g")
.attr("transform", `translate(${xScale(0)},0)`)
.call(yAxis);
return Object.assign(svg.node(), {scales: {color}});
}
</script>
</body>
</html>