Public
Edited
Oct 25, 2022
2 forks
Insert cell
Insert cell
chart = {
const svg = d3.create("svg")
.attr("class", "main-up")
.attr("viewBox", [0, 0, width, height])
svg.append("g")
.call(xAxis);

svg.append("g")
.call(yAxis);
// ADD GRID LINES
svg.append("g")
.call(gridLines)
// ADD GRID LINES

// ADD DATA
svg.append("g")
.selectAll("g")
// Enter in data = loop group per group
.data(data)
.enter()
.append("g")
.attr("class", "barGroup")
.attr("transform", function(d) { return "translate(" + x(d.group) + ",0)"; })
.selectAll("rect")
.data(d =>
subgroups
.filter(subgroup => !!d[subgroup])
.map(subgroup => ({key: subgroup, value: d[subgroup]}))
)
.enter().append("rect") // enter = new data array - selection array (previous data array)
.attr("class", "bar")
.attr("x", function(d) { return xSubgroup(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("width", xSubgroup.bandwidth())
.attr("fill", function(d) { return color(d.key); })
.exit().remove();
svg.selectAll("rect")
.transition() //assures the transition of the bars
.duration(400) //the transition lasts 800 ms
.attr("height", function(d) { return y(1) - y(d.value); })
.delay(300)
// ADD DATA

// SHOW DATA ON HOVER
let div;
// svg styles defined below in CSS clip
if (d3.selectAll("[class*='svg1-tooltip']").empty()) {
div = d3.select("body").append("div")
.attr("class", "svg1-tooltip")
} else {
div = d3.selectAll("[class*='svg1-tooltip']");
}
svg.selectAll("rect")
.on('mouseover', function (event, d) {
d3.select(this).transition()
.duration('50')
.attr('opacity', '.8')
.attr('cursor', 'pointer');
div.html(`${d.value}, ${d.key}`)
.transition().duration(50)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 15) + "px")
.style("visibility", 'visible');
})
.on('mouseout', function (event, d) {
d3.select(this).transition()
.duration('50')
.attr('opacity', '1')
.attr('cursor', 'default');
div.transition()
.duration('50')
.style("visibility", 'hidden');
});
// SHOW DATA ON HOVER

// ZOOM FUNCTIONALITY
// svg.call(zoom);
// ZOOM FUNCTIONALITY

return svg.node();
}
Insert cell
brush_chart = {
const svg_brush = d3.create("svg")
.attr("class", "show-up")
.attr("viewBox", [0, 0, width, brushHeight])

svg_brush.append("g")
.call(xAxisBrush);

// HIDDEN X-AXIS
svg_brush.append("g")
.attr("class", "hidden-date-axis")
.style("visibility", "hidden")
.call(d3.axisBottom(xDateAxis));
// HIDDEN X-AXIS

// ADD YEARLY DATA PLACEHOLDERS
svg_brush.append("g")
.selectAll("g")
// Enter in data = loop group per group
.data(data)
.enter()
.append("g")
.attr("class", "barGroup")
.attr("transform", function(d) { return "translate(" + x(d.group) + ",0)"; })
// ADD YEARLY DATA PLACEHOLDERS
// MONTHLY AXES
const monthlyAxes = getMonthlyAxes(svg_brush);
// MONTHLY AXES

// CATEGORY AXES - MONTHS
const xSubgroupMonths = d3.scaleBand()
.domain(subgroups)
.rangeRound([0, monthlyAxes[0].bandwidth()])
.padding([0.05]);
// CATEGORY AXES - MONTHS

// ADD MONTHLY DATA
svg_brush.selectAll(".barGroup")
.append("g")
.attr("class", "barGroup-Secondary")
.selectAll("g")
// Enter in data = loop group per group
.data((d) => { return dataMonth.filter(data => data.group === d.group) })
.enter()
.append("g")
.style("background-color", "pink")
.attr("transform", function(d, i) {
const xCoord = monthlyAxes
.find(x => x(`${d.group}-${d.groupSecondary}`) !== undefined)
(`${d.group}-${d.groupSecondary}`)
return "translate(" + xCoord + ",0)";
})
.selectAll("rect")
.data(d =>
subgroups
.filter(subgroup => !!d[subgroup])
.map(subgroup => ({key: subgroup, value: d[subgroup]}))
)
.enter().append("rect") // enter = new data array - selection array (previous data array)
.attr("class", "bar")
.attr("fill", function(d) { return color(d.key); })
.attr("x", function(d) { return xSubgroupMonths(d.key); })
.attr("y", function(d) { return yBrush(d.value); })
.attr("width", xSubgroupMonths.bandwidth())
.exit().remove();
svg_brush.selectAll("rect")
.transition() //assures the transition of the bars
.duration(400) //the transition lasts 800 ms
.attr("y", d => yBrush(d.value))
.attr("height", function(d) { return yBrush(1) - yBrush(d.value); })
.delay(300)
// ADD MONTHLY DATA

// BRUSH FUNCTIONALITY
// svg_brush.append("g")
// .attr("class", "brush")
// .call(brush)
// .call(brush.move, x.range());

d3.selectAll('.handle').remove();
d3.selectAll('.resize').remove();
d3.selectAll('.background').remove();
d3.selectAll('.overlay').remove();
// BRUSH FUNCTIONALITY


return svg_brush.node();
}
Insert cell
brush = d3.brushX()
.extent([[0, 0], [width, brushHeight]])
.on("brush", brushed);
Insert cell
function brushed(event) {
if (
(event.sourceEvent && event.sourceEvent.type !== "zoom")
) {
const s = event.selection;
// d3.select(".zoom").call(zoom.transform, d3.zoomIdentity
// .scale(width / (s[1] - s[0]))
// .translate(-s[0], 0));
}
}
Insert cell
function zoom(svg) {
const extent = [[margin.left, margin.top], [width - margin.right, height - margin.top]];

// This makes sure we're not zooming in too much or too little depending on the number of years
const zoomScale = x.domain().length * 1.2;

svg.call(d3.zoom()
.scaleExtent([1, zoomScale])
.translateExtent(extent)
.extent(extent)
.on("zoom", zoomed));
function zoomed(event) {
/* bit of a hack here: need to manually disable hovered data when the user zooms */
d3.selectAll('.svg1-tooltip').style("visibility", 'hidden');
/* hack end */

xSubgroup.rangeRound([0, x.bandwidth()]);
x.range([0, width].map(d => event.transform.applyX(d)));
svg.select(".x-axis").call(d3.axisBottom(x));
svg.selectAll(".barGroup").attr("transform", (d) => "translate(" + x(d.group) + ",0)");
svg.selectAll(".bar")
.attr("x", function(d) { return xSubgroup(d.key);})
.attr("width", xSubgroup.bandwidth());

/**
* we need to use a reference scale here so that zooming
* is relative to the original zoom state and not the last zoom state
* explanation: https://stackoverflow.com/a/50185040/6051241
* */
if (
(event.sourceEvent && event.sourceEvent.type) !== "brush"
) {
xDateAxis.domain(event.transform.rescaleX(xDateAxisReference).domain());
const [startPoint, endPoint] = xDateAxis.domain();
console.log('brush', brush)
// d3.select(".brush")
// .call(
// brush.move,
// [xDateAxisReference(startPoint), xDateAxisReference(endPoint)]
// );
}
/**
* the transform value we get from the brush-triggered zoom event is
* not accurate upto 6/7 decimal places, so we need to round it
* example: we get 8.99999998 instead of 9
* so we need to round it off to 9 ONLY if the zoom event is triggered by the brush
*/
// const transformValue = Math.abs(event.transform.k - zoomScale);
// if (transformValue < 0.00001) {
// if (event.sourceEvent && event.sourceEvent.type !== "brush") {
// // if the secondary axes have already been rendered then just go back
// if (svg.selectAll("[class*='x-axis-2']").empty()) {
// const x2 = showSecondaryXAxis(svg, x, height);
// if (svg.selectAll(".barGroup-Secondary").empty()) generateBarsMonth(svg, x, x2, y, height, color)
// }
// }
// } else {
// svg.selectAll(".bar")
// .attr("x", function(d) { return xSubgroup(d.key);})
// .attr("width", xSubgroup.bandwidth());
// if (!svg.selectAll(".barGroup-Secondary").empty()) {
// generateBarsYear(svg, x, y, height, color);
// svg.selectAll("[class*='x-axis-2']").remove();
// }
// }
// showDataOnHover(svg);
}
}
Insert cell
height = 400
Insert cell
brushHeight = 100

Insert cell
x = d3.scaleBand()
.domain(years)
.range([0, width])
.padding([0.2])
Insert cell
y = d3.scaleLog()
.base(2)
.domain([1, 1500])
.range([ height - margin.bottom, margin.top ]);
Insert cell
yBrush = d3.scaleLog()
.base(2)
.domain([1, 1500])
.range([ brushHeight - margin.bottom, margin.top ]);
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.attr("class", "x-axis")
.style("font-size", "12px")
.style("font-weight", "bold")
.call(d3.axisBottom(x))
Insert cell
xAxisBrush = g => g
.attr("transform", `translate(0,${brushHeight - margin.bottom})`)
.attr("class", "x-axis")
.style("font-size", "12px")
.style("font-weight", "bold")
.call(d3.axisBottom(x.copy()))
Insert cell
xDateAxis = d3.scaleTime()
.domain([ new Date(d3.min(years)), new Date(d3.max(years)) ])
.range([0, width])
Insert cell
xDateAxisReference = xDateAxis.copy();
Insert cell
xSubgroup = d3.scaleBand()
.domain(subgroups)
.rangeRound([0, x.bandwidth()])
.padding([0.05]);
Insert cell
yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.attr("class", "y-axis")
.call(d3.axisLeft(y))
Insert cell
getMonthlyAxes = svg => {
const x2s = []
const ticks = svg
.selectAll(".barGroup")
.append("g")
.attr("transform", "translate(0," + height + ")")
.style("font-size", "10px")
.style("font-weight", "bold")
.attr("class",(_, i) => `x-axis-2-${i}`)
.each((_, i) => {
const barGroup = svg.select(`.barGroup:nth-child(${i+1})`);
const currentYear = barGroup.data()[0]?.group;
const x2 = d3.scaleBand()
.domain(months.map(month => `${currentYear}-${month}`))
.range([0, x.bandwidth()])
.padding(0);
x2s.push(x2);
});
// if(false) {
// x2s.forEach((x2, i) =>
// svg.selectAll(`.x-axis-2-${i}`)
// .call(
// d3.axisBottom(x2)
// .tickFormat(d => d.split('-')[1])
// )
// )
// }
ticks.selectAll("text").attr("transform", "translate(-10, 10), rotate(-65)");
return x2s;
}
Insert cell
gridLines = svg => {
svg.append("g")
.attr("class", "grid")
.call(d3.axisLeft(y)
.tickSize(-width)
.tickFormat("")
);
svg.selectAll(".grid line, .grid path")
.attr("stroke", "#e0e0e0")
.attr("stroke-opacity", "0.5")
.attr("stroke-dasharray", "4 4")
.attr("stroke-width", "1px")
}
Insert cell
margin = ({top: 25, right: 20, bottom: 35, left: 40})
Insert cell
color = d3.scaleOrdinal()
.domain(subgroups)
.range([
'#864000',
'#D44000',
'#FF7A00',
'#FFEFCF',
'#583D72',
])
Insert cell
subgroups = ['Loan', 'Bill', 'Shopping', 'Travel', 'Restaurant']
Insert cell
years = ['2017', '2018', '2019']
Insert cell
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
Insert cell
Insert cell
data = [
{
group: '2017',
Loan: 1000,
Bill: 200,
Shopping: 400,
Travel: 150,
Restaurant: 100
},
{
group: '2018',
Loan: 1000,
Bill: 300,
Shopping: 100,
Travel: 180,
Restaurant: 50
},
{
group: '2019',
Loan: 1000,
Bill: 200,
Shopping: 50,
Travel: 80,
Restaurant: 100
},
]
Insert cell
dataMonth = [
{
group: '2017',
groupSecondary: 'Jan',
Loan: 500,
Bill: 60,
Shopping: 150,
Travel: 55,
Restaurant: 50,
},
{
group: '2018',
groupSecondary: 'Jan',
Loan: 300,
Bill: 200,
Shopping: 50,
Travel: 75,
Restaurant: 20,
},
{
group: '2018',
groupSecondary: 'Dec',
Loan: 600,
Bill: 150,
Shopping: 90,
Travel: 175,
Restaurant: 30,
},
{
group: '2018',
groupSecondary: 'Oct',
Loan: 400,
Bill: 50,
Shopping: 20,
Travel: 100,
Restaurant: 60,
},
{
group: '2019',
groupSecondary: 'Feb',
Loan: 500,
Bill: 100,
Shopping: 34,
Travel: 40,
Restaurant: 60
},
{
group: '2019',
groupSecondary: 'Mar',
Loan: 500,
Bill: 100,
Shopping: 85,
Travel: 90,
Restaurant: 100,
},
{
group: '2019',
groupSecondary: 'Apr',
Loan: 500,
Bill: 30,
Shopping: 150,
Travel: 40,
Restaurant: 200,
},
]
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