A market profile chart, using d3 and d3fc, as described in this blog post.
Built with blockbuilder.org
| license: mit | 
A market profile chart, using d3 and d3fc, as described in this blog post.
Built with blockbuilder.org
| 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); | 
| <!DOCTYPE html> | |
| <script src="https://d3js.org/d3.v4.min.js"></script> | |
| <script src="https://unpkg.com/[email protected]"></script> | |
| <script src="https://unpkg.com/[email protected]"></script> | |
| <script src="https://unpkg.com/[email protected]"></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 src='chart.js'></script> |