ihmeuw / ScrollyTeller

A JavaScript library to build scrollytelling visualizations from tabular data
https://ihmeuw.github.io/ScrollyTeller/
BSD 3-Clause "New" or "Revised" License
69 stars 9 forks source link

ScrollyTeller

ScrollyTeller is a JavaScript library that dynamically builds the HTML for a scrolling data story from a .csv file, and provides functionality for linking stateful JSON triggers to events dispatched when each scrolling text 'narration' comes into view.

ScrollyTeller image

Check out the ScrollyTeller tutorial, then come back for code examples and the full documentation on this page.


Use cases and functionality

Data stories that have used ScrollyTeller

Documentation

Terminology

Term Description
Narration The scrolling text content that 'narrates' a data story.
Narration Block A group of text and links, contained in a <div>. Actions can be triggered when each narration block comes into view.
Narration Block Contents Each narration block contains: <h2> text (title), <p> text (subtitle), and an <a href=...> (link) with some text. The height of each narration block can also be controlled to change the pacing of the story.
Section A group of narration blocks with a corresponding graph element. A story can be one or multiple sections.
Graph Container A <div> element to hold a title, caption, and a graph, chart, or any other graphic to be triggered.
Graph A <div> element to hold a user defined graph, chart, or any other graphic to be triggered. The graph is entirely user controlled.
Scrollama The underlying JavaScript library used to control the triggering of actions when each narration block comes into view.

Building a scrolling data story using ScrollyTeller

A ScrollyTeller object is created with the constructor ScrollyTeller, which takes a configuration object as argument. To actually set up the story, including loading data, building the HTML, and setting up event listeners, call the asynchronous method render. The pseudo code below shows how a to create a ScrollyTeller instance from a configuration object and then render the HTML. The configuration object is described in much more detail below.
const myAppId = 'myAppId';
const storyConfiguration = {
  /** The id of the <div> that will hold this and all other sections */
  appContainerId: myAppId,
  /** build an array of story sections.
   * Each section object should be a valid section configuration with
   * the properties defined in the next section */
  sectionList: [ 
    {
      sectionIdentifier: 'myExampleSection0',
      /** ... section properties described below */
    },
    {
      sectionIdentifier: 'myExampleSection1',
      /** ... section properties described below */
    },
    {
      sectionIdentifier: 'myExampleSection3',
      /** ... section properties described below */
    ],
    /** optional function to receive the current sectionIdentifier,
     * narrationIndex, narrationId, and narrationClass
     * when narration blocks are entered */
    onNarrationChangedFunction: function ({
      sectionIdentifier,
      narrationIndex,
      narrationId,
      narrationClass,
    }) { console.log('in ', sectionIdentifier, narrationIndex); },
    /** optional parameter to scale scroll elements on mobile devices
        to create slower scrolling */
    mobileScrollHeightMultiplier: 1.5,
  },
};

/** create the ScrollyTeller object to validate the config */
const myScrollyTellerInstance = new ScrollyTeller(storyConfiguration);

