UNPKG

highstock-release

Version:

Official shim repo for Highstock releases.

638 lines (576 loc) 26.3 kB
/** * @license Highcharts JS v6.0.1 (2017-10-05) * * Indicator series type for Highstock * * (c) 2010-2017 Paweł Dalek * * License: www.highcharts.com/license */ 'use strict'; (function(factory) { if (typeof module === 'object' && module.exports) { module.exports = factory; } else { factory(Highcharts); } }(function(Highcharts) { (function(H) { /** * (c) 2010-2017 Paweł Dalek * * Volume By Price (VBP) indicator for Highstock * * License: www.highcharts.com/license */ // Utils function arrayExtremesOHLC(data) { var dataLength = data.length, min = data[0][3], max = min, i = 1, currentPoint; for (; i < dataLength; i++) { currentPoint = data[i][3]; if (currentPoint < min) { min = currentPoint; } if (currentPoint > max) { max = currentPoint; } } return { min: min, max: max }; } var abs = Math.abs, each = H.each, noop = H.noop, addEvent = H.addEvent, correctFloat = H.correctFloat, seriesType = H.seriesType, columnPrototype = H.seriesTypes.column.prototype; /** * The Volume By Price (VBP) series type. * * @constructor seriesTypes.vbp * @augments seriesTypes.vbp */ seriesType('vbp', 'sma', /** * Volume By Price indicator. * * This series requires `linkedTo` option to be set. * * @extends {plotOptions.sma} * @product highstock * @sample {highstock} stock/indicators/volume-by-price * Volume By Price indicator * @since 6.0.0 * @optionparent plotOptions.vbp */ { name: 'Volume by Price', /** * @excluding index,period */ params: { /** * The number of price zones. * * @type {Number} * @since 6.0.0 * @product highstock */ ranges: 12, /** * The id of volume series which is mandatory. * For example using OHLC data, volumeSeriesID='volume' means the indicator will be calculated using OHLC and volume values. * * @type {String} * @since 6.0.0 * @product highstock */ volumeSeriesID: 'volume' }, /** * The styles for lines which determine price zones. * * @type {Object} * @since 6.0.0 * @product highstock */ zoneLines: { /** * Enable/disable zone lines. * * @type {Boolean} * @since 6.0.0 * @default true * @product highstock */ enabled: true, styles: { /** * Color of zone lines. * * @type {Color} * @since 6.0.0 * @product highstock */ color: '#0A9AC9', /** * The dash style of zone lines. * * @type {String} * @since 6.0.0 * @product highstock */ dashStyle: 'LongDash', /** * Pixel width of zone lines. * * @type {Number} * @since 6.0.0 * @product highstock */ lineWidth: 1 } }, /** * The styles for bars when volume is divided into positive/negative. * * @type {Object} * @since 6.0.0 * @product highstock */ volumeDivision: { /** * Option to control if volume is divided. * * @type {Boolean} * @since 6.0.0 * @product highstock */ enabled: true, styles: { /** * Color of positive volume bars. * * @type {Color} * @since 6.0.0 * @product highstock */ positiveColor: 'rgba(144, 237, 125, 0.8)', /** * Color of negative volume bars. * * @type {Color} * @since 6.0.0 * @product highstock */ negativeColor: 'rgba(244, 91, 91, 0.8)' } }, // To enable series animation; must be animationLimit > pointCount animationLimit: 1000, enableMouseTracking: false, pointPadding: 0, zIndex: -1, crisp: true, dataGrouping: { enabled: false }, dataLabels: { enabled: true, allowOverlap: true, verticalAlign: 'top', format: 'P: {point.volumePos:.2f} | N: {point.volumeNeg:.2f}', padding: 0, style: { fontSize: '7px' } } }, { bindTo: { series: false, eventName: 'afterSetExtremes' }, calculateOn: 'render', markerAttribs: noop, drawGraph: noop, getColumnMetrics: columnPrototype.getColumnMetrics, crispCol: columnPrototype.crispCol, init: function(chart) { var indicator = this, params, baseSeries, volumeSeries; H.seriesTypes.sma.prototype.init.apply(indicator, arguments); params = indicator.options.params; baseSeries = indicator.linkedParent; volumeSeries = chart.get(params.volumeSeriesID); indicator.addCustomEvents(baseSeries, volumeSeries); return indicator; }, // Adds events related with removing series addCustomEvents: function(baseSeries, volumeSeries) { var indicator = this; function toEmptyIndicator() { indicator.chart.redraw(); indicator.setData([]); indicator.zoneStarts = []; if (indicator.zoneLinesSVG) { indicator.zoneLinesSVG.destroy(); delete indicator.zoneLinesSVG; } } // If base series is deleted, indicator series data is filled with an empty array indicator.dataEventsToUnbind.push( addEvent(baseSeries, 'remove', function() { toEmptyIndicator(); }) ); // If volume series is deleted, indicator series data is filled with an empty array if (volumeSeries) { indicator.dataEventsToUnbind.push( addEvent(volumeSeries, 'remove', function() { toEmptyIndicator(); }) ); } return indicator; }, // Initial animation animate: function(init) { var series = this, attr = {}; if (H.svg && !init) { attr.translateX = series.yAxis.pos; series.group.animate(attr, H.extend(H.animObject(series.options.animation), { step: function(val, fx) { series.group.attr({ scaleX: Math.max(0.001, fx.pos) }); } })); // Delete this function to allow it only once series.animate = null; } }, drawPoints: function() { var indicator = this; if (indicator.options.volumeDivision.enabled) { indicator.posNegVolume(true, true); columnPrototype.drawPoints.apply(indicator, arguments); indicator.posNegVolume(false, false); } columnPrototype.drawPoints.apply(indicator, arguments); }, // Function responsible for dividing volume into positive and negative posNegVolume: function(initVol, pos) { var indicator = this, signOrder = pos ? ['positive', 'negative'] : ['negative', 'positive'], volumeDivision = indicator.options.volumeDivision, pointLength = indicator.points.length, posWidths = [], negWidths = [], i = 0, pointWidth, priceZone, wholeVol, point; if (initVol) { indicator.posWidths = posWidths; indicator.negWidths = negWidths; } else { posWidths = indicator.posWidths; negWidths = indicator.negWidths; } for (; i < pointLength; i++) { point = indicator.points[i]; point[signOrder[0] + 'Graphic'] = point.graphic; point.graphic = point[signOrder[1] + 'Graphic']; if (initVol) { pointWidth = point.shapeArgs.width; priceZone = indicator.priceZones[i]; wholeVol = priceZone.wholeVolumeData; if (wholeVol) { posWidths.push(pointWidth / wholeVol * priceZone.positiveVolumeData); negWidths.push(pointWidth / wholeVol * priceZone.negativeVolumeData); } else { posWidths.push(0); negWidths.push(0); } } point.color = pos ? volumeDivision.styles.positiveColor : volumeDivision.styles.negativeColor; point.shapeArgs.width = pos ? indicator.posWidths[i] : indicator.negWidths[i]; point.shapeArgs.x = pos ? point.shapeArgs.x : indicator.posWidths[i]; } }, translate: function() { var indicator = this, options = indicator.options, chart = indicator.chart, yAxis = indicator.yAxis, yAxisMin = yAxis.min, zoneLinesOptions = indicator.options.zoneLines, priceZones = indicator.priceZones, yBarOffset = 0, indicatorPoints, volumeDataArray, maxVolume, primalBarWidth, barHeight, barHeightP, oldBarHeight, barWidth, pointPadding, chartPlotTop, barX, barY; columnPrototype.translate.apply(indicator); indicatorPoints = indicator.points; // Do translate operation when points exist if (indicatorPoints.length) { pointPadding = options.pointPadding < 0.5 ? options.pointPadding : 0.1; volumeDataArray = indicator.volumeDataArray; maxVolume = H.arrayMax(volumeDataArray); primalBarWidth = chart.plotWidth / 2; chartPlotTop = chart.plotTop; barHeight = abs(yAxis.toPixels(yAxisMin) - yAxis.toPixels(yAxisMin + indicator.rangeStep)); oldBarHeight = abs(yAxis.toPixels(yAxisMin) - yAxis.toPixels(yAxisMin + indicator.rangeStep)); if (pointPadding) { barHeightP = abs(barHeight * (1 - 2 * pointPadding)); yBarOffset = abs((barHeight - barHeightP) / 2); barHeight = abs(barHeightP); } each(indicatorPoints, function(point, index) { barX = point.barX = point.plotX = 0; barY = point.plotY = yAxis.toPixels(priceZones[index].start) - chartPlotTop - (yAxis.reversed ? (barHeight - oldBarHeight) : barHeight) - yBarOffset; barWidth = correctFloat(primalBarWidth * priceZones[index].wholeVolumeData / maxVolume); point.pointWidth = barWidth; point.shapeArgs = indicator.crispCol.apply(indicator, [barX, barY, barWidth, barHeight]); point.volumeNeg = priceZones[index].negativeVolumeData; point.volumePos = priceZones[index].positiveVolumeData; point.volumeAll = priceZones[index].wholeVolumeData; }); if (zoneLinesOptions.enabled) { indicator.drawZones(chart, yAxis, indicator.zoneStarts, zoneLinesOptions.styles); } } }, getValues: function(series, params) { var indicator = this, xValues = series.processedXData, yValues = series.processedYData, chart = series.chart, ranges = params.ranges, VBP = [], xData = [], yData = [], isOHLC, volumeSeries, priceZones; // Checks if base series exists if (!chart) { return H.error( 'Base series not found! In case it has been removed, add a new one.', true ); } // Checks if volume series exists if (!(volumeSeries = chart.get(params.volumeSeriesID))) { return H.error( 'Series ' + params.volumeSeriesID + ' not found! Check `volumeSeriesID`.', true ); } // Checks if series data fits the OHLC format isOHLC = H.isArray(yValues[0]); if (isOHLC && yValues[0].length !== 4) { return H.error( 'Type of ' + series.name + ' series is different than line, OHLC or candlestick.', true ); } // Price zones contains all the information about the zones (index, start, end, volumes, etc.) priceZones = indicator.priceZones = indicator.specifyZones(isOHLC, xValues, yValues, ranges, volumeSeries); each(priceZones, function(zone, index) { VBP.push([zone.x, zone.end]); xData.push(VBP[index][0]); yData.push(VBP[index][1]); }); return { values: VBP, xData: xData, yData: yData }; }, // Specifing where each zone should start ans end specifyZones: function(isOHLC, xValues, yValues, ranges, volumeSeries) { var indicator = this, rangeExtremes = isOHLC ? arrayExtremesOHLC(yValues) : false, lowRange = rangeExtremes ? rangeExtremes.min : H.arrayMin(yValues), highRange = rangeExtremes ? rangeExtremes.max : H.arrayMax(yValues), zoneStarts = indicator.zoneStarts = [], priceZones = [], i = 0, j = 1, rangeStep, zoneStartsLength; if (!lowRange || !highRange) { if (this.points.length) { this.setData([]); this.zoneStarts = []; this.zoneLinesSVG.destroy(); } return []; } rangeStep = indicator.rangeStep = correctFloat(highRange - lowRange) / ranges; zoneStarts.push(lowRange); for (; i < ranges - 1; i++) { zoneStarts.push(correctFloat(zoneStarts[i] + rangeStep)); } zoneStarts.push(highRange); zoneStartsLength = zoneStarts.length; // Creating zones for (; j < zoneStartsLength; j++) { priceZones.push({ index: j - 1, x: xValues[0], start: zoneStarts[j - 1], end: zoneStarts[j] }); } return indicator.volumePerZone(isOHLC, priceZones, volumeSeries, xValues, yValues); }, // Calculating sum of volume values for a specific zone volumePerZone: function(isOHLC, priceZones, volumeSeries, xValues, yValues) { var indicator = this, volumeXData = volumeSeries.processedXData, volumeYData = volumeSeries.processedYData, lastZoneIndex = priceZones.length - 1, baseSeriesLength = yValues.length, volumeSeriesLength = volumeYData.length, previousValue, startFlag, endFlag, value, i; // Checks if each point has a corresponding volume value if (abs(baseSeriesLength - volumeSeriesLength)) { // If the first point don't have volume, add 0 value at the beggining of the volume array if (xValues[0] !== volumeXData[0]) { volumeYData.unshift(0); } // If the last point don't have volume, add 0 value at the end of the volume array if (xValues[baseSeriesLength - 1] !== volumeXData[volumeSeriesLength - 1]) { volumeYData.push(0); } } indicator.volumeDataArray = []; each(priceZones, function(zone) { zone.wholeVolumeData = 0; zone.positiveVolumeData = 0; zone.negativeVolumeData = 0; for (i = 0; i < baseSeriesLength; i++) { startFlag = false; endFlag = false; value = isOHLC ? yValues[i][3] : yValues[i]; previousValue = i ? (isOHLC ? yValues[i - 1][3] : yValues[i - 1]) : value; // Checks if this is the point with the lowest close value and if so, adds it calculations if (value <= zone.start && zone.index === 0) { startFlag = true; } // Checks if this is the point with the highest close value and if so, adds it calculations if (value >= zone.end && zone.index === lastZoneIndex) { endFlag = true; } if ((value > zone.start || startFlag) && (value < zone.end || endFlag)) { zone.wholeVolumeData += volumeYData[i]; if (previousValue > value) { zone.negativeVolumeData += volumeYData[i]; } else { zone.positiveVolumeData += volumeYData[i]; } } } indicator.volumeDataArray.push(zone.wholeVolumeData); }); return priceZones; }, // Function responsoble for drawing additional lines indicating zones drawZones: function(chart, yAxis, zonesValues, zonesStyles) { var indicator = this, renderer = chart.renderer, zoneLinesSVG = indicator.zoneLinesSVG, zoneLinesPath = [], leftLinePos = 0, rightLinePos = chart.plotWidth, verticalOffset = chart.plotTop, verticalLinePos; each(zonesValues, function(value) { verticalLinePos = yAxis.toPixels(value) - verticalOffset; zoneLinesPath = zoneLinesPath.concat(chart.renderer.crispLine([ 'M', leftLinePos, verticalLinePos, 'L', rightLinePos, verticalLinePos ], zonesStyles.lineWidth)); }); // Create zone lines one path or update it while animating if (zoneLinesSVG) { zoneLinesSVG.animate({ d: zoneLinesPath }); } else { zoneLinesSVG = indicator.zoneLinesSVG = renderer.path(zoneLinesPath) .attr({ 'stroke-width': zonesStyles.lineWidth, 'stroke': zonesStyles.color, 'dashstyle': zonesStyles.dashStyle, 'zIndex': indicator.group.zIndex + 0.1 }) .add(indicator.group); } } }, { // Required for destroying negative part of volume destroy: function() { if (this.negativeGraphic) { this.negativeGraphic = this.negativeGraphic.destroy(); } return H.Point.prototype.destroy.apply(this, arguments); } }); /** * A `Volume By Price (VBP)` series. If the [type](#series.vbp.type) option is not * specified, it is inherited from [chart.type](#chart.type). * * For options that apply to multiple series, it is recommended to add * them to the [plotOptions.series](#plotOptions.series) options structure. * To apply to all series of this specific type, apply it to * [plotOptions.vbp](#plotOptions.vbp). * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.vbp * @excluding data,dataParser,dataURL * @product highstock * @apioption series.vbp */ /** * @extends series.sma.data * @product highstock * @apioption series.vbp.data */ }(Highcharts)); }));