D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
35degrees
Full window
Github gist
JVScrollexample1
Built with
blockbuilder.org
<!DOCTYPE html> <html> <head> <title>Scrolling Sections</title> <link rel="stylesheet" type="text/css" href="css/bootstrap.min.css"/> <link rel="stylesheet" type="text/css" href="css/style.css"/> <script src="https://d3js.org/d3.v4.min.js"></script> </head> <body> <div class="container"> <div id='graphic'> <div id='sections'> <section class="step"> <div class="title">OpenVis Conf 2013</div> I did what no presenter should ever do: I watched my own talk.<br/><br/>My first visit to OpenVis Conf in 2013. </section> <section class="step"> <div class="title">Filler Words</div> As expected, I could only focus on the flaws: the rushed speech, the odd phrases, and, most especially, all the filler words. In fact, I found 180 filler words in my 30 minute talk. </section> <section class="step"> <div class="title">My Talk</div> Here are all 5,040 words of my talk. </section> <section class="step"> <div class="title">My Stumbles</div> And here are all the fillers I used in those 30 minutes. </section> <section class="step"> <div class="title">Um's, Ah's & Uh's</div> I almost exclusively used these three fillers. Um's and Ah's made up over 80%, with Uh's trailing behind. </section> <section class="step"> <div class="title">Fillers Over Time</div> I hoped that all these blunders were toward the beginning of my talk. And the data suggests that fewer fillers are used as I get into it. Perhaps the talk started out rough and improved as I found my groove. </section> <section class="step"> <div class="title">Ramping Back Up</div> Unfortunately, the trend does not continue. Midway into the talk my Um's and Ah's spike. I continue to use them pretty consistently throughout the rest of the talk. </section> <section class="step"> <div class="title">The Cough Effect</div> My theory is that at this critical halfway point in my talk, I heard a dry cough indicative of the audience's waning interest. This caused self-confidence to collapse and forced me out of my groove.<br/><br/>A competing theory is that I just hadn't practiced the last half of my speech as much. </section> <section class="step"> <div class="title">Best of Luck to Me in 2015</div> The world may never know, or care, but hopefully these insights improve my speaking in 2015. Though preliminary results aren't looking so good. </section> </div> <div id='vis'> </div> <div id="extra-space"> </div> </div> </div> <style> .container { width: 890px; } #graphic { padding-top: 60px; } #sections { position: relative; display: inline-block; width: 250px; top: 0px; z-index: 90; padding-bottom: 200px; } .step { margin-bottom: 200px; font-family: "TiemposTextWeb-Regular","Georgia"; font-size: 16px; line-height: 23px; color: #767678; } #sections .title { font-family: Arial,Helvetica,"san-serif"; font-size: 16px; font-weight: bold; margin-bottom: 2px; color: #262626; line-height: 1.2em; } #extra-space { height: 300px; } #vis { display: inline-block; position: fixed; top: 60px; z-index: 1; margin-left: 0; /* height: 600px; */ /* width: 600px; */ /* background-color: #ddd; */ } #vis .title { font-size:120px; text-anchor: middle; } #vis .sub-title { font-size:80px; text-anchor: middle; } .axis path, .axis line { fill: none; stroke: #666; shape-rendering: crispEdges; } .highlight { fill: #008080; font-weight: bold; } .cough-arrow { stroke: #000; stroke-width: 4px; } </style> <script> /** * scroller - handles the details * of figuring out which section * the user is currently scrolled * to. * */ function scroller() { var container = d3.select('body'); // event dispatcher var dispatch = d3.dispatch('active', 'progress'); // d3 selection of all the // text sections that will // be scrolled through var sections = null; // array that will hold the // y coordinate of each section // that is scrolled through var sectionPositions = []; var currentIndex = -1; // y coordinate of var containerStart = 0; /** * scroll - constructor function. * Sets up scroller to monitor * scrolling of els selection. * * @param els - d3 selection of * elements that will be scrolled * through by user. */ function scroll(els) { sections = els; // when window is scrolled call // position. When it is resized // call resize. d3.select(window) .on('scroll.scroller', position) .on('resize.scroller', resize); // manually call resize // initially to setup // scroller. resize(); // hack to get position // to be called once for // the scroll position on // load. // @v4 timer no longer stops if you // return true at the end of the callback // function - so here we stop it explicitly. var timer = d3.timer(function () { position(); timer.stop(); }); } /** * resize - called initially and * also when page is resized. * Resets the sectionPositions * */ function resize() { // sectionPositions will be each sections // starting position relative to the top // of the first section. sectionPositions = []; var startPos; sections.each(function (d, i) { var top = this.getBoundingClientRect().top; if (i === 0) { startPos = top; } sectionPositions.push(top - startPos); }); containerStart = container.node().getBoundingClientRect().top + window.pageYOffset; } /** * position - get current users position. * if user has scrolled to new section, * dispatch active event with new section * index. * */ function position() { var pos = window.pageYOffset - 10 - containerStart; var sectionIndex = d3.bisect(sectionPositions, pos); sectionIndex = Math.min(sections.size() - 1, sectionIndex); if (currentIndex !== sectionIndex) { // @v4 you now `.call` the dispatch callback dispatch.call('active', this, sectionIndex); currentIndex = sectionIndex; } var prevIndex = Math.max(sectionIndex - 1, 0); var prevTop = sectionPositions[prevIndex]; var progress = (pos - prevTop) / (sectionPositions[sectionIndex] - prevTop); // @v4 you now `.call` the dispatch callback dispatch.call('progress', this, currentIndex, progress); } /** * container - get/set the parent element * of the sections. Useful for if the * scrolling doesn't start at the very top * of the page. * * @param value - the new container value */ scroll.container = function (value) { if (arguments.length === 0) { return container; } container = value; return scroll; }; // @v4 There is now no d3.rebind, so this implements // a .on method to pass in a callback to the dispatcher. scroll.on = function (action, callback) { dispatch.on(action, callback); }; return scroll; } /** * scrollVis - encapsulates * all the code for the visualization * using reusable charts pattern: * https://bost.ocks.org/mike/chart/ */ var scrollVis = function () { // constants to define the size // and margins of the vis area. var width = 600; var height = 520; var margin = { top: 0, left: 20, bottom: 40, right: 10 }; // Keep track of which visualization // we are on and which was the last // index activated. When user scrolls // quickly, we want to call all the // activate functions that they pass. var lastIndex = -1; var activeIndex = 0; // Sizing for the grid visualization var squareSize = 6; var squarePad = 2; var numPerRow = width / (squareSize + squarePad); // main svg used for visualization var svg = null; // d3 selection that will be used // for displaying visualizations var g = null; // We will set the domain when the // data is processed. // @v4 using new scale names var xBarScale = d3.scaleLinear() .range([0, width]); // The bar chart display is horizontal // so we can use an ordinal scale // to get width and y locations. // @v4 using new scale type var yBarScale = d3.scaleBand() .paddingInner(0.08) .domain([0, 1, 2]) .range([0, height - 50], 0.1, 0.1); // Color is determined just by the index of the bars var barColors = { 0: '#008080', 1: '#399785', 2: '#5AAF8C' }; // The histogram display shows the // first 30 minutes of data // so the range goes from 0 to 30 // @v4 using new scale name var xHistScale = d3.scaleLinear() .domain([0, 30]) .range([0, width - 20]); // @v4 using new scale name var yHistScale = d3.scaleLinear() .range([height, 0]); // The color translation uses this // scale to convert the progress // through the section into a // color value. // @v4 using new scale name var coughColorScale = d3.scaleLinear() .domain([0, 1.0]) .range(['#008080', 'red']); // You could probably get fancy and // use just one axis, modifying the // scale, but I will use two separate // ones to keep things easy. // @v4 using new axis name var xAxisBar = d3.axisBottom() .scale(xBarScale); // @v4 using new axis name var xAxisHist = d3.axisBottom() .scale(xHistScale) .tickFormat(function (d) { return d + ' min'; }); // When scrolling to a new section // the activation function for that // section is called. var activateFunctions = []; // If a section has an update function // then it is called while scrolling // through the section with the current // progress through the section. var updateFunctions = []; /** * chart * * @param selection - the current d3 selection(s) * to draw the visualization in. For this * example, we will be drawing it in #vis */ var chart = function (selection) { selection.each(function (rawData) { // create svg and give it a width and height svg = d3.select(this).selectAll('svg').data([wordData]); var svgE = svg.enter().append('svg'); // @v4 use merge to combine enter and existing selection svg = svg.merge(svgE); svg.attr('width', width + margin.left + margin.right); svg.attr('height', height + margin.top + margin.bottom); svg.append('g'); // this group element will be used to contain all // other elements. g = svg.select('g') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); // perform some preprocessing on raw data var wordData = getWords(rawData); // filter to just include filler words var fillerWords = getFillerWords(wordData); // get the counts of filler words for the // bar chart display var fillerCounts = groupByWord(fillerWords); // set the bar scale's domain var countMax = d3.max(fillerCounts, function (d) { return d.value;}); xBarScale.domain([0, countMax]); // get aggregated histogram data var histData = getHistogram(fillerWords); // set histogram's domain var histMax = d3.max(histData, function (d) { return d.length; }); yHistScale.domain([0, histMax]); setupVis(wordData, fillerCounts, histData); setupSections(); }); }; /** * setupVis - creates initial elements for all * sections of the visualization. * * @param wordData - data object for each word. * @param fillerCounts - nested data that includes * element for each filler word type. * @param histData - binned histogram data */ var setupVis = function (wordData, fillerCounts, histData) { // axis g.append('g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + height + ')') .call(xAxisBar); g.select('.x.axis').style('opacity', 0); // count openvis title g.append('text') .attr('class', 'title openvis-title') .attr('x', width / 2) .attr('y', height / 3) .text('2013'); g.append('text') .attr('class', 'sub-title openvis-title') .attr('x', width / 2) .attr('y', (height / 3) + (height / 5)) .text('OpenVis Conf'); g.selectAll('.openvis-title') .attr('opacity', 0); // count filler word count title g.append('text') .attr('class', 'title count-title highlight') .attr('x', width / 2) .attr('y', height / 3) .text('180'); g.append('text') .attr('class', 'sub-title count-title') .attr('x', width / 2) .attr('y', (height / 3) + (height / 5)) .text('Filler Words'); g.selectAll('.count-title') .attr('opacity', 0); // square grid // @v4 Using .merge here to ensure // new and old data have same attrs applied var squares = g.selectAll('.square').data(wordData, function (d) { return d.word; }); var squaresE = squares.enter() .append('rect') .classed('square', true); squares = squares.merge(squaresE) .attr('width', squareSize) .attr('height', squareSize) .attr('fill', '#fff') .classed('fill-square', function (d) { return d.filler; }) .attr('x', function (d) { return d.x;}) .attr('y', function (d) { return d.y;}) .attr('opacity', 0); // barchart // @v4 Using .merge here to ensure // new and old data have same attrs applied var bars = g.selectAll('.bar').data(fillerCounts); var barsE = bars.enter() .append('rect') .attr('class', 'bar'); bars = bars.merge(barsE) .attr('x', 0) .attr('y', function (d, i) { return yBarScale(i);}) .attr('fill', function (d, i) { return barColors[i]; }) .attr('width', 0) .attr('height', yBarScale.bandwidth()); var barText = g.selectAll('.bar-text').data(fillerCounts); barText.enter() .append('text') .attr('class', 'bar-text') .text(function (d) { return d.key + '…'; }) .attr('x', 0) .attr('dx', 15) .attr('y', function (d, i) { return yBarScale(i);}) .attr('dy', yBarScale.bandwidth() / 1.2) .style('font-size', '110px') .attr('fill', 'white') .attr('opacity', 0); // histogram // @v4 Using .merge here to ensure // new and old data have same attrs applied var hist = g.selectAll('.hist').data(histData); var histE = hist.enter().append('rect') .attr('class', 'hist'); hist = hist.merge(histE).attr('x', function (d) { return xHistScale(d.x0); }) .attr('y', height) .attr('height', 0) .attr('width', xHistScale(histData[0].x1) - xHistScale(histData[0].x0) - 1) .attr('fill', barColors[0]) .attr('opacity', 0); // cough title g.append('text') .attr('class', 'sub-title cough cough-title') .attr('x', width / 2) .attr('y', 60) .text('cough') .attr('opacity', 0); // arrowhead from // https://logogin.blogspot.com/2013/02/d3js-arrowhead-markers.html svg.append('defs').append('marker') .attr('id', 'arrowhead') .attr('refY', 2) .attr('markerWidth', 6) .attr('markerHeight', 4) .attr('orient', 'auto') .append('path') .attr('d', 'M 0,0 V 4 L6,2 Z'); g.append('path') .attr('class', 'cough cough-arrow') .attr('marker-end', 'url(#arrowhead)') .attr('d', function () { var line = 'M ' + ((width / 2) - 10) + ' ' + 80; line += ' l 0 ' + 230; return line; }) .attr('opacity', 0); }; /** * setupSections - each section is activated * by a separate function. Here we associate * these functions to the sections based on * the section's index. * */ var setupSections = function () { // activateFunctions are called each // time the active section changes activateFunctions[0] = showTitle; activateFunctions[1] = showFillerTitle; activateFunctions[2] = showGrid; activateFunctions[3] = highlightGrid; activateFunctions[4] = showBar; activateFunctions[5] = showHistPart; activateFunctions[6] = showHistAll; activateFunctions[7] = showCough; activateFunctions[8] = showHistAll; // updateFunctions are called while // in a particular section to update // the scroll progress in that section. // Most sections do not need to be updated // for all scrolling and so are set to // no-op functions. for (var i = 0; i < 9; i++) { updateFunctions[i] = function () {}; } updateFunctions[7] = updateCough; }; /** * ACTIVATE FUNCTIONS * * These will be called their * section is scrolled to. * * General pattern is to ensure * all content for the current section * is transitioned in, while hiding * the content for the previous section * as well as the next section (as the * user may be scrolling up or down). * */ /** * showTitle - initial title * * hides: count title * (no previous step to hide) * shows: intro title * */ function showTitle() { g.selectAll('.count-title') .transition() .duration(0) .attr('opacity', 0); g.selectAll('.openvis-title') .transition() .duration(600) .attr('opacity', 1.0); } /** * showFillerTitle - filler counts * * hides: intro title * hides: square grid * shows: filler count title * */ function showFillerTitle() { g.selectAll('.openvis-title') .transition() .duration(0) .attr('opacity', 0); g.selectAll('.square') .transition() .duration(0) .attr('opacity', 0); g.selectAll('.count-title') .transition() .duration(600) .attr('opacity', 1.0); } /** * showGrid - square grid * * hides: filler count title * hides: filler highlight in grid * shows: square grid * */ function showGrid() { g.selectAll('.count-title') .transition() .duration(0) .attr('opacity', 0); g.selectAll('.square') .transition() .duration(600) .delay(function (d) { return 5 * d.row; }) .attr('opacity', 1.0) .attr('fill', '#ddd'); } /** * highlightGrid - show fillers in grid * * hides: barchart, text and axis * shows: square grid and highlighted * filler words. also ensures squares * are moved back to their place in the grid */ function highlightGrid() { hideAxis(); g.selectAll('.bar') .transition() .duration(600) .attr('width', 0); g.selectAll('.bar-text') .transition() .duration(0) .attr('opacity', 0); g.selectAll('.square') .transition() .duration(0) .attr('opacity', 1.0) .attr('fill', '#ddd'); // use named transition to ensure // move happens even if other // transitions are interrupted. g.selectAll('.fill-square') .transition('move-fills') .duration(800) .attr('x', function (d) { return d.x; }) .attr('y', function (d) { return d.y; }); g.selectAll('.fill-square') .transition() .duration(800) .attr('opacity', 1.0) .attr('fill', function (d) { return d.filler ? '#008080' : '#ddd'; }); } /** * showBar - barchart * * hides: square grid * hides: histogram * shows: barchart * */ function showBar() { // ensure bar axis is set showAxis(xAxisBar); g.selectAll('.square') .transition() .duration(800) .attr('opacity', 0); g.selectAll('.fill-square') .transition() .duration(800) .attr('x', 0) .attr('y', function (d, i) { return yBarScale(i % 3) + yBarScale.bandwidth() / 2; }) .transition() .duration(0) .attr('opacity', 0); g.selectAll('.hist') .transition() .duration(600) .attr('height', function () { return 0; }) .attr('y', function () { return height; }) .style('opacity', 0); g.selectAll('.bar') .transition() .delay(function (d, i) { return 300 * (i + 1);}) .duration(600) .attr('width', function (d) { return xBarScale(d.value); }); g.selectAll('.bar-text') .transition() .duration(600) .delay(1200) .attr('opacity', 1); } /** * showHistPart - shows the first part * of the histogram of filler words * * hides: barchart * hides: last half of histogram * shows: first half of histogram * */ function showHistPart() { // switch the axis to histogram one showAxis(xAxisHist); g.selectAll('.bar-text') .transition() .duration(0) .attr('opacity', 0); g.selectAll('.bar') .transition() .duration(600) .attr('width', 0); // here we only show a bar if // it is before the 15 minute mark g.selectAll('.hist') .transition() .duration(600) .attr('y', function (d) { return (d.x0 < 15) ? yHistScale(d.length) : height; }) .attr('height', function (d) { return (d.x0 < 15) ? height - yHistScale(d.length) : 0; }) .style('opacity', function (d) { return (d.x0 < 15) ? 1.0 : 1e-6; }); } /** * showHistAll - show all histogram * * hides: cough title and color * (previous step is also part of the * histogram, so we don't have to hide * that) * shows: all histogram bars * */ function showHistAll() { // ensure the axis to histogram one showAxis(xAxisHist); g.selectAll('.cough') .transition() .duration(0) .attr('opacity', 0); // named transition to ensure // color change is not clobbered g.selectAll('.hist') .transition('color') .duration(500) .style('fill', '#008080'); g.selectAll('.hist') .transition() .duration(1200) .attr('y', function (d) { return yHistScale(d.length); }) .attr('height', function (d) { return height - yHistScale(d.length); }) .style('opacity', 1.0); } /** * showCough * * hides: nothing * (previous and next sections are histograms * so we don't have to hide much here) * shows: histogram * */ function showCough() { // ensure the axis to histogram one showAxis(xAxisHist); g.selectAll('.hist') .transition() .duration(600) .attr('y', function (d) { return yHistScale(d.length); }) .attr('height', function (d) { return height - yHistScale(d.length); }) .style('opacity', 1.0); } /** * showAxis - helper function to * display particular xAxis * * @param axis - the axis to show * (xAxisHist or xAxisBar) */ function showAxis(axis) { g.select('.x.axis') .call(axis) .transition().duration(500) .style('opacity', 1); } /** * hideAxis - helper function * to hide the axis * */ function hideAxis() { g.select('.x.axis') .transition().duration(500) .style('opacity', 0); } /** * UPDATE FUNCTIONS * * These will be called within a section * as the user scrolls through it. * * We use an immediate transition to * update visual elements based on * how far the user has scrolled * */ /** * updateCough - increase/decrease * cough text and color * * @param progress - 0.0 - 1.0 - * how far user has scrolled in section */ function updateCough(progress) { g.selectAll('.cough') .transition() .duration(0) .attr('opacity', progress); g.selectAll('.hist') .transition('cough') .duration(0) .style('fill', function (d) { return (d.x0 >= 14) ? coughColorScale(progress) : '#008080'; }); } /** * DATA FUNCTIONS * * Used to coerce the data into the * formats we need to visualize * */ /** * getWords - maps raw data to * array of data objects. There is * one data object for each word in the speach * data. * * This function converts some attributes into * numbers and adds attributes used in the visualization * * @param rawData - data read in from file */ function getWords(rawData) { return rawData.map(function (d, i) { // is this word a filler word? d.filler = (d.filler === '1') ? true : false; // time in seconds word was spoken d.time = +d.time; // time in minutes word was spoken d.min = Math.floor(d.time / 60); // positioning for square visual // stored here to make it easier // to keep track of. d.col = i % numPerRow; d.x = d.col * (squareSize + squarePad); d.row = Math.floor(i / numPerRow); d.y = d.row * (squareSize + squarePad); return d; }); } /** * getFillerWords - returns array of * only filler words * * @param data - word data from getWords */ function getFillerWords(data) { return data.filter(function (d) {return d.filler; }); } /** * getHistogram - use d3's histogram layout * to generate histogram bins for our word data * * @param data - word data. we use filler words * from getFillerWords */ function getHistogram(data) { // only get words from the first 30 minutes var thirtyMins = data.filter(function (d) { return d.min < 30; }); // bin data into 2 minutes chuncks // from 0 - 31 minutes // @v4 The d3.histogram() produces a significantly different // data structure then the old d3.layout.histogram(). // Take a look at this block: // https://bl.ocks.org/mbostock/3048450 // to inform how you use it. Its different! return d3.histogram() .thresholds(xHistScale.ticks(10)) .value(function (d) { return d.min; })(thirtyMins); } /** * groupByWord - group words together * using nest. Used to get counts for * barcharts. * * @param words */ function groupByWord(words) { return d3.nest() .key(function (d) { return d.word; }) .rollup(function (v) { return v.length; }) .entries(words) .sort(function (a, b) {return b.value - a.value;}); } /** * activate - * * @param index - index of the activated section */ chart.activate = function (index) { activeIndex = index; var sign = (activeIndex - lastIndex) < 0 ? -1 : 1; var scrolledSections = d3.range(lastIndex + sign, activeIndex + sign, sign); scrolledSections.forEach(function (i) { activateFunctions[i](); }); lastIndex = activeIndex; }; /** * update * * @param index * @param progress */ chart.update = function (index, progress) { updateFunctions[index](progress); }; // return chart function return chart; }; /** * display - called once data * has been loaded. * sets up the scroller and * displays the visualization. * * @param data - loaded tsv data */ function display(data) { // create a new plot and // display it var plot = scrollVis(); d3.select('#vis') .datum(data) .call(plot); // setup scroll functionality var scroll = scroller() .container(d3.select('#graphic')); // pass in .step selection as the steps scroll(d3.selectAll('.step')); // setup event handling scroll.on('active', function (index) { // highlight current step text d3.selectAll('.step') .style('opacity', function (d, i) { return i === index ? 1 : 0.1; }); // activate current section plot.activate(index); }); scroll.on('progress', function (index, progress) { plot.update(index, progress); }); } // load data and display d3.tsv('words.tsv', display); </script> </body> </html>
https://d3js.org/d3.v4.min.js