|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
body { |
|
font: 10px sans-serif; |
|
margin: 0; |
|
} |
|
|
|
.line { |
|
fill: none; |
|
stroke: #666; |
|
stroke-width: 1.5px; |
|
} |
|
|
|
.area { |
|
fill: #e7e7e7; |
|
} |
|
|
|
</style> |
|
<body> |
|
<script src="//d3js.org/d3.v3.min.js"></script> |
|
<script> |
|
|
|
// Constants that underlie the layout (a.k.a. magic numbers) |
|
var margin = {top: 8, right: 10, bottom: 2, left: 10}, |
|
fullWidth = 960, |
|
fullItemHeight = 69, |
|
textOffset = 6; |
|
|
|
|
|
/** |
|
* The benefit of extracting out *layout* this way is that the same data |
|
* can be used for canvas etc. rendering;these substrate-invariant data are |
|
* ideally detached from the DOM. Explicit control over what data goes into |
|
* the view model (here: the`symbols` array) - for even tighter control |
|
* for preventing accidental global use or accidental variable capture, |
|
* structure your code into ES2016, browserify or require.js modules. IIFEs |
|
* can also be used to prevent accidental leak of variables which have use |
|
* for local computation only. |
|
*/ |
|
|
|
function width() { return fullWidth - margin.left - margin.right; } |
|
function itemHeight() { return fullItemHeight - margin.top - margin.bottom; } |
|
|
|
var layout = { |
|
width: width(), |
|
height: itemHeight(), |
|
fullWidth: fullWidth, |
|
fullItemHeight: fullItemHeight, |
|
marginTranslate: [margin.left, margin.top], // reusable for canvas |
|
textAnchor: "end", // canvas: with switch/case and ctx.measureText |
|
textX: width() - textOffset, |
|
textY: itemHeight() - textOffset |
|
} |
|
|
|
var parseDate = d3.time.format("%b %Y").parse; |
|
|
|
function type(d) { |
|
d.price = +d.price; |
|
d.date = parseDate(d.date); |
|
return d; |
|
} |
|
|
|
function symbolAccessor(value) { return value.symbol; } |
|
function timeAccessor(value) { return value.date; } |
|
function priceAccessor(value) { return value.price; } |
|
|
|
// This can obviously be used only in the SVG renderer |
|
function svgTranslate(vector) { |
|
return "translate(" + vector[0] + "," + vector[1] + ")"; |
|
} |
|
|
|
d3.tsv("stocks.tsv", type, function(error, inputData) { |
|
|
|
|
|
/** |
|
* ViewModel - logic + data preparation - independent of how we render |
|
*/ |
|
|
|
var allDates = inputData.map(timeAccessor); |
|
|
|
var commonTimeScale = d3.time.scale() |
|
.range([0, layout.width]) |
|
.domain(d3.extent(allDates)); // simpler and assumes no row order |
|
|
|
var symbols = d3.nest() |
|
.key(symbolAccessor) |
|
.entries(inputData) |
|
.map(function(nestEntry) { |
|
|
|
// Use Object.assign({}, symbol, {y: d3.scale.linear..}) in |
|
// ES2015 instead of the below code. |
|
|
|
return { |
|
|
|
// 'key', 'values' properties come from the `nest` above |
|
key: nestEntry.key, |
|
values: nestEntry.values, |
|
|
|
// Provide the additional ViewModel data |
|
// Renderer needs not be x=date or y=price specific. Or |
|
// just scale here |
|
xAccessor: timeAccessor, |
|
yAccessor: priceAccessor, |
|
xScale: commonTimeScale, // useful if x axes aren't shared |
|
yScale: d3.scale.log() // log is good for multiple years |
|
.domain(d3.extent(nestEntry.values, priceAccessor)) |
|
.range([layout.height, 0]), |
|
layout: layout |
|
}; |
|
}); |
|
|
|
|
|
/** |
|
* View - Render snippets - independent of how the data was arrived at, or |
|
* even, what the data represents |
|
* |
|
* The below functions no longer perform any logic with the data (low |
|
* coupling) and they don't extend, augment or modify the data that they |
|
* receive. Linear flow and avoidance of imperative data handling. |
|
* |
|
* All the remaining functions are much more self-contained as they simply |
|
* get their data idiomatically via data binding. Can be reused for very |
|
* different domains. |
|
* |
|
* It can be factored out into a module which receives no input except |
|
* `symbols` and `rootElement`. |
|
*/ |
|
|
|
var data = symbols; |
|
var rootElement = d3.select("body"); |
|
|
|
// each element in the data maps to a separate itemContainer (here: svg) |
|
var itemContainer = rootElement.selectAll("svg") |
|
.data(data, function(s) { return s.key; }) // use key + keyfun |
|
.enter().append("svg") |
|
.attr("width", function(s) { return s.layout.fullWidth; }) |
|
.attr("height", function(s) { return s.layout.fullItemHeight; }); |
|
|
|
// each itemContainer has a single item, adhering to the margin offset |
|
var item = itemContainer |
|
.append("g") |
|
.attr("transform", function(s) { |
|
return svgTranslate(s.layout.marginTranslate); |
|
}); |
|
|
|
// area is rendered first |
|
item.append("path") |
|
.classed("area", true) |
|
.attr("d", function(s) { |
|
var areaPathStringMaker = d3.svg.area() |
|
.x( function(d) { return s.xScale(s.xAccessor(d)); }) |
|
.y1(function(d) { return s.yScale(s.yAccessor(d)); }) |
|
.y0(s.layout.height); |
|
return areaPathStringMaker(s.values); |
|
}); |
|
|
|
// the line is superimposed on the area |
|
item.append("path") |
|
.classed("line", true) |
|
.attr("d", function(s) { |
|
var linePathStringMaker = d3.svg.line() |
|
.x(function(d) { return s.xScale(s.xAccessor(d)); }) |
|
.y(function(d) { return s.yScale(s.yAccessor(d)); }); |
|
return linePathStringMaker(s.values); |
|
}); |
|
|
|
// item title added |
|
item.append("text") |
|
.attr("x", function(s) {return s.layout.textX; }) |
|
.attr("y", function(s) {return s.layout.textY; }) |
|
.style("text-anchor", function(s) {return s.layout.textAnchor; }) |
|
.text(function(s) { return s.key; }); |
|
|
|
}); |
|
|
|
</script> |