├── .gitignore ├── BasicLateOnsetSort.java ├── BelievableDataSource.java ├── COPYRIGHT ├── CSVDataSource.java ├── ColorPicker.java ├── DataSource.java ├── InverseVolatilitySort.java ├── LastFMColorPicker.java ├── LateOnsetDataSource.java ├── LateOnsetSort.java ├── Layer.java ├── LayerLayout.java ├── LayerSort.java ├── MinimizedWiggleLayout.java ├── NoLayerSort.java ├── OnsetComparator.java ├── README ├── RandomColorPicker.java ├── StackLayout.java ├── StreamLayout.java ├── ThemeRiverLayout.java ├── VolatilityComparator.java ├── VolatilitySort.java ├── data ├── csvdata.csv ├── layers-nyt.jpg └── layers.jpg ├── images ├── example-00-standard.png ├── example-01-themeriver.png ├── example-02-layout.png ├── example-03-color.png ├── example-04-ordering.png ├── example-05-nytcoloring.png ├── example2-00-presort.png └── example2-01-postsort.png └── streamgraph_generator.pde /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /BasicLateOnsetSort.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | 3 | /** 4 | * BasicLateOnsetSort 5 | * Sorts by onset, but does not partition to the outsides of the graph in 6 | * order to illustrate short-sighted errors found during design process. 7 | * 8 | * @author Lee Byron 9 | * @author Martin Wattenberg 10 | */ 11 | public class BasicLateOnsetSort extends LayerSort { 12 | 13 | public String getName() { 14 | return "Late Onset Sorting, Top to Bottom"; 15 | } 16 | 17 | public Layer[] sort(Layer[] layers) { 18 | // first sort by onset 19 | Arrays.sort(layers, new OnsetComparator(true)); 20 | 21 | return layers; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /BelievableDataSource.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | 3 | /** 4 | * BelievableDataSource 5 | * Create test data for layout engine. 6 | * 7 | * @author Lee Byron 8 | * @author Martin Wattenberg 9 | */ 10 | public class BelievableDataSource implements DataSource { 11 | 12 | public Random rnd; 13 | 14 | public BelievableDataSource() { 15 | // seeded, so we can reproduce results 16 | this(2); 17 | } 18 | 19 | public BelievableDataSource(int seed) { 20 | rnd = new Random(seed); 21 | } 22 | 23 | public Layer[] make(int numLayers, int sizeArrayLength) { 24 | Layer[] layers = new Layer[numLayers]; 25 | 26 | for (int i = 0; i < numLayers; i++) { 27 | String name = "Layer #" + i; 28 | float[] size = new float[sizeArrayLength]; 29 | size = makeRandomArray(sizeArrayLength); 30 | layers[i] = new Layer(name, size); 31 | } 32 | 33 | return layers; 34 | } 35 | 36 | protected float[] makeRandomArray(int n) { 37 | float[] x = new float[n]; 38 | 39 | // add a handful of random bumps 40 | for (int i=0; i<5; i++) { 41 | addRandomBump(x); 42 | } 43 | 44 | return x; 45 | } 46 | 47 | protected void addRandomBump(float[] x) { 48 | float height = 1 / rnd.nextFloat(); 49 | float cx = (float)(2 * rnd.nextFloat() - 0.5); 50 | float r = rnd.nextFloat() / 10; 51 | 52 | for (int i = 0; i < x.length; i++) { 53 | float a = (i / (float)x.length - cx) / r; 54 | x[i] += height * Math.exp(-a * a); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008, Lee Byron, Martin Wattenberg 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * The names of the contributors may NOT be used to endorse or promote products 12 | derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL LEE BYRON BE LIABLE FOR ANY 18 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /CSVDataSource.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | import processing.core.*; 3 | 4 | /** 5 | * CSVDataSource 6 | * Read data from a CSV file. 7 | * Each Layer corresponds to one line, with the first entry in the line the name of the layer. 8 | * Assumes every line is the same length. 9 | * Ignores the parameters given to make. 10 | * 11 | * @author Albert Sun 12 | * @author Lee Byron 13 | * @author Martin Wattenberg 14 | */ 15 | public class CSVDataSource implements DataSource { 16 | 17 | public String[] data; 18 | 19 | public CSVDataSource(PApplet parent, String filename) { 20 | data = parent.loadStrings(filename); 21 | } 22 | 23 | public Layer[] make(int a, int b) { 24 | int numLayers = data.length; 25 | Layer[] layers = new Layer[numLayers]; 26 | 27 | for (int i = 0; i < numLayers; i++) { 28 | String[] fields = data[i].split(","); 29 | int sizeArrayLength = fields.length - 1; 30 | String name = fields[0]; 31 | size = makeDataArray(fields); 32 | layers[i] = new Layer(name, size); 33 | } 34 | 35 | return layers; 36 | } 37 | 38 | protected float[] makeDataArray(String[] a) { 39 | float[] x = new float[a.length - 1]; 40 | for (int i = 1; i < a.length; i++) { 41 | x[i-1] = Float.valueOf(a[i].trim()).floatValue(); 42 | } 43 | return x; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /ColorPicker.java: -------------------------------------------------------------------------------- 1 | /** 2 | * ColorPicker 3 | * Interface for new coloring algorithms. 4 | * 5 | * @author Lee Byron 6 | * @author Martin Wattenberg 7 | */ 8 | public interface ColorPicker { 9 | 10 | public void colorize(Layer[] layers); 11 | 12 | public String getName(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /DataSource.java: -------------------------------------------------------------------------------- 1 | /** 2 | * DataSource 3 | * Interface for creating a data source 4 | * 5 | * @author Lee Byron 6 | * @author Martin Wattenberg 7 | */ 8 | public interface DataSource { 9 | 10 | public Layer[] make(int numLayers, int sizeArrayLength); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /InverseVolatilitySort.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | 3 | /** 4 | * InverseVolatilitySort 5 | * Sorts an array of layers by their volatility, placing the most volatile 6 | * layers along the insides of the graph, illustrating how disruptive this 7 | * volatility can be to a stacked graph. 8 | * 9 | * @author Lee Byron 10 | * @author Martin Wattenberg 11 | */ 12 | public class InverseVolatilitySort extends LayerSort { 13 | 14 | public String getName() { 15 | return "Inverse Volatility Sorting, Evenly Weighted"; 16 | } 17 | 18 | public Layer[] sort(Layer[] layers) { 19 | // first sort by volatility 20 | Arrays.sort(layers, new VolatilityComparator(false)); 21 | 22 | return orderToOutside(layers); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /LastFMColorPicker.java: -------------------------------------------------------------------------------- 1 | import processing.core.*; 2 | 3 | /** 4 | * LastFMColorPicker 5 | * Loads in an image and uses it as a two-dimensional gradient 6 | * Supply two [0,1) numbers and get the color of the gradient at that point 7 | * 8 | * @author Lee Byron 9 | * @author Martin Wattenberg 10 | */ 11 | public class LastFMColorPicker implements ColorPicker { 12 | 13 | public PImage source; 14 | 15 | public LastFMColorPicker(PApplet parent, String src) { 16 | source = parent.loadImage(src); 17 | } 18 | 19 | public String getName() { 20 | return "Listening History Color Scheme"; 21 | } 22 | 23 | public void colorize(Layer[] layers) { 24 | // find the largest layer to use as a normalizer 25 | float maxSum = 0; 26 | for (int i=0; i 0) { 46 | if (onset == -1) { 47 | onset = i; 48 | } else { 49 | end = i; 50 | } 51 | } 52 | 53 | // volatility is the maximum change between any two consecutive points 54 | if (i > 0) { 55 | volatility = Math.max( 56 | volatility, 57 | Math.abs(size[i] - size[i-1]) 58 | ); 59 | } 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /LayerLayout.java: -------------------------------------------------------------------------------- 1 | /** 2 | * LayerLayout 3 | * Abstract Class for new stacked graph layout algorithms 4 | * 5 | * Note: you do not need to worry about scaling to screen dimensions. 6 | * The display applet will do that automatically for you. 7 | * 8 | * @author Lee Byron 9 | * @author Martin Wattenberg 10 | */ 11 | public abstract class LayerLayout { 12 | 13 | abstract void layout(Layer[] layers); 14 | 15 | abstract String getName(); 16 | 17 | /** 18 | * We define our stacked graphs by layers atop a baseline. 19 | * This method does the work of assigning the positions of each layer in an 20 | * ordered array of layers based on an initial baseline. 21 | */ 22 | protected void stackOnBaseline(Layer[] layers, float[] baseline) { 23 | // Put layers on top of the baseline. 24 | for (int i = 0; i < layers.length; i++) { 25 | System.arraycopy(baseline, 0, layers[i].yBottom, 0, baseline.length); 26 | for (int j = 0; j < baseline.length; j++) { 27 | baseline[j] -= layers[i].size[j]; 28 | } 29 | System.arraycopy(baseline, 0, layers[i].yTop, 0, baseline.length); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /LayerSort.java: -------------------------------------------------------------------------------- 1 | /** 2 | * LayerSort 3 | * Interface to sorting layers 4 | * 5 | * @author Lee Byron 6 | * @author Martin Wattenberg 7 | */ 8 | public abstract class LayerSort { 9 | 10 | abstract String getName(); 11 | 12 | abstract Layer[] sort(Layer[] layers); 13 | 14 | /** 15 | * Creates a 'top' and 'bottom' collection. 16 | * Iterating through the previously sorted list of layers, place each layer 17 | * in whichever collection has less total mass, arriving at an evenly 18 | * weighted graph. Reassemble such that the layers that appeared earliest 19 | * end up in the 'center' of the graph. 20 | */ 21 | protected Layer[] orderToOutside(Layer[] layers) { 22 | int j = 0; 23 | int n = layers.length; 24 | Layer[] newLayers = new Layer[n]; 25 | int topCount = 0; 26 | float topSum = 0; 27 | int[] topList = new int[n]; 28 | int botCount = 0; 29 | float botSum = 0; 30 | int[] botList = new int[n]; 31 | 32 | // partition to top or bottom containers 33 | for (int i=0; i= 0; i--) { 45 | newLayers[j++] = layers[botList[i]]; 46 | } 47 | for (int i = 0; i < topCount; i++) { 48 | newLayers[j++] = layers[topList[i]]; 49 | } 50 | 51 | return newLayers; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /MinimizedWiggleLayout.java: -------------------------------------------------------------------------------- 1 | /** 2 | * MinimizedWiggleLayout 3 | * Minimizes the sum of squares of the layer slopes at each value 4 | * 5 | * We present this as a reasonable alternative to the Stream Graph for 6 | * real-time use. While it has some drawbacks compared to StreamLayout, it is 7 | * much faster to execute and is reasonable for real-time applications. 8 | * 9 | * @author Lee Byron 10 | * @author Martin Wattenberg 11 | */ 12 | public class MinimizedWiggleLayout extends LayerLayout { 13 | 14 | public String getName() { 15 | return "Minimized Wiggle Layout"; 16 | } 17 | 18 | public void layout(Layer[] layers) { 19 | int n = layers[0].size.length; 20 | int m = layers.length; 21 | float[] baseline = new float[n]; 22 | 23 | // Set shape of baseline values. 24 | for (int i = 0; i < n; i++) { 25 | for (int j = 0; j < m; j++) { 26 | baseline[i] += (m - j - 0.5) * layers[j].size[i]; 27 | } 28 | baseline[i] /= m; 29 | } 30 | 31 | // Put layers on top of the baseline. 32 | stackOnBaseline(layers, baseline); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /NoLayerSort.java: -------------------------------------------------------------------------------- 1 | /** 2 | * NoLayerSort 3 | * Does no sorting. Identity function. 4 | * 5 | * @author Lee Byron 6 | * @author Martin Wattenberg 7 | */ 8 | public class NoLayerSort extends LayerSort { 9 | 10 | public String getName() { 11 | return "No Sorting"; 12 | } 13 | 14 | public Layer[] sort(Layer[] layers) { 15 | return layers; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /OnsetComparator.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | 3 | /** 4 | * OnsetSort 5 | * Compares two Layers based on their onset 6 | * 7 | * @author Lee Byron 8 | * @author Martin Wattenberg 9 | */ 10 | public class OnsetComparator implements Comparator { 11 | 12 | public boolean ascending; 13 | 14 | public OnsetComparator(boolean ascending) { 15 | this.ascending = ascending; 16 | } 17 | 18 | public int compare(Object p, Object q){ 19 | Layer pL = (Layer)p; 20 | Layer qL = (Layer)q; 21 | return (ascending ? 1 : -1) * (pL.onset - qL.onset); 22 | } 23 | 24 | public boolean equals(Object p, Object q){ 25 | Layer pL = (Layer)p; 26 | Layer qL = (Layer)q; 27 | return pL.onset == qL.onset; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This is the processing application used to generate the images in the paper: 2 | Stacked Graphs - Geometry & Aesthetics 3 | 4 | It is published here as an educational library, to provide code examples to support the paper. 5 | 6 | This code is copyright under the BSD license. 7 | -------------------------------------------------------------------------------- /RandomColorPicker.java: -------------------------------------------------------------------------------- 1 | import processing.core.*; 2 | import java.util.*; 3 | 4 | /** 5 | * RandomColorPicker 6 | * Chooses random colors within an acceptable HSB color spectrum 7 | * 8 | * @author Lee Byron 9 | * @author Martin Wattenberg 10 | */ 11 | public class RandomColorPicker implements ColorPicker { 12 | 13 | public Random rnd; 14 | public PApplet parent; 15 | 16 | public RandomColorPicker(PApplet parent) { 17 | this(parent, 2); 18 | } 19 | 20 | public RandomColorPicker(PApplet parent, int seed) { 21 | this.parent = parent; 22 | parent.colorMode(PApplet.RGB, 255); 23 | 24 | // seeded, so we can reproduce results 25 | rnd = new Random(seed); 26 | } 27 | 28 | public String getName() { 29 | return "Random Colors"; 30 | } 31 | 32 | public void colorize(Layer[] layers) { 33 | for (int i = 0; i < layers.length; i++) { 34 | float h = PApplet.lerp(0.6f, 0.65f, rnd.nextFloat()); 35 | float s = PApplet.lerp(0.2f, 0.25f, rnd.nextFloat()); 36 | float b = PApplet.lerp(0.4f, 0.95f, rnd.nextFloat()); 37 | 38 | layers[i].rgb = hsb2rgb(h, s, b); 39 | } 40 | } 41 | 42 | protected int hsb2rgb(float x, float y, float z) { 43 | float calcR = 0; 44 | float calcG = 0; 45 | float calcB = 0; 46 | float calcA = 1; 47 | 48 | if (y == 0) { // saturation == 0 49 | calcR = calcG = calcB = z; 50 | } else { 51 | float which = (x - (int)x) * 6.0f; 52 | float f = which - (int)which; 53 | float p = z * (1.0f - y); 54 | float q = z * (1.0f - y * f); 55 | float t = z * (1.0f - (y * (1.0f - f))); 56 | 57 | switch ((int)which) { 58 | case 0: 59 | calcR = z; 60 | calcG = t; 61 | calcB = p; 62 | break; 63 | case 1: 64 | calcR = q; 65 | calcG = z; 66 | calcB = p; 67 | break; 68 | case 2: 69 | calcR = p; 70 | calcG = z; 71 | calcB = t; 72 | break; 73 | case 3: 74 | calcR = p; 75 | calcG = q; 76 | calcB = z; 77 | break; 78 | case 4: 79 | calcR = t; 80 | calcG = p; 81 | calcB = z; 82 | break; 83 | case 5: 84 | calcR = z; 85 | calcG = p; 86 | calcB = q; 87 | break; 88 | } 89 | } 90 | 91 | int calcRi = (int)(255 * calcR); 92 | int calcGi = (int)(255 * calcG); 93 | int calcBi = (int)(255 * calcB); 94 | int calcAi = (int)(255 * calcA); 95 | int calcColor = (calcAi << 24) | (calcRi << 16) | (calcGi << 8) | calcBi; 96 | 97 | return calcColor; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /StackLayout.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | 3 | /** 4 | * StackLayout 5 | * Standard stacked graph layout, with a straight baseline 6 | * 7 | * @author Lee Byron 8 | * @author Martin Wattenberg 9 | */ 10 | public class StackLayout extends LayerLayout { 11 | 12 | public String getName() { 13 | return "Stacked Layout"; 14 | } 15 | 16 | public void layout(Layer[] layers) { 17 | int n = layers[0].size.length; 18 | 19 | // lay out layers, top to bottom. 20 | float[] baseline = new float[n]; 21 | Arrays.fill(baseline, 0); 22 | 23 | // Put layers on top of the baseline. 24 | stackOnBaseline(layers, baseline); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /StreamLayout.java: -------------------------------------------------------------------------------- 1 | /** 2 | * StreamLayout 3 | * The layout used in the Streamgraph stacked graph 4 | * 5 | * Because this layout is using numeric integration, it is likely insufficient 6 | * for real-time display, especially for larger data sets. 7 | * 8 | * @author Lee Byron 9 | * @author Martin Wattenberg 10 | */ 11 | public class StreamLayout extends LayerLayout { 12 | 13 | public String getName() { 14 | return "Original Streamgraph Layout"; 15 | } 16 | 17 | public void layout(Layer[] layers) { 18 | int n = layers[0].size.length; 19 | int m = layers.length; 20 | float[] baseline = new float[n]; 21 | float[] center = new float[n]; 22 | float totalSize; 23 | float moveUp; 24 | float increase; 25 | float belowSize; 26 | 27 | // Set shape of baseline values. 28 | for (int i = 0; i < n; i++) { 29 | // the 'center' is a rolling point. It is initialized as the previous 30 | // iteration's center value 31 | center[i] = i == 0 ? 0 : center[i-1]; 32 | 33 | // find the total size of all layers at this point 34 | totalSize = 0; 35 | for (int j = 0; j < m; j++) { 36 | totalSize += layers[j].size[i]; 37 | } 38 | 39 | // account for the change of every layer to offset the center point 40 | for (int j = 0; j < m; j++) { 41 | if (i == 0) { 42 | increase = layers[j].size[i]; 43 | moveUp = 0.5f; 44 | } else { 45 | belowSize = 0.5f * layers[j].size[i]; 46 | for (int k = j + 1; k < m; k++) { 47 | belowSize += layers[k].size[i]; 48 | } 49 | increase = layers[j].size[i] - layers[j].size[i - 1]; 50 | moveUp = totalSize == 0 ? 0 : (belowSize / totalSize); 51 | } 52 | center[i] += (moveUp - 0.5) * increase; 53 | } 54 | 55 | // set baseline to the bottom edge according to the center line 56 | baseline[i] = center[i] + 0.5f * totalSize; 57 | } 58 | 59 | // Put layers on top of the baseline. 60 | stackOnBaseline(layers, baseline); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /ThemeRiverLayout.java: -------------------------------------------------------------------------------- 1 | /** 2 | * ThemeRiverLayout 3 | * Layout used by the authors of the ThemeRiver paper 4 | * 5 | * @author Lee Byron 6 | * @author Martin Wattenberg 7 | */ 8 | public class ThemeRiverLayout extends LayerLayout { 9 | 10 | public String getName() { 11 | return "ThemeRiver"; 12 | } 13 | 14 | public void layout(Layer[] layers) { 15 | // Set shape of baseline values. 16 | int n=layers[0].size.length; 17 | int m=layers.length; 18 | float[] baseline = new float[n]; 19 | 20 | // ThemeRiver is perfectly symmetrical 21 | // the baseline is 1/2 of the total height at any point 22 | for (int i = 0; i < n; i++) { 23 | baseline[i] = 0; 24 | for (int j = 0; j < m; j++) { 25 | baseline[i] += layers[j].size[i]; 26 | } 27 | baseline[i] *= 0.5; 28 | } 29 | 30 | // Put layers on top of the baseline. 31 | stackOnBaseline(layers, baseline); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /VolatilityComparator.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | 3 | /** 4 | * VolatilityComparator 5 | * Compares two Layers based on their volatility 6 | * 7 | * @author Lee Byron 8 | * @author Martin Wattenberg 9 | */ 10 | public class VolatilityComparator implements Comparator { 11 | 12 | public boolean ascending; 13 | 14 | public VolatilityComparator(boolean ascending) { 15 | this.ascending = ascending; 16 | } 17 | 18 | public int compare(Object p, Object q) { 19 | Layer pL = (Layer)p; 20 | Layer qL = (Layer)q; 21 | float volatilityDifference = pL.volatility - qL.volatility; 22 | return (ascending ? 1 : -1) * (int)(10000000 * volatilityDifference); 23 | } 24 | 25 | public boolean equals(Object p, Object q) { 26 | Layer pL = (Layer)p; 27 | Layer qL = (Layer)q; 28 | return pL.volatility == qL.volatility; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /VolatilitySort.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | 3 | /** 4 | * VolatilitySort 5 | * Sorts an array of layers by their volatility, placing the most volatile 6 | * layers along the outsides of the graph, thus minimizing unneccessary 7 | * distortion. 8 | * 9 | * First sort by volatility, then creates a 'top' and 'bottom' collection. 10 | * Iterating through the sorted list of layers, place each layer in whichever 11 | * collection has less total mass, arriving at an evenly weighted graph. 12 | * 13 | * @author Lee Byron 14 | * @author Martin Wattenberg 15 | */ 16 | public class VolatilitySort extends LayerSort { 17 | 18 | public String getName() { 19 | return "Volatility Sorting, Evenly Weighted"; 20 | } 21 | 22 | public Layer[] sort(Layer[] layers) { 23 | // first sort by volatility 24 | Arrays.sort(layers, new VolatilityComparator(true)); 25 | 26 | return orderToOutside(layers); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /data/csvdata.csv: -------------------------------------------------------------------------------- 1 | joe,1,10,12,4,5,22,1 2 | jim,2,10,2,4,15,12,7 3 | jeff,1,10,12,4,5,22,1 4 | asd,10,10,12,4,5,22,1 5 | jsfgheff,1,2,12,4,27,22,1 6 | jeyuituyff,0,30,12,4,5,22,1 7 | jefhff,0,10,12,4,5,22,100 -------------------------------------------------------------------------------- /data/layers-nyt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leebyron/streamgraph/e7370a65f95dc50eb190c1cef7ae4d89e42faa5c/data/layers-nyt.jpg -------------------------------------------------------------------------------- /data/layers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leebyron/streamgraph/e7370a65f95dc50eb190c1cef7ae4d89e42faa5c/data/layers.jpg -------------------------------------------------------------------------------- /images/example-00-standard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leebyron/streamgraph/e7370a65f95dc50eb190c1cef7ae4d89e42faa5c/images/example-00-standard.png -------------------------------------------------------------------------------- /images/example-01-themeriver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leebyron/streamgraph/e7370a65f95dc50eb190c1cef7ae4d89e42faa5c/images/example-01-themeriver.png -------------------------------------------------------------------------------- /images/example-02-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leebyron/streamgraph/e7370a65f95dc50eb190c1cef7ae4d89e42faa5c/images/example-02-layout.png -------------------------------------------------------------------------------- /images/example-03-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leebyron/streamgraph/e7370a65f95dc50eb190c1cef7ae4d89e42faa5c/images/example-03-color.png -------------------------------------------------------------------------------- /images/example-04-ordering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leebyron/streamgraph/e7370a65f95dc50eb190c1cef7ae4d89e42faa5c/images/example-04-ordering.png -------------------------------------------------------------------------------- /images/example-05-nytcoloring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leebyron/streamgraph/e7370a65f95dc50eb190c1cef7ae4d89e42faa5c/images/example-05-nytcoloring.png -------------------------------------------------------------------------------- /images/example2-00-presort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leebyron/streamgraph/e7370a65f95dc50eb190c1cef7ae4d89e42faa5c/images/example2-00-presort.png -------------------------------------------------------------------------------- /images/example2-01-postsort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leebyron/streamgraph/e7370a65f95dc50eb190c1cef7ae4d89e42faa5c/images/example2-01-postsort.png -------------------------------------------------------------------------------- /streamgraph_generator.pde: -------------------------------------------------------------------------------- 1 | /** 2 | * streamgraph_generator 3 | * Processing Sketch 4 | * Explores different stacked graph layout, ordering and coloring methods 5 | * Used to generate example graphics for the Streamgraph paper 6 | * 7 | * Press Enter to save image 8 | * 9 | * @author Lee Byron 10 | * @author Martin Wattenberg 11 | */ 12 | 13 | boolean isGraphCurved = true; // catmull-rom interpolation 14 | int seed = 28; // random seed 15 | 16 | float DPI = 300; 17 | float widthInches = 3.5; 18 | float heightInches = 0.7; 19 | int numLayers = 50; 20 | int layerSize = 100; 21 | 22 | DataSource data; 23 | LayerLayout layout; 24 | LayerSort ordering; 25 | ColorPicker coloring; 26 | 27 | Layer[] layers; 28 | 29 | void setup() { 30 | 31 | size(int(widthInches*DPI), int(heightInches*DPI)); 32 | smooth(); 33 | noLoop(); 34 | 35 | // GENERATE DATA 36 | data = new LateOnsetDataSource(); 37 | //data = new BelievableDataSource(); 38 | //data = new CSVDataSource(this, "data/csvdata.csv"); 39 | 40 | // ORDER DATA 41 | ordering = new LateOnsetSort(); 42 | //ordering = new VolatilitySort(); 43 | //ordering = new InverseVolatilitySort(); 44 | //ordering = new BasicLateOnsetSort(); 45 | //ordering = new NoLayerSort(); 46 | 47 | // LAYOUT DATA 48 | layout = new StreamLayout(); 49 | //layout = new MinimizedWiggleLayout(); 50 | //layout = new ThemeRiverLayout(); 51 | //layout = new StackLayout(); 52 | 53 | // COLOR DATA 54 | coloring = new LastFMColorPicker(this, "layers-nyt.jpg"); 55 | //coloring = new LastFMColorPicker(this, "layers.jpg"); 56 | //coloring = new RandomColorPicker(this); 57 | 58 | //========================================================================= 59 | 60 | // calculate time to generate graph 61 | long time = System.currentTimeMillis(); 62 | 63 | // generate graph 64 | layers = data.make(numLayers, layerSize); 65 | layers = ordering.sort(layers); 66 | layout.layout(layers); 67 | coloring.colorize(layers); 68 | 69 | // fit graph to viewport 70 | scaleLayers(layers, 1, height - 1); 71 | 72 | // give report 73 | long layoutTime = System.currentTimeMillis()-time; 74 | numLayers = layers.length; 75 | layerSize = layers[0].size.length; 76 | println("Data has " + numLayers + " layers, each with " + 77 | layerSize + " datapoints."); 78 | println("Layout Method: " + layout.getName()); 79 | println("Ordering Method: " + ordering.getName()); 80 | println("Coloring Method: " + layout.getName()); 81 | println("Elapsed Time: " + layoutTime + "ms"); 82 | } 83 | 84 | // adding a pixel to the top compensate for antialiasing letting 85 | // background through. This is overlapped by following layers, so no 86 | // distortion is made to data. 87 | // detail: a pixel is not added to the top-most layer 88 | // detail: a shape is only drawn between it's non 0 values 89 | void draw() { 90 | 91 | int n = layers.length; 92 | int m = layers[0].size.length; 93 | int start; 94 | int end; 95 | int lastIndex = m - 1; 96 | int lastLayer = n - 1; 97 | int pxl; 98 | 99 | background(255); 100 | noStroke(); 101 | 102 | // calculate time to draw graph 103 | long time = System.currentTimeMillis(); 104 | 105 | // generate graph 106 | for (int i = 0; i < n; i++) { 107 | start = max(0, layers[i].onset - 1); 108 | end = min(m - 1, layers[i].end); 109 | pxl = i == lastLayer ? 0 : 1; 110 | 111 | // set fill color of layer 112 | fill(layers[i].rgb); 113 | 114 | // draw shape 115 | beginShape(); 116 | 117 | // draw top edge, left to right 118 | graphVertex(start, layers[i].yTop, isGraphCurved, i == lastLayer); 119 | for (int j = start; j <= end; j++) { 120 | graphVertex(j, layers[i].yTop, isGraphCurved, i == lastLayer); 121 | } 122 | graphVertex(end, layers[i].yTop, isGraphCurved, i == lastLayer); 123 | 124 | // draw bottom edge, right to left 125 | graphVertex(end, layers[i].yBottom, isGraphCurved, false); 126 | for (int j = end; j >= start; j--) { 127 | graphVertex(j, layers[i].yBottom, isGraphCurved, false); 128 | } 129 | graphVertex(start, layers[i].yBottom, isGraphCurved, false); 130 | 131 | endShape(CLOSE); 132 | } 133 | 134 | // give report 135 | long layoutTime = System.currentTimeMillis() - time; 136 | println("Draw Time: " + layoutTime + "ms"); 137 | } 138 | 139 | void graphVertex(int point, float[] source, boolean curve, boolean pxl) { 140 | float x = map(point, 0, layerSize - 1, 0, width); 141 | float y = source[point] - (pxl ? 1 : 0); 142 | if (curve) { 143 | curveVertex(x, y); 144 | } else { 145 | vertex(x, y); 146 | } 147 | } 148 | 149 | void scaleLayers(Layer[] layers, int screenTop, int screenBottom) { 150 | // Figure out max and min values of layers. 151 | float min = Float.MAX_VALUE; 152 | float max = Float.MIN_VALUE; 153 | for (int i = 0; i < layers[0].size.length; i++) { 154 | for (int j = 0; j < layers.length; j++) { 155 | min = min(min, layers[j].yTop[i]); 156 | max = max(max, layers[j].yBottom[i]); 157 | } 158 | } 159 | 160 | float scale = (screenBottom - screenTop) / (max - min); 161 | for (int i = 0; i < layers[0].size.length; i++) { 162 | for (int j = 0; j < layers.length; j++) { 163 | layers[j].yTop[i] = screenTop + scale * (layers[j].yTop[i] - min); 164 | layers[j].yBottom[i] = screenTop + scale * (layers[j].yBottom[i] - min); 165 | } 166 | } 167 | } 168 | 169 | void keyPressed() { 170 | if (keyCode == ENTER) { 171 | println(); 172 | println("Rendering image..."); 173 | String fileName = "images/streamgraph-" + dateString() + ".png"; 174 | save(fileName); 175 | println("Rendered image to: " + fileName); 176 | } 177 | 178 | // hack for un-responsive non looping p5 sketches 179 | if (keyCode == ESC) { 180 | redraw(); 181 | } 182 | } 183 | 184 | String dateString() { 185 | return year() + "-" + nf(month(), 2) + "-" + nf(day(), 2) + "@" + 186 | nf(hour(), 2) + "-" + nf(minute(), 2) + "-" + nf(second(), 2); 187 | } 188 | --------------------------------------------------------------------------------