Skip to content

Instantly share code, notes, and snippets.

@monfera
Last active August 10, 2016 14:48
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save monfera/200e98ef294ed73583c7 to your computer and use it in GitHub Desktop.
Real-time bandlines
license: gpl-3.0

This is an initial, real time prototype of bandlines styled with a divergent ColorBrewer2 palette that is a good choice for finance and accessibility. See my related bl.ocks here: http://bl.ocks.org/monfera or follow @monfera on twitter

The implementation is in mid-transition toward using a new functional reactive programming library, flyd.

From the original bl.ock::

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

Interactive example

Larger dashboard example with article

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

License

Uploaded with blockbuilder.org

/**
* 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;
}
/**
* 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
}
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 = 128
var historyShownLength = 16
var initialLength = 128
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 = 1000
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)
})
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) + ')'
}
}
/**
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,
};
}));
<!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>
<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 + ")");
</script>
</body>
<script src="render.js" type="text/javascript"></script>
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))
}
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)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment