Built with blockbuilder.org
-
-
Save timelyportfolio/a6ca7871defb9743efce to your computer and use it in GitHub Desktop.
Real-time bandlines
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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 | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | |
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) + ')' | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| 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, | |
| }; | |
| })); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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