Published
Edited
Apr 3, 2020
6 forks
20 stars
Insert cell
Insert cell
{
var svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
/*
Brush & Zoom area chart block to work with mulit-line charts.
Combining d3-brush and d3-zoom to implement Focus + Context.

The focus chart is the main larger one where the zooming occurs.
The context chart is the smaller one below where the brush is used to specify a focused area.
*/

// sets margins for both charts
var focusChartMargin = { top: 20, right: 20, bottom: 170, left: 60 };
var contextChartMargin = { top: 360, right: 20, bottom: 90, left: 60 };

// width of both charts
var chartWidth = width - focusChartMargin.left - focusChartMargin.right;

// height of either chart
var focusChartHeight = height - focusChartMargin.top - focusChartMargin.bottom;
var contextChartHeight = height - contextChartMargin.top - contextChartMargin.bottom;

// bootstraps the d3 parent selection
svg
.append("svg")
.attr("width", chartWidth + focusChartMargin.left + focusChartMargin.right)
.attr("height", focusChartHeight + focusChartMargin.top + focusChartMargin.bottom)
.append("g")
.attr("transform", "translate(" + focusChartMargin.left + "," + focusChartMargin.top + ")");

// function to parse date field
var parseTime = d3.timeParse("%H:%M");

//group all dates to get range for x axis later
var dates = [];
for (let key of Object.keys(data)) {
data[key].forEach(bucketRecord => {
dates.push(parseTime(bucketRecord.date));
});
}

//get max Y axis value by searching for the highest conversion rate
var maxYAxisValue = -Infinity;
for (let key of Object.keys(data)) {
let maxYAxisValuePerBucket = Math.ceil(d3.max(data[key], d => d["conversion"]));
maxYAxisValue = Math.max(maxYAxisValuePerBucket, maxYAxisValue);
}

// set the height of both y axis
var yFocus = d3.scaleLinear().range([focusChartHeight, 0]);
var yContext = d3.scaleLinear().range([contextChartHeight, 0]);

// set the width of both x axis
var xFocus = d3.scaleTime().range([0, chartWidth]);
var xContext = d3.scaleTime().range([0, chartWidth]);

// create both x axis to be rendered
var xAxisFocus = d3
.axisBottom(xFocus)
.ticks(10)
.tickFormat(d3.timeFormat("%I:%M%p"));
var xAxisContext = d3
.axisBottom(xContext)
.ticks(10)
.tickFormat(d3.timeFormat("%I:%M%p"));

// create the one y axis to be rendered
var yAxisFocus = d3.axisLeft(yFocus).tickFormat(d => d + "%");

// build brush
var brush = d3
.brushX()
.extent([
[0, -10],
[chartWidth, contextChartHeight],
])
.on("brush end", brushed);

// build zoom for the focus chart
// as specified in "filter" - zooming in/out can be done by pinching on the trackpad while mouse is over focus chart
// zooming in can also be done by double clicking while mouse is over focus chart
var zoom = d3
.zoom()
.scaleExtent([1, Infinity])
.translateExtent([
[0, 0],
[chartWidth, focusChartHeight],
])
.extent([
[0, 0],
[chartWidth, focusChartHeight],
])
.on("zoom", zoomed)
.filter(() => d3.event.ctrlKey || d3.event.type === "dblclick" || d3.event.type === "mousedown");

// create a line for focus chart
var lineFocus = d3
.line()
.x(d => xFocus(parseTime(d.date)))
.y(d => yFocus(d.conversion));

// create line for context chart
var lineContext = d3
.line()
.x(d => xContext(parseTime(d.date)))
.y(d => yContext(d.conversion));

// es lint disabled here so react won't warn about not using variable "clip"
/* eslint-disable */

// clip is created so when the focus chart is zoomed in the data lines don't extend past the borders
var clip = svg
.append("defs")
.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", chartWidth)
.attr("height", focusChartHeight)
.attr("x", 0)
.attr("y", 0);

// append the clip
var focusChartLines = svg
.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + focusChartMargin.left + "," + focusChartMargin.top + ")")
.attr("clip-path", "url(#clip)");

