Built with blockbuilder.org
Last active
October 7, 2017 19:20
-
-
Save pergamonster/c5bf638a7d8a2594535553e5b0639ab8 to your computer and use it in GitHub Desktop.
stackedchart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| license: mit |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <script src="https://d3js.org/d3.v4.min.js"></script> | |
| <script src="https://unpkg.com/d3fc@13.0.1"></script> | |
| <script src="https://unpkg.com/lodash@4.17.4"></script> | |
| <script src="https://unpkg.com/d3-scale-chromatic@1.1.1"></script> | |
| <style> | |
| g.profile g.multi { | |
| opacity: 0.8; | |
| } | |
| g.profile g.multi:hover { | |
| opacity: 1.0; | |
| } | |
| </style> | |
| <div id='chart' style='height: 500px'></div> | |
| <script> | |
| const createMarketProfile = (data, priceBuckets) => { | |
| // find the price bucket size | |
| const priceStep = priceBuckets[1] - priceBuckets[0]; | |
| // determine whether a datapoint is within a bucket | |
| const inBucket = (datum, priceBucket) => | |
| datum.low < priceBucket && datum.high > (priceBucket - priceStep); | |
| // the volume contribution for this range | |
| const volumeInBucket = (datum, priceBucket) => | |
| inBucket(datum, priceBucket) ? datum.volume / Math.ceil((datum.high - datum.low) / priceStep) : 0; | |
| // map each point in our time series, to construct the market profile | |
| const marketProfile = data.map( | |
| (datum, index) => priceBuckets.map(priceBucket => { | |
| // determine how many points to the left are also within this time bucket | |
| const base = d3.sum(data.slice(0, index) | |
| .map(d => volumeInBucket(d, priceBucket))); | |
| return { | |
| base, | |
| value: base + volumeInBucket(datum, priceBucket), | |
| price: priceBucket | |
| }; | |
| }) | |
| ); | |
| // similar to d3-stack - cache the underlying data | |
| marketProfile.data = data; | |
| return marketProfile; | |
| }; | |
| const seriesMarketProfile = () => { | |
| let xScale, yScale; | |
| let bandwidth = 20; | |
| const join = fc.dataJoin('g', 'profile'); | |
| const barSeries = fc.autoBandwidth(fc.seriesSvgBar()) | |
| .orient('horizontal') | |
| .crossValue(d => d.price) | |
| .mainValue(d => d.value) | |
| .baseValue(d => d.base); | |
| const colorScale = d3.scaleSequential(d3.interpolateSpectral); | |
| const repeatSeries = fc.seriesSvgRepeat() | |
| .series(barSeries) | |
| .orient('horizontal') | |
| .decorate((selection) => { | |
| selection.enter() | |
| .each((data, index, group) => { | |
| d3.select(group[index]) | |
| .selectAll('g.bar') | |
| .attr('fill', () => colorScale(index)); | |
| }); | |
| }); | |
| const series = (selection) => { | |
| selection.each((data, index, group) => { | |
| const xDomain = d3.extent(_.flattenDeep(data).map(d => d.value)); | |
| colorScale.domain([0, data.length]); | |
| join(d3.select(group[index]), data) | |
| .each((marketProfile, index, group) => { | |
| // create a composite scale that applies the required offset | |
| const leftEdge = xScale(marketProfile.data[0].date); | |
| const offset = d3.scaleLinear() | |
| .domain(xDomain) | |
| .range([leftEdge, leftEdge + bandwidth]); | |
| repeatSeries.yScale(yScale) | |
| .xScale(offset); | |
| d3.select(group[index]) | |
| .call(repeatSeries); | |
| }); | |
| }) | |
| }; | |
| series.xScale = (...args) => { | |
| if (!args.length) { | |
| return xScale; | |
| } | |
| xScale = args[0]; | |
| return series; | |
| }; | |
| series.bandwidth = (...args) => { | |
| if (!args.length) { | |
| return bandwidth; | |
| } | |
| bandwidth = args[0]; | |
| return series; | |
| }; | |
| series.yScale = (...args) => { | |
| if (!args.length) { | |
| return yScale; | |
| } | |
| yScale = args[0]; | |
| return series; | |
| }; | |
| return series; | |
| } | |
| const pointOfControl = (marketProfile) => | |
| _.maxBy(_.flatten(marketProfile), d => d.value).price; | |
| // create some random financial data | |
| const generator = fc.randomFinancial() | |
| .interval(d3.timeMinute) | |
| const timeSeries = generator(12 * 8); | |
| // determine the price range | |
| const extent = fc.extentLinear() | |
| .accessors([d => d.high, d => d.low]); | |
| const priceRange = extent(timeSeries); | |
| // use a d3 scale to create a set of price buckets | |
| const priceScale = d3.scaleLinear() | |
| .domain(priceRange); | |
| const priceBuckets = priceScale.ticks(40); | |
| const series = _.chunk(timeSeries, 12) | |
| .map((data) => createMarketProfile(data, priceBuckets)); | |
| const marketProfileSeries = fc.autoBandwidth(seriesMarketProfile()); | |
| const pocSeries = fc.autoBandwidth(fc.seriesSvgErrorBar()) | |
| .crossValue(d => d.date) | |
| .lowValue(d => d.value) | |
| .highValue(d => d.value) | |
| .align('left'); | |
| const multiSeries = fc.seriesSvgMulti() | |
| .series([marketProfileSeries, pocSeries]) | |
| .mapping((data, index, series) => { | |
| switch(series[index]) { | |
| case pocSeries: | |
| return data.map(d => ({ | |
| date: d.data[0].date, | |
| value: pointOfControl(d) | |
| })); | |
| case marketProfileSeries: | |
| return data; | |
| } | |
| }); | |
| const xExtent = fc.extentDate() | |
| .accessors([d => d.data[0].date]); | |
| const profileChart = fc.chartSvgCartesian( | |
| d3.scaleBand(), | |
| d3.scaleBand() | |
| ) | |
| .xDomain(series.map(s => s.data[0].date)) | |
| .yDomain(priceBuckets) | |
| .yTickValues(priceBuckets.filter((d, i) => i % 4 == 0)) | |
| .xTickFormat(d3.timeFormat('%H:%M')) | |
| .yOrient('left') | |
| .xPadding(0.3) | |
| .plotArea(multiSeries); | |
| d3.select('#chart') | |
| .datum(series) | |
| .call(profileChart); | |
| </script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment