Last active
September 21, 2022 10:10
-
-
Save chrisprice/56bdd31c144a9880c9a2 to your computer and use it in GitHub Desktop.
low barrel
This file contains 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
/* Default Chart Styles */ | |
.chart { | |
} | |
/* Transparent overlay for interactive tools */ | |
.overlay { | |
stroke-width: 0px; | |
fill-opacity: 0; | |
} | |
/* Default Axis Styles */ | |
.axis .domain, | |
.axis .tick line { | |
fill: none; | |
stroke-width: 1.0; | |
stroke: #bbb; | |
} | |
.axis text { | |
font: 10px sans-serif; | |
} | |
.gridline { | |
stroke: #ccc; | |
stroke-width: 1.0; | |
} | |
.background { | |
stroke: #eee; | |
fill: #fefefe; | |
} | |
.candlestick>path { | |
stroke: #000; | |
} | |
.candlestick.down>path { | |
fill: #c60; | |
} | |
.candlestick.up>path { | |
fill: #6c0; | |
} | |
.ohlc>path { | |
stroke: #000; | |
} | |
.ohlc.down>path { | |
stroke: #c60; | |
} | |
.ohlc.up>path { | |
stroke: #6c0; | |
} | |
path.line { | |
fill: none; | |
stroke: #06c; | |
} | |
path.area { | |
fill: #9cf; | |
fill-opacity: 0.5; | |
} | |
.point>circle { | |
stroke: #06c; | |
fill: #9cf; | |
fill-opacity: 0.5; | |
} | |
/* Ruler/Measure Component Styles */ | |
.measure>line { | |
stroke: #00c; | |
stroke-width: 1; | |
} | |
.measure>line.horizontal, | |
.measure>line.vertical { | |
stroke-dasharray: 3, 3; | |
} | |
.measure>text { | |
font: 10px sans-serif; | |
} | |
.measure>text.horizontal { | |
text-anchor: middle; | |
} | |
.measure>text.vertical { | |
dominant-baseline: middle; | |
} | |
/* Fibonacci Fan Styles */ | |
.fan>* { | |
stroke: #ccc; | |
stroke-width: 1; | |
} | |
.fan>line.trend { | |
stroke: #69f; | |
} | |
.fan>polygon.area { | |
fill: #eee; | |
fill-opacity: 0.5; | |
stroke-width: 0; | |
} | |
/* Crosshairs Styles */ | |
.crosshairs>line { | |
stroke: #69f; | |
stroke-width: 1; | |
} | |
.crosshairs>text { | |
font: 10px sans-serif; | |
text-anchor: end; | |
} | |
.crosshairs>text.vertical { | |
dominant-baseline: hanging; | |
} | |
/* Annotations */ | |
.annotation>line { | |
fill: none; | |
stroke: #69f; | |
stroke-width: 1; | |
stroke-dasharray: 3, 3; | |
} | |
.annotation>text { | |
font: 10px sans-serif; | |
text-anchor: end; | |
} |
This file contains 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
/* globals window */ | |
window.fc = { | |
version: '0.0.0', | |
charts: {}, | |
indicators: { | |
algorithms: {} | |
}, | |
scale: { | |
discontinuity: {} | |
}, | |
series: {}, | |
tools: {}, | |
utilities: {} | |
}; | |
(function(d3, fc) { | |
'use strict'; | |
/** | |
* The extent function enhances the functionality of the equivalent D3 extent function, allowing | |
* you to pass an array of fields which will be used to derive the extent of the supplied array. For | |
* example, if you have an array of items with properties of 'high' and 'low', you | |
* can use <code>fc.utilities.extent(data, ['high', 'low'])</code> to compute the extent of your data. | |
* | |
* @memberof fc.utilities | |
* @param {array} data an array of data points, or an array of arrays of data points | |
* @param {array} fields the names of object properties that represent field values | |
*/ | |
fc.utilities.extent = function(data, fields) { | |
if (fields === null) { | |
return d3.extent(data); | |
} | |
// the function only operates on arrays of arrays, but we can pass non-array types in | |
if (!Array.isArray(data)) { | |
data = [data]; | |
} | |
// we need an array of arrays if we don't have one already | |
if (!Array.isArray(data[0])) { | |
data = [data]; | |
} | |
// the fields parameter must be an array of field names, but we can pass non-array types in | |
if (!Array.isArray(fields)) { | |
fields = [fields]; | |
} | |
// Return the smallest and largest | |
return [ | |
d3.min(data, function(d0) { | |
return d3.min(d0, function(d1) { | |
return d3.min(fields.map(function(f) { | |
return d1[f]; | |
})); | |
}); | |
}), | |
d3.max(data, function(d0) { | |
return d3.max(d0, function(d1) { | |
return d3.max(fields.map(function(f) { | |
return d1[f]; | |
})); | |
}); | |
}) | |
]; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.utilities.fn = { | |
identity: function(d) { return d; }, | |
index: function(d, i) { return i; }, | |
noop: function(d) { } | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
// the barWidth property of the various series takes a function which, when given an | |
// array of x values, returns a suitable width. This function creates a width which is | |
// equal to the smallest distance between neighbouring datapoints multiplied | |
// by the given factor | |
fc.utilities.fractionalBarWidth = function(fraction) { | |
return function(pixelValues) { | |
// return some default value if there are not enough datapoints to compute the width | |
if (pixelValues.length <= 1) { | |
return 10; | |
} | |
pixelValues.sort(); | |
// creates a new array as a result of applying the 'fn' function to | |
// the consecutive pairs of items in the source array | |
function pair(arr, fn) { | |
var res = []; | |
for (var i = 1; i < arr.length; i++) { | |
res.push(fn(arr[i], arr[i - 1])); | |
} | |
return res; | |
} | |
// compute the distance between neighbouring items | |
var neighbourDistances = pair(pixelValues, function(first, second) { | |
return Math.abs(first - second); | |
}); | |
var minDistance = d3.min(neighbourDistances); | |
return fraction * minDistance; | |
}; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.utilities.pointSnap = function(xScale, yScale, xValue, yValue, data) { | |
return function(xPixel, yPixel) { | |
var x = xScale.invert(xPixel), | |
y = yScale.invert(yPixel), | |
nearest = null, | |
minDiff = Number.MAX_VALUE; | |
for (var i = 0, l = data.length; i < l; i++) { | |
var d = data[i], | |
dx = x - xValue(d), | |
dy = y - yValue(d), | |
diff = Math.sqrt(dx * dx + dy * dy); | |
if (diff < minDiff) { | |
minDiff = diff; | |
nearest = d; | |
} else { | |
break; | |
} | |
} | |
return { | |
datum: nearest, | |
x: nearest ? xScale(xValue(nearest)) : xPixel, | |
y: nearest ? yScale(yValue(nearest)) : yPixel | |
}; | |
}; | |
}; | |
fc.utilities.seriesPointSnap = function(series, data) { | |
return function(xPixel, yPixel) { | |
var xScale = series.xScale(), | |
yScale = series.yScale(), | |
xValue = series.xValue ? series.xValue() : function(d) { return d.date; }, | |
yValue = (series.yValue || series.yCloseValue).call(series); | |
return fc.utilities.pointSnap(xScale, yScale, xValue, yValue, data)(xPixel, yPixel); | |
}; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
// a property that follows the D3 component convention for accessors | |
// see: http://bost.ocks.org/mike/chart/ | |
fc.utilities.property = function(initialValue) { | |
var accessor = function(newValue) { | |
if (!arguments.length) { | |
return accessor.value; | |
} | |
accessor.value = newValue; | |
return this; | |
}; | |
accessor.value = initialValue; | |
return accessor; | |
}; | |
// a property that follows the D3 component convention for accessors | |
// see: http://bost.ocks.org/mike/chart/ | |
fc.utilities.functorProperty = function(initialValue) { | |
var accessor = function(newValue) { | |
if (!arguments.length) { | |
return accessor.value; | |
} | |
accessor.value = d3.functor(newValue); | |
return this; | |
}; | |
accessor.value = d3.functor(initialValue); | |
return accessor; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
/** | |
* An overload of the d3.rebind method which allows the source methods | |
* to be rebound to the target with a different name. In the mappings object | |
* keys represent the target method names and values represent the source | |
* object names. | |
*/ | |
fc.utilities.rebind = function(target, source, mappings) { | |
if (typeof(mappings) !== 'object') { | |
return d3.rebind.apply(d3, arguments); | |
} | |
Object.keys(mappings) | |
.forEach(function(targetName) { | |
var method = source[mappings[targetName]]; | |
target[targetName] = function() { | |
var value = method.apply(source, arguments); | |
return value === source ? target : value; | |
}; | |
}); | |
return target; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.utilities.simpleDataJoin = function(parent, className, data, dataKey) { | |
// "Caution: avoid interpolating to or from the number zero when the interpolator is used to generate | |
// a string (such as with attr). | |
// Very small values, when stringified, may be converted to scientific notation and | |
// cause a temporarily invalid attribute or style property value. | |
// For example, the number 0.0000001 is converted to the string "1e-7". | |
// This is particularly noticeable when interpolating opacity values. | |
// To avoid scientific notation, start or end the transition at 1e-6, | |
// which is the smallest value that is not stringified in exponential notation." | |
// - https://github.com/mbostock/d3/wiki/Transitions#d3_interpolateNumber | |
var effectivelyZero = 1e-6; | |
// update | |
var updateSelection = parent.selectAll('g.' + className) | |
.data(data, dataKey || fc.utilities.fn.index); | |
// enter | |
// entering elements fade in (from transparent to opaque) | |
var enterSelection = updateSelection.enter() | |
.append('g') | |
.classed(className, true) | |
.style('opacity', effectivelyZero); | |
// exit | |
// exiting elements fade out (from opaque to transparent) | |
var exitSelection = d3.transition(updateSelection.exit()) | |
.style('opacity', effectivelyZero) | |
.remove(); | |
// all properties of the selection (which can be interpolated) will transition | |
updateSelection = d3.transition(updateSelection) | |
.style('opacity', 1); | |
updateSelection.enter = d3.functor(enterSelection); | |
updateSelection.exit = d3.functor(exitSelection); | |
return updateSelection; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.charts.linearTimeSeries = function() { | |
var xScale = fc.scale.dateTime(); | |
var yScale = d3.scale.linear(); | |
var xAxis = d3.svg.axis() | |
.scale(xScale) | |
.orient('bottom'); | |
var yAxis = d3.svg.axis() | |
.scale(yScale) | |
.orient('left'); | |
var linearTimeSeries = function(selection) { | |
selection.each(function(data) { | |
var container = d3.select(this); | |
var mainContainer = container.selectAll('svg') | |
.data([data]); | |
mainContainer.enter() | |
.append('svg') | |
.attr('overflow', 'hidden') | |
.layout('flex', 1); | |
var background = mainContainer.selectAll('rect.background') | |
.data([data]); | |
background.enter() | |
.append('rect') | |
.attr('class', 'background') | |
.layout({ | |
position: 'absolute', | |
top: 0, | |
right: 0, | |
bottom: 0, | |
left: 0 | |
}); | |
var plotAreaContainer = mainContainer.selectAll('g.plot-area') | |
.data([data]); | |
plotAreaContainer.enter() | |
.append('g') | |
.attr('class', 'plot-area') | |
.layout({ | |
position: 'absolute', | |
top: 0, | |
right: 0, | |
bottom: 0, | |
left: 0 | |
}); | |
var yAxisContainer = mainContainer.selectAll('g.y-axis') | |
.data([data]); | |
yAxisContainer.enter() | |
.append('g') | |
.attr('class', 'axis y-axis') | |
.layout({ | |
position: 'absolute', | |
top: 0, | |
right: 0, | |
bottom: 0 | |
}); | |
var xAxisContainer = container.selectAll('g.x-axis') | |
.data([data]); | |
xAxisContainer.enter() | |
.append('g') | |
.attr('class', 'axis x-axis') | |
.layout('height', 20); | |
container.layout(); | |
xScale.range([0, xAxisContainer.layout('width')]); | |
yScale.range([yAxisContainer.layout('height'), 0]); | |
xAxisContainer.call(xAxis); | |
yAxisContainer.call(yAxis); | |
var plotArea = linearTimeSeries.plotArea.value; | |
plotArea.xScale(xScale) | |
.yScale(yScale); | |
plotAreaContainer.call(plotArea); | |
}); | |
}; | |
fc.utilities.rebind(linearTimeSeries, xScale, { | |
xDiscontinuityProvider: 'discontinuityProvider', | |
xDomain: 'domain', | |
xNice: 'nice' | |
}); | |
fc.utilities.rebind(linearTimeSeries, yScale, { | |
yDomain: 'domain', | |
yNice: 'nice' | |
}); | |
fc.utilities.rebind(linearTimeSeries, xAxis, { | |
xTicks: 'ticks' | |
}); | |
fc.utilities.rebind(linearTimeSeries, yAxis, { | |
yTicks: 'ticks' | |
}); | |
linearTimeSeries.xScale = function() { return xScale; }; | |
linearTimeSeries.yScale = function() { return yScale; }; | |
linearTimeSeries.plotArea = fc.utilities.property(fc.series.line()); | |
return linearTimeSeries; | |
}; | |
})(d3, fc); | |
(function(fc) { | |
'use strict'; | |
fc.dataGenerator = function() { | |
var calculateOHLC = function(days, prices, volumes) { | |
var ohlcv = [], | |
daySteps, | |
currentStep = 0, | |
currentIntraStep = 0, | |
stepsPerDay = gen.stepsPerDay.value; | |
while (ohlcv.length < days) { | |
daySteps = prices.slice(currentIntraStep, currentIntraStep + stepsPerDay); | |
ohlcv.push({ | |
date: new Date(gen.startDate.value.getTime()), | |
open: daySteps[0], | |
high: Math.max.apply({}, daySteps), | |
low: Math.min.apply({}, daySteps), | |
close: daySteps[stepsPerDay - 1], | |
volume: volumes[currentStep] | |
}); | |
currentIntraStep += stepsPerDay; | |
currentStep += 1; | |
gen.startDate.value.setUTCDate(gen.startDate.value.getUTCDate() + 1); | |
} | |
return ohlcv; | |
}; | |
var gen = function(days) { | |
var toDate = new Date(gen.startDate.value.getTime()); | |
toDate.setUTCDate(gen.startDate.value.getUTCDate() + days); | |
var millisecondsPerYear = 3.15569e10, | |
years = (toDate.getTime() - gen.startDate.value.getTime()) / millisecondsPerYear; | |
var prices = randomWalk( | |
years, | |
days * gen.stepsPerDay.value, | |
gen.mu.value, | |
gen.sigma.value, | |
gen.startPrice.value | |
); | |
var volumes = randomWalk( | |
years, | |
days, | |
0, | |
gen.sigma.value, | |
gen.startVolume.value | |
); | |
// Add random noise | |
volumes = volumes.map(function(vol) { | |
var boundedNoiseFactor = Math.min(0, Math.max(gen.volumeNoiseFactor.value, 1)); | |
var multiplier = 1 + (boundedNoiseFactor * (1 - 2 * Math.random())); | |
return Math.floor(vol * multiplier); | |
}); | |
// Save the new start values | |
gen.startPrice.value = prices[prices.length - 1]; | |
gen.startVolume.value = volumes[volumes.length - 1]; | |
return calculateOHLC(days, prices, volumes).filter(function(d) { | |
return !gen.filter.value || gen.filter.value(d.date); | |
}); | |
}; | |
var randomWalk = function(period, steps, mu, sigma, initial) { | |
var randomNormal = d3.random.normal(), | |
timeStep = period / steps, | |
increments = new Array(steps + 1), | |
increment, | |
step; | |
// Compute step increments for the discretized GBM model. | |
for (step = 1; step < increments.length; step += 1) { | |
increment = randomNormal(); | |
increment *= Math.sqrt(timeStep); | |
increment *= sigma; | |
increment += (mu - ((sigma * sigma) / 2)) * timeStep; | |
increments[step] = Math.exp(increment); | |
} | |
// Return the cumulative product of increments from initial value. | |
increments[0] = initial; | |
for (step = 1; step < increments.length; step += 1) { | |
increments[step] = increments[step - 1] * increments[step]; | |
} | |
return increments; | |
}; | |
gen.mu = fc.utilities.property(0.1); | |
gen.sigma = fc.utilities.property(0.1); | |
gen.startPrice = fc.utilities.property(100); | |
gen.startVolume = fc.utilities.property(100000); | |
gen.startDate = fc.utilities.property(new Date()); | |
gen.stepsPerDay = fc.utilities.property(50); | |
gen.volumeNoiseFactor = fc.utilities.property(0.3); | |
gen.filter = fc.utilities.property(function(date) { | |
return !(date.getDay() === 0 || date.getDay() === 6); | |
}); | |
return gen; | |
}; | |
}(fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.indicators.algorithms.bollingerBands = function() { | |
var slidingWindow = fc.indicators.algorithms.slidingWindow() | |
.accumulator(function(values) { | |
var avg = d3.mean(values); | |
var stdDev = d3.deviation(values); | |
var multiplier = bollingerBands.multiplier.value.apply(this, arguments); | |
return { | |
upper: avg + multiplier * stdDev, | |
average: avg, | |
lower: avg - multiplier * stdDev | |
}; | |
}); | |
var bollingerBands = function(data) { | |
return slidingWindow(data); | |
}; | |
bollingerBands.multiplier = fc.utilities.functorProperty(2); | |
d3.rebind(bollingerBands, slidingWindow, 'windowSize', 'inputValue', 'outputValue'); | |
return bollingerBands; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.indicators.algorithms.percentageChange = function() { | |
var percentageChange = function(data) { | |
if (data.length === 0) { | |
return []; | |
} | |
var baseIndex = percentageChange.baseIndex.value(data); | |
var baseValue = percentageChange.inputValue.value(data[baseIndex]); | |
return data.map(function(d) { | |
var result = (percentageChange.inputValue.value(d) - baseValue) / baseValue; | |
return percentageChange.outputValue.value(d, result); | |
}); | |
}; | |
percentageChange.baseIndex = fc.utilities.functorProperty(0); | |
percentageChange.inputValue = fc.utilities.property(fc.utilities.fn.identity); | |
percentageChange.outputValue = fc.utilities.property(function(obj, value) { return value; }); | |
return percentageChange; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.indicators.algorithms.relativeStrengthIndicator = function() { | |
var slidingWindow = fc.indicators.algorithms.slidingWindow() | |
.windowSize(14) | |
.accumulator(function(values) { | |
var downCloses = []; | |
var upCloses = []; | |
for (var i = 0, l = values.length; i < l; i++) { | |
var value = values[i]; | |
var openValue = rsi.openValue.value(value); | |
var closeValue = rsi.closeValue.value(value); | |
downCloses.push(openValue > closeValue ? openValue - closeValue : 0); | |
upCloses.push(openValue < closeValue ? closeValue - openValue : 0); | |
} | |
var downClosesAvg = rsi.averageAccumulator.value(downCloses); | |
if (downClosesAvg === 0) { | |
return 100; | |
} | |
var rs = rsi.averageAccumulator.value(upCloses) / downClosesAvg; | |
return 100 - (100 / (1 + rs)); | |
}); | |
var rsi = function(data) { | |
return slidingWindow(data); | |
}; | |
rsi.openValue = fc.utilities.property(function(d) { return d.open; }); | |
rsi.closeValue = fc.utilities.property(function(d) { return d.close; }); | |
rsi.averageAccumulator = fc.utilities.property(function(values) { | |
var alpha = 1 / values.length; | |
var result = values[0]; | |
for (var i = 1, l = values.length; i < l; i++) { | |
result = alpha * values[i] + (1 - alpha) * result; | |
} | |
return result; | |
}); | |
d3.rebind(rsi, slidingWindow, 'windowSize', 'outputValue'); | |
return rsi; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.indicators.algorithms.slidingWindow = function() { | |
var slidingWindow = function(data) { | |
var size = slidingWindow.windowSize.value.apply(this, arguments); | |
var accumulator = slidingWindow.accumulator.value; | |
var inputValue = slidingWindow.inputValue.value; | |
var outputValue = slidingWindow.outputValue.value; | |
var windowData = data.slice(0, size).map(inputValue); | |
return data.slice(size - 1, data.length) | |
.map(function(d, i) { | |
if (i > 0) { | |
// Treat windowData as FIFO rolling buffer | |
windowData.shift(); | |
windowData.push(inputValue(d)); | |
} | |
var result = accumulator(windowData); | |
return outputValue(d, result); | |
}); | |
}; | |
slidingWindow.windowSize = fc.utilities.functorProperty(10); | |
slidingWindow.accumulator = fc.utilities.property(fc.utilities.fn.noop); | |
slidingWindow.inputValue = fc.utilities.property(fc.utilities.fn.identity); | |
slidingWindow.outputValue = fc.utilities.property(function(obj, value) { return value; }); | |
return slidingWindow; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.indicators.bollingerBands = function() { | |
var algorithm = fc.indicators.algorithms.bollingerBands(); | |
var readCalculatedValue = function(d) { | |
return bollingerBands.readCalculatedValue.value(d) || {}; | |
}; | |
var area = fc.series.area() | |
.y0Value(function(d) { | |
return readCalculatedValue(d).upper; | |
}) | |
.y1Value(function(d) { | |
return readCalculatedValue(d).lower; | |
}); | |
var upperLine = fc.series.line() | |
.yValue(function(d) { | |
return readCalculatedValue(d).upper; | |
}); | |
var averageLine = fc.series.line() | |
.yValue(function(d) { | |
return readCalculatedValue(d).average; | |
}); | |
var lowerLine = fc.series.line() | |
.yValue(function(d) { | |
return readCalculatedValue(d).lower; | |
}); | |
var bollingerBands = function(selection) { | |
algorithm.inputValue(bollingerBands.yValue.value) | |
.outputValue(bollingerBands.writeCalculatedValue.value); | |
area.xScale(bollingerBands.xScale.value) | |
.yScale(bollingerBands.yScale.value) | |
.xValue(bollingerBands.xValue.value); | |
upperLine.xScale(bollingerBands.xScale.value) | |
.yScale(bollingerBands.yScale.value) | |
.xValue(bollingerBands.xValue.value); | |
averageLine.xScale(bollingerBands.xScale.value) | |
.yScale(bollingerBands.yScale.value) | |
.xValue(bollingerBands.xValue.value); | |
lowerLine.xScale(bollingerBands.xScale.value) | |
.yScale(bollingerBands.yScale.value) | |
.xValue(bollingerBands.xValue.value); | |
selection.each(function(data) { | |
algorithm(data); | |
var container = d3.select(this); | |
var areaContianer = container.selectAll('g.area') | |
.data([data]); | |
areaContianer.enter() | |
.append('g') | |
.attr('class', 'area'); | |
areaContianer.call(area); | |
var upperLineContainer = container.selectAll('g.upper') | |
.data([data]); | |
upperLineContainer.enter() | |
.append('g') | |
.attr('class', 'upper'); | |
upperLineContainer.call(upperLine); | |
var averageLineContainer = container.selectAll('g.average') | |
.data([data]); | |
averageLineContainer.enter() | |
.append('g') | |
.attr('class', 'average'); | |
averageLineContainer.call(averageLine); | |
var lowerLineContainer = container.selectAll('g.lower') | |
.data([data]); | |
lowerLineContainer.enter() | |
.append('g') | |
.attr('class', 'lower'); | |
lowerLineContainer.call(lowerLine); | |
}); | |
}; | |
bollingerBands.xScale = fc.utilities.property(d3.time.scale()); | |
bollingerBands.yScale = fc.utilities.property(d3.scale.linear()); | |
bollingerBands.yValue = fc.utilities.property(function(d) { return d.close; }); | |
bollingerBands.xValue = fc.utilities.property(function(d) { return d.date; }); | |
bollingerBands.writeCalculatedValue = fc.utilities.property(function(d, value) { d.bollingerBands = value; }); | |
bollingerBands.readCalculatedValue = fc.utilities.property(function(d) { return d.bollingerBands; }); | |
d3.rebind(bollingerBands, algorithm, 'multiplier', 'windowSize'); | |
return bollingerBands; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.indicators.movingAverage = function() { | |
var algorithm = fc.indicators.algorithms.slidingWindow() | |
.accumulator(d3.mean); | |
var averageLine = fc.series.line(); | |
var movingAverage = function(selection) { | |
algorithm.inputValue(movingAverage.yValue.value) | |
.outputValue(movingAverage.writeCalculatedValue.value); | |
averageLine.xScale(movingAverage.xScale.value) | |
.yScale(movingAverage.yScale.value) | |
.xValue(movingAverage.xValue.value) | |
.yValue(movingAverage.readCalculatedValue.value); | |
selection.each(function(data) { | |
algorithm(data); | |
d3.select(this) | |
.call(averageLine); | |
}); | |
}; | |
movingAverage.xScale = fc.utilities.property(d3.time.scale()); | |
movingAverage.yScale = fc.utilities.property(d3.scale.linear()); | |
movingAverage.yValue = fc.utilities.property(function(d) { return d.close; }); | |
movingAverage.xValue = fc.utilities.property(function(d) { return d.date; }); | |
movingAverage.writeCalculatedValue = fc.utilities.property(function(d, value) { d.movingAverage = value; }); | |
movingAverage.readCalculatedValue = fc.utilities.property(function(d) { return d.movingAverage; }); | |
d3.rebind(movingAverage, algorithm, 'windowSize'); | |
return movingAverage; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.indicators.relativeStrengthIndicator = function() { | |
var algorithm = fc.indicators.algorithms.relativeStrengthIndicator(); | |
var annotations = fc.tools.annotation(); | |
var rsiLine = fc.series.line(); | |
var rsi = function(selection) { | |
algorithm.outputValue(rsi.writeCalculatedValue.value); | |
annotations.xScale(rsi.xScale.value) | |
.yScale(rsi.yScale.value); | |
rsiLine.xScale(rsi.xScale.value) | |
.yScale(rsi.yScale.value) | |
.xValue(rsi.xValue.value) | |
.yValue(rsi.readCalculatedValue.value); | |
selection.each(function(data) { | |
algorithm(data); | |
var container = d3.select(this); | |
var annotationsContainer = container.selectAll('g.annotations') | |
.data([[ | |
rsi.upperValue.value.apply(this, arguments), | |
50, | |
rsi.lowerValue.value.apply(this, arguments) | |
]]); | |
annotationsContainer.enter() | |
.append('g') | |
.attr('class', 'annotations'); | |
annotationsContainer.call(annotations); | |
var rsiLineContainer = container.selectAll('g.indicator') | |
.data([data]); | |
rsiLineContainer.enter() | |
.append('g') | |
.attr('class', 'indicator'); | |
rsiLineContainer.call(rsiLine); | |
}); | |
}; | |
rsi.xScale = fc.utilities.property(d3.time.scale()); | |
rsi.yScale = fc.utilities.property(d3.scale.linear()); | |
rsi.xValue = fc.utilities.property(function(d) { return d.date; }); | |
rsi.writeCalculatedValue = fc.utilities.property(function(d, value) { d.rsi = value; }); | |
rsi.readCalculatedValue = fc.utilities.property(function(d) { return d.rsi; }); | |
rsi.upperValue = fc.utilities.functorProperty(70); | |
rsi.lowerValue = fc.utilities.functorProperty(30); | |
d3.rebind(rsi, algorithm, 'openValue', 'closeValue', 'windowSize'); | |
return rsi; | |
}; | |
}(d3, fc)); | |
/* globals computeLayout */ | |
(function(d3, fc, cssLayout) { | |
'use strict'; | |
d3.selection.prototype.layout = function(name, value) { | |
var layout = fc.layout(); | |
var n = arguments.length; | |
if (n === 2) { | |
if (typeof name !== 'string') { | |
// layout(number, number) - sets the width and height and performs layout | |
layout.width(name).height(value); | |
this.call(layout); | |
} else { | |
// layout(name, value) - sets a layout- attribute | |
this.attr('layout-css', name + ':' + value); | |
} | |
} else if (n === 1) { | |
if (typeof name !== 'string') { | |
// layout(object) - sets the layout-css property to the given object | |
var styleObject = name; | |
var layoutCss = Object.keys(styleObject) | |
.map(function(property) { | |
return property + ':' + styleObject[property]; | |
}) | |
.join(';'); | |
this.attr('layout-css', layoutCss); | |
} else { | |
// layout(name) - returns the value of the layout-name attribute | |
return Number(this.attr('layout-' + name)); | |
} | |
} else if (n === 0) { | |
// layout() - executes layout | |
this.call(layout); | |
} | |
return this; | |
}; | |
fc.layout = function() { | |
// parses the style attribute, converting it into a JavaScript object | |
function parseStyle(style) { | |
if (!style) { | |
return {}; | |
} | |
var properties = style.split(';'); | |
var json = {}; | |
properties.forEach(function(property) { | |
var components = property.split(':'); | |
if (components.length === 2) { | |
var name = components[0].trim(); | |
var value = components[1].trim(); | |
json[name] = isNaN(value) ? value : Number(value); | |
} | |
}); | |
return json; | |
} | |
// creates the structure required by the layout engine | |
function createNodes(el) { | |
function getChildNodes() { | |
var children = []; | |
for (var i = 0; i < el.childNodes.length; i++) { | |
var child = el.childNodes[i]; | |
if (child.nodeType === 1) { | |
if (child.getAttribute('layout-css')) { | |
children.push(createNodes(child)); | |
} | |
} | |
} | |
return children; | |
} | |
return { | |
style: parseStyle(el.getAttribute('layout-css')), | |
children: getChildNodes(el), | |
element: el, | |
layout: { | |
width: undefined, | |
height: undefined, | |
top: 0, | |
left: 0 | |
} | |
}; | |
} | |
// takes the result of layout and applied it to the SVG elements | |
function applyLayout(node) { | |
node.element.setAttribute('layout-width', node.layout.width); | |
node.element.setAttribute('layout-height', node.layout.height); | |
if (node.element.nodeName.match(/(?:svg|rect)/i)) { | |
node.element.setAttribute('width', node.layout.width); | |
node.element.setAttribute('height', node.layout.height); | |
node.element.setAttribute('x', node.layout.left); | |
node.element.setAttribute('y', node.layout.top); | |
} else { | |
node.element.setAttribute('transform', | |
'translate(' + node.layout.left + ', ' + node.layout.top + ')'); | |
} | |
node.children.forEach(applyLayout); | |
} | |
var layout = function(selection) { | |
selection.each(function(data) { | |
// compute the width and height of the SVG element | |
var style = getComputedStyle(this); | |
var width, height; | |
if (layout.width.value !== -1) { | |
width = layout.width.value; | |
} else { | |
width = parseFloat(style.width) - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight); | |
} | |
if (layout.height.value !== -1) { | |
height = layout.height.value; | |
} else { | |
height = parseFloat(style.height) - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom); | |
} | |
// create the layout nodes | |
var layoutNodes = createNodes(this); | |
// set the width / height of the root | |
layoutNodes.style.width = width; | |
layoutNodes.style.height = height; | |
// use the Facebook CSS goodness | |
cssLayout.computeLayout(layoutNodes); | |
// apply the resultant layout | |
applyLayout(layoutNodes); | |
}); | |
}; | |
layout.width = fc.utilities.property(-1); | |
layout.height = fc.utilities.property(-1); | |
return layout; | |
}; | |
}(d3, fc, computeLayout)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.scale.dateTime = function() { | |
return dateTimeScale(); | |
}; | |
// obtains the ticks from the given scale, transforming the result to ensure | |
// it does not include any discontinuities | |
fc.scale.dateTime.tickTransformer = function(ticks, discontinuityProvider, domain) { | |
var clampedTicks = ticks.map(function(tick, index) { | |
if (index < ticks.length - 1) { | |
return discontinuityProvider.clampUp(tick); | |
} else { | |
var clampedTick = discontinuityProvider.clampUp(tick); | |
return clampedTick < domain[1] ? | |
clampedTick : discontinuityProvider.clampDown(tick); | |
} | |
}); | |
var uniqueTicks = clampedTicks.reduce(function(arr, tick) { | |
if (arr.filter(function(f) { return f.getTime() === tick.getTime(); }).length === 0) { | |
arr.push(tick); | |
} | |
return arr; | |
}, []); | |
return uniqueTicks; | |
}; | |
/** | |
* The `fc.scale.dateTime` scale renders a discontinuous date time scale, i.e. a time scale that incorporates gaps. | |
* As an example, you can use this scale to render a chart where the weekends are skipped. | |
* | |
* @type {object} | |
* @memberof fc.scale | |
* @class fc.scale.dateTime | |
*/ | |
function dateTimeScale(adaptedScale, discontinuityProvider) { | |
if (!arguments.length) { | |
adaptedScale = d3.time.scale(); | |
discontinuityProvider = fc.scale.discontinuity.identity(); | |
} | |
function discontinuities() { return scale.discontinuityProvider.value; } | |
function scale(date) { | |
var domain = adaptedScale.domain(); | |
var range = adaptedScale.range(); | |
// The discontinuityProvider is responsible for determine the distance between two points | |
// along a scale that has discontinuities (i.e. sections that have been removed). | |
// the scale for the given point 'x' is calculated as the ratio of the discontinuous distance | |
// over the domain of this axis, versus the discontinuous distance to 'x' | |
var totalDomainDistance = discontinuities().distance(domain[0], domain[1]); | |
var distanceToX = discontinuities().distance(domain[0], date); | |
var ratioToX = distanceToX / totalDomainDistance; | |
var scaledByRange = ratioToX * (range[1] - range[0]) + range[0]; | |
return scaledByRange; | |
} | |
scale.invert = function(x) { | |
var domain = adaptedScale.domain(); | |
var range = adaptedScale.range(); | |
var ratioToX = (x - range[0]) / (range[1] - range[0]); | |
var totalDomainDistance = discontinuities().distance(domain[0], domain[1]); | |
var distanceToX = ratioToX * totalDomainDistance; | |
return discontinuities().offset(domain[0], distanceToX); | |
}; | |
scale.domain = function(x) { | |
if (!arguments.length) { | |
return adaptedScale.domain(); | |
} | |
// clamp the upper and lower domain values to ensure they | |
// do not fall within a discontinuity | |
var domainLower = discontinuities().clampUp(x[0]); | |
var domainUpper = discontinuities().clampDown(x[1]); | |
adaptedScale.domain([domainLower, domainUpper]); | |
return scale; | |
}; | |
scale.nice = function() { | |
adaptedScale.nice(); | |
var domain = adaptedScale.domain(); | |
var domainLower = discontinuities().clampUp(domain[0]); | |
var domainUpper = discontinuities().clampDown(domain[1]); | |
adaptedScale.domain([domainLower, domainUpper]); | |
return scale; | |
}; | |
scale.ticks = function() { | |
var ticks = adaptedScale.ticks.apply(this, arguments); | |
return fc.scale.dateTime.tickTransformer(ticks, discontinuities(), scale.domain()); | |
}; | |
scale.copy = function() { | |
return dateTimeScale(adaptedScale.copy(), discontinuities().copy()); | |
}; | |
scale.discontinuityProvider = fc.utilities.property(discontinuityProvider); | |
return d3.rebind(scale, adaptedScale, 'range', 'rangeRound', 'interpolate', 'clamp', | |
'tickFormat'); | |
} | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.scale.discontinuity.identity = function() { | |
var identity = {}; | |
identity.distance = function(startDate, endDate) { | |
return endDate.getTime() - startDate.getTime(); | |
}; | |
identity.offset = function(startDate, ms) { | |
return new Date(startDate.getTime() + ms); | |
}; | |
identity.clampUp = fc.utilities.fn.identity; | |
identity.clampDown = fc.utilities.fn.identity; | |
identity.copy = function() { return identity; }; | |
return identity; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.scale.discontinuity.skipWeekends = function() { | |
var millisPerDay = 24 * 3600 * 1000; | |
var millisPerWorkWeek = millisPerDay * 5; | |
var millisPerWeek = millisPerDay * 7; | |
var skipWeekends = {}; | |
function isWeekend(date) { | |
return date.getDay() === 0 || date.getDay() === 6; | |
} | |
skipWeekends.clampDown = function(date) { | |
if (isWeekend(date)) { | |
var daysToSubtract = date.getDay() === 0 ? 2 : 1; | |
// round the date up to midnight | |
var newDate = d3.time.day.ceil(date); | |
// then subtract the required number of days | |
return d3.time.day.offset(newDate, -daysToSubtract); | |
} else { | |
return date; | |
} | |
}; | |
skipWeekends.clampUp = function(date) { | |
if (isWeekend(date)) { | |
var daysToAdd = date.getDay() === 0 ? 1 : 2; | |
// round the date down to midnight | |
var newDate = d3.time.day.floor(date); | |
// then add the required number of days | |
return d3.time.day.offset(newDate, daysToAdd); | |
} else { | |
return date; | |
} | |
}; | |
// returns the number of included milliseconds (i.e. those which do not fall) | |
// within discontinuities, along this scale | |
skipWeekends.distance = function(startDate, endDate) { | |
startDate = skipWeekends.clampUp(startDate); | |
endDate = skipWeekends.clampDown(endDate); | |
// move the start date to the end of week boundary | |
var offsetStart = d3.time.saturday.ceil(startDate); | |
if (endDate < offsetStart) { | |
return endDate.getTime() - startDate.getTime(); | |
} | |
var msAdded = offsetStart.getTime() - startDate.getTime(); | |
// move the end date to the end of week boundary | |
var offsetEnd = d3.time.saturday.ceil(endDate); | |
var msRemoved = offsetEnd.getTime() - endDate.getTime(); | |
// determine how many weeks there are between these two dates | |
var weeks = (offsetEnd.getTime() - offsetStart.getTime()) / millisPerWeek; | |
return weeks * millisPerWorkWeek + msAdded - msRemoved; | |
}; | |
skipWeekends.offset = function(startDate, ms) { | |
var date = isWeekend(startDate) ? skipWeekends.clampUp(startDate) : startDate; | |
var remainingms = ms; | |
// move to the end of week boundary | |
var endOfWeek = d3.time.saturday.ceil(date); | |
remainingms -= (endOfWeek.getTime() - date.getTime()); | |
// if the distance to the boundary is greater than the number of ms | |
// simply add the ms to the current date | |
if (remainingms < 0) { | |
return new Date(date.getTime() + ms); | |
} | |
// skip the weekend | |
date = d3.time.day.offset(endOfWeek, 2); | |
// add all of the complete weeks to the date | |
var completeWeeks = Math.floor(remainingms / millisPerWorkWeek); | |
date = d3.time.day.offset(date, completeWeeks * 7); | |
remainingms -= completeWeeks * millisPerWorkWeek; | |
// add the remaining time | |
date = new Date(date.getTime() + remainingms); | |
return date; | |
}; | |
skipWeekends.copy = function() { return skipWeekends; }; | |
return skipWeekends; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.scale.gridlines = function() { | |
var gridlines = function(selection) { | |
selection.each(function() { | |
var container = d3.select(this); | |
var xLines = fc.utilities.simpleDataJoin(container, 'x', | |
gridlines.xScale.value.ticks(gridlines.xTicks.value)); | |
xLines.enter() | |
.append('line') | |
.attr('class', 'gridline'); | |
xLines.select('line') | |
.attr({ | |
'x1': gridlines.xScale.value, | |
'x2': gridlines.xScale.value, | |
'y1': gridlines.yScale.value.range()[0], | |
'y2': gridlines.yScale.value.range()[1] | |
}); | |
var yLines = fc.utilities.simpleDataJoin(container, 'y', | |
gridlines.yScale.value.ticks(gridlines.yTicks.value)); | |
yLines.enter() | |
.append('line') | |
.attr('class', 'gridline'); | |
yLines.select('line') | |
.attr({ | |
'x1': gridlines.xScale.value.range()[0], | |
'x2': gridlines.xScale.value.range()[1], | |
'y1': gridlines.yScale.value, | |
'y2': gridlines.yScale.value | |
}); | |
}); | |
}; | |
gridlines.xScale = fc.utilities.property(d3.time.scale()); | |
gridlines.yScale = fc.utilities.property(d3.scale.linear()); | |
gridlines.xTicks = fc.utilities.property(10); | |
gridlines.yTicks = fc.utilities.property(10); | |
return gridlines; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.series.area = function() { | |
// convenience functions that return the x & y screen coords for a given point | |
var x = function(d) { return area.xScale.value(area.xValue.value(d)); }; | |
var y0 = function(d) { return area.yScale.value(area.y0Value.value(d)); }; | |
var y1 = function(d) { return area.yScale.value(area.y1Value.value(d)); }; | |
var areaData = d3.svg.area() | |
.defined(function(d) { | |
return !isNaN(y0(d)) && !isNaN(y1(d)); | |
}) | |
.x(x) | |
.y0(y0) | |
.y1(y1); | |
var area = function(selection) { | |
selection.each(function(data) { | |
var path = d3.select(this) | |
.selectAll('path.area') | |
.data([data]); | |
path.enter() | |
.append('path') | |
.attr('class', 'area'); | |
path.attr('d', areaData); | |
area.decorate.value(path); | |
}); | |
}; | |
area.decorate = fc.utilities.property(fc.utilities.fn.noop); | |
area.xScale = fc.utilities.property(d3.time.scale()); | |
area.yScale = fc.utilities.property(d3.scale.linear()); | |
area.y0Value = fc.utilities.functorProperty(0); | |
area.y1Value = fc.utilities.property(function(d) { return d.close; }); | |
area.xValue = fc.utilities.property(function(d) { return d.date; }); | |
return area; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.series.bar = function() { | |
// convenience functions that return the x & y screen coords for a given point | |
var x = function(d) { return bar.xScale.value(bar.xValue.value(d)); }; | |
var barTop = function(d) { return bar.yScale.value(bar.y0Value.value(d) + bar.yValue.value(d)); }; | |
var barBottom = function(d) { return bar.yScale.value(bar.y0Value.value(d)); }; | |
var bar = function(selection) { | |
selection.each(function(data) { | |
var container = d3.select(this); | |
var series = fc.utilities.simpleDataJoin(container, 'bar', data, bar.xValue.value); | |
// enter | |
series.enter() | |
.append('rect'); | |
var width = bar.barWidth.value(data.map(x)); | |
// update | |
series.select('rect') | |
.attr('x', function(d) { | |
return x(d) - width / 2; | |
}) | |
.attr('y', barTop) | |
.attr('width', width) | |
.attr('height', function(d) { | |
return barBottom(d) - barTop(d); | |
}); | |
// properties set by decorate will transition too | |
bar.decorate.value(series); | |
}); | |
}; | |
bar.decorate = fc.utilities.property(fc.utilities.fn.noop); | |
bar.xScale = fc.utilities.property(d3.time.scale()); | |
bar.yScale = fc.utilities.property(d3.scale.linear()); | |
bar.barWidth = fc.utilities.functorProperty(fc.utilities.fractionalBarWidth(0.75)); | |
bar.yValue = fc.utilities.property(function(d) { return d.close; }); | |
bar.xValue = fc.utilities.property(function(d) { return d.date; }); | |
bar.y0Value = fc.utilities.functorProperty(0); | |
return bar; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.series.candlestick = function() { | |
// convenience functions that return the x & y screen coords for a given point | |
var x = function(d) { return candlestick.xScale.value(candlestick.xValue.value(d)); }; | |
var yOpen = function(d) { return candlestick.yScale.value(candlestick.yOpenValue.value(d)); }; | |
var yHigh = function(d) { return candlestick.yScale.value(candlestick.yHighValue.value(d)); }; | |
var yLow = function(d) { return candlestick.yScale.value(candlestick.yLowValue.value(d)); }; | |
var yClose = function(d) { return candlestick.yScale.value(candlestick.yCloseValue.value(d)); }; | |
var candlestick = function(selection) { | |
selection.each(function(data) { | |
var container = d3.select(this); | |
var g = fc.utilities.simpleDataJoin(container, 'candlestick', data, candlestick.xValue.value); | |
g.enter() | |
.append('path'); | |
g.classed({ | |
'up': function(d) { | |
return candlestick.yCloseValue.value(d) > candlestick.yOpenValue.value(d); | |
}, | |
'down': function(d) { | |
return candlestick.yCloseValue.value(d) < candlestick.yOpenValue.value(d); | |
} | |
}); | |
var barWidth = candlestick.barWidth.value(data.map(x)); | |
g.select('path') | |
.attr('d', function(d) { | |
// Move to the opening price | |
var body = 'M' + (x(d) - barWidth / 2) + ',' + yOpen(d) + | |
// Draw the width | |
'h' + barWidth + | |
// Draw to the closing price (vertically) | |
'V' + yClose(d) + | |
// Draw the width | |
'h' + -barWidth + | |
// Move back to the opening price | |
'V' + yOpen(d) + | |
// Close the path | |
'z'; | |
// Move to the max price of close or open; draw the high wick | |
// N.B. Math.min() is used as we're dealing with pixel values, | |
// the lower the pixel value, the higher the price! | |
var highWick = 'M' + x(d) + ',' + Math.min(yClose(d), yOpen(d)) + | |
'V' + yHigh(d); | |
// Move to the min price of close or open; draw the low wick | |
// N.B. Math.max() is used as we're dealing with pixel values, | |
// the higher the pixel value, the lower the price! | |
var lowWick = 'M' + x(d) + ',' + Math.max(yClose(d), yOpen(d)) + | |
'V' + yLow(d); | |
return body + highWick + lowWick; | |
}); | |
candlestick.decorate.value(g); | |
}); | |
}; | |
candlestick.decorate = fc.utilities.property(fc.utilities.fn.noop); | |
candlestick.xScale = fc.utilities.property(d3.time.scale()); | |
candlestick.yScale = fc.utilities.property(d3.scale.linear()); | |
candlestick.barWidth = fc.utilities.functorProperty(fc.utilities.fractionalBarWidth(0.75)); | |
candlestick.yOpenValue = fc.utilities.property(function(d) { return d.open; }); | |
candlestick.yHighValue = fc.utilities.property(function(d) { return d.high; }); | |
candlestick.yLowValue = fc.utilities.property(function(d) { return d.low; }); | |
candlestick.yCloseValue = fc.utilities.property(function(d) { return d.close; }); | |
candlestick.xValue = fc.utilities.property(function(d) { return d.date; }); | |
return candlestick; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.series.line = function() { | |
// convenience functions that return the x & y screen coords for a given point | |
var x = function(d) { return line.xScale.value(line.xValue.value(d)); }; | |
var y = function(d) { return line.yScale.value(line.yValue.value(d)); }; | |
var lineData = d3.svg.line() | |
.defined(function(d) { | |
return !isNaN(y(d)); | |
}) | |
.x(x) | |
.y(y); | |
var line = function(selection) { | |
selection.each(function(data) { | |
var path = d3.select(this) | |
.selectAll('path.line') | |
.data([data]); | |
path.enter() | |
.append('path') | |
.attr('class', 'line'); | |
path.attr('d', lineData); | |
line.decorate.value(path); | |
}); | |
}; | |
line.decorate = fc.utilities.property(fc.utilities.fn.noop); | |
line.xScale = fc.utilities.property(d3.time.scale()); | |
line.yScale = fc.utilities.property(d3.scale.linear()); | |
line.yValue = fc.utilities.property(function(d) { return d.close; }); | |
line.xValue = fc.utilities.property(function(d) { return d.date; }); | |
return line; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.series.multi = function() { | |
var multi = function(selection) { | |
selection.each(function(data) { | |
var container = d3.select(this); | |
var g = container.selectAll('g.multi-outer') | |
.data(multi.series.value); | |
g.enter() | |
.append('g') | |
.attr('class', 'multi-outer') | |
.append('g') | |
.attr('class', 'multi-inner'); | |
g.exit() | |
.remove(); | |
g.select('g.multi-inner') | |
.each(function() { | |
var series = d3.select(this.parentNode) | |
.datum(); | |
(series.xScale || series.x).call(series, multi.xScale.value); | |
(series.yScale || series.y).call(series, multi.yScale.value); | |
d3.select(this) | |
.datum(multi.mapping.value(data, series)) | |
.call(series); | |
}); | |
}); | |
}; | |
multi.xScale = fc.utilities.property(d3.time.scale()); | |
multi.yScale = fc.utilities.property(d3.scale.linear()); | |
multi.series = fc.utilities.property([]); | |
multi.mapping = fc.utilities.property(fc.utilities.fn.identity); | |
return multi; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.series.ohlc = function(drawMethod) { | |
// convenience functions that return the x & y screen coords for a given point | |
var x = function(d) { return ohlc.xScale.value(ohlc.xValue.value(d)); }; | |
var yOpen = function(d) { return ohlc.yScale.value(ohlc.yOpenValue.value(d)); }; | |
var yHigh = function(d) { return ohlc.yScale.value(ohlc.yHighValue.value(d)); }; | |
var yLow = function(d) { return ohlc.yScale.value(ohlc.yLowValue.value(d)); }; | |
var yClose = function(d) { return ohlc.yScale.value(ohlc.yCloseValue.value(d)); }; | |
var ohlc = function(selection) { | |
selection.each(function(data) { | |
var container = d3.select(this); | |
var g = fc.utilities.simpleDataJoin(container, 'ohlc', data, ohlc.xValue.value); | |
g.enter() | |
.append('path'); | |
g.classed({ | |
'up': function(d) { | |
return ohlc.yCloseValue.value(d) > ohlc.yOpenValue.value(d); | |
}, | |
'down': function(d) { | |
return ohlc.yCloseValue.value(d) < ohlc.yOpenValue.value(d); | |
} | |
}); | |
var width = ohlc.barWidth.value(data.map(x)); | |
var halfWidth = width / 2; | |
g.select('path') | |
.attr('d', function(d) { | |
var moveToLow = 'M' + x(d) + ',' + yLow(d), | |
verticalToHigh = 'V' + yHigh(d), | |
openTick = 'M' + x(d) + ',' + yOpen(d) + 'h' + (-halfWidth), | |
closeTick = 'M' + x(d) + ',' + yClose(d) + 'h' + halfWidth; | |
return moveToLow + verticalToHigh + openTick + closeTick; | |
}); | |
ohlc.decorate.value(g); | |
}); | |
}; | |
ohlc.decorate = fc.utilities.property(fc.utilities.fn.noop); | |
ohlc.xScale = fc.utilities.property(d3.time.scale()); | |
ohlc.yScale = fc.utilities.property(d3.scale.linear()); | |
ohlc.barWidth = fc.utilities.functorProperty(fc.utilities.fractionalBarWidth(0.75)); | |
ohlc.yOpenValue = fc.utilities.property(function(d) { return d.open; }); | |
ohlc.yHighValue = fc.utilities.property(function(d) { return d.high; }); | |
ohlc.yLowValue = fc.utilities.property(function(d) { return d.low; }); | |
ohlc.yCloseValue = fc.utilities.property(function(d) { return d.close; }); | |
ohlc.xValue = fc.utilities.property(function(d) { return d.date; }); | |
return ohlc; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.series.point = function() { | |
// convenience functions that return the x & y screen coords for a given point | |
var x = function(d) { return point.xScale.value(point.xValue.value(d)); }; | |
var y = function(d) { return point.yScale.value(point.yValue.value(d)); }; | |
var point = function(selection) { | |
selection.each(function(data) { | |
var container = d3.select(this); | |
var g = fc.utilities.simpleDataJoin(container, 'point', data, point.xValue.value); | |
g.enter() | |
.append('circle'); | |
g.select('circle') | |
.attr('cx', x) | |
.attr('cy', y) | |
.attr('r', point.radius.value); | |
point.decorate.value(g); | |
}); | |
}; | |
point.decorate = fc.utilities.property(fc.utilities.fn.noop); | |
point.xScale = fc.utilities.property(d3.time.scale()); | |
point.yScale = fc.utilities.property(d3.scale.linear()); | |
point.yValue = fc.utilities.property(function(d) { return d.close; }); | |
point.xValue = fc.utilities.property(function(d) { return d.date; }); | |
point.radius = fc.utilities.functorProperty(5); | |
return point; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.series.stackedBar = function() { | |
var stackLayout = d3.layout.stack(); | |
var stackedBar = function(selection) { | |
var bar = fc.series.bar() | |
.xScale(stackedBar.xScale.value) | |
.yScale(stackedBar.yScale.value) | |
.xValue(stackLayout.x()) | |
.yValue(stackLayout.y()) | |
.y0Value(stackedBar.y0Value.value); | |
selection.each(function(data) { | |
var layers = stackLayout(data); | |
var container = d3.select(this); | |
// Pull data from series objects. | |
var layeredData = layers.map(stackLayout.values()); | |
var series = container.selectAll('g.stacked-bar') | |
.data(layeredData) | |
.enter() | |
.append('g') | |
.attr('class', 'stacked-bar') | |
.call(bar); | |
stackedBar.decorate.value(series); | |
}); | |
}; | |
stackedBar.decorate = fc.utilities.property(fc.utilities.fn.noop); | |
stackedBar.barWidth = fc.utilities.functorProperty(fc.utilities.fractionalBarWidth(0.75)); | |
stackedBar.xScale = fc.utilities.property(d3.time.scale()); | |
stackedBar.yScale = fc.utilities.property(d3.scale.linear()); | |
// Implicitly dependant on the implementation of the stack layout's `out`. | |
stackedBar.y0Value = fc.utilities.property(function(d) { | |
return d.y0; | |
}); | |
return fc.utilities.rebind(stackedBar, stackLayout, { | |
xValue: 'x', | |
yValue: 'y', | |
out: 'out', | |
offset: 'offset', | |
values: 'values', | |
order: 'order' | |
}); | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.tools.annotation = function() { | |
var annotation = function(selection) { | |
selection.each(function(data) { | |
var xScaleRange = annotation.xScale.value.range(), | |
y = function(d) { return annotation.yScale.value(annotation.yValue.value(d)); }; | |
var container = d3.select(this); | |
// Create a group for each annotation | |
var g = fc.utilities.simpleDataJoin(container, 'annotation', data, annotation.keyValue.value); | |
// Added the required elements - each annotation consists of a line and text label | |
var enter = g.enter(); | |
enter.append('line'); | |
enter.append('text'); | |
// Update the line | |
g.select('line') | |
.attr('x1', xScaleRange[0]) | |
.attr('y1', y) | |
.attr('x2', xScaleRange[1]) | |
.attr('y2', y); | |
// Update the text label | |
var paddingValue = annotation.padding.value.apply(this, arguments); | |
g.select('text') | |
.attr('x', xScaleRange[1] - paddingValue) | |
.attr('y', function(d) { return y(d) - paddingValue; }) | |
.text(annotation.label.value); | |
annotation.decorate.value(g); | |
}); | |
}; | |
annotation.xScale = fc.utilities.property(d3.time.scale()); | |
annotation.yScale = fc.utilities.property(d3.scale.linear()); | |
annotation.yValue = fc.utilities.functorProperty(fc.utilities.fn.identity); | |
annotation.keyValue = fc.utilities.functorProperty(fc.utilities.fn.index); | |
annotation.label = fc.utilities.functorProperty(annotation.yValue.value); | |
annotation.padding = fc.utilities.functorProperty(2); | |
annotation.decorate = fc.utilities.property(fc.utilities.fn.noop); | |
return annotation; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.tools.crosshairs = function() { | |
var event = d3.dispatch('trackingstart', 'trackingmove', 'trackingend'); | |
var crosshairs = function(selection) { | |
selection.each(function(data) { | |
var container = d3.select(this) | |
.style('pointer-events', 'all') | |
.on('mouseenter.crosshairs', mouseenter); | |
var overlay = container.selectAll('rect') | |
.data([data]); | |
overlay.enter() | |
.append('rect') | |
.style('visibility', 'hidden'); | |
// ordinal axes have a rangeExtent function, this adds any padding that | |
// was applied to the range. This functions returns the rangeExtent | |
// if present, or range otherwise | |
function rangeForScale(scaleProperty) { | |
return scaleProperty.value.rangeExtent ? | |
scaleProperty.value.rangeExtent() : scaleProperty.value.range(); | |
} | |
function rangeStart(scaleProperty) { | |
return rangeForScale(scaleProperty)[0]; | |
} | |
function rangeEnd(scaleProperty) { | |
return rangeForScale(scaleProperty)[1]; | |
} | |
container.select('rect') | |
.attr('x', rangeStart(crosshairs.xScale)) | |
.attr('y', rangeEnd(crosshairs.yScale)) | |
.attr('width', rangeEnd(crosshairs.xScale)) | |
.attr('height', rangeStart(crosshairs.yScale)); | |
var g = fc.utilities.simpleDataJoin(container, 'crosshairs', data); | |
var enter = g.enter(); | |
enter.append('line') | |
.attr('class', 'horizontal'); | |
enter.append('line') | |
.attr('class', 'vertical'); | |
enter.append('text') | |
.attr('class', 'horizontal'); | |
enter.append('text') | |
.attr('class', 'vertical'); | |
g.select('line.horizontal') | |
.attr('x1', rangeStart(crosshairs.xScale)) | |
.attr('x2', rangeEnd(crosshairs.xScale)) | |
.attr('y1', function(d) { return d.y; }) | |
.attr('y2', function(d) { return d.y; }); | |
g.select('line.vertical') | |
.attr('y1', rangeStart(crosshairs.yScale)) | |
.attr('y2', rangeEnd(crosshairs.yScale)) | |
.attr('x1', function(d) { return d.x; }) | |
.attr('x2', function(d) { return d.x; }); | |
var paddingValue = crosshairs.padding.value.apply(this, arguments); | |
g.select('text.horizontal') | |
.attr('x', rangeEnd(crosshairs.xScale) - paddingValue) | |
.attr('y', function(d) { | |
return d.y - paddingValue; | |
}) | |
.text(crosshairs.yLabel.value); | |
g.select('text.vertical') | |
.attr('x', function(d) { | |
return d.x - paddingValue; | |
}) | |
.attr('y', paddingValue) | |
.text(crosshairs.xLabel.value); | |
crosshairs.decorate.value(g); | |
}); | |
}; | |
function mouseenter() { | |
var mouse = d3.mouse(this); | |
var container = d3.select(this) | |
.on('mousemove.crosshairs', mousemove) | |
.on('mouseleave.crosshairs', mouseleave); | |
var snapped = crosshairs.snap.value.apply(this, mouse); | |
var data = container.datum(); | |
data.push(snapped); | |
container.call(crosshairs); | |
event.trackingstart.apply(this, arguments); | |
} | |
function mousemove() { | |
var mouse = d3.mouse(this); | |
var container = d3.select(this); | |
var snapped = crosshairs.snap.value.apply(this, mouse); | |
var data = container.datum(); | |
data[data.length - 1] = snapped; | |
container.call(crosshairs); | |
event.trackingmove.apply(this, arguments); | |
} | |
function mouseleave() { | |
var container = d3.select(this); | |
var data = container.datum(); | |
data.pop(); | |
container.call(crosshairs) | |
.on('mousemove.crosshairs', null) | |
.on('mouseleave.crosshairs', null); | |
event.trackingend.apply(this, arguments); | |
} | |
crosshairs.xScale = fc.utilities.property(d3.time.scale()); | |
crosshairs.yScale = fc.utilities.property(d3.scale.linear()); | |
crosshairs.snap = fc.utilities.property(function(x, y) { return {x: x, y: y}; }); | |
crosshairs.decorate = fc.utilities.property(fc.utilities.fn.noop); | |
crosshairs.xLabel = fc.utilities.functorProperty(''); | |
crosshairs.yLabel = fc.utilities.functorProperty(''); | |
crosshairs.padding = fc.utilities.functorProperty(2); | |
d3.rebind(crosshairs, event, 'on'); | |
return crosshairs; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.tools.fibonacciFan = function() { | |
var event = d3.dispatch('fansource', 'fantarget', 'fanclear'); | |
var fan = function(selection) { | |
selection.each(function(data) { | |
var container = d3.select(this) | |
.style('pointer-events', 'all') | |
.on('mouseenter.fan', mouseenter); | |
var overlay = container.selectAll('rect') | |
.data([data]); | |
overlay.enter() | |
.append('rect') | |
.style('visibility', 'hidden'); | |
container.select('rect') | |
.attr('x', fan.xScale.value.range()[0]) | |
.attr('y', fan.yScale.value.range()[1]) | |
.attr('width', fan.xScale.value.range()[1]) | |
.attr('height', fan.yScale.value.range()[0]); | |
var g = fc.utilities.simpleDataJoin(container, 'fan', data); | |
g.each(function(d) { | |
d.x = fan.xScale.value.range()[1]; | |
d.ay = d.by = d.cy = d.target.y; | |
if (d.source.x !== d.target.x) { | |
if (d.state === 'DONE' && d.source.x > d.target.x) { | |
var temp = d.source; | |
d.source = d.target; | |
d.target = temp; | |
} | |
var gradient = (d.target.y - d.source.y) / | |
(d.target.x - d.source.x); | |
var deltaX = d.x - d.source.x; | |
var deltaY = gradient * deltaX; | |
d.ay = 0.618 * deltaY + d.source.y; | |
d.by = 0.500 * deltaY + d.source.y; | |
d.cy = 0.382 * deltaY + d.source.y; | |
} | |
}); | |
var enter = g.enter(); | |
enter.append('line') | |
.attr('class', 'trend'); | |
enter.append('line') | |
.attr('class', 'a'); | |
enter.append('line') | |
.attr('class', 'b'); | |
enter.append('line') | |
.attr('class', 'c'); | |
enter.append('polygon') | |
.attr('class', 'area'); | |
g.select('line.trend') | |
.attr('x1', function(d) { return d.source.x; }) | |
.attr('y1', function(d) { return d.source.y; }) | |
.attr('x2', function(d) { return d.target.x; }) | |
.attr('y2', function(d) { return d.target.y; }); | |
g.select('line.a') | |
.attr('x1', function(d) { return d.source.x; }) | |
.attr('y1', function(d) { return d.source.y; }) | |
.attr('x2', function(d) { return d.x; }) | |
.attr('y2', function(d) { return d.ay; }) | |
.style('visibility', function(d) { return d.state !== 'DONE' ? 'hidden' : 'visible'; }); | |
g.select('line.b') | |
.attr('x1', function(d) { return d.source.x; }) | |
.attr('y1', function(d) { return d.source.y; }) | |
.attr('x2', function(d) { return d.x; }) | |
.attr('y2', function(d) { return d.by; }) | |
.style('visibility', function(d) { return d.state !== 'DONE' ? 'hidden' : 'visible'; }); | |
g.select('line.c') | |
.attr('x1', function(d) { return d.source.x; }) | |
.attr('y1', function(d) { return d.source.y; }) | |
.attr('x2', function(d) { return d.x; }) | |
.attr('y2', function(d) { return d.cy; }) | |
.style('visibility', function(d) { return d.state !== 'DONE' ? 'hidden' : 'visible'; }); | |
g.select('polygon.area') | |
.attr('points', function(d) { | |
return d.source.x + ',' + d.source.y + ' ' + | |
d.x + ',' + d.ay + ' ' + | |
d.x + ',' + d.cy; | |
}) | |
.style('visibility', function(d) { return d.state !== 'DONE' ? 'hidden' : 'visible'; }); | |
fan.decorate.value(g); | |
}); | |
}; | |
function updatePositions() { | |
var container = d3.select(this); | |
var datum = container.datum()[0]; | |
if (datum.state !== 'DONE') { | |
var mouse = d3.mouse(this); | |
var snapped = fan.snap.value.apply(this, mouse); | |
if (datum.state === 'SELECT_SOURCE') { | |
datum.source = datum.target = snapped; | |
} else if (datum.state === 'SELECT_TARGET') { | |
datum.target = snapped; | |
} else { | |
throw new Error('Unknown state ' + datum.state); | |
} | |
} | |
} | |
function mouseenter() { | |
var container = d3.select(this) | |
.on('click.fan', mouseclick) | |
.on('mousemove.fan', mousemove) | |
.on('mouseleave.fan', mouseleave); | |
var data = container.datum(); | |
if (data[0] == null) { | |
data.push({ | |
state: 'SELECT_SOURCE' | |
}); | |
} | |
updatePositions.call(this); | |
container.call(fan); | |
} | |
function mousemove() { | |
var container = d3.select(this); | |
updatePositions.call(this); | |
container.call(fan); | |
} | |
function mouseleave() { | |
var container = d3.select(this); | |
var data = container.datum(); | |
if (data[0] != null && data[0].state === 'SELECT_SOURCE') { | |
data.pop(); | |
} | |
container.on('click.fan', null) | |
.on('mousemove.fan', null) | |
.on('mouseleave.fan', null); | |
} | |
function mouseclick() { | |
var container = d3.select(this); | |
var datum = container.datum()[0]; | |
switch (datum.state) { | |
case 'SELECT_SOURCE': | |
updatePositions.call(this); | |
event.fansource.apply(this, arguments); | |
datum.state = 'SELECT_TARGET'; | |
break; | |
case 'SELECT_TARGET': | |
updatePositions.call(this); | |
event.fantarget.apply(this, arguments); | |
datum.state = 'DONE'; | |
break; | |
case 'DONE': | |
event.fanclear.apply(this, arguments); | |
datum.state = 'SELECT_SOURCE'; | |
updatePositions.call(this); | |
break; | |
default: | |
throw new Error('Unknown state ' + datum.state); | |
} | |
container.call(fan); | |
} | |
fan.xScale = fc.utilities.property(d3.time.scale()); | |
fan.yScale = fc.utilities.property(d3.scale.linear()); | |
fan.snap = fc.utilities.property(function(x, y) { return {x: x, y: y}; }); | |
fan.decorate = fc.utilities.property(fc.utilities.fn.noop); | |
d3.rebind(fan, event, 'on'); | |
return fan; | |
}; | |
}(d3, fc)); | |
(function(d3, fc) { | |
'use strict'; | |
fc.tools.measure = function() { | |
var event = d3.dispatch('measuresource', 'measuretarget', 'measureclear'); | |
var measure = function(selection) { | |
selection.each(function(data) { | |
var container = d3.select(this) | |
.style('pointer-events', 'all') | |
.on('mouseenter.measure', mouseenter); | |
var overlay = container.selectAll('rect') | |
.data([data]); | |
overlay.enter() | |
.append('rect') | |
.style('visibility', 'hidden'); | |
container.select('rect') | |
.attr('x', measure.xScale.value.range()[0]) | |
.attr('y', measure.yScale.value.range()[1]) | |
.attr('width', measure.xScale.value.range()[1]) | |
.attr('height', measure.yScale.value.range()[0]); | |
var g = fc.utilities.simpleDataJoin(container, 'measure', data); | |
var enter = g.enter(); | |
enter.append('line') | |
.attr('class', 'tangent'); | |
enter.append('line') | |
.attr('class', 'horizontal'); | |
enter.append('line') | |
.attr('class', 'vertical'); | |
enter.append('text') | |
.attr('class', 'horizontal'); | |
enter.append('text') | |
.attr('class', 'vertical'); | |
g.select('line.tangent') | |
.attr('x1', function(d) { return d.source.x; }) | |
.attr('y1', function(d) { return d.source.y; }) | |
.attr('x2', function(d) { return d.target.x; }) | |
.attr('y2', function(d) { return d.target.y; }); | |
g.select('line.horizontal') | |
.attr('x1', function(d) { return d.source.x; }) | |
.attr('y1', function(d) { return d.source.y; }) | |
.attr('x2', function(d) { return d.target.x; }) | |
.attr('y2', function(d) { return d.source.y; }) | |
.style('visibility', function(d) { return d.state !== 'DONE' ? 'hidden' : 'visible'; }); | |
g.select('line.vertical') | |
.attr('x1', function(d) { return d.target.x; }) | |
.attr('y1', function(d) { return d.target.y; }) | |
.attr('x2', function(d) { return d.target.x; }) | |
.attr('y2', function(d) { return d.source.y; }) | |
.style('visibility', function(d) { return d.state !== 'DONE' ? 'hidden' : 'visible'; }); | |
var paddingValue = measure.padding.value.apply(this, arguments); | |
g.select('text.horizontal') | |
.attr('x', function(d) { return d.source.x + (d.target.x - d.source.x) / 2; }) | |
.attr('y', function(d) { return d.source.y - paddingValue; }) | |
.style('visibility', function(d) { return d.state !== 'DONE' ? 'hidden' : 'visible'; }) | |
.text(measure.xLabel.value); | |
g.select('text.vertical') | |
.attr('x', function(d) { return d.target.x + paddingValue; }) | |
.attr('y', function(d) { return d.source.y + (d.target.y - d.source.y) / 2; }) | |
.style('visibility', function(d) { return d.state !== 'DONE' ? 'hidden' : 'visible'; }) | |
.text(measure.yLabel.value); | |
measure.decorate.value(g); | |
}); | |
}; | |
function updatePositions() { | |
var container = d3.select(this); | |
var datum = container.datum()[0]; | |
if (datum.state !== 'DONE') { | |
var mouse = d3.mouse(this); | |
var snapped = measure.snap.value.apply(this, mouse); | |
if (datum.state === 'SELECT_SOURCE') { | |
datum.source = datum.target = snapped; | |
} else if (datum.state === 'SELECT_TARGET') { | |
datum.target = snapped; | |
} else { | |
throw new Error('Unknown state ' + datum.state); | |
} | |
} | |
} | |
function mouseenter() { | |
var container = d3.select(this) | |
.on('click.measure', mouseclick) | |
.on('mousemove.measure', mousemove) | |
.on('mouseleave.measure', mouseleave); | |
var data = container.datum(); | |
if (data[0] == null) { | |
data.push({ | |
state: 'SELECT_SOURCE' | |
}); | |
} | |
updatePositions.call(this); | |
container.call(measure); | |
} | |
function mousemove() { | |
var container = d3.select(this); | |
updatePositions.call(this); | |
container.call(measure); | |
} | |
function mouseleave() { | |
var container = d3.select(this); | |
var data = container.datum(); | |
if (data[0] != null && data[0].state === 'SELECT_SOURCE') { | |
data.pop(); | |
} | |
container.on('click.measure', null) | |
.on('mousemove.measure', null) | |
.on('mouseleave.measure', null); | |
} | |
function mouseclick() { | |
var container = d3.select(this); | |
var datum = container.datum()[0]; | |
switch (datum.state) { | |
case 'SELECT_SOURCE': | |
updatePositions.call(this); | |
event.measuresource.apply(this, arguments); | |
datum.state = 'SELECT_TARGET'; | |
break; | |
case 'SELECT_TARGET': | |
updatePositions.call(this); | |
event.measuretarget.apply(this, arguments); | |
datum.state = 'DONE'; | |
break; | |
case 'DONE': | |
event.measureclear.apply(this, arguments); | |
datum.state = 'SELECT_SOURCE'; | |
updatePositions.call(this); | |
break; | |
default: | |
throw new Error('Unknown state ' + datum.state); | |
} | |
container.call(measure); | |
} | |
measure.xScale = fc.utilities.property(d3.time.scale()); | |
measure.yScale = fc.utilities.property(d3.scale.linear()); | |
measure.snap = fc.utilities.property(function(x, y) { return {x: x, y: y}; }); | |
measure.decorate = fc.utilities.property(fc.utilities.fn.noop); | |
measure.xLabel = fc.utilities.functorProperty(''); | |
measure.yLabel = fc.utilities.functorProperty(''); | |
measure.padding = fc.utilities.functorProperty(2); | |
d3.rebind(measure, event, 'on'); | |
return measure; | |
}; | |
}(d3, fc)); |
This file contains 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> | |
<meta charset="utf-8"> | |
<body> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="Layout.js"></script> | |
<script src="d3-financial-components.js"></script> | |
<script src="tooltip.js"></script> | |
<link href="d3-financial-components.css" rel="stylesheet"/> | |
<style> | |
body { | |
font: 16px sans-serif; | |
} | |
.main-row>td { | |
height: 240px; | |
} | |
.volume-row>td { | |
height: 160px; | |
padding-bottom: 20px; | |
} | |
.navigator-row>td { | |
height: 80px; | |
} | |
.chart { | |
width: 640px; | |
} | |
svg { | |
width: 100%; | |
height: 100%; | |
} | |
span { | |
display:block; | |
transform: rotate(90deg); | |
} | |
rect.background { | |
fill: none; | |
stroke: #C0C0C0; | |
} | |
.gridlines line { | |
stroke: #C0C0C0; | |
stroke-width: 0.5px; | |
} | |
.candlestick.up rect { | |
fill: #fff; | |
} | |
.candlestick.down rect { | |
fill: #7CB5EC; | |
} | |
rect.extent { | |
fill: rgba(128, 179, 236, 0.3); | |
stroke: #C0C0C0; | |
stroke-width: 1px; | |
} | |
.line { | |
stroke: rgba(128, 179, 236, 1); | |
stroke-width: 1px; | |
} | |
.area { | |
fill: rgba(128, 179, 236, 0.05); | |
} | |
.crosshairs .vertical { | |
stroke: #C0C0C0; | |
stroke-width: 1px; | |
} | |
.crosshairs .horizontal { | |
display: none; | |
} | |
.crosshairs .info { | |
font: 10px sans-serif; | |
} | |
.crosshairs .info rect { | |
fill: rgba(249, 249, 249, 0.85); | |
stroke: rgba(124, 181, 236, 1); | |
stroke-width: 1px; | |
} | |
</style> | |
<table id="low-barrel"> | |
<tr class="main-row"> | |
<td class="chart"> | |
<svg class="main"></svg> | |
</td> | |
<td> | |
<span>OHLC</span> | |
</td> | |
</tr> | |
<tr class="volume-row"> | |
<td class="chart"> | |
<svg class="volume"></svg> | |
</td> | |
<td> | |
<span>Volume</span> | |
</td> | |
</tr> | |
<tr class="navigator-row"> | |
<td class="chart"> | |
<svg class="navigator"></svg> | |
</td> | |
<td></td> | |
</tr> | |
</table> | |
<script src="low-barrel.js"></script> | |
</body> | |
</html> |
This file contains 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
/** | |
* Copyright (c) 2014, Facebook, Inc. | |
* All rights reserved. | |
* | |
* This source code is licensed under the BSD-style license found in the | |
* LICENSE file in the root directory of this source tree. An additional grant | |
* of patent rights can be found in the PATENTS file in the same directory. | |
*/ | |
var computeLayout = (function() { | |
var CSS_UNDEFINED; | |
var CSS_FLEX_DIRECTION_ROW = 'row'; | |
var CSS_FLEX_DIRECTION_COLUMN = 'column'; | |
// var CSS_JUSTIFY_FLEX_START = 'flex-start'; | |
var CSS_JUSTIFY_CENTER = 'center'; | |
var CSS_JUSTIFY_FLEX_END = 'flex-end'; | |
var CSS_JUSTIFY_SPACE_BETWEEN = 'space-between'; | |
var CSS_JUSTIFY_SPACE_AROUND = 'space-around'; | |
var CSS_ALIGN_FLEX_START = 'flex-start'; | |
var CSS_ALIGN_CENTER = 'center'; | |
// var CSS_ALIGN_FLEX_END = 'flex-end'; | |
var CSS_ALIGN_STRETCH = 'stretch'; | |
var CSS_POSITION_RELATIVE = 'relative'; | |
var CSS_POSITION_ABSOLUTE = 'absolute'; | |
var leading = { | |
row: 'left', | |
column: 'top' | |
}; | |
var trailing = { | |
row: 'right', | |
column: 'bottom' | |
}; | |
var pos = { | |
row: 'left', | |
column: 'top' | |
}; | |
var dim = { | |
row: 'width', | |
column: 'height' | |
}; | |
function capitalizeFirst(str) { | |
return str.charAt(0).toUpperCase() + str.slice(1); | |
} | |
function getSpacing(node, type, suffix, location) { | |
var key = type + capitalizeFirst(location) + suffix; | |
if (key in node.style) { | |
return node.style[key]; | |
} | |
key = type + suffix; | |
if (key in node.style) { | |
return node.style[key]; | |
} | |
return 0; | |
} | |
function fillNodes(node) { | |
node.layout = { | |
width: undefined, | |
height: undefined, | |
top: 0, | |
left: 0 | |
}; | |
if (!node.style) { | |
node.style = {}; | |
} | |
if (!node.children || node.style.measure) { | |
node.children = []; | |
} | |
node.children.forEach(fillNodes); | |
return node; | |
} | |
function extractNodes(node) { | |
var layout = node.layout; | |
delete node.layout; | |
if (node.children && node.children.length > 0) { | |
layout.children = node.children.map(extractNodes); | |
} else { | |
delete node.children; | |
} | |
return layout; | |
} | |
function getPositiveSpacing(node, type, suffix, location) { | |
var key = type + capitalizeFirst(location) + suffix; | |
if (key in node.style && node.style[key] >= 0) { | |
return node.style[key]; | |
} | |
key = type + suffix; | |
if (key in node.style && node.style[key] >= 0) { | |
return node.style[key]; | |
} | |
return 0; | |
} | |
function isUndefined(value) { | |
return value === undefined; | |
} | |
function getMargin(node, location) { | |
return getSpacing(node, 'margin', '', location); | |
} | |
function getPadding(node, location) { | |
return getPositiveSpacing(node, 'padding', '', location); | |
} | |
function getBorder(node, location) { | |
return getPositiveSpacing(node, 'border', 'Width', location); | |
} | |
function getPaddingAndBorder(node, location) { | |
return getPadding(node, location) + getBorder(node, location); | |
} | |
function getMarginAxis(node, axis) { | |
return getMargin(node, leading[axis]) + getMargin(node, trailing[axis]); | |
} | |
function getPaddingAndBorderAxis(node, axis) { | |
return getPaddingAndBorder(node, leading[axis]) + getPaddingAndBorder(node, trailing[axis]); | |
} | |
function getJustifyContent(node) { | |
if ('justifyContent' in node.style) { | |
return node.style.justifyContent; | |
} | |
return 'flex-start'; | |
} | |
function getAlignItem(node, child) { | |
if ('alignSelf' in child.style) { | |
return child.style.alignSelf; | |
} | |
if ('alignItems' in node.style) { | |
return node.style.alignItems; | |
} | |
return 'stretch'; | |
} | |
function getFlexDirection(node) { | |
if ('flexDirection' in node.style) { | |
return node.style.flexDirection; | |
} | |
return 'column'; | |
} | |
function getPositionType(node) { | |
if ('position' in node.style) { | |
return node.style.position; | |
} | |
return 'relative'; | |
} | |
function getFlex(node) { | |
return node.style.flex; | |
} | |
function isFlex(node) { | |
return ( | |
getPositionType(node) === CSS_POSITION_RELATIVE && | |
getFlex(node) > 0 | |
); | |
} | |
function isFlexWrap(node) { | |
return node.style.flexWrap === 'wrap'; | |
} | |
function getDimWithMargin(node, axis) { | |
return node.layout[dim[axis]] + getMarginAxis(node, axis); | |
} | |
function isDimDefined(node, axis) { | |
return !isUndefined(node.style[dim[axis]]) && node.style[dim[axis]] >= 0; | |
} | |
function isPosDefined(node, pos) { | |
return !isUndefined(node.style[pos]); | |
} | |
function isMeasureDefined(node) { | |
return 'measure' in node.style; | |
} | |
function getPosition(node, pos) { | |
if (pos in node.style) { | |
return node.style[pos]; | |
} | |
return 0; | |
} | |
function fmaxf(a, b) { | |
if (a > b) { | |
return a; | |
} | |
return b; | |
} | |
// When the user specifically sets a value for width or height | |
function setDimensionFromStyle(node, axis) { | |
// The parent already computed us a width or height. We just skip it | |
if (!isUndefined(node.layout[dim[axis]])) { | |
return; | |
} | |
// We only run if there's a width or height defined | |
if (!isDimDefined(node, axis)) { | |
return; | |
} | |
// The dimensions can never be smaller than the padding and border | |
node.layout[dim[axis]] = fmaxf( | |
node.style[dim[axis]], | |
getPaddingAndBorderAxis(node, axis) | |
); | |
} | |
// If both left and right are defined, then use left. Otherwise return | |
// +left or -right depending on which is defined. | |
function getRelativePosition(node, axis) { | |
if (leading[axis] in node.style) { | |
return getPosition(node, leading[axis]); | |
} | |
return -getPosition(node, trailing[axis]); | |
} | |
function layoutNode(node, parentMaxWidth) { | |
var/*css_flex_direction_t*/ mainAxis = getFlexDirection(node); | |
var/*css_flex_direction_t*/ crossAxis = mainAxis === CSS_FLEX_DIRECTION_ROW ? | |
CSS_FLEX_DIRECTION_COLUMN : | |
CSS_FLEX_DIRECTION_ROW; | |
// Handle width and height style attributes | |
setDimensionFromStyle(node, mainAxis); | |
setDimensionFromStyle(node, crossAxis); | |
// The position is set by the parent, but we need to complete it with a | |
// delta composed of the margin and left/top/right/bottom | |
node.layout[leading[mainAxis]] += getMargin(node, leading[mainAxis]) + | |
getRelativePosition(node, mainAxis); | |
node.layout[leading[crossAxis]] += getMargin(node, leading[crossAxis]) + | |
getRelativePosition(node, crossAxis); | |
if (isMeasureDefined(node)) { | |
var/*float*/ width = CSS_UNDEFINED; | |
if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { | |
width = node.style.width; | |
} else if (!isUndefined(node.layout[dim[CSS_FLEX_DIRECTION_ROW]])) { | |
width = node.layout[dim[CSS_FLEX_DIRECTION_ROW]]; | |
} else { | |
width = parentMaxWidth - | |
getMarginAxis(node, CSS_FLEX_DIRECTION_ROW); | |
} | |
width -= getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); | |
// We only need to give a dimension for the text if we haven't got any | |
// for it computed yet. It can either be from the style attribute or because | |
// the element is flexible. | |
var/*bool*/ isRowUndefined = !isDimDefined(node, CSS_FLEX_DIRECTION_ROW) && | |
isUndefined(node.layout[dim[CSS_FLEX_DIRECTION_ROW]]); | |
var/*bool*/ isColumnUndefined = !isDimDefined(node, CSS_FLEX_DIRECTION_COLUMN) && | |
isUndefined(node.layout[dim[CSS_FLEX_DIRECTION_COLUMN]]); | |
// Let's not measure the text if we already know both dimensions | |
if (isRowUndefined || isColumnUndefined) { | |
var/*css_dim_t*/ measureDim = node.style.measure( | |
/*(c)!node->context,*/ | |
width | |
); | |
if (isRowUndefined) { | |
node.layout.width = measureDim.width + | |
getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); | |
} | |
if (isColumnUndefined) { | |
node.layout.height = measureDim.height + | |
getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_COLUMN); | |
} | |
} | |
return; | |
} | |
var/*int*/ i; | |
var/*int*/ ii; | |
var/*css_node_t**/ child; | |
var/*css_flex_direction_t*/ axis; | |
// Pre-fill some dimensions straight from the parent | |
for (i = 0; i < node.children.length; ++i) { | |
child = node.children[i]; | |
// Pre-fill cross axis dimensions when the child is using stretch before | |
// we call the recursive layout pass | |
if (getAlignItem(node, child) === CSS_ALIGN_STRETCH && | |
getPositionType(child) === CSS_POSITION_RELATIVE && | |
!isUndefined(node.layout[dim[crossAxis]]) && | |
!isDimDefined(child, crossAxis)) { | |
child.layout[dim[crossAxis]] = fmaxf( | |
node.layout[dim[crossAxis]] - | |
getPaddingAndBorderAxis(node, crossAxis) - | |
getMarginAxis(child, crossAxis), | |
// You never want to go smaller than padding | |
getPaddingAndBorderAxis(child, crossAxis) | |
); | |
} else if (getPositionType(child) === CSS_POSITION_ABSOLUTE) { | |
// Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both | |
// left and right or top and bottom). | |
for (ii = 0; ii < 2; ii++) { | |
axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; | |
if (!isUndefined(node.layout[dim[axis]]) && | |
!isDimDefined(child, axis) && | |
isPosDefined(child, leading[axis]) && | |
isPosDefined(child, trailing[axis])) { | |
child.layout[dim[axis]] = fmaxf( | |
node.layout[dim[axis]] - | |
getPaddingAndBorderAxis(node, axis) - | |
getMarginAxis(child, axis) - | |
getPosition(child, leading[axis]) - | |
getPosition(child, trailing[axis]), | |
// You never want to go smaller than padding | |
getPaddingAndBorderAxis(child, axis) | |
); | |
} | |
} | |
} | |
} | |
var/*float*/ definedMainDim = CSS_UNDEFINED; | |
if (!isUndefined(node.layout[dim[mainAxis]])) { | |
definedMainDim = node.layout[dim[mainAxis]] - | |
getPaddingAndBorderAxis(node, mainAxis); | |
} | |
// We want to execute the next two loops one per line with flex-wrap | |
var/*int*/ startLine = 0; | |
var/*int*/ endLine = 0; | |
// var/*int*/ nextOffset = 0; | |
var/*int*/ alreadyComputedNextLayout = 0; | |
// We aggregate the total dimensions of the container in those two variables | |
var/*float*/ linesCrossDim = 0; | |
var/*float*/ linesMainDim = 0; | |
while (endLine < node.children.length) { | |
// <Loop A> Layout non flexible children and count children by type | |
// mainContentDim is accumulation of the dimensions and margin of all the | |
// non flexible children. This will be used in order to either set the | |
// dimensions of the node if none already exist, or to compute the | |
// remaining space left for the flexible children. | |
var/*float*/ mainContentDim = 0; | |
// There are three kind of children, non flexible, flexible and absolute. | |
// We need to know how many there are in order to distribute the space. | |
var/*int*/ flexibleChildrenCount = 0; | |
var/*float*/ totalFlexible = 0; | |
var/*int*/ nonFlexibleChildrenCount = 0; | |
var/*float*/ maxWidth; | |
for (i = startLine; i < node.children.length; ++i) { | |
child = node.children[i]; | |
var/*float*/ nextContentDim = 0; | |
// It only makes sense to consider a child flexible if we have a computed | |
// dimension for the node. | |
if (!isUndefined(node.layout[dim[mainAxis]]) && isFlex(child)) { | |
flexibleChildrenCount++; | |
totalFlexible += getFlex(child); | |
// Even if we don't know its exact size yet, we already know the padding, | |
// border and margin. We'll use this partial information to compute the | |
// remaining space. | |
nextContentDim = getPaddingAndBorderAxis(child, mainAxis) + | |
getMarginAxis(child, mainAxis); | |
} else { | |
maxWidth = CSS_UNDEFINED; | |
if (mainAxis !== CSS_FLEX_DIRECTION_ROW) { | |
maxWidth = parentMaxWidth - | |
getMarginAxis(node, CSS_FLEX_DIRECTION_ROW) - | |
getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); | |
if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { | |
maxWidth = node.layout[dim[CSS_FLEX_DIRECTION_ROW]] - | |
getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); | |
} | |
} | |
// This is the main recursive call. We layout non flexible children. | |
if (alreadyComputedNextLayout === 0) { | |
layoutNode(child, maxWidth); | |
} | |
// Absolute positioned elements do not take part of the layout, so we | |
// don't use them to compute mainContentDim | |
if (getPositionType(child) === CSS_POSITION_RELATIVE) { | |
nonFlexibleChildrenCount++; | |
// At this point we know the final size and margin of the element. | |
nextContentDim = getDimWithMargin(child, mainAxis); | |
} | |
} | |
// The element we are about to add would make us go to the next line | |
if (isFlexWrap(node) && | |
!isUndefined(node.layout[dim[mainAxis]]) && | |
mainContentDim + nextContentDim > definedMainDim && | |
// If there's only one element, then it's bigger than the content | |
// and needs its own line | |
i !== startLine) { | |
alreadyComputedNextLayout = 1; | |
break; | |
} | |
alreadyComputedNextLayout = 0; | |
mainContentDim += nextContentDim; | |
endLine = i + 1; | |
} | |
// <Loop B> Layout flexible children and allocate empty space | |
// In order to position the elements in the main axis, we have two | |
// controls. The space between the beginning and the first element | |
// and the space between each two elements. | |
var/*float*/ leadingMainDim = 0; | |
var/*float*/ betweenMainDim = 0; | |
// The remaining available space that needs to be allocated | |
var/*float*/ remainingMainDim = 0; | |
if (!isUndefined(node.layout[dim[mainAxis]])) { | |
remainingMainDim = definedMainDim - mainContentDim; | |
} else { | |
remainingMainDim = fmaxf(mainContentDim, 0) - mainContentDim; | |
} | |
// If there are flexible children in the mix, they are going to fill the | |
// remaining space | |
if (flexibleChildrenCount !== 0) { | |
var/*float*/ flexibleMainDim = remainingMainDim / totalFlexible; | |
// The non flexible children can overflow the container, in this case | |
// we should just assume that there is no space available. | |
if (flexibleMainDim < 0) { | |
flexibleMainDim = 0; | |
} | |
// We iterate over the full array and only apply the action on flexible | |
// children. This is faster than actually allocating a new array that | |
// contains only flexible children. | |
for (i = startLine; i < endLine; ++i) { | |
child = node.children[i]; | |
if (isFlex(child)) { | |
// At this point we know the final size of the element in the main | |
// dimension | |
child.layout[dim[mainAxis]] = flexibleMainDim * getFlex(child) + | |
getPaddingAndBorderAxis(child, mainAxis); | |
maxWidth = CSS_UNDEFINED; | |
if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { | |
maxWidth = node.layout[dim[CSS_FLEX_DIRECTION_ROW]] - | |
getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); | |
} else if (mainAxis !== CSS_FLEX_DIRECTION_ROW) { | |
maxWidth = parentMaxWidth - | |
getMarginAxis(node, CSS_FLEX_DIRECTION_ROW) - | |
getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); | |
} | |
// And we recursively call the layout algorithm for this child | |
layoutNode(child, maxWidth); | |
} | |
} | |
// We use justifyContent to figure out how to allocate the remaining | |
// space available | |
} else { | |
var/*css_justify_t*/ justifyContent = getJustifyContent(node); | |
if (justifyContent === CSS_JUSTIFY_CENTER) { | |
leadingMainDim = remainingMainDim / 2; | |
} else if (justifyContent === CSS_JUSTIFY_FLEX_END) { | |
leadingMainDim = remainingMainDim; | |
} else if (justifyContent === CSS_JUSTIFY_SPACE_BETWEEN) { | |
remainingMainDim = fmaxf(remainingMainDim, 0); | |
if (flexibleChildrenCount + nonFlexibleChildrenCount - 1 !== 0) { | |
betweenMainDim = remainingMainDim / | |
(flexibleChildrenCount + nonFlexibleChildrenCount - 1); | |
} else { | |
betweenMainDim = 0; | |
} | |
} else if (justifyContent === CSS_JUSTIFY_SPACE_AROUND) { | |
// Space on the edges is half of the space between elements | |
betweenMainDim = remainingMainDim / | |
(flexibleChildrenCount + nonFlexibleChildrenCount); | |
leadingMainDim = betweenMainDim / 2; | |
} | |
} | |
// <Loop C> Position elements in the main axis and compute dimensions | |
// At this point, all the children have their dimensions set. We need to | |
// find their position. In order to do that, we accumulate data in | |
// variables that are also useful to compute the total dimensions of the | |
// container! | |
var/*float*/ crossDim = 0; | |
var/*float*/ mainDim = leadingMainDim + | |
getPaddingAndBorder(node, leading[mainAxis]); | |
for (i = startLine; i < endLine; ++i) { | |
child = node.children[i]; | |
if (getPositionType(child) === CSS_POSITION_ABSOLUTE && | |
isPosDefined(child, leading[mainAxis])) { | |
// In case the child is position absolute and has left/top being | |
// defined, we override the position to whatever the user said | |
// (and margin/border). | |
child.layout[pos[mainAxis]] = getPosition(child, leading[mainAxis]) + | |
getBorder(node, leading[mainAxis]) + | |
getMargin(child, leading[mainAxis]); | |
} else { | |
// If the child is position absolute (without top/left) or relative, | |
// we put it at the current accumulated offset. | |
child.layout[pos[mainAxis]] += mainDim; | |
} | |
// Now that we placed the element, we need to update the variables | |
// We only need to do that for relative elements. Absolute elements | |
// do not take part in that phase. | |
if (getPositionType(child) === CSS_POSITION_RELATIVE) { | |
// The main dimension is the sum of all the elements dimension plus | |
// the spacing. | |
mainDim += betweenMainDim + getDimWithMargin(child, mainAxis); | |
// The cross dimension is the max of the elements dimension since there | |
// can only be one element in that cross dimension. | |
crossDim = fmaxf(crossDim, getDimWithMargin(child, crossAxis)); | |
} | |
} | |
var/*float*/ containerMainAxis = node.layout[dim[mainAxis]]; | |
// If the user didn't specify a width or height, and it has not been set | |
// by the container, then we set it via the children. | |
if (isUndefined(containerMainAxis)) { | |
containerMainAxis = fmaxf( | |
// We're missing the last padding at this point to get the final | |
// dimension | |
mainDim + getPaddingAndBorder(node, trailing[mainAxis]), | |
// We can never assign a width smaller than the padding and borders | |
getPaddingAndBorderAxis(node, mainAxis) | |
); | |
} | |
var/*float*/ containerCrossAxis = node.layout[dim[crossAxis]]; | |
if (isUndefined(node.layout[dim[crossAxis]])) { | |
containerCrossAxis = fmaxf( | |
// For the cross dim, we add both sides at the end because the value | |
// is aggregate via a max function. Intermediate negative values | |
// can mess this computation otherwise | |
crossDim + getPaddingAndBorderAxis(node, crossAxis), | |
getPaddingAndBorderAxis(node, crossAxis) | |
); | |
} | |
// <Loop D> Position elements in the cross axis | |
for (i = startLine; i < endLine; ++i) { | |
child = node.children[i]; | |
if (getPositionType(child) === CSS_POSITION_ABSOLUTE && | |
isPosDefined(child, leading[crossAxis])) { | |
// In case the child is absolutely positionned and has a | |
// top/left/bottom/right being set, we override all the previously | |
// computed positions to set it correctly. | |
child.layout[pos[crossAxis]] = getPosition(child, leading[crossAxis]) + | |
getBorder(node, leading[crossAxis]) + | |
getMargin(child, leading[crossAxis]); | |
} else { | |
var/*float*/ leadingCrossDim = getPaddingAndBorder(node, leading[crossAxis]); | |
// For a relative children, we're either using alignItems (parent) or | |
// alignSelf (child) in order to determine the position in the cross axis | |
if (getPositionType(child) === CSS_POSITION_RELATIVE) { | |
var/*css_align_t*/ alignItem = getAlignItem(node, child); | |
if (alignItem === CSS_ALIGN_STRETCH) { | |
// You can only stretch if the dimension has not already been set | |
// previously. | |
if (!isDimDefined(child, crossAxis)) { | |
child.layout[dim[crossAxis]] = fmaxf( | |
containerCrossAxis - | |
getPaddingAndBorderAxis(node, crossAxis) - | |
getMarginAxis(child, crossAxis), | |
// You never want to go smaller than padding | |
getPaddingAndBorderAxis(child, crossAxis) | |
); | |
} | |
} else if (alignItem !== CSS_ALIGN_FLEX_START) { | |
// The remaining space between the parent dimensions+padding and child | |
// dimensions+margin. | |
var/*float*/ remainingCrossDim = containerCrossAxis - | |
getPaddingAndBorderAxis(node, crossAxis) - | |
getDimWithMargin(child, crossAxis); | |
if (alignItem === CSS_ALIGN_CENTER) { | |
leadingCrossDim += remainingCrossDim / 2; | |
} else { // CSS_ALIGN_FLEX_END | |
leadingCrossDim += remainingCrossDim; | |
} | |
} | |
} | |
// And we apply the position | |
child.layout[pos[crossAxis]] += linesCrossDim + leadingCrossDim; | |
} | |
} | |
linesCrossDim += crossDim; | |
linesMainDim = fmaxf(linesMainDim, mainDim); | |
startLine = endLine; | |
} | |
// If the user didn't specify a width or height, and it has not been set | |
// by the container, then we set it via the children. | |
if (isUndefined(node.layout[dim[mainAxis]])) { | |
node.layout[dim[mainAxis]] = fmaxf( | |
// We're missing the last padding at this point to get the final | |
// dimension | |
linesMainDim + getPaddingAndBorder(node, trailing[mainAxis]), | |
// We can never assign a width smaller than the padding and borders | |
getPaddingAndBorderAxis(node, mainAxis) | |
); | |
} | |
if (isUndefined(node.layout[dim[crossAxis]])) { | |
node.layout[dim[crossAxis]] = fmaxf( | |
// For the cross dim, we add both sides at the end because the value | |
// is aggregate via a max function. Intermediate negative values | |
// can mess this computation otherwise | |
linesCrossDim + getPaddingAndBorderAxis(node, crossAxis), | |
getPaddingAndBorderAxis(node, crossAxis) | |
); | |
} | |
// <Loop E> Calculate dimensions for absolutely positioned elements | |
for (i = 0; i < node.children.length; ++i) { | |
child = node.children[i]; | |
if (getPositionType(child) === CSS_POSITION_ABSOLUTE) { | |
// Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both | |
// left and right or top and bottom). | |
for (ii = 0; ii < 2; ii++) { | |
axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; | |
if (!isUndefined(node.layout[dim[axis]]) && | |
!isDimDefined(child, axis) && | |
isPosDefined(child, leading[axis]) && | |
isPosDefined(child, trailing[axis])) { | |
child.layout[dim[axis]] = fmaxf( | |
node.layout[dim[axis]] - | |
getPaddingAndBorderAxis(node, axis) - | |
getMarginAxis(child, axis) - | |
getPosition(child, leading[axis]) - | |
getPosition(child, trailing[axis]), | |
// You never want to go smaller than padding | |
getPaddingAndBorderAxis(child, axis) | |
); | |
} | |
} | |
for (ii = 0; ii < 2; ii++) { | |
axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; | |
if (isPosDefined(child, trailing[axis]) && | |
!isPosDefined(child, leading[axis])) { | |
child.layout[leading[axis]] = | |
node.layout[dim[axis]] - | |
child.layout[dim[axis]] - | |
getPosition(child, trailing[axis]); | |
} | |
} | |
} | |
} | |
} | |
return { | |
computeLayout: layoutNode, | |
fillNodes: fillNodes, | |
extractNodes: extractNodes | |
}; | |
})(); | |
// UMD (Universal Module Definition) | |
// See https://github.com/umdjs/umd for reference | |
(function (root, factory) { | |
if (typeof define === 'function' && define.amd) { | |
// AMD. Register as an anonymous module. | |
define([], factory); | |
} else if (typeof exports === 'object') { | |
// Node. Does not work with strict CommonJS, but | |
// only CommonJS-like environments that support module.exports, | |
// like Node. | |
module.exports = factory(); | |
} else { | |
// Browser globals (root is window) | |
root.returnExports = factory(); | |
} | |
}(this, function () { | |
return computeLayout; | |
})); |
This file contains 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(d3, fc) { | |
'use strict'; | |
var dataGenerator = fc.dataGenerator() | |
.startDate(new Date(2014, 1, 1)); | |
var container = d3.select('#low-barrel') | |
.datum(dataGenerator(250)); | |
function mainChart(selection) { | |
var data = selection.datum(); | |
var gridlines = fc.scale.gridlines() | |
.yTicks(3); | |
var candlestick = fc.series.candlestick(); | |
var crosshairs = fc.tools.crosshairs() | |
.decorate(fc.tooltip()) | |
.snap(fc.utilities.seriesPointSnap(candlestick, data)) | |
.on('trackingmove.link', render); | |
var multi = fc.series.multi() | |
.series([gridlines, candlestick, crosshairs]) | |
.mapping(function(data, series) { | |
switch (series) { | |
case crosshairs: | |
return data.crosshairs; | |
default: | |
return data; | |
} | |
}); | |
var chart = fc.charts.linearTimeSeries() | |
.xDomain(data.dateDomain) | |
.xTicks(0) | |
.yDomain(fc.utilities.extent(data, ['high', 'low'])) | |
.yNice() | |
.yTicks(3) | |
.plotArea(multi); | |
selection.call(chart); | |
} | |
function volumeChart(selection) { | |
var data = selection.datum(); | |
var gridlines = fc.scale.gridlines() | |
.yTicks(2); | |
var bar = fc.series.bar() | |
.yValue(function(d) { return d.volume; }); | |
var crosshairs = fc.tools.crosshairs() | |
.snap(fc.utilities.seriesPointSnap(bar, data)) | |
.on('trackingmove.link', render); | |
var multi = fc.series.multi() | |
.series([gridlines, bar, crosshairs]) | |
.mapping(function(data, series) { | |
switch (series) { | |
case crosshairs: | |
return data.crosshairs; | |
default: | |
return data; | |
} | |
}); | |
var chart = fc.charts.linearTimeSeries() | |
.xDomain(data.dateDomain) | |
.yDomain(fc.utilities.extent(data, 'volume')) | |
.yNice() | |
.yTicks(2) | |
.plotArea(multi); | |
selection.call(chart); | |
} | |
function navigatorChart(selection) { | |
var data = selection.datum(); | |
var yDomain = fc.utilities.extent(data, 'close'); | |
var chart = fc.charts.linearTimeSeries() | |
.xDomain(fc.utilities.extent(data, 'date')) | |
.yDomain(yDomain) | |
.yNice() | |
.xTicks(3) | |
.yTicks(0); | |
var gridlines = fc.scale.gridlines() | |
.xTicks(3) | |
.yTicks(0); | |
var line = fc.series.line(); | |
var area = fc.series.area(); | |
var brush = d3.svg.brush() | |
.on('brush', function() { | |
var domain = [brush.extent()[0][0], brush.extent()[1][0]]; | |
// Scales with a domain delta of 0 === NaN | |
if (domain[0] - domain[1] !== 0) { | |
data.dateDomain = domain; | |
render(); | |
} | |
}); | |
var multi = fc.series.multi() | |
.series([gridlines, line, area, brush]) | |
.mapping(function(data, series) { | |
// Need to set the extent AFTER the scales | |
// are set AND their ranges defined | |
if (series === brush) { | |
brush.extent([ | |
[data.dateDomain[0], chart.yDomain()[0]], | |
[data.dateDomain[1], chart.yDomain()[1]] | |
]); | |
} | |
return data; | |
}); | |
chart.plotArea(multi); | |
selection.call(chart); | |
} | |
function render() { | |
var data = container.datum(); | |
// Enhance data with interactive state | |
if (data.crosshairs == null) { | |
data.crosshairs = []; | |
} | |
if (data.dateDomain == null) { | |
var maxDate = fc.utilities.extent(container.datum(), 'date')[1]; | |
var dateScale = d3.time.scale() | |
.domain([maxDate - 50 * 24 * 60 * 60 * 1000, maxDate]) | |
.nice(); | |
data.dateDomain = dateScale.domain(); | |
} | |
// Calculate visible data for main/volume charts | |
var bisector = d3.bisector(function(d) { return d.date; }); | |
var visibleData = data.slice( | |
// Pad and clamp the bisector values to ensure extents can be calculated | |
Math.max(0, bisector.left(data, data.dateDomain[0]) - 1), | |
Math.min(bisector.right(data, data.dateDomain[1]) + 1, data.length) | |
); | |
visibleData.dateDomain = data.dateDomain; | |
visibleData.crosshairs = data.crosshairs; | |
container.select('svg.main') | |
.datum(visibleData) | |
.call(mainChart); | |
container.select('svg.volume') | |
.datum(visibleData) | |
.call(volumeChart); | |
container.select('svg.navigator') | |
.call(navigatorChart); | |
// TODO: Add pan/zoom behaviour | |
} | |
render(); | |
})(d3, fc); |
This file contains 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(d3, fc) { | |
'use strict'; | |
// Assigning to fc is nasty but there's not a lot of choice I don't think... | |
fc.tooltip = function() { | |
var tooltip = function(selection) { | |
var container = selection.enter() | |
.append('g') | |
.attr('class', 'info'); | |
container.append('rect') | |
.attr({ | |
width: 130, | |
height: 76, | |
fill: 'white' | |
}); | |
container.append('text'); | |
container = selection.select('g.info') | |
.attr('transform', function(d) { | |
var dx = Number(d.x); | |
var x = dx < 150 ? dx + 10 : dx - 150 + 10; | |
return 'translate(' + x + ',' + 10 + ')'; | |
}); | |
var tspan = container.select('text') | |
.selectAll('tspan') | |
.data(tooltip.items.value); | |
tspan.enter() | |
.append('tspan') | |
.attr('x', 4) | |
.attr('dy', 12); | |
tspan.text(function(d) { | |
return d(container.datum().datum); | |
}); | |
}; | |
function format(type, value) { | |
return tooltip.formatters.value[type](value); | |
} | |
tooltip.items = fc.utilities.property([ | |
function(d) { return format('date', d.date); }, | |
function(d) { return 'Open: ' + format('price', d.open); }, | |
function(d) { return 'High: ' + format('price', d.high); }, | |
function(d) { return 'Low: ' + format('price', d.low); }, | |
function(d) { return 'Close: ' + format('price', d.close); }, | |
function(d) { return 'Volume: ' + format('volume', d.volume); } | |
]); | |
tooltip.formatters = fc.utilities.property({ | |
date: d3.time.format('%A, %b %e, %Y'), | |
price: d3.format('.2f'), | |
volume: d3.format('0,5p') | |
}); | |
return tooltip; | |
}; | |
})(d3, fc); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment