Skip to content

Instantly share code, notes, and snippets.

@monfera
Last active April 29, 2017 14:18
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save monfera/35be5626a47110312c35 to your computer and use it in GitHub Desktop.
Save monfera/35be5626a47110312c35 to your computer and use it in GitHub Desktop.
Fluid configuration of d3.js bandlines with FRP
license: gpl-3.0

Fine-grained FRP interactivity for real-time configuration and live update with d3.js and a simplified version of flyd.js. Now best run on Chrome.

Summary: instead of the customary rerendering with d3.js, each interaction cascades through only the necessary blocks in the data flow. For example, changing the sparkstrip circle radius invokes merely one .attr('r', ...) invocation, while the rendering code shape resembles regular d3 code. Also, only the necessary parts of the data model are recomputed. The dependencies are explicit.

MVC is enjoying a renaissance, however it is suboptimal for views and interactions that are of some complexity. Good descriptive or predictive analytics, financial performance analytics, data visualization and even real time behavior and interactivity are best modeled as an explicit DAG, or for practical purposes, a data flow realization inspired by Functional Reactive Programming.

There is a stream of FRP libraries and having used RxJS and kefir.js, I wanted to try flyd for its compatibility with functional programming. The approach should work with any of the libraries but flyd is cool and minimal.

Note that this is just an experiment with certain goals, not an example for any of these: performance optimized code (streams proliferate in this version and is choppy on non-Chrome); idiomatic use of d3.js, FRP or ramda; production quality code; easy to debug code that is complete or free of glitches.

The goal of the experiment was to take the FRP paradigm to an unusually granular level to learn new lessons. In particular, FRP streams (approximations of mathematical variables) are being passed around, as opposed to JavaScript variables. As a result, entire swaths of the d3.js rendering tree are avoided - which has key performance benefits - such a way that it doesn't mandate the unnatural slicing up of complex views into 'containers' or 'widgets'. Modularity is a good idea, best done on natural boundaries, which may or may not coincide with performance considerations. An FRP approach allows only those parts to be updated that need to change. Admittedly there's contention between approaches of d3.js, FRP and react.js for the control of who mutates the DOM, and the current example can be changed to work with vanilla HTML5 mutations, react.js or canvas/WebGL operations. In fact, the biggest problem with the example is that it clashes with d3 transitions. A next version will take care of transitions such that increasing the number of visible points doesn't result in choppiness.

The performance benefits partly hinge on conscious structuring of the scene graph, which was the subject of an earlier experiment. For example, a zooming functionality could rely on using a element as the slippy layer so that scaling is only done on a single element (with vector-effect: non-scaling-stroke).

Important benefits come from FRP not just via incremental updates, and architecture (e.g. safer cacheability of pure functions). Due to its asynchronous nature, one can steer processing steps into a Web Worker (example of that will be posted), which takes advantage of modern hardware and more importantly, helps avoid locking up or framerate issues in the main event loop. With FRP, it's easier to loop in server communication as well.

Input processing, such as new data points, and datavis manipulation (mouse, keyboard) is done naturally with FRP. While this example just uses a dat.gui component for tweaking various levers, the pertaining principles of FRP hold true. For lack of time this example doesn't provide direct manipulation via inverse scales etc. but another small example, for mobile devices, does.

I think the main benefit of reactive programming is that the shape of the code, and the easy ability to trace dependencies in the code (input/output of data flow graph nodes) correspond to the domain (e.g. visualizing some aggregate statistics, or on a more specific example: finance, or healthcare...) rather than a triangle that focuses on the means, not the content, causing object lifecycle, asynchronous communication verbosity, fragility and testability issues.

A larger purpose of the exercise was to make a nod toward direct manipulation as advocated by Bret Victor. While there are other, really interesting efforts in progress, this experiment aimed at sticking to the almost bare-DOM d3.js abstractions rather than tying the experiment to a declarative datavis language the way the awesome Grammar of Graphics inspired tools does. Inventing new, declarative data visualization languages is very promising but on client projects I need to work with the freedom and common standard that SVG, HTML, Canvas and WebGL can give.

In the case of data visualization, there may be an enormous difference between the several seconds latency that code change involves, and the immediacy that direct manipulation yields. Ideally, sizes, constraints, layout and shapes would evolve under the hands of the maker, trying to turn the datavis medium malleable and fluid. It might lead to lower cost of bespoke data visualizations, possibilities in citizen journalism, timeboxed newsroom processes, or accidental discovery of interesting graphics.

Bandlines were chosen for the experiment due to the fact that they're simple and clean, yet of sufficient complexity to serve as demonstration ground for the data flow concept. As seen in Stephen Few's design document, they require the calculation of aggregate statistics. Also, I'm interested in innovative, data-rich approaches such as bandlines when it comes to time series, financial charting, indicators and on-chart annotations in general. Since horizon charts, bandlines and Tufte's banded sparklines are relatively new type of graphics, it's exciting to tweak them and explore the continuity of visualization space among such differing approaches, their edge cases and configuration.

License

