npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

storycurve

v1.0.1

Published

Visualization method for comparing two temporal orders with additional metadata

Readme

Story Curve

  • A technique for visualizing nonlinear narratives
  • Demo: http://storycurve.namwkim.org/
  • If you have any quesiton, please feel free to contact: Nam Wook Kim, [email protected]

Story Explorer

Story Explorer

Story explorer is a system built using story curves to visually explore narrative structures of popular nonlinear films like Memento and Pulp Fiction. Try it.

Install

Dependencies

<link rel="stylesheet" type="text/css" href="https://raw.githubusercontent.com/namwkim/storycurve/master/dist/storycurve.css">
<script src="https://raw.githubusercontent.com/namwkim/storycurve/master/dist/storycurve.js"></script>
  • Or, install using npm
npm install storycurve --save

Data

A story curve is designed to visualize any data that involves comparison of two orders for the same set of elelemts (e.g., story order vs narrative order of same events). An example of movie data we use in the demo is below:

{
 "movie_info": {
 ...
 },
 "script_info":{
   "scenes":[
     {
       "narrative_order":0,
       "story_order": 29,
       "scene_metadata":{
         "size":6288,
         "location": "Coffee Shop",
         "time":"Morning",
         ...
       },
       "characters":[
         "Young Man",
         "Young Woman",
         ...
       ]
       ...
     }
   ]
 }
}

You can take a look at the Pulp Fiction data using online json viewer.

Using Custom Data Format

You can use accessor functions in order to use your own custom data format. For example, if you have a dataset like the following,

[
 {
   x:0,
   y:29,
   size:6288,
   metadata1:"Coffee Shop",
   metadata2:"Morning",
   children:[
     "Young Man",
     "Young Woman"
   ]
 }
 ... 
]

and using accessors as below, the story curve will be able to find appropriate data attributes to visualize.

 var vis = new storycurve('#pulp_fiction');
 vis.x(d=>d.x)
   .y(d=>d.y)
   .size(d=>d.size)
   .band(d=>d.metadata1)
   .backdrop(d=>d.metadata2)
   .children(d=>d.children);
   

How to Use

Here, we will demonstrate how we used the Pulp Fiction data to create its story curve. You can find more example codes that were used in the demo: here.