/** parse data and build all HMTL */
myScrollyTellerInstance.render();
Each section in the sectionList array must have a unique property sectionIdentifier as well as the properties listed in the table below
Section Property Property function
sectionIdentifier | Unique identifier to distinguish each section. No two sectionIdentifier's should be the same in the ScrollyTeller configuration object
cssNames | Optional: instance of CSSNames class. Can be used to override the default naming of id's and css classes. If left undefined, the default naming will be used.
narration Defines the scrolling narration of the story. The narration can be either of the following 3 options: 1) a URL string with the absolute file path to a file of type 'csv', 'tsv', 'json', 'html', 'txt', 'xml', 2) an array of narration objects, or 3) a promise to return an array of narration objects. SEE DOCUMENTATION BELOW FOR SPECIFICATION OF THE NARRATION TABLE / ARRAY
data Optional: user defined data. Data can be either of the following 4 options: 1) a URL string with the absolute file path to a file of type 'csv', 'tsv', 'json', 'html', 'txt', 'xml', 2) an array of data objects, 3) a promise to return an array of data objects, or 4) undefined
reshapeDataFunction | Optional: Called AFTER data is fetched as reshapeDataFunction(data) This method can be used to filter or reshape data after the datahas been fetched or parsed from a file. It should return the reshaped data, which will overwrite the data proprerty for this section.
buildGraphFunction | Optional: called as buildGraphFunction(graphId, sectionConfig) AFTER the data is fetched and reshaped by reshapeDataFunction. This method should build an instance of the graph and return that instance, which will be stored as a property on this section configuration object within ScrollyTeller. The sectionConfig object will be passed as an arguments to the onScrollFunction and onActivateNarration functions for later access to the data and graph properties.
onActivateNarration | Called when a narration block hits the top of the page, causing it to become active (and classed as graph-scroll-active. See argument list below, this function is called as onActivateNarrationFunction({ index, progress, element, graphContainerId, graphId, sectionConfig, trigger }), and can be used to handle scrolling actions.
onScrollFunction | Called upon scrolling of the section when the section is active. See argument list below, this function is called as onScrollFunction({ index, progress, element, graphContainerId, graphId, sectionConfig, trigger }), and can be used to handle data loading, or graph show-hide actions for a given narration block.
onResizeFunction | Called upon resize of the graph container onResizeFunction({ graphElement, graphId, sectionConfig }), and can be used to resize the chart appropriately when the container is resized.
convertTriggerToObject Optional: Option to parse the JSON trigger for narration steps to an object and update the state object, default is true.
Here's an example of a section configuration that gets added to storyConfiguration
const myAppId = 'myAppId';
const myExampleSection0Name = 'myExampleSection0';
const myExampleSection0 = {
    sectionIdentifier: myExampleSection0Name,
    narration: 'demo_app/exampleSection0/data/narrationSectionChart.csv',
    data: 'demo_app/exampleSection0/data/dataBySeries.csv',
    reshapeDataFunction:
      function processDataFunction(data) { return data; },

    buildGraphFunction:
      function buildChart(graphId, sectionConfig) {
        const myChart = {}; // build your own chart instance
        return myChart; // return it
      },

    onScrollFunction:
      function onScroll({ index, progress, element, trigger, graphContainerId, graphId, sectionConfig }) {
      },

    onActivateNarrationFunction:
      function onActivateNarration({ index, progress, element, trigger, direction, graphContainerId, graphId, sectionConfig }) {
      },

    onResizeFunction:
      function onResize({ graphElement, graphId, graphContainerId, sectionConfig }) {
        sectionConfig.graph.resize(graphElement.offsetWidth, graphElement.offsetHeight);
      },
  };

/** Now add the section configuration to the overall ScrollyTeller config */
const storyConfiguration = {
  appContainerId: myAppId,
  sectionList: [ 
    myExampleSection0,
    /** add another section here... */
  ],
};
Finally, pass the configuration to the ScrollyTeller constructor. The configuration will be validated, and will throw errors if the configuration is not valid.
/** create the ScrollyTeller object to validate the config */
const myScrollyTellerInstance = new ScrollyTeller(storyConfiguration);

/** parse data and build all HMTL (here we're throwing away the result of the Promise, and just calling the method) */
myScrollyTellerInstance.render();
See demo_app/app.js for fully implemented examples that handle scrolling and narration actions. Section configurations are created in demo_app/00_introduction/scrollyTellerConfig.js and demo_app/01_wealthAndHealthOfNations/scrollyTellerConfig.js.

Narration file/object format

See this google spreadsheet for a sample configuration file, which is the same file as: demo_app/00_introduction/data/narration.csv

If you are using a csv or tsv file, format your narration file as follows, keeping the header column names EXACTLY alike (they can be in any order). If narration objects are json, each narration block should have a property named in the same manner as the Column Headers below. Each row represents a unique narration block.
narrationId narrationClass marginTopVh marginBottomVh minHeightVh h2Text paragraphText hRef hRefText trigger graphTitle graphCaption
0 myClass 40 40 100 Some text that will be formatted as <h2> | Some text that will be formatted as <p> www.link.com I'm a link to link.com
{ show_chart: true }
This is a Graph Title This is a graph caption
1 myOtherClass 40 40 200 More Narration... Here's why this chart is important!
{ show_data_1: true }
This is a Graph Title This is a graph caption

Here's a description of what each column header controls:

Column Header/Property Name Effect
narrationId Appended to the id field of each narration block as "narration_ + narrationId". Can be non unique. Provided mainly as a means of distinguishing narration blocks easily.
narrationClass Appended to the class name of each narration block. Can be non unique.
marginTopVh Specifies the size of the margin above any text in each narration block. Units are in viewport height vh, but are converted to pixels px upon load or resize to avoid issues with mobile browsers using a reduced view height.
marginBottomVh Specifies the size the margin below any text in each narration block. Units are in viewport height vh, but are converted to pixels px upon load or resize to avoid issues with mobile browsers using a reduced view height.
minHeightVh Specifies the minimum height of the narration block. Units are in viewport height vh, but are converted to pixels px upon load or resize to avoid issues with mobile browsers using a reduced view height.
h2Text Optional larger text at the top of each narration block. If unspecified, no <h2> text is added to the narration block
paragraphText Optional paragraph text below the h2Text in each narration block. If unspecified, no <p> text is added to the narration block
hRef & hRefText Optional link for each narration block. If either hRef or hRefText is unspecified, no <a> link is added to the narration block
trigger Optional user customizable JSON to help trigger actions such as chart visiblity, styles, data ranges, etc. Must be a valid JSON string with the optional keyword $progress that refers to scroll progress within the current narration block. Invalid JSON strings are indicated by console warnings.
graphTitle Optional User customizable graph title that is placed in the <div class=graph_container> container.
graphCaption Optional User customizable graph caption that is placed in the <div class=graph_container> container.

Auto-scrolling to a specific section or narration block

The method scrollTo on the ScrollyTeller object allows you to auto-scroll to a specific section or narration block. It disabled triggers during scrolling, only firing triggers for the target section or narration block. This functionality can be useful for hooking up with navigation controls. Note that this method is asynchronous, so if you need to script any actions following the scroll, you should await resolution of the Promise it returns.

/**
 * @param {string|number} sectionIdentifier - `sectionIdentifier` of the target section
 * @param {string|number|undefined} [narrationIdStringOrNumericIndex]
 *  - optional: if undefined, defaults to the first narration block of target section
 *              if number, argument is treated as the index of the narration block to scroll to
 *              if string, argument is treated as the `narrationId` of the target narration block
 * @param {object} [options] - optional: configuration object passed to `scrollIntoView`
 *              (https://github.com/KoryNunn/scroll-into-view)
 * @returns {Promise<void>} - returns empty promise
 */
async scrollTo(sectionIdentifier, narrationIdStringOrNumericIndex, options) { ... }

Auto-scrolling to the previous/next narration block

The methods scrollToPreviousNarration/scrollToNextNarration on the ScrollyTeller object allows you to auto-scroll to the previous/next narration blocks. ScrollyTeller automatically adds event listeners to scroll to the previous narration block with keyboard events <ArrowUp> and <ArrowLeft>, and to the next narration block with keyboard events <Space>, <ArrowDown>, and <ArrowRight>, but the asynchronous functions could be used for custom navigation functionality as well.

/**
 * Scrolls "up" to the previous narration block in the story
 * @return {Promise<void>} - returns empty promise
 */
async scrollToPreviousNarration() { ... }

/**
 * Scrolls "down" to the next narration block in the story
 * @return {Promise<void>} - returns empty promise
 */
async scrollToNextNarration() { ... }

Tracking Scroll Progress using Google Analytics

ScrollyTeller can automatically send google analytics events when sections or narration blocks are entered, and when the scrollTo() function is called. To enable these features, add the following booleans to the ScrollyTeller configuration object.

ScrollyTeller requires that the ga() function from google's analytics.js be in the global scope with valid credentials. See these instructions for how to set up google analytics. In the future, we hope to support the new gtag.js library in addition to analytics.js.

const myScrollyTellerConfig = {
  appContainerId: 'myAppId',
  /** section list */
  sectionList: [ /** ... see above for section list properties */ ],

  /** optional google analytics properties */
  sendSectionAnalytics: true, // send analytics events when a section is entered or exited
  sendNarrationAnalytics: true, // send analytics events when each narration block is entered or exited
  sendScrollToAnalytics: true, // send analytics events when scrollTo() is called
  maxTimeInSeconds: 60, // optionally cap the maximum amount of time that can be sent to google (think of this as the maximum user "idle" time
};

// call render methods...

Page Events that ScrollyTeller can send are:

ScrollyTeller config flag (sends events when true) EventCategory EventAction EventLabel EventValue
sendSectionAnalytics Time in Narration (seconds) [sectionIndex]--[sectionIdentifier] Exited [sectionIdentifier], narration #[exitingNarrationIndex] after (EventValue) seconds [seconds a user spent in section]
sendSectionAnalytics Section Entry Time (seconds since page load) [sectionIndex]--[sectionIdentifier] Entered section [enteringSectionId] (EventValue) seconds since page load [seconds since page load section was entered]
sendNarrationAnalytics Time in Narration (seconds) [sectionIndex]--[sectionIdentifier]--[narrationIndex] Exited [section], narration #[exitingNarrationIndex] after (EventValue) seconds [seconds a user spent in a given narration block]
sendNarrationAnalytics Narration Entry Time (seconds since page load) [sectionIndex]--[sectionIdentifier]--[narrationIndex] Entered narration #[enteringNarrationIndex] (EventValue) seconds since page load [seconds since page load narration block was entered]
sendScrollToAnalytics Scroll From-To (SectionIndex---SectionId---NarrationIndex) From [from section]--[narration index of exit] To [to section]--[narration index of entry] [seconds since page load that scrollTo() occurred]
sendScrollToAnalytics Scroll To-From (SectionIndex---SectionId---NarrationIndex) To [to section]--[narration index of entry] From [from section]--[narration index of exit] [seconds since page load that scrollTo() occurred]

All of the times are reported as PageEvents in seconds, and can be given an upper bound by specifying maxTimeInSeconds, which defaults to Infinity.


Sample implementations of reshapeDataFunction(), buildGraphFunction(), onActivateNavigationFunction(), and onScrollFunction()

reshapeDataFunction()

buildGraphFunction()

onActivateNarrationFunction()

onScrollFunction

onResizeFunction