Skip to content

Instantly share code, notes, and snippets.

@monfera
Last active September 10, 2016 11:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save monfera/023eb5fd07b575bf07cf7a0e2cf8cf43 to your computer and use it in GitHub Desktop.
Save monfera/023eb5fd07b575bf07cf7a0e2cf8cf43 to your computer and use it in GitHub Desktop.
Watercolor bandlines / sparklines
license: gpl-3.0

Watercolor and paper texture effect without programming. Prototype - best run on Chrome (Safari 10 will support arrow functions and Firefox will hopefully become faster). A cleanup of what appeared here.

Bandlines, sparklines weren't meant to operate as sketchy charts, it's just an example for such styling with plot types that I like a lot.

Forked from monfera's block: Fluid configuration of d3.js bandlines with FRP

Changes to the original:

  • the presence of filters in SVG tag in index.html
  • three lines in bandline.js link the new SVG filters to the bands, lines and extent box paths
  • line thickness was increased and opacity decreased for .valueLine

Inspired by Elijah Meek's work on sketchy visualizations.

Unlike these sketchy visuals, the watercolor and paper effects are done with filters:

  • noise based displacement filters yield wiggly lines and contours
  • a stripe pattern in the filter adds rough paper texture

A lesson learned is that what works with smooth paint doesn't work as well with texture. The texturization of the bands reveal that the bands (rect elements) don't move in sync with the value lines. Time permitting a real solution should transform such elements if there's a chance for rough fill style (patterns, sketchy and/or filter based texture). Also, in this specific solution all the bands are of a similar contour.

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: 2;
stroke-opacity: 0.5;
stroke: black;
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, {
filter: $('url(#bandFilter)'),
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, 'filter', $('url(#lineFilter)'))
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')
streamsAttr(holder, 'filter', $('url(#lineFilter)'))
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: cursive, sans-serif;
font-style: italic;
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 version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<filter id="lineFilter">
<feTurbulence baseFrequency=".05" type="fractalNoise" numOctaves="2"/>
<feDisplacementMap in="SourceGraphic" scale="6"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
<filter id="bandFilter">
<feImage x="0" y="0" width="100" height="200" result="imageFill"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="data:image/svg+xml;charset=utf-8,%3Csvg%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%22100px%22%20height%3D%22200px%22%20%20%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3Cpattern%20id%3D%22pattern%22%20patternUnits%3D%22userSpaceOnUse%22%20width%3D%2210%22%20height%3D%2210%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M0%2C8.239V10h1.761L0%2C8.239z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M5%2C0l5%2C5l0%2C0V3.238L6.762%2C0H5z%22%2F%3E%0A%20%20%20%20%20%20%3Cpolygon%20points%3D%220%2C3.239%200%2C5%205%2C10%206.761%2C10%20%22%2F%3E%0A%20%20%20%20%20%20%3Cpolygon%20points%3D%221.762%2C0%200%2C0%2010%2C10%2010%2C8.238%20%22%2F%3E%0A%20%20%20%20%3C%2Fpattern%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url%28%23pattern%29%22%20%2F%3E%0A%3C%2Fsvg%3E"/>
<feTile in="imageFill" result ="tilePattern"/>
<feComposite operator="in" in2="SourceGraphic" result="striped"/>
<feTurbulence baseFrequency=".05" type="fractalNoise" numOctaves="4"
result="texture" />
<feDisplacementMap in="striped" in2="texture"
scale="10" xChannelSelector="R" yChannelSelector="G"
result="displaced"/>
<feDisplacementMap in="SourceGraphic" in2="texture"
scale="10" xChannelSelector="R" yChannelSelector="G"
result="wiggled"/>
<feMerge>
<feMergeNode in="displaced" />
<feMergeNode in="wiggled" />
</feMerge>
</filter>
</defs>
</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