/**
* Bandline styling
*/
body {
background-color: white;
}
g.bands .band {stroke-width: 1px;}
g.bandline,
g.sparkStrip {
fill: none;
}
g.sparkStrip .valuePoints,
g.bandline .valuePoints .point.highOutlier {
fill-opacity: 0
}
g.bandline .valuePoints .point {
fill-opacity: 0.5;
}
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.lowOutlier {
fill-opacity: 1;
}
g.bandline .valuePoints .point.highOutlier {
fill: white;
fill-opacity: 1;
}
g.sparkStrip .valueBox {
fill: white;
fill-opacity: 0.75;
}
define(['datgui'], function(datgui) {
var v = datgui('Bandline tweaks', 'close', {
ease: {value: 'elastic', type: Categorical, categories: easeCategories},
advanceEase: {value: 'linear', type: Categorical, categories: easeCategories},
duration: {value: 300, type: Number, min: 0, max: 1000}
})
var styling = datgui('Bandline colors', 'close', {
bandlineColor: {value: [5, 48, 97], type: Color},
opacity: {value: 1, type: Number, min: 0, max: 1},
fillOpacity: {value: 0.75, type: Number, min: 0, max: 1},
fillBand0: {value: [0,0,0] , type: Color},
fillBand1: {value: [253, 219, 199], type: Color},
fillBand2: {value: [244, 165, 130], type: Color},
fillBand3: {value: [146, 197, 222], type: Color},
fillBand4: {value: [209, 229, 240], type: Color},
fillBand5: {value: [0,0,0,0] , type: Color},
fillBand6: {value: [0,0,0,0] , type: Color},
strokeOpacity: {value: 1, type: Number, min: 0, max: 1},
strokeBand0: {value: [244, 165, 130], type: Color},
strokeBand1: {value: [0,0,0,0] , type: Color},
strokeBand2: {value: [0,0,0,0] , type: Color},
strokeBand3: {value: [0,0,0,0] , type: Color},
strokeBand4: {value: [0,0,0,0] , type: Color},
strokeBand5: {value: [146, 197, 222], type: Color},
strokeBand6: {value: [255, 255, 255], type: Color}
})
var bandlineFills = _.tupleOf(
styling.fillBand0,
styling.fillBand1,
styling.fillBand2,
styling.fillBand3,
styling.fillBand4,
styling.fillBand5,
styling.fillBand6
)
var bandlineStrokes = _.tupleOf(
styling.strokeBand0,
styling.strokeBand1,
styling.strokeBand2,
styling.strokeBand3,
styling.strokeBand4,
styling.strokeBand5,
styling.strokeBand6
)
var bandlineColor = toColor(styling.bandlineColor)
var transitionedAttribute = transitionAttribute(v.duration)
var easeAttribute = transitionedAttribute(v.ease)
var quickEasedAttribute = transitionedAttribute($('cubic-out'))
var advanceTransform = transitionAttribute(R.__, v.advanceEase, R.__, 'transform')
/**
* Bandline renderer
*/
var rectanglePath = function(xr, yr) {return d3.svg.line()([[xr[0], yr[0]], [xr[1], yr[0]], [xr[1], yr[1]], [xr[0], yr[1]]]) + 'Z'}
var bandlinePath = R.curry(function(valueAccessor, xScale, d) {
return d3.svg.line().defined(R.compose(defined, R.prop(1)))(valueAccessor(d).map(function(s) {return [xScale(s.key), s.value]}))
})
var bandData = _(R.curry(function(bands, yScaler, d) {return bands.map(function(band, i) {return {key: i, value: band, yScale: yScaler(d)}})}))
var bandPath = _(R.unapply(R.converge(rectanglePath)))
function renderBands(root, bands, yScaler, xRanger, yRanger) {
var bandSet = bind(root, 'bands', 'g', of)
var band = bind(bandSet, 'band', 'path', bandData(bands, yScaler))
var color = _(R.curry(function(a, d, i) {return R.type(a[i]) === 'String' ? a[i] : 'rgb(' + a[i].map(Math.round).join(',') + ')'}))
setAttributes(band, {
fill: color(bandlineFills),
stroke: color(bandlineStrokes),
opacity: styling.opacity,
'fill-opacity': styling.fillOpacity,
'stroke-opacity': styling.strokeOpacity
})
easeAttribute(band, 'd', bandPath(xRanger, yRanger))
}
var pointData = _(R.curry(function(valueAccessor, d) {
return valueAccessor(d).map(function(value) {return {key: value.key, value: value.value, o: d}}).filter(R.compose(defined, value))
}))
function renderPoints(root, valueAccessor, pointStyleAccessor, rScale, xSpec, ySpec) {
var points = pointData(valueAccessor)
var valuePoints = bind(root, 'valuePoints', 'g', points)
setAttributes(valuePoints, {transform: _(translate)(xSpec, ySpec)})
var point = bind(valuePoints, 'point', 'circle', of)
setAttributes(point, {
class: _(R.curry(function(accessor, d) {return 'point ' + accessor(d.value)}))(pointStyleAccessor),
fill: bandlineColor,
stroke: bandlineColor
})
quickEasedAttribute(point, 'r', _(R.curry(function(accessor, scale, d) {return scale(accessor(d.value))}))(pointStyleAccessor, rScale))
exitRemove(valuePoints)
}
var valuesExtent = function(valueAccessor, d) {return d3.extent(valueAccessor(d).map(value).filter(defined))}
var extentBoxPathStream = _(R.curry(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]
)
return path
}
))
function renderExtent(root, valueAccessor, xScale, yRange) {
var extentBox = streamBind(root, 'valueBox', 'path')
streamsAttr(extentBox, 'stroke', bandlineColor)
easeAttribute(extentBox, 'd', extentBoxPathStream(valueAccessor, xScale, yRange))
}
function renderValueLine(root, valueAccessor, xScale, yScaler) {
var line = streamBind(root, 'valueLine', 'path')
var scaler = R.curry(function(yScaler, d) {return 'scale(1,' + (yScaler(d)(1) - yScaler(d)(0)) + ') translate(0,' + -d3.mean(yScaler(d).domain()) + ') '})
setAttributes(line, {
d: _(bandlinePath)(valueAccessor, xScale),
stroke: bandlineColor
})
easeAttribute(line, 'transform', _(scaler)(yScaler))
}
function renderBandLineData(root, valueAccessor, xScaleOfBandLine, yScalerOfBandLine, pointStyleAccessor, rScaleOfBandLine, advanceDuration) {
var clippedRoot = streamBind(root, 'sparklineSliderClippedStationaryPart')
streamsAttr(clippedRoot, 'clip-path', $('url(#bandlinePaddedClippath)'))
var xform = _(function(scale) {return translateX(scale(0) - scale(1))})
var holder = streamBind(clippedRoot, 'bandlineHolder')
var holderTransform = advanceTransform(R.__, holder)
holderTransform($(0), $(null))
holderTransform(advanceDuration, xform(xScaleOfBandLine))
renderValueLine(holder, valueAccessor, xScaleOfBandLine, yScalerOfBandLine)
renderPoints(holder, valueAccessor, pointStyleAccessor, rScaleOfBandLine,
_(function(scale) {return R.compose(scale, key)})(xScaleOfBandLine),
_(R.curry(function(scale, d) {return scale(d.o)(d.value)}))(yScalerOfBandLine))
}
var deriveScale = _(R.curry(function(domain, range, d3scaleBasis) {return makeScale(d3scaleBasis.copy(), domain, range)}))
var streamBandlineYScaler = _(R.curry(function(accessor, range, d) {return d3.scale.linear().domain(valuesExtent(accessor, d)).range(range)}))
var streamConstantMeanScale = _(function(a) {return R.always(d3.mean(a))})
var streamSortedArray = function(yRange, direction) {return _(function(range) {return range.slice().sort(direction)})(yRange)}
var clippathPaddingStream = _(function(scale) {return d3.max(scale.range()) + 1}) // assuming a 1px thick circle stroke
var paddedRange = _(function(range, padding) {return [range[0] - padding, range[1] + padding]})
var bandlineUnpaddedClippath = "bandlineUnpaddedClippath"
return R.curry(function bandline(valueAccessor, contextValueAccessor, rDomainOfBandLine, bands, pointStyleAccessor,
xDomainOfBandLine, xDomainOfSparkStrip) {
return function bandline_view(xRangeOfBandLine, xRangeOfSparkStrip, rRangeOfBandLine,
rScaleOfSparkStrip, yRange, yRangeOfSparkStrip, advanceDuration, rootSvgStream) {
// Scale streams
var xScaleOfBandLine = deriveScale(xDomainOfBandLine, xRangeOfBandLine, $(d3.scale.linear()))
var xScaleOfSparkStrip = deriveScale(xDomainOfSparkStrip, xRangeOfSparkStrip, $(d3.scale.linear()))
var rScaleOfBandLine = deriveScale(rDomainOfBandLine, rRangeOfBandLine, $(d3.scale.ordinal()))
var yScalerOfBandLine = streamBandlineYScaler(contextValueAccessor, yRange)
var yScalerOfSparkStrip = streamConstantMeanScale(yRangeOfSparkStrip)
var clipPath = _(function(xScale, yRange) {return rectanglePath(xScale.range(), yRange)})
function addDefs(rootSvg) {
var yRangeUnpadded = streamSortedArray(yRange, d3.ascending)
var clippathPadding = clippathPaddingStream(rScaleOfBandLine)
var yRangePadded = paddedRange(yRangeUnpadded, clippathPadding)
var defs = bind(rootSvg, 'defs', 'defs', _(function(root) {return root.datum() ? root.data : [{key: 0}]})(rootSvg))
var paddedClip = streamBind(defs, 'paddedClipPath', 'clipPath')
streamsAttr(paddedClip, 'id', $('bandlinePaddedClippath'))
var paddedClipPath = streamBind(paddedClip, 'path', 'path', [{key: 0}])
streamsAttr(paddedClipPath, 'd', clipPath(xScaleOfBandLine, yRangePadded))
var unpaddedClip = streamBind(defs, 'unpaddedClipPath', 'clipPath')
streamsAttr(unpaddedClip, 'id', $(bandlineUnpaddedClippath))
var unpaddedClipPath = streamBind(unpaddedClip, 'path', 'path', [{key: 0}])
streamsAttr(unpaddedClipPath, 'd', clipPath(xScaleOfBandLine, yRangeUnpadded))
}
function renderBandLine(root) {
var bandline = streamBind(root, 'bandline')
var clippedBands = streamBind(bandline, 'bandlineHolderClipped')
streamsAttr(clippedBands, 'clip-path', $(url(bandlineUnpaddedClippath)))
renderBands(clippedBands, bands, yScalerOfBandLine, _(function(scale) {
return R.always(scale.range())
})(xScaleOfBandLine), $(function(d) {return d.value.map(d.yScale)}))
renderBandLineData(bandline, valueAccessor, xScaleOfBandLine, yScalerOfBandLine, pointStyleAccessor, rScaleOfBandLine, advanceDuration)
}
function renderSparkStrip(root) {
var sparkStrip = streamBind(root, 'sparkStrip')
renderBands(sparkStrip, bands, yScalerOfSparkStrip, _(R.curry(function(scale, d) {return d.value.map(scale)}))(xScaleOfSparkStrip), _(R.always)(yRangeOfSparkStrip))
renderExtent(sparkStrip, valueAccessor, xScaleOfSparkStrip, yRangeOfSparkStrip)
renderPoints(sparkStrip, valueAccessor, pointStyleAccessor, rScaleOfSparkStrip, _(function(fun) {return R.compose(fun, value)})(xScaleOfSparkStrip), yScalerOfSparkStrip)
}
addDefs(rootSvgStream)
return function(_bandlineRoot, _sparkStripRoot) {
renderBandLine(_bandlineRoot)
renderSparkStrip(_sparkStripRoot)
}
}
})
})
/*
The MIT License (MIT)
Copyright (c) 2014 Liam Brummitt
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.
*/
.dg.main.taller-than-window .close-button {
border-top: 1px solid #ddd;
}
.dg.main .close-button {
background-color: #e8e8e8;
}
.dg.main .close-button:hover {
background-color: #ddd;
}
.dg {
color: #555;
text-shadow: none !important;
}
.dg.main::-webkit-scrollbar {
background: #fafafa;
}
.dg.main::-webkit-scrollbar-thumb {
background: #bbb;
}
.dg li:not(.folder) {
background: #fafafa;
border-bottom: 1px solid #ddd;
}
.dg li.save-row .button {
text-shadow: none !important;
}
.dg li.title {
background: #e8e8e8 url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==) 6px 10px no-repeat;
}
.dg .cr.function:hover,.dg .cr.boolean:hover {
background: #fff;
}
.dg .c input[type=text] {
background: #e9e9e9;
}
.dg .c input[type=text]:hover {
background: #eee;
}
.dg .c input[type=text]:focus {
background: #eee;
color: #555;
}
.dg .c .slider {
background: #e9e9e9;
}
.dg .c .slider:hover {
background: #eee;
}
.dg .cr.number input[type=text],
.dg .cr.string input[type=text] {
color: darkslategrey;
}
.dg .c .slider-fg {
background-color: lightgrey;
}
define(['datgui'], function(datgui) {
var v = datgui('Data tweaks', 'close', {
historyShownLength: {value: 15, type: Integer, min: 3, max: 60},
historyContextLength: {value: 15, type: Integer, min: 15, max: 600},
tserLabelString: {value: ['Insulin-like growth factor', 'Von Willebrand Factor', 'Voltage-gated 6T & 1P',
'Mechanosensitive ion ch.', 'GABAA receptor positive', 'Epidermal growth factor',
'Signal recognition particle'].join(','), type: String},
timeCadence: {value: 1000, type: Number, min: 0, max: 5000},
pause: {value: false, type: Boolean}
}
)
var tserLabels = _(R.split(','))(v.tserLabelString)
var samples = __([], R.always(void(0)))
var sampler = R.curry(function(tserLabels, time) {
return R.zipObj(tserLabels, tserLabels.map(function() {return {key: time, value: rnorm(15, 1)}}))
})
var initialLength = v.historyContextLength()
var initialHistory = R.map(sampler(tserLabels()))(R.range(0, initialLength))
function generateSample(tserLabels, time) {
samples(sampler(tserLabels, time))
}
var time = initialLength
var samplesHistoricalContext = __([samples], function() {
var newHistory = R.concat(samplesHistoricalContext(), [samples()])
samplesHistoricalContext(R.slice(-v.historyContextLength(), newHistory.length, newHistory))
})(initialHistory)
function sample() {
if (!v.pause()) generateSample(tserLabels(), time++)
window.setTimeout(sample, v.timeCadence())
}
window.setTimeout(sample, 0)
function tserMaker(historyShownLength, 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][d]
})(range)
return {
key: d,
contextValue: full,
value: R.compose(R.slice(R.__, full.length, full), R.negate)(historyShownLength)
}
})
return tsers
}
return _(tserMaker)(v.historyShownLength, samplesHistoricalContext)
})
define([], function() {
var settings = {
"preset": "Few/Monochrome",
"closed": false,
"remembered": {
"Default": {
"0": {
"historyShownLength": 15,
"historyContextLength": 15,
"tserLabelString": "Insulin-like growth factor,Von Willebrand Factor,Voltage-gated 6T & 1P,Mechanosensitive ion ch.,GABAA receptor positive,Epidermal growth factor,Signal recognition particle",
"timeCadence": 1000,
"pause": false
},
"1": {
"ease": "elastic",
"advanceEase": "linear",
"duration": 300
},
"2": {
"bandlineColor": "#053061",
"opacity": 1,
"fillOpacity": 0.75,
"fillBand0": [
0,
0,
0
],
"fillBand1": [
253,
219,
199
],
"fillBand2": [
244,
165,
130
],
"fillBand3": [
146,
197,
222
],
"fillBand4": [
209,
229,
240
],
"fillBand5": [
0,
0,
0,
0
],
"fillBand6": [
0,
0,
0,
0
],
"strokeOpacity": 1,
"strokeBand0": [
244,
165,
130
],
"strokeBand1": [
0,
0,
0,
0
],
"strokeBand2": [
0,
0,
0,
0
],
"strokeBand3": [
0,
0,
0,
0
],
"strokeBand4": [
0,
0,
0,
0
],
"strokeBand5": [
146,
197,
222
],
"strokeBand6": [
255,
255,
255
]
},
"3": {
"iqrDistanceMultiplier": 1.5,
"interQuantileRange": 0.5
},
"4": {
"rowPitch": 33.72014574440797,
"sparkstripHeightRatio": 0.8155373009661485,
"bandlineHeightRatio": 0.8155373009661485,
"nameColumnWidth": 165,
"sparkStripWidth": 50,
"bandlineWidth": 160,
"columnSeparation": 6.050760620071425,
"rOfSparkStrip": 2,
"panelWidth": 600,
"panelHeight": 370,
"nameColumnName": "Protein",
"spreadColumnName": "Spread",
"bandlineColumnName": "Time Series",
"advanceDuration": 1000,
"ease": "cubic-out",
"duration": 300
}
},
"Few/Monochrome": {
"0": {
"historyShownLength": 15,
"historyContextLength": 15,
"tserLabelString": "Insulin-like growth factor,Von Willebrand Factor,Voltage-gated 6T & 1P,Mechanosensitive ion ch.,GABAA receptor positive,Epidermal growth factor,Signal recognition particle",
"timeCadence": 1000,
"pause": false
},
"1": {
"ease": "cubic-out",
"advanceEase": "linear",
"duration": 300
},
"2": {
"bandlineColor": "#c71ad2",
"opacity": 1,
"fillOpacity": 0.75,
"fillBand0": [
0,
0,
0
],
"fillBand1": [
227.49999999999997,
227.49999999999997,
227.49999999999997
],
"fillBand2": [
200,
200,
200
],
"fillBand3": [
190.61274509803923,
191.87919246646027,
192.5
],
"fillBand4": [
147.49999999999997,
147.49999999999997,
147.49999999999997
],
"fillBand5": [
0,
0,
0,
0
],
"fillBand6": [
0,
0,
0,
0
],
"strokeOpacity": 1,
"strokeBand0": [
17.499999999999993,
17.024423804609558,
16.813725490196074
],
"strokeBand1": [
0,
0,
0,
0
],
"strokeBand2": [
0,
0,
0,
0
],
"strokeBand3": [
0,
0,
0,
0
],
"strokeBand4": [
0,
0,
0,
0
],
"strokeBand5": "#0c0c0c",
"strokeBand6": [
255,
255,
255
]
},
"3": {
"iqrDistanceMultiplier": 1.2824981749064432,
"interQuantileRange": 0.5
},
"4": {
"rowPitch": 46.80820404217116,
"sparkstripHeightRatio": 0.85,
"bandlineHeightRatio": 0.85,
"nameColumnWidth": 165,
"sparkStripWidth": 69.36701809308963,
"bandlineWidth": 160,
"columnSeparation": 7.8922964609627275,
"rOfSparkStrip": 2.433458075463508,
"panelWidth": 600,
"panelHeight": 489.35329207416123,
"nameColumnName": "Protein",
"spreadColumnName": "Sparkstrip",
"bandlineColumnName": "Bandline",
"advanceDuration": 1000,
"ease": "cubic-out",
"duration": 300
}
},
"Few/ColorBrewer": {
"0": {
"historyShownLength": 15,
"historyContextLength": 15,
"tserLabelString": "Insulin-like growth factor,Von Willebrand Factor,Voltage-gated 6T & 1P,Mechanosensitive ion ch.,GABAA receptor positive,Epidermal growth factor,Signal recognition particle",
"timeCadence": 1000,
"pause": false
},
"1": {
"ease": "elastic",
"duration": 300
},
"2": {
"bandlineColor": "#053061",
"opacity": 1,
"fillOpacity": 0.75,
"fillBand0": [
0,
0,
0
],
"fillBand1": [
253,
219,
199
],
"fillBand2": [
244,
165,
130
],
"fillBand3": [
146,
197,
222
],
"fillBand4": [
209,
229,
240
],
"fillBand5": [
0,
0,
0,
0
],
"fillBand6": [
0,
0,
0,
0
],
"strokeOpacity": 1,
"strokeBand0": [
244,
165,
130
],
"strokeBand1": [
0,
0,
0,
0
],
"strokeBand2": [
0,
0,
0,
0
],
"strokeBand3": [
0,
0,
0,
0
],
"strokeBand4": [
0,
0,
0,
0
],
"strokeBand5": [
146,
197,
222
],
"strokeBand6": [
255,
255,
255
]
},
"3": {
"iqrDistanceMultiplier": 1.5,
"interQuantileRange": 0.5
},
"4": {
"rowPitch": 33.72014574440797,
"sparkstripHeightRatio": 1,
"bandlineHeightRatio": 0.8155373009661485,
"nameColumnWidth": 165,
"sparkStripWidth": 50,
"bandlineWidth": 160,
"columnSeparation": 6.050760620071425,
"rOfSparkStrip": 2,
"panelWidth": 600,
"panelHeight": 370,
"nameColumnName": "Protein",
"spreadColumnName": "Spread",
"bandlineColumnName": "Time Series",
"advanceDuration": 1000,
"ease": "cubic-out",
"duration": 300
}
},
"Tufte/Bands": {
"0": {
"historyShownLength": 15,
"historyContextLength": 15,
"tserLabelString": "Insulin-like growth factor,Von Willebrand Factor,Voltage-gated 6T & 1P,Mechanosensitive ion ch.,GABAA receptor positive,Epidermal growth factor,Signal recognition particle",
"timeCadence": 1000,
"pause": false
},
"1": {
"ease": "elastic",
"duration": 300
},
"2": {
"bandlineColor": "#000000",
"opacity": 1,
"fillOpacity": 0.75,
"fillBand0": [
0,
0,
0
],
"fillBand1": "#ffffff",
"fillBand2": "#a0ddff",
"fillBand3": "#a2e0ff",
"fillBand4": [
255,
255,
255
],
"fillBand5": [
0,
0,
0,
0
],
"fillBand6": [
0,
0,
0,
0
],
"strokeOpacity": 1,
"strokeBand0": [
255,
255,
255
],
"strokeBand1": [
0,
0,
0,
0
],
"strokeBand2": [
0,
0,
0,
0
],
"strokeBand3": [
0,
0,
0,
0
],
"strokeBand4": [
0,
0,
0,
0
],
"strokeBand5": [
255,
255,
255
],
"strokeBand6": [
255,
255,
255
]
},
"3": {
"iqrDistanceMultiplier": 1.5,
"interQuantileRange": 0.5
},
"4": {
"rowPitch": 40,
"sparkstripHeightRatio": 0,
"bandlineHeightRatio": 0.85,
"nameColumnWidth": 165,
"sparkStripWidth": 1,
"bandlineWidth": 160,
"columnSeparation": 6,
"rOfSparkStrip": 0,
"panelWidth": 600,
"panelHeight": 370,
"nameColumnName": "Name",
"spreadColumnName": "",
"bandlineColumnName": "Time Series",
"advanceDuration": 1000,
"ease": "cubic-out",
"duration": 300
}
},
"Tufte/Sparkline": {
"0": {
"historyShownLength": 15,
"historyContextLength": 15,
"tserLabelString": "Insulin-like growth factor,Von Willebrand Factor,Voltage-gated 6T & 1P,Mechanosensitive ion ch.,GABAA receptor positive,Epidermal growth factor,Signal recognition particle",
"timeCadence": 1000,
"pause": true
},
"1": {
"ease": "elastic",
"duration": 300
},
"2": {
"bandlineColor": "#000000",
"opacity": 1,
"fillOpacity": 0.75,
"fillBand0": [
0,
0,
0
],
"fillBand1": "#ffffff",
"fillBand2": "#ffffff",
"fillBand3": [
255,
255,
255
],
"fillBand4": [
255,
255,
255
],
"fillBand5": [
0,
0,
0,
0
],
"fillBand6": [
0,
0,
0,
0
],
"strokeOpacity": 1,
"strokeBand0": [
255,
255,
255
],
"strokeBand1": [
0,
0,
0,
0
],
"strokeBand2": [
0,
0,
0,
0
],
"strokeBand3": [
0,
0,
0,
0
],
"strokeBand4": [
0,
0,
0,
0
],
"strokeBand5": [
255,
255,
255
],
"strokeBand6": [
210,
210,
210
]
},
"3": {
"iqrDistanceMultiplier": 1.5,
"interQuantileRange": 0.5
},
"4": {
"rowPitch": 40,
"sparkstripHeightRatio": 0,
"bandlineHeightRatio": 0.85,
"nameColumnWidth": 165,
"sparkStripWidth": 1,
"bandlineWidth": 160,
"columnSeparation": 6,
"rOfSparkStrip": 0,
"panelWidth": 600,
"panelHeight": 370,
"nameColumnName": "Name",
"spreadColumnName": "",
"bandlineColumnName": "Time Series",
"advanceDuration": 1000,
"ease": "cubic-out",
"duration": 300
}
}
},
"folders": {
"Data tweaks": {
"preset": "Default",
"closed": true,
"folders": {}
},
"Bandline tweaks": {
"preset": "Default",
"closed": true,
"folders": {}
},
"Bandline colors": {
"preset": "Default",
"closed": true,
"folders": {}
},
"Model tweaks": {
"preset": "Default",
"closed": true,
"folders": {}
},
"Layout tweaks": {
"preset": "Default",
"closed": true,
"folders": {}
}
}
}
function inputStreams(folderName, status, variables) {
var v = variableDescriptionsToStreams(variables)
var tweaksGui = gui.addFolder(folderName)
if(status === 'open') tweaksGui.open(); else tweaksGui.close()
addToDatGui(gui, tweaksGui, variables, v)
return v
}
var variableDescriptionsToStreams = R.mapObj(function(varDesc) {return __([], R.always(varDesc.value))})
function addToDatGui(datgui, layoutTweaksGui, variables, v) {
var datguiConfigObject = R.mapObj(R.call)(v)
datgui.remember(datguiConfigObject)
Object.keys(v).forEach(function (streamKey) {
var stream = v[streamKey]
var varDesc = variables[streamKey]
var lowBound, highBound
if (varDesc.type === Number || varDesc.type === Integer) {
layoutTweaksGui.add(datguiConfigObject, streamKey, varDesc.min, varDesc.max).onChange(stream)
} else if (varDesc.type === Categorical) {
layoutTweaksGui.add(datguiConfigObject, streamKey, varDesc.categories).onChange(stream)
} else if (varDesc.type === Color) {
layoutTweaksGui.addColor(datguiConfigObject, streamKey).onChange(stream)
} else {
layoutTweaksGui.add(datguiConfigObject, streamKey).onChange(stream)
}
})
}
var gui = new dat.GUI({
width: 400,
load: settings,
preset: 'Default',
closed: true // doesn't work with the linked cdn version
})
return inputStreams
})
var key = R.prop('key')
var value = R.prop('value')
var window2 = R.aperture(2)
var nullary = R.nAry(0)
var __ = flyd
function Integer() {}
function Categorical() {}
function Color() {}
var defined = R.complement(R.isNil)
// lookup :: dByFunction -> array -> d -> array[d(dByFunction)]
var lookup = R.flip(R.useWith(R.compose, [R.flip(R.prop), R.call]))
_.tupleOf = _(R.unapply(R.identity))
var exitRemove = _(function(binding) {binding.exit().remove()})
var entered = _(function(binding) {return binding.entered})
function url(string) {
return 'url(#' + string + ')'
}
function streamBind(stream, cssClass, element, data) {
return __([stream], function() {
return bind0(stream(), cssClass, element, data)
})
}
function $(constant) {
return _(R.always(constant))()
}
function bind(stream, cssClass, element, data) {
return __([stream, data], function() {
return bind0(stream(), cssClass, element, data())
})
}
var streamsAttr = R.curry(function(stream, attrName, attrFunStream) {
return _(function(stream, attrFunStream) {
stream.attr(attrName, attrFunStream)
})(stream, attrFunStream)
})
var easeCategories = ['elastic', 'cubic-out', 'cubic-in', 'cubic-in-out', 'linear', 'bounce']
var compose2 = R.curry(R.binary(R.compose))
var pipe2 = R.curry(R.binary(R.pipe))
// vectorAddMultiply :: [a] -> [b] -> m -> [a] * m + [b]
var vectorAddMultiply = R.curry(R.compose(compose2(R.map(R.apply(R.add))), R.apply(R.useWith(R.call, [R.compose(pipe2(R.zip), R.compose(pipe2, pipe2(R.multiply), R.flip(R.map))), R.identity])), R.pair))
var oldStreamsAttrs = R.compose(pipe2(R.toPairs), R.forEach, R.apply, streamsAttr)
var setAttributes = R.curry(function(binding, attrsObj) {
oldStreamsAttrs(binding)(attrsObj)
})
var transitionAttribute = R.curry(function(durationStream, easeStream, stream, attrName, attrFunStream) {
return _(function(stream, attrFun, duration, ease) {
stream.entered.attr(attrName, attrFun)
stream.transition().duration(duration).ease(ease).attr(attrName, attrFun)
})(stream, attrFunStream, durationStream || $(250), easeStream || $('cubic-out'))
})
var setText = _(function(stream, attrFunStream) {
stream.text(attrFunStream)
})
R.compose2 = compose2
var r = R.fromPairs(R.map(function(pair) {return [pair[0], R.curry(R.nAry(pair[1].length, _(pair[1])))]})(R.filter(function(pair) {return typeof pair[1] === 'function' && pair[1].length > 0})(R.toPairs(R))))
var of = $(R.of)
var makeScale = R.curry(function(scale, domain, range) {
return scale.domain(domain).range(range)
})
function _(fun) {
function streamify(fun) {
return function(/* stream1, stream2, .. , streamN */) {
var streams = Array.prototype.slice.call(arguments)
var i
var values = []
return __(streams, function () {
for (i = 0; i < streams.length; i++) values[i] = streams[i]()
return fun.apply(undefined, values)
})
}
}
var result = streamify(fun)
return R.curry(result)
}
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)
}
function bind0(rootSelection, cssClass, element, dataFlow) {
if(!cssClass) {throw Error('cssClass must be defined.')}
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 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) + ')'
}
}
var toColor = _(function(a) {
return R.type(a) === 'String' ? a : 'rgb(' + a.map(Math.round).join(',') + ')'
})
<!DOCTYPE html>
<meta charset='utf-8'>
<style>
body {
margin-left: 60px;
margin-top: 40px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
}
.header {
font-weight: bold;
fill: #777;
}
</style>
<link href="bandline.css" rel="stylesheet" type="text/css" />
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.18.0/ramda.min.js"></script>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.min.js" type="text/javascript"></script>
<link href="dat-gui-light-theme.css" rel="stylesheet" type="text/css" />
<script src="mini-flyd.js" type="text/javascript"></script>
<script src="du.js" type="text/javascript"></script>
<script data-main="main" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.22/require.min.js"></script>
<body>
<svg xmlns="http://www.w3.org/2000/svg"></svg>
</body>
Copyright (c) 2015, Robert Monfera.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of sf-student-dashboard nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
requirejs(['render'])
/*
flyd.js - The extraction of flyd's central function which was sufficient for the example to work.
It lacks many important functions of flyd, so use the original version on your project:
https://github.com/paldepind/flyd
However it is changed slightly to ensure that subsequent updates run in the same order
as the original order, which simplified the initial implementation. It may also be
simpler to study operations on this 100-line version.
Original license of flyd is included below.
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.
*/
var flyd = (function () {
var inStream
var order = []
var orderNextIdx = -1
// pure functions start here
function allDependentsHaveValues(stream) {
return stream.deps.every(function(s) {
return s.val !== void(0)
})
}
// pure functions end here
function updateStream(s) {
if (allDependentsHaveValues(s)) {
inStream = s
var returnVal = s.fn(s)
if (returnVal !== undefined) {
s(returnVal)
}
inStream = undefined
s.shouldUpdate = false
}
}
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]
list.shouldUpdate = true
findDeps(list)
}
for (; orderNextIdx >= 0; --orderNextIdx) {
o = order[orderNextIdx]
if (o.shouldUpdate === true) updateStream(o)
o.queued = false
}
}
function createStream() {
function stream(n) {
var i, list
if (arguments.length === 0) {
return stream.val
} else {
stream.val = n
if (inStream === undefined) {
updateDeps(stream)
} else if (inStream === stream) {
for (i = 0; i < stream.listeners.length; ++i) {
list = stream.listeners[i]
list.shouldUpdate = true
}
} else {
debugger;
throw new Error('Restore toUpdate') // was: toUpdate.push(s)
}
return stream
}
}
stream.val = void(0)
stream.listeners = []
stream.queued = false
return stream
}
function createDependentStream(deps, fn) {
var i, s = createStream()
s.fn = fn
s.deps = deps
s.shouldUpdate = false
for (i = 0; i < deps.length; ++i) {
deps[i].listeners.unshift(s)
}
return s
}
function stream(arg, fn) {
var s = createDependentStream(arg, fn)
updateStream(s)
return s
}
return stream
})()
define(['bandline', 'datgui'], function(bandline, datgui) {
var v = datgui('Model tweaks', 'close', {
iqrDistanceMultiplier: {value: 1.5, type: Number, min: 0, max: 3},
interQuantileRange: {value: 0.5, type: Number, min: 0, max: 1}
})
// Specification of q-quantiles (now q = 4 i.e. quartiles), with extra logic for tweaking the two bands adjacent
// to the midpoint (thus allowing the deviation from the standard IQR)
//
// minimum band (zero size) 1st band 2nd band 3rd band 4th band maximum band (zero size)
// ⤵ ↓ ↓ ↓ ↓ ⤹
// | ⭅⭆ | ⭅⭆ | ⭅⭆ | ⭅⭆ | ⭅⭆ | ⭅⭆ |
// q0, q0, q1, q2, q3, q4, q4
var thresholdAnchors = [ 0, 0, 0.5, 0.5, 0.5, 1, 1 ] // these are p values [0, 1]
var thresholdModifiers = [ 0, 0, -1 , 0 , 1 , 0, 0 ] // all zeros except a 1 and a -1
// q1 and q4 are adjustable - we can modify it by subtracting / adding, represented by the -1 / 1 value in the vector
// q0 (min) and q4 (max) are duplicated as they are both band edges and zero-height bands highlighting the domain extent
var iqrKeys = $(R.pair(1, -1))
var outlierClassifications = $(['lowOutlier', 'normal', 'highOutlier'])
var indexOfThresholdModifiers = R.indexOf(R.__, thresholdModifiers)
var iqrIndices = r.map($(indexOfThresholdModifiers), iqrKeys)
var atIqrIndices = r.props(iqrIndices)
var halfInterquantileRange = r.divide(v.interQuantileRange, $(2))
var medianLineBand = R.compose(R.of, R.compose(R.converge(R.pair, [R.identity, R.identity]), d3.median))
var outlierDomain = R.apply(R.curry(function(fun, x, y) {return fun(x)(y)})(R.converge(R.compose, [vectorAddMultiply([-1, 1]), R.compose(R.multiply, R.apply(R.subtract))])))
var makeOutlierScale = _(R.useWith(makeScale(d3.scale.threshold()), [outlierDomain, R.identity]))
var flattenAndSort = _(R.compose(R.sort(d3.ascending), R.map(R.propOr(null, 'value')), R.flatten, R.filter(defined), R.map(R.prop('contextValue'))))
var quantiles = _(vectorAddMultiply(thresholdModifiers, thresholdAnchors))
var thresholds = _(R.flip(R.useWith(R.map, [R.curry(d3.quantile), R.identity])))
var tserTimestamps = R.compose(R.map(key), R.filter(defined), R.flatten, R.map(value))
var tserTemporalDomain = _(R.compose2(d3.extent, tserTimestamps))
var bandBounds = _(R.useWith(R.concat, [window2, medianLineBand]))
var valueAccessor = $(R.compose(R.filter(defined), R.prop('value')))
var contextValueAccessor = $(R.compose(R.filter(defined), R.prop('contextValue')))
var bandQuantiles = quantiles(halfInterquantileRange)
var bandlineWithAccessors = bandline(valueAccessor, contextValueAccessor, outlierClassifications)
return function setupBandline(tsersStream) {
var temporalDomain = tserTemporalDomain(tsersStream)
var contextValuesSorted = flattenAndSort(tsersStream)
var bandThresholds = thresholds(bandQuantiles, contextValuesSorted)
return bandlineWithAccessors(
bandBounds(bandThresholds, contextValuesSorted),
makeOutlierScale(_.tupleOf(atIqrIndices(bandThresholds), v.iqrDistanceMultiplier), outlierClassifications),
temporalDomain,
_(d3.extent)(bandThresholds)
)
}
})
define(['data', 'model', 'datgui'], function(data, setupBandline, datgui) {
var v = datgui('Layout tweaks', 'close', {
rowPitch: {value: 40, type: Number, min: 1, max: 200},
sparkstripHeightRatio: {value: 0.85, type: Number, min: 0, max: 1},
bandlineHeightRatio: {value: 0.85, type: Number, min: 0, max: 1},
nameColumnWidth: {value: 165, type: Number, min: 50, max: 300},
sparkStripWidth: {value: 50, type: Number, min: 1, max: 100},
bandlineWidth: {value: 160, type: Number, min: 1, max: 300},
columnSeparation: {value: 6, type: Number, min: 0, max: 20},
rOfSparkStrip: {value: 2, type: Number, min: 0, max: 10},
panelWidth: {value: 600, type: Number, min: 300, max: 900},
panelHeight: {value: 450, type: Number, min: 100, max: 900},
nameColumnName: {value: 'Name', type: String},
spreadColumnName: {value: 'Spread', type: String},
bandlineColumnName: {value: 'Time Series', type: String},
advanceDuration: {value: 1000, type: Number, min: 0, max: 5000},
ease: {value: 'cubic-out', type: Categorical, categories: easeCategories},
duration: {value: 300, type: Number, min: 0, max: 1000}
})
// same as d3 .attr except on streams, and we preset the duration and easing here
var easedAttr = transitionAttribute(v.duration, v.ease)
// actual height of graphics is determined by rowPitch and respective height ratio streams
var rowPitchTimes = r.multiply(v.rowPitch)
var bandlineHeight = rowPitchTimes(v.bandlineHeightRatio)
var sparkStripHeight = rowPitchTimes(v.sparkstripHeightRatio)
// Top level
var svg = _(R.always(d3.selectAll('svg')))()
setAttributes(svg, {width: v.panelWidth, height: v.panelHeight})
var dashboard = bind(svg, 'dashboard', 'g', $([{key: 0}]))
var columnSet = bind(dashboard, 'columnSet', 'g', of)
easedAttr(columnSet, 'transform', _(translateY)(v.rowPitch))
// Columns
var columnNames = _.tupleOf(v.nameColumnName, v.spreadColumnName, v.bandlineColumnName)
var columnDescriptors = [
{key: 'nameColumnName', value: 0},
{key: 'spreadColumnName', value: 1},
{key: 'bandlineColumnName', value: 2}
]
var column = bind(columnSet, 'column', 'g', $(columnDescriptors))
// using IIFEs only to encapsulate internal values
var columnsX = function withLocalScope() {
var columnsXArray = function withLocalScope() {
var append = R.compose(R.converge(R.pair, [R.identity, R.identity]), R.add)
var cumulateWithZeroBasedSum = R.compose(R.nth(1), R.mapAccum(append, 0))
var separatedColumnWidths = R.useWith(R.map, [R.add, R.identity])
return R.compose(cumulateWithZeroBasedSum, R.concat([0]), separatedColumnWidths)
}()
var columnWidths = _.tupleOf(v.nameColumnWidth, v.sparkStripWidth)
return _(columnsXArray)(v.columnSeparation, columnWidths)
}()
var byIndexTranslateX = R.compose(translateX, lookup(value))
var columnTransformX = _(byIndexTranslateX)(columnsX)
easedAttr(column, 'transform', columnTransformX)
var headerCell = bind(column, 'headerCell', 'text', of)
var headerTextAccessor = R.curry(R.useWith(R.flip(R.prop), [R.identity, value]))
setText(headerCell, _(headerTextAccessor)(columnNames))
var row = bind(dashboard, 'row', 'g', data)
exitRemove(row)
var rowTransformY = R.compose(translateY, R.flip, pipe2(R.add(2)), R.multiply)
var half = R.multiply(0.5)
var minushalf = R.compose(R.negate, half)
var makePairOf = R.unapply(R.converge(R.pair))
var yRangeFun = makePairOf(half, minushalf)
easedAttr(row, 'transform', _(rowTransformY)(v.rowPitch))
var rowText = bind(row, 'nameCellText', 'text', of)
easedAttr(rowText, 'transform', _(R.compose(translateX, R.prop(0)))(columnsX))
setAttributes(rowText, {y: $('0.5em')})
setText(rowText, $(key))
var sparkstripCell = bind(row, 'sparkstripCell', 'g', of)
easedAttr(sparkstripCell, 'transform', _(R.compose(translateX, R.prop(1)))(columnsX))
var bandlineCell = bind(row, 'bandlineCell', 'g', of)
easedAttr(bandlineCell, 'transform', _(R.compose(translateX, R.prop(2)))(columnsX))
var xRangeOfSparkStrip = _(R.pair)($(0), v.sparkStripWidth)
var rScaleOfSparkStrip = _(R.always)(v.rOfSparkStrip)
var yRangeOfSparkStrip = _(yRangeFun)(sparkStripHeight)
var xRangeOfBandLine = _(R.pair)($(0), v.bandlineWidth)
var rRangeOfBandLine = $([2, 0, 2])
var yRange = _(yRangeFun)(bandlineHeight)
var bandlineCurriedWithModel = setupBandline(data)
var bandlineCurriedWithModelAndViewModel = bandlineCurriedWithModel(
xRangeOfBandLine,
xRangeOfSparkStrip,
rRangeOfBandLine,
rScaleOfSparkStrip,
yRange,
yRangeOfSparkStrip,
v.advanceDuration,
svg
)
bandlineCurriedWithModelAndViewModel(bandlineCell, sparkstripCell)
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment