Public
Edited
Dec 26, 2021
2 forks
Insert cell
Insert cell
Insert cell
Insert cell
fantasyFB_position_MGs = [
[[10, 9, 8, 12], [55, 10, 30, 5], [1], "Mile High Stadium Club"],
[[20, 9, 8, 2], [65, 5, 20, 10], [0], "Team ATX"],
[[19, 29, 8, 12], [45, 5, 40, 10], [1], "Menagee a Trois"],
[[10, 9, 8, 12], [55, 10, 30, 5], [1], "Mile High Stadium Club"],
[[20, 9, 8, 2], [65, 5, 20, 10], [0], "Team ATX"],
[[19, 29, 8, 12], [45, 5, 40, 10], [1], "Menagee a Trois"],
[[5, 9, 8, 12], [55, 10, 30, 5], [1], "Mile High Stadium Club"],
[[2, 9, 8, 2], [65, 5, 20, 10], [0], "Team ATX"],
[[19, 2, 8, 12], [45, 5, 40, 10], [1], "Menagee a Trois"],
[[10, 9, 8, 12], [55, 10, 30, 5], [1], "Mile High Stadium Club"],
[[20, 9, 8, 2], [65, 5, 20, 10], [0], "Team ATX"],
[[19, 29, 8, 12], [45, 5, 40, 10], [1], "Menagee a Trois"],
[[10, 9, 8, 12], [55, 10, 30, 5], [1], "Mile High Stadium Club"],
[[20, 9, 8, 2], [65, 5, 20, 10], [0], "Team ATX"],
[[19, 29, 8, 12], [45, 5, 40, 10], [1], "Menagee a Trois"],
[[10, 9, 8, 12], [55, 10, 30, 5], [1], "Mile High Stadium Club"]
//[[20, 9, 8, 2], [65, 5, 20, 10], [0], "Team ATX"]
]
Insert cell
chart = stackedToGroupedBarChart()
.width(width)
.height(400)
//.initialLayout("grouped")
.initialLayout("stacked")
.legendLabels(["QBs", "WRs", "RBs", "TEs"])
.xAxisLabels({
stacked: [
"Week 1",
"Week 2",
"Week 3",
"Week 3",
"Week 4",
"Week 5",
"Week 6",
"Week 7",
"Week 8",
"Week 9",
"Week 10",
"Week 11",
"Week 12",
"Week 13",
"Week 14",
"Week 15",
"Week 16",
"Week 17"
],
grouped: [
"Week 1",
"Week 2",
"Week 3",
"Week 3",
"Week 4",
"Week 5",
"Week 6",
"Week 7",
"Week 8",
"Week 9",
"Week 10",
"Week 11",
"Week 12",
"Week 13",
"Week 14",
"Week 15",
"Week 16",
"Week 17"
]
})
.groups([
{
stacked: fantasyFB_position_MGs[0][1],
grouped: fantasyFB_position_MGs[0][0]
},
{
stacked: fantasyFB_position_MGs[1][1],
grouped: fantasyFB_position_MGs[1][0]
},
{
stacked: fantasyFB_position_MGs[2][1],
grouped: fantasyFB_position_MGs[2][0]
},
{
stacked: fantasyFB_position_MGs[3][1],
grouped: fantasyFB_position_MGs[3][0]
},
{
stacked: fantasyFB_position_MGs[4][1],
grouped: fantasyFB_position_MGs[4][0]
},
{
stacked: fantasyFB_position_MGs[5][1],
grouped: fantasyFB_position_MGs[5][0]
},
{
stacked: fantasyFB_position_MGs[6][1],
grouped: fantasyFB_position_MGs[6][0]
},
{
stacked: fantasyFB_position_MGs[7][1],
grouped: fantasyFB_position_MGs[7][0]
},
{
stacked: fantasyFB_position_MGs[8][1],
grouped: fantasyFB_position_MGs[8][0]
},
{
stacked: fantasyFB_position_MGs[9][1],
grouped: fantasyFB_position_MGs[9][0]
},
{
stacked: fantasyFB_position_MGs[10][1],
grouped: fantasyFB_position_MGs[10][0]
},
{
stacked: fantasyFB_position_MGs[11][1],
grouped: fantasyFB_position_MGs[11][0]
},
{
stacked: fantasyFB_position_MGs[12][1],
grouped: fantasyFB_position_MGs[12][0]
},
{
stacked: fantasyFB_position_MGs[13][1],
grouped: fantasyFB_position_MGs[13][0]
},
{
stacked: fantasyFB_position_MGs[14][1],
grouped: fantasyFB_position_MGs[14][0]
},
{
stacked: fantasyFB_position_MGs[15][1],
grouped: fantasyFB_position_MGs[15][0]
}
// {
// stacked: fantasyFB_position_MGs[16][1],
// grouped: fantasyFB_position_MGs[16][0]
// }
])
.formatBarLabel((val) => val + "%")
Insert cell
d3.select("#chart").call(chart)
Insert cell
chart.update(layout)

