diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..3a1fa04 Binary files /dev/null and b/.DS_Store differ diff --git a/BasicLateOnsetSort.java b/BasicLateOnsetSort.java new file mode 100644 index 0000000..0d9acb0 --- /dev/null +++ b/BasicLateOnsetSort.java @@ -0,0 +1,24 @@ +import java.util.*; + +/** + * BasicLateOnsetSort + * Sorts by onset, but does not partition to the outsides of the graph in + * order to illustrate short-sighted errors found during design process. + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public class BasicLateOnsetSort extends LayerSort { + + public String getName() { + return "Late Onset Sorting, Top to Bottom"; + } + + public Layer[] sort(Layer[] layers) { + // first sort by onset + Arrays.sort(layers, new OnsetComparator(true)); + + return layers; + } + +} diff --git a/BelievableDataSource.java b/BelievableDataSource.java new file mode 100644 index 0000000..03e1491 --- /dev/null +++ b/BelievableDataSource.java @@ -0,0 +1,58 @@ +import java.util.*; + +/** + * BelievableDataSource + * Create test data for layout engine. + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public class BelievableDataSource implements DataSource { + + public Random rnd; + + public BelievableDataSource() { + // seeded, so we can reproduce results + this(2); + } + + public BelievableDataSource(int seed) { + rnd = new Random(seed); + } + + public Layer[] make(int numLayers, int sizeArrayLength) { + Layer[] layers = new Layer[numLayers]; + + for (int i = 0; i < numLayers; i++) { + String name = "Layer #" + i; + float[] size = new float[sizeArrayLength]; + size = makeRandomArray(sizeArrayLength); + layers[i] = new Layer(name, size); + } + + return layers; + } + + protected float[] makeRandomArray(int n) { + float[] x = new float[n]; + + // add a handful of random bumps + for (int i=0; i<5; i++) { + addRandomBump(x); + } + + return x; + } + + protected void addRandomBump(float[] x) { + float height = 1 / rnd.nextFloat(); + float cx = (float)(2 * rnd.nextFloat() - 0.5); + float r = rnd.nextFloat() / 10; + + for (int i = 0; i < x.length; i++) { + float a = (i / (float)x.length - cx) / r; + x[i] += height * Math.exp(-a * a); + } + } + +} diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..9eda5a4 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,23 @@ +Copyright (c) 2008, Lee Byron, Martin Wattenberg +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The names of the contributors may NOT be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL LEE BYRON BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ColorPicker.java b/ColorPicker.java new file mode 100644 index 0000000..afb3d94 --- /dev/null +++ b/ColorPicker.java @@ -0,0 +1,14 @@ +/** + * ColorPicker + * Interface for new coloring algorithms. + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public interface ColorPicker { + + public void colorize(Layer[] layers); + + public String getName(); + +} diff --git a/DataSource.java b/DataSource.java new file mode 100644 index 0000000..4dd2d92 --- /dev/null +++ b/DataSource.java @@ -0,0 +1,12 @@ +/** + * DataSource + * Interface for creating a data source + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public interface DataSource { + + public Layer[] make(int numLayers, int sizeArrayLength); + +} diff --git a/InverseVolatilitySort.java b/InverseVolatilitySort.java new file mode 100644 index 0000000..1d16332 --- /dev/null +++ b/InverseVolatilitySort.java @@ -0,0 +1,25 @@ +import java.util.*; + +/** + * InverseVolatilitySort + * Sorts an array of layers by their volatility, placing the most volatile + * layers along the insides of the graph, illustrating how disruptive this + * volatility can be to a stacked graph. + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public class InverseVolatilitySort extends LayerSort { + + public String getName() { + return "Inverse Volatility Sorting, Evenly Weighted"; + } + + public Layer[] sort(Layer[] layers) { + // first sort by volatility + Arrays.sort(layers, new VolatilityComparator(false)); + + return orderToOutside(layers); + } + +} diff --git a/LastFMColorPicker.java b/LastFMColorPicker.java new file mode 100644 index 0000000..53ca145 --- /dev/null +++ b/LastFMColorPicker.java @@ -0,0 +1,53 @@ +import processing.core.*; + +/** + * LastFMColorPicker + * Loads in an image and uses it as a two-dimensional gradient + * Supply two [0,1) numbers and get the color of the gradient at that point + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public class LastFMColorPicker implements ColorPicker { + + public PImage source; + + public LastFMColorPicker(PApplet parent, String src) { + source = parent.loadImage(src); + } + + public String getName() { + return "Listening History Color Scheme"; + } + + public void colorize(Layer[] layers) { + // find the largest layer to use as a normalizer + float maxSum = 0; + for (int i=0; i 0) { + if (onset == -1) { + onset = i; + } else { + end = i; + } + } + + // volatility is the maximum change between any two consecutive points + if (i > 0) { + volatility = Math.max( + volatility, + Math.abs(size[i] - size[i-1]) + ); + } + } + } + +} diff --git a/LayerLayout.java b/LayerLayout.java new file mode 100644 index 0000000..0a75ca9 --- /dev/null +++ b/LayerLayout.java @@ -0,0 +1,33 @@ +/** + * LayerLayout + * Abstract Class for new stacked graph layout algorithms + * + * Note: you do not need to worry about scaling to screen dimensions. + * The display applet will do that automatically for you. + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public abstract class LayerLayout { + + abstract void layout(Layer[] layers); + + abstract String getName(); + + /** + * We define our stacked graphs by layers atop a baseline. + * This method does the work of assigning the positions of each layer in an + * ordered array of layers based on an initial baseline. + */ + protected void stackOnBaseline(Layer[] layers, float[] baseline) { + // Put layers on top of the baseline. + for (int i = 0; i < layers.length; i++) { + System.arraycopy(baseline, 0, layers[i].yBottom, 0, baseline.length); + for (int j = 0; j < baseline.length; j++) { + baseline[j] -= layers[i].size[j]; + } + System.arraycopy(baseline, 0, layers[i].yTop, 0, baseline.length); + } + } + +} diff --git a/LayerSort.java b/LayerSort.java new file mode 100644 index 0000000..95402f4 --- /dev/null +++ b/LayerSort.java @@ -0,0 +1,54 @@ +/** + * LayerSort + * Interface to sorting layers + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public abstract class LayerSort { + + abstract String getName(); + + abstract Layer[] sort(Layer[] layers); + + /** + * Creates a 'top' and 'bottom' collection. + * Iterating through the previously sorted list of layers, place each layer + * in whichever collection has less total mass, arriving at an evenly + * weighted graph. Reassemble such that the layers that appeared earliest + * end up in the 'center' of the graph. + */ + protected Layer[] orderToOutside(Layer[] layers) { + int j = 0; + int n = layers.length; + Layer[] newLayers = new Layer[n]; + int topCount = 0; + float topSum = 0; + int[] topList = new int[n]; + int botCount = 0; + float botSum = 0; + int[] botList = new int[n]; + + // partition to top or bottom containers + for (int i=0; i= 0; i--) { + newLayers[j++] = layers[botList[i]]; + } + for (int i = 0; i < topCount; i++) { + newLayers[j++] = layers[topList[i]]; + } + + return newLayers; + } + +} diff --git a/MinimizedWiggleLayout.java b/MinimizedWiggleLayout.java new file mode 100644 index 0000000..a4987e2 --- /dev/null +++ b/MinimizedWiggleLayout.java @@ -0,0 +1,34 @@ +/** + * MinimizedWiggleLayout + * Minimizes the sum of squares of the layer slopes at each value + * + * We present this as a reasonable alternative to the Stream Graph for + * real-time use. While it has some drawbacks compared to StreamLayout, it is + * much faster to execute and is reasonable for real-time applications. + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public class MinimizedWiggleLayout extends LayerLayout { + + public String getName() { + return "Minimized Wiggle Layout"; + } + + public void layout(Layer[] layers) { + int n = layers[0].size.length; + int m = layers.length; + float[] baseline = new float[n]; + + // Set shape of baseline values. + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + baseline[i] += (m - j - 0.5) * layers[j].size[i]; + } + baseline[i] /= m; + } + + // Put layers on top of the baseline. + stackOnBaseline(layers, baseline); + } +} diff --git a/NoLayerSort.java b/NoLayerSort.java new file mode 100644 index 0000000..1cb59cd --- /dev/null +++ b/NoLayerSort.java @@ -0,0 +1,18 @@ +/** + * NoLayerSort + * Does no sorting. Identity function. + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public class NoLayerSort extends LayerSort { + + public String getName() { + return "No Sorting"; + } + + public Layer[] sort(Layer[] layers) { + return layers; + } + +} diff --git a/OnsetComparator.java b/OnsetComparator.java new file mode 100644 index 0000000..5419a82 --- /dev/null +++ b/OnsetComparator.java @@ -0,0 +1,30 @@ +import java.util.*; + +/** + * OnsetSort + * Compares two Layers based on their onset + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public class OnsetComparator implements Comparator { + + public boolean ascending; + + public OnsetComparator(boolean ascending) { + this.ascending = ascending; + } + + public int compare(Object p, Object q){ + Layer pL = (Layer)p; + Layer qL = (Layer)q; + return (ascending ? 1 : -1) * (pL.onset - qL.onset); + } + + public boolean equals(Object p, Object q){ + Layer pL = (Layer)p; + Layer qL = (Layer)q; + return pL.onset == qL.onset; + } + +} diff --git a/README b/README new file mode 100644 index 0000000..4a300de --- /dev/null +++ b/README @@ -0,0 +1,6 @@ +This is the processing application used to generate the images in the paper: +Stacked Graphs - Geometry & Aesthetics + +It is published here as an educational library, to provide code examples to support the paper. + +This code is copyright under the BSD license. diff --git a/RandomColorPicker.java b/RandomColorPicker.java new file mode 100644 index 0000000..f2cb467 --- /dev/null +++ b/RandomColorPicker.java @@ -0,0 +1,100 @@ +import processing.core.*; +import java.util.*; + +/** + * RandomColorPicker + * Chooses random colors within an acceptable HSB color spectrum + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public class RandomColorPicker implements ColorPicker { + + public Random rnd; + public PApplet parent; + + public RandomColorPicker(PApplet parent) { + this(parent, 2); + } + + public RandomColorPicker(PApplet parent, int seed) { + this.parent = parent; + parent.colorMode(PApplet.RGB, 255); + + // seeded, so we can reproduce results + rnd = new Random(seed); + } + + public String getName() { + return "Random Colors"; + } + + public void colorize(Layer[] layers) { + for (int i = 0; i < layers.length; i++) { + float h = PApplet.lerp(0.6f, 0.65f, rnd.nextFloat()); + float s = PApplet.lerp(0.2f, 0.25f, rnd.nextFloat()); + float b = PApplet.lerp(0.4f, 0.95f, rnd.nextFloat()); + + layers[i].rgb = hsb2rgb(h, s, b); + } + } + + protected int hsb2rgb(float x, float y, float z) { + float calcR = 0; + float calcG = 0; + float calcB = 0; + float calcA = 1; + + if (y == 0) { // saturation == 0 + calcR = calcG = calcB = z; + } else { + float which = (x - (int)x) * 6.0f; + float f = which - (int)which; + float p = z * (1.0f - y); + float q = z * (1.0f - y * f); + float t = z * (1.0f - (y * (1.0f - f))); + + switch ((int)which) { + case 0: + calcR = z; + calcG = t; + calcB = p; + break; + case 1: + calcR = q; + calcG = z; + calcB = p; + break; + case 2: + calcR = p; + calcG = z; + calcB = t; + break; + case 3: + calcR = p; + calcG = q; + calcB = z; + break; + case 4: + calcR = t; + calcG = p; + calcB = z; + break; + case 5: + calcR = z; + calcG = p; + calcB = q; + break; + } + } + + int calcRi = (int)(255 * calcR); + int calcGi = (int)(255 * calcG); + int calcBi = (int)(255 * calcB); + int calcAi = (int)(255 * calcA); + int calcColor = (calcAi << 24) | (calcRi << 16) | (calcGi << 8) | calcBi; + + return calcColor; + } + +} diff --git a/StackLayout.java b/StackLayout.java new file mode 100644 index 0000000..0abea4d --- /dev/null +++ b/StackLayout.java @@ -0,0 +1,27 @@ +import java.util.*; + +/** + * StackLayout + * Standard stacked graph layout, with a straight baseline + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public class StackLayout extends LayerLayout { + + public String getName() { + return "Stacked Layout"; + } + + public void layout(Layer[] layers) { + int n = layers[0].size.length; + + // lay out layers, top to bottom. + float[] baseline = new float[n]; + Arrays.fill(baseline, 0); + + // Put layers on top of the baseline. + stackOnBaseline(layers, baseline); + } + +} diff --git a/StreamLayout.java b/StreamLayout.java new file mode 100644 index 0000000..7f38a04 --- /dev/null +++ b/StreamLayout.java @@ -0,0 +1,63 @@ +/** + * StreamLayout + * The layout used in the Streamgraph stacked graph + * + * Because this layout is using numeric integration, it is likely insufficient + * for real-time display, especially for larger data sets. + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public class StreamLayout extends LayerLayout { + + public String getName() { + return "Original Streamgraph Layout"; + } + + public void layout(Layer[] layers) { + int n = layers[0].size.length; + int m = layers.length; + float[] baseline = new float[n]; + float[] center = new float[n]; + float totalSize; + float moveUp; + float increase; + float belowSize; + + // Set shape of baseline values. + for (int i = 0; i < n; i++) { + // the 'center' is a rolling point. It is initialized as the previous + // iteration's center value + center[i] = i == 0 ? 0 : center[i-1]; + + // find the total size of all layers at this point + totalSize = 0; + for (int j = 0; j < m; j++) { + totalSize += layers[j].size[i]; + } + + // account for the change of every layer to offset the center point + for (int j = 0; j < m; j++) { + if (i == 0) { + increase = layers[j].size[i]; + moveUp = 0.5f; + } else { + belowSize = 0.5f * layers[j].size[i]; + for (int k = j + 1; k < m; k++) { + belowSize += layers[k].size[i]; + } + increase = layers[j].size[i] - layers[j].size[i - 1]; + moveUp = totalSize == 0 ? 0 : (belowSize / totalSize); + } + center[i] += (moveUp - 0.5) * increase; + } + + // set baseline to the bottom edge according to the center line + baseline[i] = center[i] + 0.5f * totalSize; + } + + // Put layers on top of the baseline. + stackOnBaseline(layers, baseline); + } + +} diff --git a/ThemeRiverLayout.java b/ThemeRiverLayout.java new file mode 100644 index 0000000..9c2953d --- /dev/null +++ b/ThemeRiverLayout.java @@ -0,0 +1,33 @@ +/** + * ThemeRiverLayout + * Layout used by the authors of the ThemeRiver paper + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public class ThemeRiverLayout extends LayerLayout { + + public String getName() { + return "ThemeRiver"; + } + + public void layout(Layer[] layers) { + // Set shape of baseline values. + int n=layers[0].size.length; + int m=layers.length; + float[] baseline = new float[n]; + + // ThemeRiver is perfectly symmetrical + // the baseline is 1/2 of the total height at any point + for (int i = 0; i < n; i++) { + baseline[i] = 0; + for (int j = 0; j < m; j++) { + baseline[i] += layers[j].size[i]; + } + baseline[i] *= 0.5; + } + + // Put layers on top of the baseline. + stackOnBaseline(layers, baseline); + } +} diff --git a/VolatilityComparator.java b/VolatilityComparator.java new file mode 100644 index 0000000..d9ffdb4 --- /dev/null +++ b/VolatilityComparator.java @@ -0,0 +1,31 @@ +import java.util.*; + +/** + * VolatilityComparator + * Compares two Layers based on their volatility + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public class VolatilityComparator implements Comparator { + + public boolean ascending; + + public VolatilityComparator(boolean ascending) { + this.ascending = ascending; + } + + public int compare(Object p, Object q) { + Layer pL = (Layer)p; + Layer qL = (Layer)q; + float volatilityDifference = pL.volatility - qL.volatility; + return (ascending ? 1 : -1) * (int)(10000000 * volatilityDifference); + } + + public boolean equals(Object p, Object q) { + Layer pL = (Layer)p; + Layer qL = (Layer)q; + return pL.volatility == qL.volatility; + } + +} diff --git a/VolatilitySort.java b/VolatilitySort.java new file mode 100644 index 0000000..25dca87 --- /dev/null +++ b/VolatilitySort.java @@ -0,0 +1,29 @@ +import java.util.*; + +/** + * VolatilitySort + * Sorts an array of layers by their volatility, placing the most volatile + * layers along the outsides of the graph, thus minimizing unneccessary + * distortion. + * + * First sort by volatility, then creates a 'top' and 'bottom' collection. + * Iterating through the sorted list of layers, place each layer in whichever + * collection has less total mass, arriving at an evenly weighted graph. + * + * @author Lee Byron + * @author Martin Wattenberg + */ +public class VolatilitySort extends LayerSort { + + public String getName() { + return "Volatility Sorting, Evenly Weighted"; + } + + public Layer[] sort(Layer[] layers) { + // first sort by volatility + Arrays.sort(layers, new VolatilityComparator(true)); + + return orderToOutside(layers); + } + +} diff --git a/data/layers-nyt.jpg b/data/layers-nyt.jpg new file mode 100644 index 0000000..c505f51 Binary files /dev/null and b/data/layers-nyt.jpg differ diff --git a/data/layers.jpg b/data/layers.jpg new file mode 100644 index 0000000..2a79b78 Binary files /dev/null and b/data/layers.jpg differ diff --git a/images/example-00-standard.png b/images/example-00-standard.png new file mode 100644 index 0000000..022d846 Binary files /dev/null and b/images/example-00-standard.png differ diff --git a/images/example-01-themeriver.png b/images/example-01-themeriver.png new file mode 100644 index 0000000..2b0d1e7 Binary files /dev/null and b/images/example-01-themeriver.png differ diff --git a/images/example-02-layout.png b/images/example-02-layout.png new file mode 100644 index 0000000..6ad0c1f Binary files /dev/null and b/images/example-02-layout.png differ diff --git a/images/example-03-color.png b/images/example-03-color.png new file mode 100644 index 0000000..fd9ac12 Binary files /dev/null and b/images/example-03-color.png differ diff --git a/images/example-04-ordering.png b/images/example-04-ordering.png new file mode 100644 index 0000000..cfc6b24 Binary files /dev/null and b/images/example-04-ordering.png differ diff --git a/images/example-05-nytcoloring.png b/images/example-05-nytcoloring.png new file mode 100644 index 0000000..fd151be Binary files /dev/null and b/images/example-05-nytcoloring.png differ diff --git a/images/example2-00-presort.png b/images/example2-00-presort.png new file mode 100644 index 0000000..8a3d6c6 Binary files /dev/null and b/images/example2-00-presort.png differ diff --git a/images/example2-01-postsort.png b/images/example2-01-postsort.png new file mode 100644 index 0000000..1205f87 Binary files /dev/null and b/images/example2-01-postsort.png differ diff --git a/streamgraph_generator.pde b/streamgraph_generator.pde new file mode 100644 index 0000000..3ab05e7 --- /dev/null +++ b/streamgraph_generator.pde @@ -0,0 +1,186 @@ +/** + * streamgraph_generator + * Processing Sketch + * Explores different stacked graph layout, ordering and coloring methods + * Used to generate example graphics for the Streamgraph paper + * + * Press Enter to save image + * + * @author Lee Byron + * @author Martin Wattenberg + */ + +boolean isGraphCurved = true; // catmull-rom interpolation +int seed = 28; // random seed + +float DPI = 300; +float widthInches = 3.5; +float heightInches = 0.7; +int numLayers = 50; +int layerSize = 100; + +DataSource data; +LayerLayout layout; +LayerSort ordering; +ColorPicker coloring; + +Layer[] layers; + +void setup() { + + size(int(widthInches*DPI), int(heightInches*DPI)); + smooth(); + noLoop(); + + // GENERATE DATA + data = new LateOnsetDataSource(); + //data = new BelievableDataSource(); + + // ORDER DATA + ordering = new LateOnsetSort(); + //ordering = new VolatilitySort(); + //ordering = new InverseVolatilitySort(); + //ordering = new BasicLateOnsetSort(); + //ordering = new NoLayerSort(); + + // LAYOUT DATA + layout = new StreamLayout(); + //layout = new MinimizedWiggleLayout(); + //layout = new ThemeRiverLayout(); + //layout = new StackLayout(); + + // COLOR DATA + coloring = new LastFMColorPicker(this, "layers-nyt.jpg"); + //coloring = new LastFMColorPicker(this, "layers.jpg"); + //coloring = new RandomColorPicker(this); + + //========================================================================= + + // calculate time to generate graph + long time = System.currentTimeMillis(); + + // generate graph + layers = data.make(numLayers, layerSize); + layers = ordering.sort(layers); + layout.layout(layers); + coloring.colorize(layers); + + // fit graph to viewport + scaleLayers(layers, 1, height - 1); + + // give report + long layoutTime = System.currentTimeMillis()-time; + int numLayers = layers.length; + int layerSize = layers[0].size.length; + println("Data has " + numLayers + " layers, each with " + + layerSize + " datapoints."); + println("Layout Method: " + layout.getName()); + println("Ordering Method: " + ordering.getName()); + println("Coloring Method: " + layout.getName()); + println("Elapsed Time: " + layoutTime + "ms"); +} + +// adding a pixel to the top compensate for antialiasing letting +// background through. This is overlapped by following layers, so no +// distortion is made to data. +// detail: a pixel is not added to the top-most layer +// detail: a shape is only drawn between it's non 0 values +void draw() { + + int n = layers.length; + int m = layers[0].size.length; + int start; + int end; + int lastIndex = m - 1; + int lastLayer = n - 1; + int pxl; + + background(255); + noStroke(); + + // calculate time to draw graph + long time = System.currentTimeMillis(); + + // generate graph + for (int i = 0; i < n; i++) { + start = max(0, layers[i].onset - 1); + end = min(m - 1, layers[i].end); + pxl = i == lastLayer ? 0 : 1; + + // set fill color of layer + fill(layers[i].rgb); + + // draw shape + beginShape(); + + // draw top edge, left to right + graphVertex(start, layers[i].yTop, isGraphCurved, i == lastLayer); + for (int j = start; j <= end; j++) { + graphVertex(j, layers[i].yTop, isGraphCurved, i == lastLayer); + } + graphVertex(end, layers[i].yTop, isGraphCurved, i == lastLayer); + + // draw bottom edge, right to left + graphVertex(end, layers[i].yBottom, isGraphCurved, false); + for (int j = end; j >= start; j--) { + graphVertex(j, layers[i].yBottom, isGraphCurved, false); + } + graphVertex(start, layers[i].yBottom, isGraphCurved, false); + + endShape(CLOSE); + } + + // give report + long layoutTime = System.currentTimeMillis() - time; + println("Draw Time: " + layoutTime + "ms"); +} + +void graphVertex(int point, float[] source, boolean curve, boolean pxl) { + float x = map(point, 0, layerSize - 1, 0, width); + float y = source[point] - (pxl ? 1 : 0); + if (curve) { + curveVertex(x, y); + } else { + vertex(x, y); + } +} + +void scaleLayers(Layer[] layers, int screenTop, int screenBottom) { + // Figure out max and min values of layers. + float min = Float.MAX_VALUE; + float max = Float.MIN_VALUE; + for (int i = 0; i < layers[0].size.length; i++) { + for (int j = 0; j < layers.length; j++) { + min = min(min, layers[j].yTop[i]); + max = max(max, layers[j].yBottom[i]); + } + } + + float scale = (screenBottom - screenTop) / (max - min); + for (int i = 0; i < layers[0].size.length; i++) { + for (int j = 0; j < layers.length; j++) { + layers[j].yTop[i] = screenTop + scale * (layers[j].yTop[i] - min); + layers[j].yBottom[i] = screenTop + scale * (layers[j].yBottom[i] - min); + } + } +} + +void keyPressed() { + if (keyCode == ENTER) { + println(); + println("Rendering image..."); + String fileName = "images/streamgraph-" + dateString() + ".png"; + save(fileName); + println("Rendered image to: " + fileName); + } + + // hack for un-responsive non looping p5 sketches + if (keyCode == ESC) { + redraw(); + } +} + +String dateString() { + return year() + "-" + nf(month(), 2) + "-" + nf(day(), 2) + "@" + + nf(hour(), 2) + "-" + nf(minute(), 2) + "-" + nf(second(), 2); +}