<div id="pulp_fiction" class="storycurve"></div>
fetch('datasets/pulp_fiction_simple.json').then(function(response){
  response.json().then(function(data){
  
    var vis = new storycurve('#pulp_fiction');
    
    // ranking and extracting top elements
    var characters = rankCharacterByVerbosity(data.script_info, 8);
    var locations = rankMetadataBySceneSize(data.script_info, 'location', 4);
    var times = rankMetadataBySceneSize(data.script_info, 'time', 2);
    
    // set domain for color scale, otherwise story curve will display all not just top ones.
    vis.bandColorScale().domain(locations);
    vis.backdropColorScale().domain(times);
    vis.childColorScale().domain(characters);
      
    // use a custom format and setting display options
    vis.tooltipFormat(tooltipFormat.bind(vis))
          .showBackdrop(true)
          .showBand(true)
          .showChildren(true)
          .zoomEnabled(false)
          .draw(data.script_info.scenes);
  }
}

The output of the code is the figure below. You can find the definitions of the functions (e.g., rankCharacterByVerbosity) used in the code above at the bottom of this file.

Pulp Fiction code outcome

API Reference

# vis.x([accessor])

Sets an accessor function for the horizontal position of a data point. If no accessor is provided, returns the current accessor. The default accessor is as below:

vis.x(d=>d.narrative_order);

# vis.y([accessor])

Sets an accessor function for the vertical position of a data point. If no accessor is provided, returns the current accessor. The default accessor is as below:

vis.x(d=>d.story_order);

# vis.size([accessor])

Sets an accessor function for the size of a data point. If no accessor is provided, returns the current accessor. The default accessor is as below:

vis.size(d=>d.scene_metadata.size);

# vis.children([accessor])

Sets an accessor function for children of a data point. Child elements are vertically stacked with different colors. If no accessor is provided, returns the current accessor. The default accessor is as below:

vis.children(d=>d.characters);

# vis.child([accessor])

Sets an accessor function for a child in the children list. That is, if the child object contains multiple attributes, it needs to be specified what attribute is used as a label for the child. If no accessor is provided, returns the current accessor. The default accessor is as below:

vis.child(child=>child);

# vis.band([accessor])

Sets an accessor function for a metadata for a data point which is visualized as a surrounding band. If the categories of the metadata are too many, it is desirable to filter them in advance as they can overload the visualization. If no accessor is provided, returns the current accessor. The default accessor is as below:

vis.band(d=>d.scene_metadata.location);

# vis.backdrop([accessor])

Sets an accessor function for a metadata for a data point which is visualized as a backdrop. If the categories of the metadata are too many, it is desirable to filter them in advance as they can overload the visualization. If no accessor is provided, returns the current accessor. The default accessor is as below:

vis.band(d=>d.scene_metadata.time);

# vis.showBand([boolean])

Determins whether to show bands or not. If a boolean is not specified, returns the current setting. The default is false:

vis.showBand(false);

# vis.showBackdrop([boolean])

Determins whether to show backdrops or not. If a boolean is not specified, returns the current setting. The default is false:

vis.showBackdrop(false);

# vis.showChildren([boolean])

Determins whether to show children or not. If not true, shows a single visual mark per column. If a boolean is not specified, returns the current setting. The default is false:

vis.showChildren(false);

# vis.zoomEnabled([boolean])

Sets whether to enable zooming and panning. If a boolean is not specified, returns the current setting. The default is true:

vis.zoomEnabled(true);

# vis.yaxisTitle([string])

Sets y-axis title. If a string is not specified, returns the current title. The default is '← Story order'':

vis.yaxisTitle('← Story order');

# vis.xaxisTitle([string])

Sets x-axis title. If a string is not specified, returns the current title. The default is 'Narrative order →'':

vis.xaxisTitle('Narrative order →');

# vis.tooltipFormat([format])

Sets the format of the tooptip. A data point corresponding to the mouse location is supplied to the callback. The default is just printing x and y orders:

vis.tooltipFormat(d=>{
  let content = '<table>';
  content += ('<tr><td><span style="color:#FBBD08">(X,Y)</span></td><td>&nbsp; ' + d.xo + ', ' + d.yo + '</td></tr>');
  content += '</table>';
  return content;
});

The tooltip formatting function used in the demo is below:

function tooltipFormat(d){
  var childColor = this.childColorScale();

  var scene = d.orgData;

  var content = '<p>';
  content += '<strong style="color:#757575">N, S = ' + d.xo + ', ' + d.yo + '</strong><br>';

  scene.characters.map(function(c){
    content += ('<strong style="color:'+ childColor(c)+'">'+c+'</strong><br>');
  });
  var loc = scene.scene_metadata.location;
  if (loc){
    content += ('<strong style="color:#9E9E9E">'+loc+'</strong><br>');
  }

  var time = scene.scene_metadata.time;
  if (time){
    content += ('<strong style="color:#9E9E9E">'+time+'</strong><br>');
  }
  content += '</p>';

  return content;

# vis.bandColorScale([categorical_scale])

Sets and gets the color scale for band categorical metadata. It is a categorical color scale. The default palette is ['#eedaf1','#fad1df','#cfe8fc','#daddf1'], which means you need to set the domain for the 4 colors if showBand is set true. You need to filter data beforehand by setting unused categories to null (e.g., setting all but top 4 to null). Otherwise, unknown category is assigned no color (transparent).

var locations = rankMetadataBySceneSize(data.script_info, 'location', 4); //get top 4 locations
vis.backdropColorScale.domain(locations);

If you want to support more than 4 cateogories, you change the range of the scale:

var locations = rankMetadataBySceneSize(data.script_info, 'location', 5); //get top 5 locations
vis.backdropColorScale.domain(locations);
vis.backdropColorScale().range(['#d7191c', '#fdae61', '#ffffbf', '#abd9e9', '#2c7bb6']);

# vis.backdropColorScale([categorical_scale])

Sets and gets the color scale for backdrop categorical metadata. It is a categorical color scale. The default palette is ['#CFD8DC', '#90A4AE', '#607D8B'], which means you need to set the domain for the 8 colors if showBackdrop is set true. You need to filter data beforehand by setting unused categories to null (e.g., setting all but top 3 to null). Otherwise, unknown category is assigned no color (transparent). You can modify the scale as you want by changing the domain and range of the scale similar to bandColorScale.

# vis.childColorScale([categorical_scale])

Similar to other color scales, it allows you to modify the color scale of children. The default is ['#db2828','#f2711c','#fbbd08','#b5cc18','#21ba45','#00b5ad','#2185d0','#6435c9'] which means you need to set the domain for the 8 colors if showChildren is set true. You need to filter data beforehand by setting them to null (e.g., setting all but top 8 to null). Otherwise, unknown category is assigned '#9E9E9E'. You can modify the scale as you want by changing the domain and range of the scale similar to bandColorScale.

# vis.highlights([highlights])

Immediately highlights the elements specified. It is used with isHighlighted, which checks if a data point needs to be highlighted. This is used in Story Explorer, when a user selectively highlights characters or locations, etc. An example of the input can be as below:

vis.highlights([
  {
    type:'child',
    filter:'Jules'
    
  },
  {
    type: 'band',
    filter: 'Morning'
   }
])

This filters a child named 'Jules' and a band named 'Morning'. You need to set an appropriate function for isHighlighted as well.

# vis.isHighlighted([checker])

It is used in conjuntion with highlights. checker receives three arguments target, d, highlights:

let highlightAll = function(target, d, highlights){
  return target.data==null?false:(highlights.length==0? true:
    highlights.some(h=>target.data==h.filter));
}

The above function highlights all visual marks that match highlights. checker is called for each child, band, and backdrop. target contains type to indicate the caller, i.e., 'child', 'band' or 'backdrop'. It also contains data that contains a corresponding value, e.g., 'Jules'. highlights is the input you specify in highlights.

For your information, the definition of highlights is below:

highlights(_) {
  if (!arguments.length) return this._highlights;
  this._highlights = _;

  //highlight marks
  this.container.selectAll('.scene-group')
    .select('.band')
    .style('fill-opacity',
      d => this._isHighlighted({
          type: 'band',
          data: this._band(d.orgData)
        },
        d.orgData, this._highlights) ? 1.0 : 0.0);

  this.container.selectAll('.scene-group')
    .select('.backdrop')
    .style('fill-opacity',
      d => this._isHighlighted({
          type: 'backdrop',
          data: this._backdrop(d.orgData)
        },
        d.orgData, this._highlights) ? 0.25 : 0.0);

  this.container.selectAll('.scene-group')
    .select('.children')
    .selectAll('.mark')
    .style('fill-opacity',
      d => this._isHighlighted({
          type: 'child',
          data: d.orgData
        },
        d.parentOrgDdata, this._highlights) ? 1.0 : 0.05);

  return this;
}

This function will immediately highlight highlights, meaning that draw does not need to be called.

You can use the following function to highlight co-occurrence.

let highlightCooccur = function(target, d, highlights){
  return target.data==null?false:(highlights.length==0? true:
    highlights.every(h=>
      h.type=='characters'?d[h.type].includes(h.filter):
        d.scene_metadata[h.type]==h.filter));
};

For a use case, please refer to the code of Story Explorer (link).

# vis.width([accessor])

Sets or gets the width of the visualization. When a new width is set, draw needs to be called again.

# vis.height([accessor])

Sets or gets the height of the visualization. When a new height is set, draw needs to be called again.

# vis.draw([data])

Draw or update a story curve with data. If any settings are updated, this function needs to be called with the same data.

# vis.on([name, listener])

Sets or gets a listner for events occurring on the story curve. Supported events include 'zoom', 'mouseover', 'mouseout', 'click' (i.e., name argument). listener for 'mouseover', 'mouseout', and 'click' events takes data, index, nodes as arguments, while 'zoom' receives transform so that you can coordinate with other visualizations.

let onZoom = function(transform){
  othervis.forEach(vis=>vis.transform('transform', transform));
};
vis.on('zoom', onZoom);

# vis.transform([op, param])

Reveals transform functions in d3-zoom. op can be any method of d3.zoom() such as 'scaleBy' or 'translateBy' and param is the parameters of the operator. For example, if you want to manually scale a story curve with a zoom factor 1.2:

vis.transform('scaleBy', 1.2);