//.formatBarLabel((val) => (layout == "grouped" ? val + "pts" : val + "%"))
Insert cell
function stackedToGroupedBarChart() {
// TODO: remove when transferring out of Observable
d3.select("#chart").selectAll("svg").remove();

// defined via accessor functions
let width,
height,
groups,
legendLabels,
xAxisLabels,
yAxisLabels,
formatBarLabel,
initialLayout = "stacked";

// defined internally
let svg,
rect,
rectLabel,
layout,
groupedLabels,
stackedLabels,
o,
n, // number of series
m, // number of values per series
xz, // x-values shared by all series
yz, // y-values of each of the n series
y01z, // stacked y-values of each of the n series
yMax,
y1Max,
y2Max,
x,
y,
z,
margin = { top: 50, right: 0, bottom: 22, left: 40 };

//chart.update(layout);

// generate chart
function chart(selection) {
selection.each(function (data) {
// initialize svg
svg = selection.append("svg").attr("width", width).attr("height", height);

// initialize layout
layout = initialLayout;

// calculate number of series
n = Object.keys(groups[0].stacked).length;

// o = Object.keys(groups[0].grouped).length;

// calculate number of vals per series
m = groups.length;

// calculate x-values shared by all series
xz = d3.range(m);

// calculate y-values of each of the n series
yz = {
stacked: d3.range(n).map((i) => {
return d3.range(m).map((gi) => groups[gi].stacked[i] / 1);
}),
grouped: d3.range(n).map((i) => {
return d3.range(m).map((gi) => groups[gi].grouped[i] / 1);
})
};

// calculate stacked y-values of each of the n series
y01z = {
stacked: d3
.stack()
.keys(d3.range(n))(d3.transpose(yz.stacked))
.map((data, i) => data.map(([y0, y1]) => [y0, y1, i])),
grouped: d3
.stack()
.keys(d3.range(n))(d3.transpose(yz.grouped))
.map((data, i) => data.map(([y0, y1]) => [y0, y1, i]))
};

// configure y maxes
yMax = d3.max(yz[layout], (y) => d3.max(y));
y1Max = d3.max(y01z[layout], (y) => d3.max(y, (d) => d[1]));

// y2Max = d3.max(y01z[layout], (y) => d3.max(y, (d) => d[0]));

// configure x scale
x = d3
.scaleBand()
.domain(xz)
.rangeRound([margin.left, width - margin.right])
.padding(0.18);

// configure y scale
y = d3
.scaleLinear()
.domain([0, y1Max])
.range([height - margin.bottom, margin.top]);

// yAx = d3
// .scaleLinear()
// .domain([0, y1Max])
// .range([height - margin.bottom, margin.top]);

// configure z scale (color dimension)
z = d3.scaleSequential(d3.schemePastel1).domain([-0.5 * n, 0.65 * n]);

// initialize bars
rect = svg
.selectAll("g")
.data(y01z[layout])
.enter()
.append("g")
.attr("class", "bar-group")
.attr("fill", (d, i) => z(i))
.selectAll("rect")
.data((d) => d)
.join("rect")
.attr("class", "bar")
.attr("x", (d, i) => x(i))
.attr("y", height - margin.bottom)
.attr("width", x.bandwidth())
.attr("height", 0);

// let buffer = margin.top - 2;

// initialize bar labels
rectLabel = svg
.append("g")
.attr("class", "bar-labels")
.selectAll("g")
.data(y01z[layout])
.enter()
.append("g")
.attr("class", "bar-label")
.selectAll("rect")
.data((d) => d)
.join("text")
.attr("x", (d, i) => x(i))
.attr("y", height - margin.top)
.attr("font-size", "0.75rem")
//.attr("font-weight", 100)
.attr("font-family", "System monospace")
.attr("stroke", "rgba(20,5,15,0.05)")
.attr("opacity", 0.34)
.text((d, i) => {
let val = groups[i][layout][d[2]];
return "function" === typeof formatBarLabel
? formatBarLabel(val)
: val;
});

// render x-axis
svg.call(xAxis);

// render y-axis
svg.call(yAxis);

// render legend
svg.call(legend);

if (layout === "stacked") {
transitionStacked();
svg.call(yAxis);
}

// if (layout === "grouped") transitionGrouped();
if (layout === "grouped") {
transitionGrouped();
svg.call(yAxis);
}
});
}

// --- helpers ------------------------------------------------
function transitionGrouped(params) {
// configure timing vars
let tDuration = 500,
tDelay = 20;

// update y scale domain
y.domain([0, y1Max]);

// animate bars
rect
.transition()
.duration(tDuration)
.delay((d, i) => i * tDelay)
.attr("x", (d, i) => x(i) + (x.bandwidth() / n) * d[2])
.attr("width", x.bandwidth() / n)
.transition()
.attr("y", (d) => y(d[1] - d[0]))
.attr("height", (d) => y(0) - y(d[1] - d[0]));

// animate bar labels
rectLabel.each(function (d, i) {
let width = d3.select(this).node().getBBox().width;
d3.select(this)
.transition()
.duration(tDuration)
.delay(i * tDelay)
.attr(
"x",
x(i) +
(x.bandwidth() / n) * d[2] +
x.bandwidth() / n / 2 -
width / 2 +
4
)
// .attr("y", y(d))

.on("end", () =>
params && typeof params.atHalfway === "function"
? params.atHalfway()
: null
)
.transition()
.attr("y", y(d[1] - d[0]) - 2)
.attr("opacity", 1);
});
}

function transitionStacked(params) {
// configure timing vars
let tDuration = 500,
tDelay = 20;

// update y scale domain
y.domain([0, y1Max]);

// animate bars
rect
.transition()
.duration(tDuration)
.delay((d, i) => i * tDelay)
.attr("y", (d) => y(d[1]))
.attr("height", (d) => y(d[0]) - y(d[1]))
.transition()
.attr("x", (d, i) => x(i))
.attr("width", x.bandwidth());

// animate bar labels
rectLabel.each(function (d, i) {
let width = d3.select(this).node().getBBox().width;
d3.select(this)
.transition()
.duration(tDuration)
.delay(i * tDelay)
.attr("y", (d) => y(d[1]) + (y(d[0]) - y(d[1])) / 1.35)
.on("end", () =>
params && typeof params.atHalfway === "function"
? params.atHalfway()
: null
)
.transition()
.attr("x", x(i) + (x.bandwidth() - width) / 2)
.attr("opacity", 1);
});
}

function xAxis(selection) {
selection.each(function () {
let _this = d3.select(this);

// remove previous x-axis
_this.selectAll("g.x-axis").remove();

// add new x-axis
svg
.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(
d3
.axisBottom(x)
.tickSizeOuter(0)
.tickFormat((i) => xAxisLabels[layout][i])
);
});
}

function yAxis(selection) {
selection.each(function () {
let _this = d3.select(this);

// remove previous y-axis
_this.selectAll("g.y-axis").remove();

// add new y-axis
svg
.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${margin.left - 10}, 0)`)
.call(
d3.axisLeft(y).tickSizeOuter(0)
//.tickFormat((i) => yAxisLabels[layout][i])
);
});
}

function legend(selection) {
selection.each(function () {
let _this = d3.select(this);

let shapePadding = 50,
shapeSize = 220,
offsetLeft = 8;

let ordinal = d3
.scaleOrdinal()
.domain(legendLabels)
.range(legendLabels.map((l, i) => z(i)));

let legend = d3
.legendColor()
.orient("horizontal")
// .orient("vertical")
.shapeWidth(width / legendLabels.length)
.labelOffset(8)
.labelWrap(width / legendLabels.length)
.scale(ordinal);

// remove previous legend
_this.selectAll("g.legend").remove();

// add new legend
_this
.append("g")
.attr("class", "legend")
.attr("transform", `translate(30, 0)`)
.call(legend);
});
}

// --- updaters -------------------------------------------------
chart.update = function (value) {
if (!arguments.length || !value || layout === value) return;

// update global layout var
layout = value;

// update y maxes
yMax = d3.max(yz[layout], (y) => d3.max(y));
y1Max = d3.max(y01z[layout], (y) => d3.max(y, (d) => d[1]));

// rebind bar rect data to values from new layout
svg
.selectAll("g.bar-group")
.data(y01z[layout])
.selectAll("rect")
.data((d) => d);

// rebind bar label data to values from new layout
svg
.selectAll("g.bar-label")
.data(y01z[layout])
.selectAll("text")
.data((d) => d);

const updateBarLabelText = () =>
svg
.selectAll("g.bar-label")
.selectAll("text")
.text((d, i) => {
let val = groups[i][layout][d[2]];
return "function" === typeof formatBarLabel
? formatBarLabel(val)
: val;
});

//(val) => (layout == "grouped" ? val + "pts" : val + "%")

// rerender x-axis to reflect labels from new layout
//svg.call(xAxis);

svg.call(yAxis);

// transition to new layout
if (value === "stacked")
transitionStacked({ atHalfway: updateBarLabelText });
if (value === "grouped")
transitionGrouped({ atHalfway: updateBarLabelText });
};

// --- accessors ------------------------------------------------
chart.width = function (value) {
if (!arguments.length) return width;
width = value;
return chart;
};

chart.height = function (value) {
if (!arguments.length) return height;
height = value;
return chart;
};

chart.initialLayout = function (value) {
if (!arguments.length) return initialLayout;
initialLayout = value;
return chart;
};

chart.groups = function (value) {
if (!arguments.length) return groups;
groups = value;
return chart;
};

chart.legendLabels = function (value) {
if (!arguments.length) return legendLabels;
legendLabels = value;
return chart;
};

chart.xAxisLabels = function (value) {
if (!arguments.length) return xAxisLabels;
xAxisLabels = value;
return chart;
};

chart.yAxisLabels = function (value) {
if (!arguments.length) return yAxisLabels;
yAxisLabels = value;
// yAxisLabels.formatBarLabel((val) =>
// layout == "grouped" ? val + "pts" : val + "%"
// );
return chart;
};

chart.formatBarLabel = function (value) {
if (!arguments.length) return formatBarLabel;
formatBarLabel = (value) =>
layout === "grouped" ? value + "" : value + "%";
return chart;
};

return chart;
}
Insert cell
Insert cell
//fantasyFB_position_MGs[0][1]
Insert cell
layout
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more