/* eslint-enable */

// create focus chart
var focus = svg
.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + focusChartMargin.left + "," + focusChartMargin.top + ")");

// create context chart
var context = svg
.append("g")
.attr("class", "context")
.attr("transform", "translate(" + contextChartMargin.left + "," + (contextChartMargin.top + 50) + ")");

// add data info to axis
xFocus.domain(d3.extent(dates));
yFocus.domain([0, maxYAxisValue]);
xContext.domain(d3.extent(dates));
yContext.domain(yFocus.domain());

// add axis to focus chart
focus
.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + focusChartHeight + ")")
.call(xAxisFocus);
focus
.append("g")
.attr("class", "y-axis")
.call(yAxisFocus);

// get list of bucket names
var bucketNames = [];
for (let key of Object.keys(data)) {
bucketNames.push(key);
}

// match colors to bucket name
var colors = d3
.scaleOrdinal()
.domain(bucketNames)
.range(["#3498db", "#3cab4b", "#e74c3c", "#73169e", "#2ecc71"]);

// go through data and create/append lines to both charts
for (let key of Object.keys(data)) {
let bucket = data[key];
focusChartLines
.append("path")
.datum(bucket)
.attr("class", "line")
.attr("fill", "none")
.attr("stroke", d => colors(key))
.attr("stroke-width", 1.5)
.attr("d", lineFocus);
context
.append("path")
.datum(bucket)
.attr("class", "line")
.attr("fill", "none")
.attr("stroke", d => colors(key))
.attr("stroke-width", 1.5)
.attr("d", lineContext);
}

// add x axis to context chart (y axis is not needed)
context
.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + contextChartHeight + ")")
.call(xAxisContext);

// add bush to context chart
var contextBrush = context
.append("g")
.attr("class", "brush")
.call(brush);

// style brush resize handle
var brushHandlePath = d => {
var e = +(d.type === "e"),
x = e ? 1 : -1,
y = contextChartHeight + 10;
return (
"M" +
0.5 * x +
"," +
y +
"A6,6 0 0 " +
e +
" " +
6.5 * x +
"," +
(y + 6) +
"V" +
(2 * y - 6) +
"A6,6 0 0 " +
e +
" " +
0.5 * x +
"," +
2 * y +
"Z" +
"M" +
2.5 * x +
"," +
(y + 8) +
"V" +
(2 * y - 8) +
"M" +
4.5 * x +
"," +
(y + 8) +
"V" +
(2 * y - 8)
);
};

var brushHandle = contextBrush
.selectAll(".handle--custom")
.data([{ type: "w" }, { type: "e" }])
.enter()
.append("path")
.attr("class", "handle--custom")
.attr("stroke", "#000")
.attr("cursor", "ew-resize")
.attr("d", brushHandlePath);

// overlay the zoom area rectangle on top of the focus chart
svg
.append("rect")
.attr("cursor", "move")
.attr("fill", "none")
.attr("pointer-events", "all")
.attr("class", "zoom")
.attr("width", chartWidth)
.attr("height", focusChartHeight)
.attr("transform", "translate(" + focusChartMargin.left + "," + focusChartMargin.top + ")")
.call(zoom);

contextBrush.call(brush.move, [0, chartWidth / 2]);

// focus chart x label
focus
.append("text")
.attr("transform", "translate(" + chartWidth / 2 + " ," + (focusChartHeight + focusChartMargin.top + 25) + ")")
.style("text-anchor", "middle")
.style("font-size", "18px")
.text("Time (UTC)");

// focus chart y label
focus
.append("text")
.attr("text-anchor", "middle")
.attr("transform", "translate(" + (-focusChartMargin.left + 20) + "," + focusChartHeight / 2 + ")rotate(-90)")
.style("font-size", "18px")
.text("Conversion Rate");

