Skip to content

Instantly share code, notes, and snippets.

@timelyportfolio
Forked from monfera/.block
Created November 14, 2015 13:20
Show Gist options
  • Save timelyportfolio/a6ca7871defb9743efce to your computer and use it in GitHub Desktop.
Save timelyportfolio/a6ca7871defb9743efce to your computer and use it in GitHub Desktop.

Revisions

  1. Building blocks revised this gist Nov 13, 2015. 1 changed file with 0 additions and 0 deletions.
    Binary file modified thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  2. @monfera monfera revised this gist Nov 13, 2015. No changes.
  3. @monfera monfera revised this gist Nov 13, 2015. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,9 @@
    This is a real time version of [bandlines](http://bl.ocks.org/monfera/8dbaabf493fbc0c4ae0c) styled with a divergent ColorBrewer2 palette that is a good choice for finance and accessibility.
    This is an initial, real time prototype of [bandlines](http://bl.ocks.org/monfera/8dbaabf493fbc0c4ae0c) styled with a divergent ColorBrewer2 palette that is a good choice for finance and accessibility.

    The implementation is in mid-transition toward using a new functional reactive programming library, [flyd](https://github.com/paldepind/flyd).

    From the original [bl.ock:](http://bl.ocks.org/monfera/8dbaabf493fbc0c4ae0c):

    **Bandlines** were invented by Stephen Few as background for sparklines to aid quick interpretation, crucial on dashboards: *"Bandlines use horizontal bands of color in the background of the plot area to display information about the distribution of values. This information is similar to that found in a box plot. To do this you must gather information about how values related to the measure that will be featured in the sparkline are distributed during a period of history that usually extends further into the past than the values that will appear in the sparkline itself. [...] Bandlines may be modified to represent other meaningful ranges besides quartile-based distributions."*.

    The design document also describes glyphs for identifying **outliers**, as well as a complementary **sparkstrip** to show the value range bar, optionally with **strip plots** - transparent circles that show individual points and reveal their distribution (all shown), as well as other options.
  4. @monfera monfera revised this gist Nov 13, 2015. No changes.
  5. @monfera monfera revised this gist Nov 13, 2015. 1 changed file with 24 additions and 1 deletion.
    25 changes: 24 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1 +1,24 @@
    Built with [blockbuilder.org](http://blockbuilder.org)
    This is a real time version of [bandlines](http://bl.ocks.org/monfera/8dbaabf493fbc0c4ae0c) styled with a divergent ColorBrewer2 palette that is a good choice for finance and accessibility.

    The implementation is in mid-transition toward using a new functional reactive programming library, [flyd](https://github.com/paldepind/flyd).

    **Bandlines** were invented by Stephen Few as background for sparklines to aid quick interpretation, crucial on dashboards: *"Bandlines use horizontal bands of color in the background of the plot area to display information about the distribution of values. This information is similar to that found in a box plot. To do this you must gather information about how values related to the measure that will be featured in the sparkline are distributed during a period of history that usually extends further into the past than the values that will appear in the sparkline itself. [...] Bandlines may be modified to represent other meaningful ranges besides quartile-based distributions."*.

    The design document also describes glyphs for identifying **outliers**, as well as a complementary **sparkstrip** to show the value range bar, optionally with **strip plots** - transparent circles that show individual points and reveal their distribution (all shown), as well as other options.

    Stephen Few provides an exceptionally detailed and pertinent guide to show how to make, as well as **how to use** this specific type of visualization:

    [**Introducing Bandlines by Stephen Few**](https://www.perceptualedge.com/articles/visual_business_intelligence/introducing_bandlines.pdf)

    [Interactive example](http://bl.ocks.org/monfera/8dbaabf493fbc0c4ae0c)

    [Larger dashboard example with article](https://www.perceptualedge.com/blog/?p=2138)

    The example tser sampler generates a lot of outliers on purpose - to make them appear regularly, and to illustrate how outliers effect the bands.

    [Source code](https://github.com/monfera/bandlines)

    [License](https://opensource.org/licenses/BSD-3-Clause)


    Uploaded with [blockbuilder.org](http://blockbuilder.org)
  6. @monfera monfera revised this gist Nov 13, 2015. No changes.
  7. @monfera monfera revised this gist Nov 13, 2015. 1 changed file with 0 additions and 8 deletions.
    8 changes: 0 additions & 8 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -37,13 +37,5 @@
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // Feel free to change or delete any of the code you see!
    svg.append("rect")
    .attr({x: 100, y: 10, width: width - 200, height: height - 20})
    .style({ fill: "#a72d1a"})
    .transition().duration(3000).ease("bounce")
    .style({ fill: "#5db9e3"})

    console.log("you are now rocking with d3", d3);
    </script>
    </body>
  8. @monfera monfera revised this gist Nov 13, 2015. 7 changed files with 1009 additions and 0 deletions.
    74 changes: 74 additions & 0 deletions bandline.css
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,74 @@
    /**
    * Bandline styling
    */

    body {
    background-color: white;
    }

    g.bandLine .band,
    g.sparkStrip .band {
    fill: #053061;
    }

    g.bands .band {
    fill-opacity: 0.75;
    }

    g.bands .band.s0 {stroke: #f4a582; stroke-width: 1px; fill-opacity: 1}
    g.bands .band.s1 {fill: #fddbc7}
    g.bands .band.s2 {fill: #f4a582}
    g.bands .band.s3 {fill: #92c5de}
    g.bands .band.s4 {fill: #d1e5f0}
    g.bands .band.s5 {stroke: #92c5de; stroke-width: 1px; fill-opacity: 1}

    g.bands .band.s6 {stroke: white; stroke-width: 1px; fill: none;}

    g.bandLine,
    g.sparkStrip {
    fill: none;
    }

    g.bandLine .valueLine,
    g.sparkStrip .valueBox,
    g.sparkStrip .valuePoints,
    g.bandLine .valuePoints .point.highOutlier {
    stroke: #053061; /*rgb(226, 60, 180);*/
    }

    g.bandLine .valuePoints .point {
    fill: #053061; /*rgb(226, 60, 180);*/
    }

    g.bandLine .valueLine {
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
    }

    g.sparkStrip .valueBox,
    g.sparkStrip .valuePoints {
    stroke-width: 0.5;
    }

    g.sparkStrip .valuePoints {
    stroke-opacity: 0.5;
    }

    g.bandLine .valuePoints .point {
    fill: #053061;
    fill-opacity: 0.5;
    }

    g.bandLine .valuePoints .point.lowOutlier {
    fill-opacity: 1;
    }

    g.bandLine .valuePoints .point.highOutlier {
    fill: white;
    fill-opacity: 1;
    }

    g.sparkStrip .valueBox {
    fill: white;
    fill-opacity: 0.75;
    }
    287 changes: 287 additions & 0 deletions bandline.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,287 @@
    /**
    * Bandline renderer
    */

    var defined = R.complement(R.isNil)
    var ease = 'cubic-out'


    function rectanglePath(xr, yr) {
    if(xr[0] < 1e-100 && xr[0] > -1e-100 && xr[0] !== 0) debugger
    return d3.svg.line()([[xr[0], yr[0]], [xr[1], yr[0]], [xr[1], yr[1]], [xr[0], yr[1]]]) + 'Z'
    }

    function bandLinePath(valueAccessor, xScale, yScaler, d) {
    var drawer = d3.svg.line().defined(R.compose(defined, R.prop(1)))
    return drawer(valueAccessor(d).map(function(s) {return [xScale(s.key), yScaler(d)(s.value)]}))
    }

    function bandData(bands, yScaler, d) {
    var yScale = yScaler(d)
    return bands.map(function(band, i) {
    return {key: i, value: band, yScale: yScale}
    })
    }

    function renderBands(root, bands, yScaler, xRanger, yRanger) {
    bind(bind(root, 'bands'), 'band', 'path', bandData.bind(0, bands, yScaler))
    .transition()
    .ease(ease)
    .attr('class', function(d, i) {return 'band s' + i})
    .attr('d', function(d) {return rectanglePath(xRanger(d), yRanger(d))})
    }

    function pointData(valueAccessor, d) {
    return valueAccessor(d)
    .map(function(value) {if(value.key === undefined) debugger; return {key: value.key, value: value.value, o: d}})
    .filter(R.compose(defined, value))
    }

    function renderPoints(root, valueAccessor, pointStyleAccessor, rScale, xSpec, ySpec) {
    bind(root, 'valuePoints', 'g', pointData.bind(0, valueAccessor))
    .entered
    .attr('transform', translate(xSpec, ySpec))
    root['valuePoints']
    .attr('transform', translate(xSpec, ySpec))
    bind(root['valuePoints'], 'point', 'circle')
    .attr('class', function(d) {return 'point ' + pointStyleAccessor(d.value)})
    .transition()
    .attr('r', function(d) {return rScale(pointStyleAccessor(d.value))})
    root['valuePoints'].exit().remove()
    }

    function valuesExtent(valueAccessor, d) {
    return d3.extent(valueAccessor(d).map(value).filter(defined))
    }

    function sparkStripBoxPath(valueAccessor, xScale, yRange, d) {
    var midY = d3.mean(yRange)
    var halfHeight = (yRange[1] - yRange[0]) / 2
    var path = rectanglePath(
    valuesExtent(valueAccessor, d).map(xScale).map(Math.floor),
    [midY - halfHeight / 3, midY + halfHeight / 3]
    )
    //console.log(path)
    return path
    }

    function renderExtent(root, valueAccessor, xScale, yRange) {
    bind(root, 'valueBox', 'path')
    .transition()
    .ease(ease)
    .attr('d', sparkStripBoxPath.bind(0, valueAccessor, xScale, yRange))
    }

    function renderValueLine(root, valueAccessor, xScale, yScaler) {

    var line = bind(root, 'valueLine', 'path')

    var scaler = function(d) {
    var y = yScaler(d)
    return 'scale(1,' + (y(1) - y(0)) + ') translate(0,' + -d3.mean(y.domain()) + ') '
    }

    line
    .attr('d', bandLinePath.bind(0, valueAccessor, xScale, function() {return d3.scale.linear()}))
    .entered
    .attr('transform', scaler)

    line
    .transition()
    .ease(ease)
    .attr('transform', scaler)
    }

    function renderBandLineData(root, _valueAccessor, _xScaleOfBandLine, _yScalerOfBandLine, _pointStyleAccessor, _rScaleOfBandLine) {

    var clippedRoot = bind(root)
    .attr('clip-path', 'url(#bandlinePaddedClippath)')
    var holder = bind(clippedRoot, 'bandLineHolder')
    holder
    .transition()
    .attr('transform', null)
    holder
    .transition().duration(timeCadence)
    .ease('linear')
    .attr('transform', translateX(_xScaleOfBandLine(0) - _xScaleOfBandLine(1)))

    var clippedHolder = bind(holder)
    //.attr('clip-path', 'url(#bandlineClippath)')

    renderValueLine(holder, _valueAccessor, _xScaleOfBandLine, _yScalerOfBandLine)
    renderPoints(holder, _valueAccessor, _pointStyleAccessor, _rScaleOfBandLine,
    R.compose(_xScaleOfBandLine, key), function(d) {return _yScalerOfBandLine(d.o)(d.value)})

    }

    function bandLine() {

    function addDefs(rootSvg) {
    var yRange = _yRange.slice().sort(d3.ascending)
    var clippathPadding = d3.max(_rScaleOfBandLine.range())
    var yRangePadded = [yRange[0] - clippathPadding, yRange[1] + clippathPadding]
    var defs = bind(rootSvg, 'defs', 'defs', rootSvg.datum() ? rootSvg.data() : [{key: 0}])
    bind(defs, 'paddedClipPath', 'clipPath')
    .attr('id', 'bandlinePaddedClippath')
    bind(defs['paddedClipPath'], 'path', 'path', [{key: 0}])
    .attr('d', rectanglePath(_xScaleOfBandLine.range(), yRangePadded))
    bind(defs, 'unpaddedClipPath', 'clipPath')
    .attr('id', 'bandlineUnpaddedClippath')
    bind(defs['unpaddedClipPath'], 'path', 'path', [{key: 0}])
    .attr('d', rectanglePath(_xScaleOfBandLine.range(), yRange))
    }

    function renderBandLine(root) {

    var bandLine = bind(root, 'bandLine')
    var clippedBands = bind(bandLine)
    .attr('clip-path', 'url(#bandlineUnpaddedClippath)')
    renderBands(clippedBands, _bands, _yScalerOfBandLine, R.always(_xScaleOfBandLine.range()),
    function(d) {return d.value.map(d.yScale)})
    renderBandLineData(bandLine, _valueAccessor, _xScaleOfBandLine, _yScalerOfBandLine, _pointStyleAccessor, _rScaleOfBandLine)
    }

    function renderSparkStrip(root) {

    var sparkStrip = bind(root, 'sparkStrip')
    renderBands(sparkStrip, _bands, _yScalerOfSparkStrip, function(d) {
    return d.value.map(_xScaleOfSparkStrip)
    }, R.always(_yRangeOfSparkStrip))
    renderExtent(sparkStrip, _valueAccessor, _xScaleOfSparkStrip, _yRange)
    renderPoints(sparkStrip, _valueAccessor, _pointStyleAccessor, _rScaleOfSparkStrip,
    R.compose(_xScaleOfSparkStrip, value), _yScalerOfSparkStrip())
    }

    function yScalerOfBandLineCalc() {
    return function(d) {
    return d3.scale.linear()
    .domain(valuesExtent(_contextValueAccessor, d))
    .range(_yRange)
    }
    }

    var _bands = [[0, 0.25], [0.25, 0.5], [0.5, 0.75], [0.75, 1]]
    var bands = function(spec) {
    if(spec !== void(0)) {
    _bands = spec
    return functionalObject
    } else {
    return bands
    }
    }

    var _valueAccessor = function(d) {return {key: d.key, value: d.value}}
    var valueAccessor = function(spec) {
    if(spec !== void(0)) {
    _valueAccessor = spec
    _yScalerOfBandLine = yScalerOfBandLineCalc()
    return functionalObject
    } else {
    return _valueAccessor
    }
    }

    var _contextValueAccessor = function(d) {return {key: d.key, value: d.value}}
    var contextValueAccessor = function(spec) {
    if(spec !== void(0)) {
    _contextValueAccessor = spec
    _yScalerOfBandLine = yScalerOfBandLineCalc()
    return functionalObject
    } else {
    return _contextValueAccessor
    }
    }

    var _xScaleOfBandLine = d3.scale.linear()
    var xScaleOfBandLine = function(spec) {
    if(spec !== void(0)) {
    _xScaleOfBandLine = spec
    return functionalObject
    } else {
    return _xScaleOfBandLine
    }
    }

    var _xScaleOfSparkStrip = d3.scale.linear()
    var xScaleOfSparkStrip = function(spec) {
    if(spec !== void(0)) {
    _xScaleOfSparkStrip = spec
    return functionalObject
    } else {
    return _xScaleOfSparkStrip
    }
    }

    var _rScaleOfBandLine = R.always(2)
    var rScaleOfBandLine = function(spec) {
    if(spec !== void(0)) {
    _rScaleOfBandLine = spec
    return functionalObject
    } else {
    return _rScaleOfBandLine
    }
    }

    var _rScaleOfSparkStrip = R.always(2)
    var rScaleOfSparkStrip = function(spec) {
    if(spec !== void(0)) {
    _rScaleOfSparkStrip = spec
    return functionalObject
    } else {
    return _rScaleOfSparkStrip
    }
    }

    var _yRange = [0, 1]
    var _yScalerOfBandLine
    var yRange = function(spec) {
    if(spec !== void(0)) {
    _yRange = spec
    _yScalerOfBandLine = yScalerOfBandLineCalc()
    return functionalObject
    } else {
    return _yRange
    }
    }

    var _yRangeOfSparkStrip = [0, 1]
    var _yScalerOfSparkStrip
    var yRangeOfSparkStrip = function(spec) {
    if(spec !== void(0)) {
    _yRangeOfSparkStrip = spec
    _yScalerOfSparkStrip = R.always(d3.mean(_yRangeOfSparkStrip))
    return functionalObject
    } else {
    return _yRangeOfSparkStrip
    }
    }

    var _pointStyleAccessor = R.always('normal')
    var pointStyleAccessor = function(spec) {
    if(spec !== void(0)) {
    _pointStyleAccessor = spec
    return functionalObject
    } else {
    return _pointStyleAccessor
    }
    }

    var functionalObject = {
    // For reference: http://bost.ocks.org/mike/chart/
    renderBandLine: renderBandLine,
    renderSparkStrip: renderSparkStrip,
    addDefs: addDefs,
    bands: bands,
    valueAccessor: valueAccessor,
    contextValueAccessor: contextValueAccessor,
    xScaleOfBandLine: xScaleOfBandLine,
    xScaleOfSparkStrip: xScaleOfSparkStrip,
    rScaleOfBandLine: rScaleOfBandLine,
    rScaleOfSparkStrip: rScaleOfSparkStrip,
    yRange: yRange,
    yRangeOfSparkStrip: yRangeOfSparkStrip,
    pointStyleAccessor: pointStyleAccessor
    }

    return functionalObject
    }
    60 changes: 60 additions & 0 deletions data.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,60 @@
    var rnorm = function(bias, pow) {
    // using a mu just to avoid the special case of 0 centering
    return bias + (Math.random() > 0.5 ? 1 : -1) * Math.pow(Math.abs(
    Math.random() + Math.random() + Math.random()
    + Math.random() + Math.random() + Math.random() - 3) / 3, pow)
    }

    tserLabels = R.always(['Insulin-like growth factor', 'Von Willebrand Factor', 'Voltage-gated 6T & 1P',
    'Mechanosensitive ion ch.', 'GABAA receptor positive ', 'Epidermal growth factor',
    'Signal recognition particle'].slice(0,7))

    var samples = flyd.stream()

    function sampler(time) {
    return tserLabels().map(function(d, i) {return {key: time, value: rnorm(15, 0.25 + (2 - 0.25) * i / tserLabels().length)}})
    }

    var historyContextLength = 64
    var historyShownLength = 16
    var initialLength = 16

    var initialHistory = R.map(sampler)(R.range(0, initialLength)) // ensuring 2 data points to give sufficient input to the Y scale

    function generateSample(time) {
    samples(sampler(time))
    }

    var time = initialLength

    var samplesHistoricalContext = flyd.stream([samples], function() {
    var newHistory = R.concat(samplesHistoricalContext(), [samples()])
    samplesHistoricalContext(R.slice(-historyContextLength, newHistory.length, newHistory))
    })(initialHistory)

    var pause = false

    var timeCadence = 100

    window.setInterval(function(){if(!pause) generateSample(time++)}, timeCadence)

    function tserMaker(history) {
    var tserLength = history.length
    var range = R.range(0, tserLength)
    var tsers = tserLabels().map(function(d, i) {
    var full = R.map(function(time) {
    return history[time][i]
    })(range)
    return {
    key: d,
    contextValue: full,
    value: R.slice(-historyShownLength, full.length, full)
    }
    })
    return tsers
    }

    var model = flyd.stream([samplesHistoricalContext], function() {
    var history = samplesHistoricalContext()
    return tserMaker(history)
    })
    43 changes: 43 additions & 0 deletions du.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,43 @@
    var key = R.prop('key')
    var value = R.prop('value')
    var window2 = R.aperture(2)

    function bind0(rootSelection, cssClass, element, dataFlow) {
    element = element || 'g' // fixme switch from variadic to curried
    dataFlow = typeof dataFlow === 'function' ? dataFlow
    : (dataFlow === void(0) ? function(d) {return [d]} : R.always(dataFlow))
    var binding = rootSelection.selectAll('.' + cssClass).data(dataFlow, key)

    binding.entered = binding.enter().append(element)
    binding.entered.classed(cssClass, true)

    return binding
    }

    function bind(object, key) {
    var result = bind0.apply(null, arguments)
    object[key] = result
    return result
    }

    function translate(funX, funY) {
    return function(d, i) {
    var x = typeof funX === 'function' ? funX(d, i) : funX
    var y = typeof funY === 'function' ? funY(d, i) : funY
    if(isNaN(x)) throw Error('x is NaN')
    if(isNaN(y)) throw Error('y is NaN')
    return 'translate(' + x + ',' + y + ')'
    }
    }

    function translateX(funX) {
    return function(d, i) {
    return 'translate(' + (typeof funX === 'function' ? funX(d, i) : funX) + ', 0)'
    }
    }

    function translateY(funY) {
    return function(d, i) {
    return 'translate(0, ' + (typeof funY === 'function' ? funY(d, i) : funY) + ')'
    }
    }
    408 changes: 408 additions & 0 deletions flyd.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,408 @@
    /**
    The MIT License (MIT)
    Copyright (c) 2015 Simon Friis Vindum
    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:
    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
    */

    (function (root, factory) {
    if (typeof define === 'function' && define.amd) {
    define([], factory); // AMD. Register as an anonymous module.
    } else if (typeof exports === 'object') {
    module.exports = factory(); // NodeJS
    } else { // Browser globals (root is window)
    root.flyd = factory();
    }
    }(this, function () {

    'use strict';

    function isFunction(obj) {
    return !!(obj && obj.constructor && obj.call && obj.apply);
    }

    // Globals
    var toUpdate = [];
    var inStream;

    function map(f, s) {
    return stream([s], function(self) { self(f(s())); });
    }

    function boundMap(f) { return map(f, this); }

    var scan = curryN(3, function(f, acc, s) {
    var ns = stream([s], function() {
    return (acc = f(acc, s()));
    });
    if (!ns.hasVal) ns(acc);
    return ns;
    });

    var merge = curryN(2, function(s1, s2) {
    var s = immediate(stream([s1, s2], function(n, changed) {
    return changed[0] ? changed[0]()
    : s1.hasVal ? s1()
    : s2();
    }));
    endsOn(stream([s1.end, s2.end], function(self, changed) {
    return true;
    }), s);
    return s;
    });

    function ap(s2) {
    var s1 = this;
    return stream([s1, s2], function() { return s1()(s2()); });
    }

    function initialDepsNotMet(stream) {
    stream.depsMet = stream.deps.every(function(s) {
    return s.hasVal;
    });
    return !stream.depsMet;
    }

    function updateStream(s) {
    if ((s.depsMet !== true && initialDepsNotMet(s)) ||
    (s.end !== undefined && s.end.val === true)) return;
    inStream = s;
    var returnVal = s.fn(s, s.depsChanged);
    if (returnVal !== undefined) {
    s(returnVal);
    }
    inStream = undefined;
    if (s.depsChanged !== undefined) {
    while (s.depsChanged.length > 0) s.depsChanged.shift();
    }
    s.shouldUpdate = false;
    }

    var order = [];
    var orderNextIdx = -1;

    function findDeps(s) {
    var i, listeners = s.listeners;
    if (s.queued === false) {
    s.queued = true;
    for (i = 0; i < listeners.length; ++i) {
    findDeps(listeners[i]);
    }
    order[++orderNextIdx] = s;
    }
    }

    function updateDeps(s) {
    var i, o, list, listeners = s.listeners;
    for (i = 0; i < listeners.length; ++i) {
    list = listeners[i];
    if (list.end === s) {
    endStream(list);
    } else {
    if (list.depsChanged !== undefined) list.depsChanged.push(s);
    list.shouldUpdate = true;
    findDeps(list);
    }
    }
    for (; orderNextIdx >= 0; --orderNextIdx) {
    o = order[orderNextIdx];
    if (o.shouldUpdate === true) updateStream(o);
    o.queued = false;
    }
    }

    function flushUpdate() {
    while (toUpdate.length > 0) updateDeps(toUpdate.shift());
    }

    function isStream(stream) {
    return isFunction(stream) && 'hasVal' in stream;
    }

    function streamToString() {
    return 'stream(' + this.val + ')';
    }

    function createStream() {
    function s(n) {
    var i, list;
    if (arguments.length === 0) {
    return s.val;
    } else {
    if (n !== undefined && n !== null && isFunction(n.then)) {
    n.then(s);
    return;
    }
    s.val = n;
    s.hasVal = true;
    if (inStream === undefined) {
    updateDeps(s);
    if (toUpdate.length > 0) flushUpdate();
    } else if (inStream === s) {
    for (i = 0; i < s.listeners.length; ++i) {
    list = s.listeners[i];
    if (list.end !== s) {
    if (list.depsChanged !== undefined) {
    list.depsChanged.push(s);
    }
    list.shouldUpdate = true;
    } else {
    endStream(list);
    }
    }
    } else {
    toUpdate.push(s);
    }
    return s;
    }
    }
    s.hasVal = false;
    s.val = undefined;
    s.listeners = [];
    s.queued = false;
    s.end = undefined;

    s.map = boundMap;
    s.ap = ap;
    s.of = stream;
    s.toString = streamToString;

    return s;
    }

    function createDependentStream(deps, fn) {
    var i, s = createStream();
    s.fn = fn;
    s.deps = deps;
    s.depsMet = false;
    s.depsChanged = fn.length > 1 ? [] : undefined;
    s.shouldUpdate = false;
    for (i = 0; i < deps.length; ++i) {
    deps[i].listeners.push(s);
    }
    return s;
    }

    function immediate(s) {
    if (s.depsMet === false) {
    s.depsMet = true;
    updateStream(s);
    if (toUpdate.length > 0) flushUpdate();
    }
    return s;
    }

    function removeListener(s, listeners) {
    var idx = listeners.indexOf(s);
    listeners[idx] = listeners[listeners.length - 1];
    listeners.length--;
    }

    function detachDeps(s) {
    for (var i = 0; i < s.deps.length; ++i) {
    removeListener(s, s.deps[i].listeners);
    }
    s.deps.length = 0;
    }

    function endStream(s) {
    if (s.deps !== undefined) detachDeps(s);
    if (s.end !== undefined) detachDeps(s.end);
    }

    function endsOn(endS, s) {
    detachDeps(s.end);
    endS.listeners.push(s.end);
    s.end.deps.push(endS);
    return s;
    }

    function stream(arg, fn) {
    var i, s, deps, depEndStreams;
    var endStream = createDependentStream([], function() { return true; });
    if (arguments.length > 1) {
    deps = []; depEndStreams = [];
    for (i = 0; i < arg.length; ++i) {
    if (arg[i] !== undefined) {
    deps.push(arg[i]);
    if (arg[i].end !== undefined) depEndStreams.push(arg[i].end);
    }
    }
    s = createDependentStream(deps, fn);
    s.end = endStream;
    endStream.listeners.push(s);
    endsOn(createDependentStream(depEndStreams, function() { return true; }, true), s);
    updateStream(s);
    if (toUpdate.length > 0) flushUpdate();
    } else {
    s = createStream();
    s.end = endStream;
    endStream.listeners.push(s);
    if (arguments.length === 1) s(arg);
    }
    return s;
    }

    var transduce = curryN(2, function(xform, source) {
    xform = xform(new StreamTransformer());
    return stream([source], function(self) {
    var res = xform['@@transducer/step'](undefined, source());
    if (res && res['@@transducer/reduced'] === true) {
    self.end(true);
    return res['@@transducer/value'];
    } else {
    return res;
    }
    });
    });

    function StreamTransformer() { }
    StreamTransformer.prototype['@@transducer/init'] = function() { };
    StreamTransformer.prototype['@@transducer/result'] = function() { };
    StreamTransformer.prototype['@@transducer/step'] = function(s, v) { return v; };

    // Own curry implementation snatched from Ramda
    // Figure out something nicer later on
    var _ = {placeholder: true};

    // Detect both own and Ramda placeholder
    function isPlaceholder(p) {
    return p === _ || (p && p.ramda === 'placeholder');
    }

    function toArray(arg) {
    var arr = [];
    for (var i = 0; i < arg.length; ++i) {
    arr[i] = arg[i];
    }
    return arr;
    }

    // Modified versions of arity and curryN from Ramda
    function ofArity(n, fn) {
    if (arguments.length === 1) {
    return ofArity.bind(undefined, n);
    }
    switch (n) {
    case 0:
    return function () {
    return fn.apply(this, arguments);
    };
    case 1:
    return function (a0) {
    void a0;
    return fn.apply(this, arguments);
    };
    case 2:
    return function (a0, a1) {
    void a1;
    return fn.apply(this, arguments);
    };
    case 3:
    return function (a0, a1, a2) {
    void a2;
    return fn.apply(this, arguments);
    };
    case 4:
    return function (a0, a1, a2, a3) {
    void a3;
    return fn.apply(this, arguments);
    };
    case 5:
    return function (a0, a1, a2, a3, a4) {
    void a4;
    return fn.apply(this, arguments);
    };
    case 6:
    return function (a0, a1, a2, a3, a4, a5) {
    void a5;
    return fn.apply(this, arguments);
    };
    case 7:
    return function (a0, a1, a2, a3, a4, a5, a6) {
    void a6;
    return fn.apply(this, arguments);
    };
    case 8:
    return function (a0, a1, a2, a3, a4, a5, a6, a7) {
    void a7;
    return fn.apply(this, arguments);
    };
    case 9:
    return function (a0, a1, a2, a3, a4, a5, a6, a7, a8) {
    void a8;
    return fn.apply(this, arguments);
    };
    case 10:
    return function (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) {
    void a9;
    return fn.apply(this, arguments);
    };
    default:
    throw new Error('First argument to arity must be a non-negative integer no greater than ten');
    }
    }

    function curryN(length, fn) {
    return ofArity(length, function () {
    var n = arguments.length;
    var shortfall = length - n;
    var idx = n;
    while (--idx >= 0) {
    if (isPlaceholder(arguments[idx])) {
    shortfall += 1;
    }
    }
    if (shortfall <= 0) {
    return fn.apply(this, arguments);
    } else {
    var initialArgs = toArray(arguments);
    return curryN(shortfall, function () {
    var currentArgs = toArray(arguments);
    var combinedArgs = [];
    var idx = -1;
    while (++idx < n) {
    var val = initialArgs[idx];
    combinedArgs[idx] = isPlaceholder(val) ? currentArgs.shift() : val;
    }
    return fn.apply(this, combinedArgs.concat(currentArgs));
    });
    }
    });
    }


    return {
    stream: stream,
    isStream: isStream,
    transduce: transduce,
    merge: merge,
    reduce: scan, // Legacy
    scan: scan,
    endsOn: endsOn,
    map: curryN(2, map),
    curryN: curryN,
    _: _,
    immediate: immediate,
    };

    }));
    49 changes: 49 additions & 0 deletions model.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,49 @@
    function setupBandline(tsers) {

    var contextValuesSorted = [].concat.apply([], R.flatten(tsers.map(R.prop('contextValue'))).map(R.prop('value'))).sort(d3.ascending)
    var bandThresholds = [
    d3.min(contextValuesSorted),
    d3.min(contextValuesSorted),
    d3.quantile(contextValuesSorted, 1/4),
    d3.quantile(contextValuesSorted, 2/4),
    d3.quantile(contextValuesSorted, 3/4),
    d3.max(contextValuesSorted),
    d3.max(contextValuesSorted)
    ]

    var outlierClassifications = ['lowOutlier', 'normal', 'highOutlier']

    function makeOutlierScale(sortedValues) {
    var iqrDistanceMultiplier = 1.5 // As per Stephen Few's specification
    var iqr = [d3.quantile(sortedValues, 0.25), d3.quantile(sortedValues, 0.75)]
    var midspread = iqr[1] - iqr[0]
    return d3.scale.threshold()
    .domain([
    iqr[0] - iqrDistanceMultiplier * midspread,
    iqr[1] + iqrDistanceMultiplier * midspread
    ])
    .range(outlierClassifications)
    }

    function medianLineBand(sortedValues) {
    // The median line is approximated as a band of 0 extent (CSS styling is via 'stroke').
    // This 'band' is to be tacked on last so it isn't occluded by other bands
    // (SVG uses the painter's algorithm for Z ordering).
    var median = d3.median(sortedValues)
    return [median, median]
    }

    var timestamps = R.flatten(tsers.map(value)).map(key)
    var temporalDomain = [d3.min(timestamps), d3.max(timestamps)]

    // Setting up the bandLine with the domain dependent values only (FP curry style applied on
    // 'functional objects'). This helps decouple the Model and the viewModel (MVC-like principle).
    return bandLine()
    .bands(window2(bandThresholds).concat([medianLineBand(contextValuesSorted)]))
    .valueAccessor(R.prop('value'))
    .contextValueAccessor(R.prop('contextValue'))
    .pointStyleAccessor(makeOutlierScale(contextValuesSorted))
    .xScaleOfBandLine(d3.scale.linear().domain(temporalDomain))
    .xScaleOfSparkStrip(d3.scale.linear().domain(d3.extent(bandThresholds)))
    .rScaleOfBandLine(d3.scale.ordinal().domain(outlierClassifications))
    }
    88 changes: 88 additions & 0 deletions render.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,88 @@
    var curriedBandline = flyd.stream([model, model], function() {
    return setupBandline(model())
    })

    var config = flyd.stream()({
    rowPitch: 40
    })

    flyd.stream([curriedBandline, model, config], function() {
    render(curriedBandline(), model(), config())
    })

    function render(curriedBandLine, tsers, config) {

    var margin = {top: 5, right: 40, bottom: 20, left: 120}
    var width = 370 - margin.left - margin.right
    var height = 370 - margin.top - margin.bottom

    var rowPitch = config.rowPitch
    var bandLineHeight = rowPitch * 0.75

    // Column widths
    var nameColumnWidth = 165
    var bandLineWidth = 100
    var sparkStripWidth = 50
    var columnSeparation = 6

    // The bandline gets augmented with the View specific settings (screen widths etc.)
    var bandLine = curriedBandLine // fixme implement bandline .copy

    // Augment partially set up elements
    bandLine.xScaleOfBandLine().range([0, bandLineWidth])
    bandLine.xScaleOfSparkStrip().range([0, sparkStripWidth])
    bandLine.rScaleOfBandLine().range([2, 0, 2])

    // Add new elements
    bandLine
    .rScaleOfSparkStrip(R.always(2))
    .yRange([bandLineHeight / 2 , -bandLineHeight / 2])
    .yRangeOfSparkStrip([rowPitch / 2 , -rowPitch / 2])

    // Initialise the bandline renderer with SVG defs
    var svg = d3.selectAll('svg')
    bandLine.addDefs(svg)

    /**
    * Root
    */

    svg
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)

    var dashboard = bind(svg, 'dashboard', 'g', [{key: 0}])


    /**
    * Headers
    */

    bind(dashboard, 'header', 'text', [{key: 'Name'}, {key: 'Spread'}, {key: 'Time Series'}])
    .entered
    .text(key)
    .attr('transform', translate(function(d, i) {
    return [0, nameColumnWidth, nameColumnWidth + sparkStripWidth + 3 * columnSeparation][i]
    }, rowPitch))


    /**
    * Rows
    */

    var row = bind(dashboard, 'row', 'g', tsers)

    row.attr('transform', function rowTransform(d, i) {return translateY((i + 2) * rowPitch)()})

    bind(row, 'nameCellText', 'text')
    .text(key)
    .attr('y', '0.5em')

    bind(row, 'assignmentScoresCell')
    .attr('transform', translateX(nameColumnWidth + sparkStripWidth + 2 * columnSeparation))
    .call(bandLine.renderBandLine)

    bind(row, 'assignmentScoresVerticalCell')
    .attr('transform', translateX(nameColumnWidth + columnSeparation))
    .call(bandLine.renderSparkStrip)
    }
  9. @monfera monfera revised this gist Nov 13, 2015. No changes.
  10. Building blocks revised this gist Nov 13, 2015. 1 changed file with 0 additions and 0 deletions.
    Binary file modified thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  11. Building blocks revised this gist Nov 13, 2015. 1 changed file with 0 additions and 0 deletions.
    Binary file added thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  12. @monfera monfera created this gist Nov 13, 2015.
    1 change: 1 addition & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    Built with [blockbuilder.org](http://blockbuilder.org)
    49 changes: 49 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,49 @@
    <!DOCTYPE html>
    <head>
    <meta charset="utf-8">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.18.0/ramda.min.js"></script>
    <link href="bandline.css" rel="stylesheet" type="text/css" />
    <script src="flyd.js" type="text/javascript"></script>
    <script src="du.js" type="text/javascript"></script>
    <script src="data.js" type="text/javascript"></script>
    <script src="model.js" type="text/javascript"></script>
    <script src="bandline.js" type="text/javascript"></script>
    <script defer="defer" src="render.js" type="text/javascript"></script>

    <style>
    body {
    margin-left: 160px;
    margin-top: 40px;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 14px;
    }

    .header {
    font-weight: bold;
    fill: #777;
    }
    </style>
    </head>

    <body>
    <script>
    var margin = {top: 20, right: 10, bottom: 20, left: 10};
    var width = 960 - margin.left - margin.right;
    var height = 500 - margin.top - margin.bottom;
    var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // Feel free to change or delete any of the code you see!
    svg.append("rect")
    .attr({x: 100, y: 10, width: width - 200, height: height - 20})
    .style({ fill: "#a72d1a"})
    .transition().duration(3000).ease("bounce")
    .style({ fill: "#5db9e3"})

    console.log("you are now rocking with d3", d3);
    </script>
    </body>