123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- import uPlot from 'uplot';
- import { colorManipulator } from '@grafana/data';
- import { VizDisplayMode, ColorStrategy, CandleStyle } from './models.gen';
- const { alpha } = colorManipulator;
- export type FieldIndices = Record<string, number>;
- interface RendererOpts {
- mode: VizDisplayMode;
- candleStyle: CandleStyle;
- fields: FieldIndices;
- colorStrategy: ColorStrategy;
- upColor: string;
- downColor: string;
- flatColor: string;
- volumeAlpha: number;
- flatAsUp: boolean;
- }
- export function drawMarkers(opts: RendererOpts) {
- let { mode, candleStyle, fields, colorStrategy, upColor, downColor, flatColor, volumeAlpha, flatAsUp = true } = opts;
- const drawPrice = mode !== VizDisplayMode.Volume && fields.high != null && fields.low != null;
- const asCandles = drawPrice && candleStyle === CandleStyle.Candles;
- const drawVolume = mode !== VizDisplayMode.Candles && fields.volume != null;
- function selectPath(priceDir: number, flatPath: Path2D, upPath: Path2D, downPath: Path2D, flatAsUp: boolean) {
- return priceDir > 0 ? upPath : priceDir < 0 ? downPath : flatAsUp ? upPath : flatPath;
- }
- let tIdx = 0,
- oIdx = fields.open,
- hIdx = fields.high,
- lIdx = fields.low,
- cIdx = fields.close,
- vIdx = fields.volume;
- return (u: uPlot) => {
- // split by discrete color to reduce draw calls
- let downPath, upPath, flatPath;
- // with adjusted reduced
- let downPathVol, upPathVol, flatPathVol;
- if (drawPrice) {
- flatPath = new Path2D();
- upPath = new Path2D();
- downPath = new Path2D();
- }
- if (drawVolume) {
- downPathVol = new Path2D();
- upPathVol = new Path2D();
- flatPathVol = new Path2D();
- }
- let hollowPath = new Path2D();
- let ctx = u.ctx;
- let tData = u.data[tIdx!];
- let oData = u.data[oIdx!];
- let cData = u.data[cIdx!];
- let hData = drawPrice ? u.data[hIdx!] : null;
- let lData = drawPrice ? u.data[lIdx!] : null;
- let vData = drawVolume ? u.data[vIdx!] : null;
- let zeroPx = vIdx != null ? Math.round(u.valToPos(0, u.series[vIdx!].scale!, true)) : null;
- let [idx0, idx1] = u.series[0].idxs!;
- let dataX = u.data[0];
- let dataY = oData;
- let colWidth = u.bbox.width;
- if (dataX.length > 1) {
- // prior index with non-undefined y data
- let prevIdx = null;
- // scan full dataset for smallest adjacent delta
- // will not work properly for non-linear x scales, since does not do expensive valToPosX calcs till end
- for (let i = 0, minDelta = Infinity; i < dataX.length; i++) {
- if (dataY[i] !== undefined) {
- if (prevIdx != null) {
- let delta = Math.abs(dataX[i] - dataX[prevIdx]);
- if (delta < minDelta) {
- minDelta = delta;
- colWidth = Math.abs(u.valToPos(dataX[i], 'x') - u.valToPos(dataX[prevIdx], 'x'));
- }
- }
- prevIdx = i;
- }
- }
- }
- let barWidth = Math.round(0.6 * colWidth);
- let stickWidth = 2;
- let outlineWidth = 2;
- if (barWidth <= 12) {
- stickWidth = outlineWidth = 1;
- }
- let halfWidth = Math.floor(barWidth / 2);
- for (let i = idx0; i <= idx1; i++) {
- let tPx = Math.round(u.valToPos(tData[i]!, 'x', true));
- // current close vs prior close
- let interDir = i === idx0 ? 0 : Math.sign(cData[i]! - cData[i - 1]!);
- // current close vs current open
- let intraDir = Math.sign(cData[i]! - oData[i]!);
- // volume
- if (drawVolume) {
- let outerPath = selectPath(
- colorStrategy === ColorStrategy.CloseClose ? interDir : intraDir,
- flatPathVol as Path2D,
- upPathVol as Path2D,
- downPathVol as Path2D,
- i === idx0 && ColorStrategy.CloseClose ? false : flatAsUp
- );
- let vPx = Math.round(u.valToPos(vData![i]!, u.series[vIdx!].scale!, true));
- outerPath.rect(tPx - halfWidth, vPx, barWidth, zeroPx! - vPx);
- }
- if (drawPrice) {
- let outerPath = selectPath(
- colorStrategy === ColorStrategy.CloseClose ? interDir : intraDir,
- flatPath as Path2D,
- upPath as Path2D,
- downPath as Path2D,
- i === idx0 && ColorStrategy.CloseClose ? false : flatAsUp
- );
- // stick
- let hPx = Math.round(u.valToPos(hData![i]!, u.series[hIdx!].scale!, true));
- let lPx = Math.round(u.valToPos(lData![i]!, u.series[lIdx!].scale!, true));
- outerPath.rect(tPx - Math.floor(stickWidth / 2), hPx, stickWidth, lPx - hPx);
- let oPx = Math.round(u.valToPos(oData[i]!, u.series[oIdx!].scale!, true));
- let cPx = Math.round(u.valToPos(cData[i]!, u.series[cIdx!].scale!, true));
- if (asCandles) {
- // rect
- let top = Math.min(oPx, cPx);
- let btm = Math.max(oPx, cPx);
- let hgt = Math.max(1, btm - top);
- outerPath.rect(tPx - halfWidth, top, barWidth, hgt);
- if (colorStrategy === ColorStrategy.CloseClose) {
- if (intraDir >= 0 && hgt > outlineWidth * 2) {
- hollowPath.rect(
- tPx - halfWidth + outlineWidth,
- top + outlineWidth,
- barWidth - outlineWidth * 2,
- hgt - outlineWidth * 2
- );
- }
- }
- } else {
- outerPath.rect(tPx - halfWidth, oPx, halfWidth, stickWidth);
- outerPath.rect(tPx, cPx, halfWidth, stickWidth);
- }
- }
- }
- ctx.save();
- ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
- ctx.clip();
- if (drawVolume) {
- ctx.fillStyle = alpha(upColor, volumeAlpha);
- ctx.fill(upPathVol as Path2D);
- ctx.fillStyle = alpha(downColor, volumeAlpha);
- ctx.fill(downPathVol as Path2D);
- ctx.fillStyle = alpha(flatColor, volumeAlpha);
- ctx.fill(flatPathVol as Path2D);
- }
- if (drawPrice) {
- ctx.fillStyle = upColor;
- ctx.fill(upPath as Path2D);
- ctx.fillStyle = downColor;
- ctx.fill(downPath as Path2D);
- ctx.fillStyle = flatColor;
- ctx.fill(flatPath as Path2D);
- ctx.globalCompositeOperation = 'destination-out';
- ctx.fill(hollowPath);
- }
- ctx.restore();
- };
- }
|