function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
var s = d3.event.selection || xContext.range();
xFocus.domain(s.map(xContext.invert, xContext));
focusChartLines.selectAll(".line").attr("d", lineFocus);
focus.select(".x-axis").call(xAxisFocus);
svg.select(".zoom").call(zoom.transform, d3.zoomIdentity.scale(chartWidth / (s[1] - s[0])).translate(-s[0], 0));
brushHandle
.attr("display", null)
.attr("transform", (d, i) => "translate(" + [s[i], -contextChartHeight - 20] + ")");
}

function zoomed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
var t = d3.event.transform;
xFocus.domain(t.rescaleX(xContext).domain());
focusChartLines.selectAll(".line").attr("d", lineFocus);
focus.select(".x-axis").call(xAxisFocus);
var brushSelection = xFocus.range().map(t.invertX, t);
context.select(".brush").call(brush.move, brushSelection);
brushHandle
.attr("display", null)
.attr("transform", (d, i) => "translate(" + [brushSelection[i], -contextChartHeight - 20] + ")");
}
return svg.node()
}
Insert cell
width = 900
Insert cell
height = 500
Insert cell
d3 = require("d3@5")
Insert cell
data = {
return {
"plethora-control": [
{
date: "00:00",
consumed: 2229,
seen: 25136,
conversion: 8.867759388924252,
},
{
date: "01:00",
consumed: 2120,
seen: 36640,
conversion: 5.786026200873363,
},
{
date: "02:00",
consumed: 2774,
seen: 39388,
conversion: 7.042754138316239,
},
{
date: "03:00",
consumed: 2301,
seen: 33609,
conversion: 6.846380433812371,
},
{
date: "04:00",
consumed: 1877,
seen: 25224,
conversion: 7.441325721535046,
},
{
date: "05:00",
consumed: 985,
seen: 16134,
conversion: 6.105119623156068,
},
{
date: "06:00",
consumed: 485,
seen: 7535,
conversion: 6.436629064366291,
},
{
date: "07:00",
consumed: 504,
seen: 4823,
conversion: 10.449927431059507,
},
{
date: "08:00",
consumed: 321,
seen: 2963,
conversion: 10.833614579817752,
},
{
date: "09:00",
consumed: 208,
seen: 2130,
conversion: 9.765258215962442,
},
{
date: "10:00",
consumed: 269,
seen: 5304,
conversion: 5.071644042232277,
},
{
date: "11:00",
consumed: 1124,
seen: 10638,
conversion: 10.565895845083663,
},
{
date: "12:00",
consumed: 1130,
seen: 11897,
conversion: 9.498192821719762,
},
{
date: "13:00",
consumed: 862,
seen: 13455,
conversion: 6.406540319583797,
},
{
date: "14:00",
consumed: 800,
seen: 12024,
conversion: 6.65335994677312,
},
{
date: "15:00",
consumed: 750,
seen: 10679,
conversion: 7.023129506508099,
},
{
date: "16:00",
consumed: 608,
seen: 11426,
conversion: 5.321197269385611,
},
{
date: "17:00",
consumed: 964,
seen: 17691,
conversion: 5.449098411621729,
},
{
date: "18:00",
consumed: 984,
seen: 12307,
conversion: 7.995449744048103,
},
{
date: "19:00",
consumed: 1312,
seen: 14086,
conversion: 9.314212693454493,
},
{
date: "20:00",
consumed: 1452,
seen: 19743,
conversion: 7.354505394316973,
},
{
date: "21:00",
consumed: 1009,
seen: 21686,
conversion: 4.652771373236189,
},
{
date: "22:00",
consumed: 1188,
seen: 18603,
conversion: 6.386066763425253,
},
{
date: "23:00",
consumed: 1128,
seen: 17052,
conversion: 6.615059817030261,
},
],
plethora4: [
{
date: "00:00",
consumed: 579,
seen: 14016,
conversion: 4.130993150684931,
},
{
date: "01:00",
consumed: 595,
seen: 16220,
conversion: 3.6683107274969173,
},
{
date: "02:00",
consumed: 814,
seen: 17481,
conversion: 4.656484182827069,
},
{
date: "03:00",
consumed: 840,
seen: 16276,
conversion: 5.160973212091423,
},
{
date: "04:00",
consumed: 524,
seen: 12164,
conversion: 4.3077934889838865,
},
{
date: "05:00",
consumed: 446,
seen: 11034,
conversion: 4.04205183976799,
},
{
date: "06:00",
consumed: 284,
seen: 6424,
conversion: 4.420921544209215,
},
{
date: "07:00",
consumed: 128,
seen: 2741,
conversion: 4.669828529733674,
},
{
date: "08:00",
consumed: 46,
seen: 977,
conversion: 4.7082906857727735,
},
{
date: "09:00",
consumed: 101,
seen: 1951,
conversion: 5.176832393644285,
},
{
date: "10:00",
consumed: 76,
seen: 1605,
conversion: 4.7352024922118385,
},
{
date: "11:00",
consumed: 294,
seen: 4015,
conversion: 7.322540473225405,
},
{
date: "12:00",
consumed: 247,
seen: 4919,
conversion: 5.0213458019922745,
},
{
date: "13:00",
consumed: 308,
seen: 5402,
conversion: 5.701592002961866,
},
{
date: "14:00",
consumed: 293,
seen: 5641,
conversion: 5.194114518702357,
},
{
date: "15:00",
consumed: 441,
seen: 6540,
conversion: 6.7431192660550465,
},
{
date: "16:00",
consumed: 522,
seen: 6049,
conversion: 8.629525541411804,
},
{
date: "17:00",
consumed: 344,
seen: 6569,
conversion: 5.236717917491247,
},
{
date: "18:00",
consumed: 280,
seen: 5526,
conversion: 5.066956207021354,
},
{
date: "19:00",
consumed: 497,
seen: 7131,
conversion: 6.969569485345674,
},
{
date: "20:00",
consumed: 454,
seen: 7148,
conversion: 6.351426972579742,
},
{
date: "21:00",
consumed: 704,
seen: 8925,
conversion: 7.8879551820728295,
},
{
date: "22:00",
consumed: 483,
seen: 7502,
conversion: 6.438283124500133,
},
{
date: "23:00",
consumed: 358,
seen: 5655,
conversion: 6.330680813439433,
},
],
plethora5: [
{
date: "00:00",
consumed: 493,
seen: 10747,
conversion: 4.58732669582209,
},
{
date: "01:00",
consumed: 550,
seen: 14305,
conversion: 3.8448095071653268,
},
{
date: "02:00",
consumed: 697,
seen: 16177,
conversion: 4.308586264449527,
},
{
date: "03:00",
consumed: 797,
seen: 15661,
conversion: 5.089074771725944,
},
{
date: "04:00",
consumed: 465,
seen: 11742,
conversion: 3.960143076136944,
},
{
date: "05:00",
consumed: 590,
seen: 8987,
conversion: 6.565038388783799,
},
{
date: "06:00",
consumed: 193,
seen: 3856,
conversion: 5.005186721991701,
},
{
date: "07:00",
consumed: 158,
seen: 2453,
conversion: 6.441092539747248,
},
{
date: "08:00",
consumed: 28,
seen: 1535,
conversion: 1.8241042345276872,
},
{
date: "09:00",
consumed: 62,
seen: 1163,
conversion: 5.331040412725709,
},
{
date: "10:00",
consumed: 96,
seen: 1816,
conversion: 5.286343612334802,
},
{
date: "11:00",
consumed: 142,
seen: 2751,
conversion: 5.161759360232643,
},
{
date: "12:00",
consumed: 219,
seen: 4262,
conversion: 5.138432660722665,
},
{
date: "13:00",
consumed: 205,
seen: 6358,
conversion: 3.224284366152878,
},
{
date: "14:00",
consumed: 200,
seen: 5668,
conversion: 3.5285815102328866,
},
{
date: "15:00",
consumed: 258,
seen: 6132,
conversion: 4.207436399217221,
},
{
date: "16:00",
consumed: 213,
seen: 5140,
conversion: 4.14396887159533,
},
{
date: "17:00",
consumed: 296,
seen: 5550,
conversion: 5.333333333333334,
},
{
date: "18:00",
consumed: 267,
seen: 4986,
conversion: 5.354993983152828,
},
{
date: "19:00",
consumed: 176,
seen: 6375,
conversion: 2.76078431372549,
},
{
date: "20:00",
consumed: 308,
seen: 7876,
conversion: 3.910614525139665,
},
{
date: "21:00",
consumed: 367,
seen: 8620,
conversion: 4.25754060324826,
},
{
date: "22:00",
consumed: 277,
seen: 5790,
conversion: 4.784110535405873,
},
{
date: "23:00",
consumed: 280,
seen: 6599,
conversion: 4.243067131383543,
},
],
plethora6: [
{
date: "00:00",
consumed: 537,
seen: 14980,
conversion: 3.5847797062750333,
},
{
date: "01:00",
consumed: 598,
seen: 15810,
conversion: 3.7824161922833652,
},
{
date: "02:00",
consumed: 881,
seen: 20144,
conversion: 4.37351072279587,
},
{
date: "03:00",
consumed: 530,
seen: 16060,
conversion: 3.300124533001245,
},
{
date: "04:00",
consumed: 417,
seen: 12949,
conversion: 3.2203258938914203,
},
{
date: "05:00",
consumed: 395,
seen: 9650,
conversion: 4.093264248704663,
},
{
date: "06:00",
consumed: 169,
seen: 3910,
conversion: 4.32225063938619,
},
{
date: "07:00",
consumed: 82,
seen: 2825,
conversion: 2.9026548672566372,
},
{
date: "08:00",
consumed: 95,
seen: 3842,
conversion: 2.472670484122853,
},
{
date: "09:00",
consumed: 40,
seen: 2454,
conversion: 1.6299918500407498,
},
{
date: "10:00",
consumed: 74,
seen: 2032,
conversion: 3.6417322834645667,
},
{
date: "11:00",
consumed: 371,
seen: 5709,
conversion: 6.498511122788579,
},
{
date: "12:00",
consumed: 236,
seen: 5074,
conversion: 4.651162790697675,
},
{
date: "13:00",
consumed: 169,
seen: 7138,
conversion: 2.3676099747828525,
},
{
date: "14:00",
consumed: 192,
seen: 6159,
conversion: 3.117389186556259,
},
{
date: "15:00",
consumed: 366,
seen: 6122,
conversion: 5.97843841881738,
},
{
date: "16:00",
consumed: 252,
seen: 6625,
conversion: 3.8037735849056604,
},
{
date: "17:00",
consumed: 220,
seen: 7126,
conversion: 3.0872859949480773,
},
{
date: "18:00",
consumed: 255,
seen: 6056,
conversion: 4.2107001321003965,
},
{
date: "19:00",
consumed: 446,
seen: 8181,
conversion: 5.4516562767387855,
},
{
date: "20:00",
consumed: 467,
seen: 7679,
conversion: 6.081521031384295,
},
{
date: "21:00",
consumed: 436,
seen: 7665,
conversion: 5.688193085453359,
},
{
date: "22:00",
consumed: 410,
seen: 7234,
conversion: 5.667680398119989,
},
{
date: "23:00",
consumed: 249,
seen: 8722,
conversion: 2.8548498050905753,
},
],
};
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more