mirror of https://github.com/grafana/grafana
MarketTrend: add new alpha panel (#40909)
parent
af61839a26
commit
f0a108afb3
@ -0,0 +1,316 @@ |
||||
// this file is pretty much a copy-paste of TimeSeriesPanel.tsx :(
|
||||
// with some extra renderers passed to the <TimeSeries> component
|
||||
|
||||
import React, { useMemo } from 'react'; |
||||
import { DataFrame, Field, getDisplayProcessor, PanelProps } from '@grafana/data'; |
||||
import { TooltipDisplayMode } from '@grafana/schema'; |
||||
import { usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin, UPlotConfigBuilder } from '@grafana/ui'; |
||||
import { getFieldLinksForExplore } from 'app/features/explore/utils/links'; |
||||
import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin'; |
||||
import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin'; |
||||
import { ExemplarsPlugin } from '../timeseries/plugins/ExemplarsPlugin'; |
||||
import { prepareGraphableFields } from '../timeseries/utils'; |
||||
import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin'; |
||||
import { ThresholdControlsPlugin } from '../timeseries/plugins/ThresholdControlsPlugin'; |
||||
import { config } from 'app/core/config'; |
||||
import { drawMarkers, FieldIndices } from './utils'; |
||||
import { defaultColors, MarketOptions, MarketTrendMode } from './models.gen'; |
||||
import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder'; |
||||
import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; |
||||
import { findField } from 'app/features/dimensions'; |
||||
|
||||
interface MarketPanelProps extends PanelProps<MarketOptions> {} |
||||
|
||||
function findFieldInFrames(frames?: DataFrame[], name?: string): Field | undefined { |
||||
if (frames?.length) { |
||||
for (const frame of frames) { |
||||
const f = findField(frame, name); |
||||
if (f) { |
||||
return f; |
||||
} |
||||
} |
||||
} |
||||
return undefined; |
||||
} |
||||
|
||||
export const MarketTrendPanel: React.FC<MarketPanelProps> = ({ |
||||
data, |
||||
timeRange, |
||||
timeZone, |
||||
width, |
||||
height, |
||||
options, |
||||
fieldConfig, |
||||
onChangeTimeRange, |
||||
replaceVariables, |
||||
}) => { |
||||
const { sync, canAddAnnotations, onThresholdsChange, canEditThresholds, onSplitOpen } = usePanelContext(); |
||||
|
||||
const getFieldLinks = (field: Field, rowIndex: number) => { |
||||
return getFieldLinksForExplore({ field, rowIndex, splitOpenFn: onSplitOpen, range: timeRange }); |
||||
}; |
||||
|
||||
const { frames, warn } = useMemo( |
||||
() => prepareGraphableFields(data?.series, config.theme2), |
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[data, options] |
||||
); |
||||
|
||||
const { renderers, tweakScale, tweakAxis } = useMemo(() => { |
||||
let tweakScale = (opts: ScaleProps) => opts; |
||||
let tweakAxis = (opts: AxisProps) => opts; |
||||
|
||||
let doNothing = { |
||||
renderers: [], |
||||
tweakScale, |
||||
tweakAxis, |
||||
}; |
||||
|
||||
if (options.fieldMap == null) { |
||||
return doNothing; |
||||
} |
||||
|
||||
const { mode, priceStyle, fieldMap, colorStrategy } = options; |
||||
const colors = { ...defaultColors, ...options.colors }; |
||||
let { open, high, low, close, volume } = fieldMap; |
||||
|
||||
if ( |
||||
open == null || |
||||
close == null || |
||||
findFieldInFrames(frames, open) == null || |
||||
findFieldInFrames(frames, close) == null |
||||
) { |
||||
return doNothing; |
||||
} |
||||
|
||||
let volumeAlpha = 0.5; |
||||
|
||||
let volumeIdx = -1; |
||||
|
||||
let shouldRenderVolume = false; |
||||
|
||||
// find volume field and set overrides
|
||||
if (volume != null && mode !== MarketTrendMode.Price) { |
||||
let volumeField = findFieldInFrames(frames, volume); |
||||
|
||||
if (volumeField != null) { |
||||
shouldRenderVolume = true; |
||||
|
||||
let { fillOpacity } = volumeField.config.custom; |
||||
|
||||
if (fillOpacity) { |
||||
volumeAlpha = fillOpacity / 100; |
||||
} |
||||
|
||||
// we only want to put volume on own shorter axis when rendered with price
|
||||
if (mode !== MarketTrendMode.Volume) { |
||||
volumeField.config = { ...volumeField.config }; |
||||
volumeField.config.unit = 'short'; |
||||
volumeField.display = getDisplayProcessor({ |
||||
field: volumeField, |
||||
theme: config.theme2, |
||||
}); |
||||
|
||||
tweakAxis = (opts: AxisProps) => { |
||||
if (opts.scaleKey === 'short') { |
||||
let filter = (u: uPlot, splits: number[]) => { |
||||
let _splits = []; |
||||
let max = u.series[volumeIdx].max as number; |
||||
|
||||
for (let i = 0; i < splits.length; i++) { |
||||
_splits.push(splits[i]); |
||||
|
||||
if (splits[i] > max) { |
||||
break; |
||||
} |
||||
} |
||||
|
||||
return _splits; |
||||
}; |
||||
|
||||
opts.space = 20; // reduce tick spacing
|
||||
opts.filter = filter; // hide tick labels
|
||||
opts.ticks = { ...opts.ticks, filter }; // hide tick marks
|
||||
} |
||||
|
||||
return opts; |
||||
}; |
||||
|
||||
tweakScale = (opts: ScaleProps) => { |
||||
if (opts.scaleKey === 'short') { |
||||
opts.range = (u: uPlot, min: number, max: number) => [0, max * 7]; |
||||
} |
||||
|
||||
return opts; |
||||
}; |
||||
} |
||||
} |
||||
} |
||||
|
||||
let shouldRenderPrice = |
||||
mode !== MarketTrendMode.Volume && |
||||
high != null && |
||||
low != null && |
||||
findFieldInFrames(frames, high) != null && |
||||
findFieldInFrames(frames, low) != null; |
||||
|
||||
if (!shouldRenderPrice && !shouldRenderVolume) { |
||||
return doNothing; |
||||
} |
||||
|
||||
let fields: Record<string, string> = {}; |
||||
let indicesOnly = []; |
||||
|
||||
if (shouldRenderPrice) { |
||||
fields = { open, high, low, close }; |
||||
|
||||
// hide series from legend that are rendered as composite markers
|
||||
for (let key in fields) { |
||||
let field = findFieldInFrames(frames, fields[key])!; |
||||
field.config = { |
||||
...field.config, |
||||
custom: { |
||||
...field.config.custom, |
||||
hideFrom: { legend: true, tooltip: false, viz: false }, |
||||
}, |
||||
}; |
||||
} |
||||
} else { |
||||
// these fields should not be omitted from normal rendering if they arent rendered
|
||||
// as part of price markers. they're only here so we can get back their indicies in the
|
||||
// init callback below. TODO: remove this when field mapping happens in the panel instead of deep
|
||||
indicesOnly.push(open, close); |
||||
} |
||||
|
||||
if (shouldRenderVolume) { |
||||
fields.volume = volume; |
||||
fields.open = open; |
||||
fields.close = close; |
||||
} |
||||
|
||||
return { |
||||
renderers: [ |
||||
{ |
||||
fieldMap: fields, |
||||
indicesOnly, |
||||
init: (builder: UPlotConfigBuilder, fieldIndices: FieldIndices) => { |
||||
volumeIdx = fieldIndices.volume!; |
||||
|
||||
builder.addHook( |
||||
'drawAxes', |
||||
drawMarkers({ |
||||
mode, |
||||
fields: fieldIndices, |
||||
upColor: config.theme2.visualization.getColorByName(colors.up), |
||||
downColor: config.theme2.visualization.getColorByName(colors.down), |
||||
flatColor: config.theme2.visualization.getColorByName(colors.flat), |
||||
volumeAlpha, |
||||
colorStrategy, |
||||
priceStyle, |
||||
flatAsUp: true, |
||||
}) |
||||
); |
||||
}, |
||||
}, |
||||
], |
||||
tweakScale, |
||||
tweakAxis, |
||||
}; |
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [options, data.structureRev]); |
||||
|
||||
if (!frames || warn) { |
||||
return ( |
||||
<div className="panel-empty"> |
||||
<p>{warn ?? 'No data found in response'}</p> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); |
||||
|
||||
return ( |
||||
<TimeSeries |
||||
frames={frames} |
||||
structureRev={data.structureRev} |
||||
timeRange={timeRange} |
||||
timeZone={timeZone} |
||||
width={width} |
||||
height={height} |
||||
legend={options.legend} |
||||
renderers={renderers} |
||||
tweakAxis={tweakAxis} |
||||
tweakScale={tweakScale} |
||||
options={options} |
||||
> |
||||
{(config, alignedDataFrame) => { |
||||
return ( |
||||
<> |
||||
<ZoomPlugin config={config} onZoom={onChangeTimeRange} /> |
||||
<TooltipPlugin |
||||
data={alignedDataFrame} |
||||
config={config} |
||||
mode={TooltipDisplayMode.Multi} |
||||
sync={sync} |
||||
timeZone={timeZone} |
||||
/> |
||||
{/* Renders annotation markers*/} |
||||
{data.annotations && ( |
||||
<AnnotationsPlugin annotations={data.annotations} config={config} timeZone={timeZone} /> |
||||
)} |
||||
{/* Enables annotations creation*/} |
||||
<AnnotationEditorPlugin data={alignedDataFrame} timeZone={timeZone} config={config}> |
||||
{({ startAnnotating }) => { |
||||
return ( |
||||
<ContextMenuPlugin |
||||
data={alignedDataFrame} |
||||
config={config} |
||||
timeZone={timeZone} |
||||
replaceVariables={replaceVariables} |
||||
defaultItems={ |
||||
enableAnnotationCreation |
||||
? [ |
||||
{ |
||||
items: [ |
||||
{ |
||||
label: 'Add annotation', |
||||
ariaLabel: 'Add annotation', |
||||
icon: 'comment-alt', |
||||
onClick: (e, p) => { |
||||
if (!p) { |
||||
return; |
||||
} |
||||
startAnnotating({ coords: p.coords }); |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
] |
||||
: [] |
||||
} |
||||
/> |
||||
); |
||||
}} |
||||
</AnnotationEditorPlugin> |
||||
{data.annotations && ( |
||||
<ExemplarsPlugin |
||||
config={config} |
||||
exemplars={data.annotations} |
||||
timeZone={timeZone} |
||||
getFieldLinks={getFieldLinks} |
||||
/> |
||||
)} |
||||
|
||||
{canEditThresholds && onThresholdsChange && ( |
||||
<ThresholdControlsPlugin |
||||
config={config} |
||||
fieldConfig={fieldConfig} |
||||
onThresholdsChange={onThresholdsChange} |
||||
/> |
||||
)} |
||||
</> |
||||
); |
||||
}} |
||||
</TimeSeries> |
||||
); |
||||
}; |
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,29 @@ |
||||
// Copyright 2021 Grafana Labs |
||||
// |
||||
// Licensed under the Apache License, Version 2.0 (the "License"); |
||||
// you may not use this file except in compliance with the License. |
||||
// You may obtain a copy of the License at |
||||
// |
||||
// http://www.apache.org/licenses/LICENSE-2.0 |
||||
// |
||||
// Unless required by applicable law or agreed to in writing, software |
||||
// distributed under the License is distributed on an "AS IS" BASIS, |
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
// See the License for the specific language governing permissions and |
||||
// limitations under the License. |
||||
|
||||
package grafanaschema |
||||
|
||||
Panel: { |
||||
lineages: [ |
||||
[ |
||||
{ |
||||
PanelOptions: { |
||||
// anything for now |
||||
... |
||||
} |
||||
} |
||||
] |
||||
] |
||||
migrations: [] |
||||
} |
@ -0,0 +1,52 @@ |
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// NOTE: This file will be auto generated from models.cue
|
||||
// It is currenty hand written but will serve as the target for cuetsy
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
import { TimeSeriesOptions } from '../timeseries/types'; |
||||
|
||||
export const modelVersion = Object.freeze([1, 0]); |
||||
|
||||
export enum MarketTrendMode { |
||||
Price = 'price', |
||||
Volume = 'volume', |
||||
PriceVolume = 'pricevolume', |
||||
} |
||||
|
||||
export enum PriceStyle { |
||||
Candles = 'candles', |
||||
OHLCBars = 'ohlcbars', |
||||
} |
||||
|
||||
export enum ColorStrategy { |
||||
// up/down color depends on current close vs current open
|
||||
// filled always
|
||||
Intra = 'intra', |
||||
// up/down color depends on current close vs prior close
|
||||
// filled/hollow depends on current close vs current open
|
||||
Inter = 'inter', |
||||
} |
||||
|
||||
interface SemanticFieldMap { |
||||
[semanticName: string]: string; |
||||
} |
||||
|
||||
export interface MarketTrendColors { |
||||
up: string; |
||||
down: string; |
||||
flat: string; |
||||
} |
||||
|
||||
export const defaultColors: MarketTrendColors = { |
||||
up: 'green', |
||||
down: 'red', |
||||
flat: 'gray', |
||||
}; |
||||
|
||||
export interface MarketOptions extends TimeSeriesOptions { |
||||
mode: MarketTrendMode; |
||||
priceStyle: PriceStyle; |
||||
colorStrategy: ColorStrategy; |
||||
fieldMap: SemanticFieldMap; |
||||
colors: MarketTrendColors; |
||||
} |
@ -0,0 +1,102 @@ |
||||
import { GraphFieldConfig } from '@grafana/schema'; |
||||
import { FieldConfigProperty, PanelPlugin, SelectableValue } from '@grafana/data'; |
||||
import { commonOptionsBuilder } from '@grafana/ui'; |
||||
import { MarketTrendPanel } from './MarketTrendPanel'; |
||||
import { defaultColors, MarketOptions, MarketTrendMode, ColorStrategy, PriceStyle } from './models.gen'; |
||||
import { defaultGraphConfig, getGraphFieldConfig } from '../timeseries/config'; |
||||
|
||||
const modeOptions = [ |
||||
{ label: 'Price & Volume', value: MarketTrendMode.PriceVolume }, |
||||
{ label: 'Price', value: MarketTrendMode.Price }, |
||||
{ label: 'Volume', value: MarketTrendMode.Volume }, |
||||
] as Array<SelectableValue<MarketTrendMode>>; |
||||
|
||||
const priceStyle = [ |
||||
{ label: 'Candles', value: PriceStyle.Candles }, |
||||
{ label: 'OHLC Bars', value: PriceStyle.OHLCBars }, |
||||
] as Array<SelectableValue<PriceStyle>>; |
||||
|
||||
const colorStrategy = [ |
||||
{ label: 'Since Open', value: 'intra' }, |
||||
{ label: 'Since Prior Close', value: 'inter' }, |
||||
] as Array<SelectableValue<ColorStrategy>>; |
||||
|
||||
function getMarketFieldConfig() { |
||||
const v = getGraphFieldConfig(defaultGraphConfig); |
||||
v.standardOptions![FieldConfigProperty.Unit] = { |
||||
settings: {}, |
||||
defaultValue: 'currencyUSD', |
||||
}; |
||||
return v; |
||||
} |
||||
|
||||
export const plugin = new PanelPlugin<MarketOptions, GraphFieldConfig>(MarketTrendPanel) |
||||
.useFieldConfig(getMarketFieldConfig()) |
||||
.setPanelOptions((builder) => { |
||||
builder |
||||
.addRadio({ |
||||
path: 'mode', |
||||
name: 'Mode', |
||||
description: '', |
||||
defaultValue: MarketTrendMode.PriceVolume, |
||||
settings: { |
||||
options: modeOptions, |
||||
}, |
||||
}) |
||||
.addRadio({ |
||||
path: 'priceStyle', |
||||
name: 'Price style', |
||||
description: '', |
||||
defaultValue: PriceStyle.Candles, |
||||
settings: { |
||||
options: priceStyle, |
||||
}, |
||||
showIf: (opts) => opts.mode !== MarketTrendMode.Volume, |
||||
}) |
||||
.addRadio({ |
||||
path: 'colorStrategy', |
||||
name: 'Color strategy', |
||||
description: '', |
||||
defaultValue: ColorStrategy.Intra, |
||||
settings: { |
||||
options: colorStrategy, |
||||
}, |
||||
}) |
||||
.addColorPicker({ |
||||
path: 'colors.up', |
||||
name: 'Up color', |
||||
defaultValue: defaultColors.up, |
||||
}) |
||||
.addColorPicker({ |
||||
path: 'colors.down', |
||||
name: 'Down color', |
||||
defaultValue: defaultColors.down, |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'fieldMap.open', |
||||
name: 'Open field', |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'fieldMap.high', |
||||
name: 'High field', |
||||
showIf: (opts) => opts.mode !== MarketTrendMode.Volume, |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'fieldMap.low', |
||||
name: 'Low field', |
||||
showIf: (opts) => opts.mode !== MarketTrendMode.Volume, |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'fieldMap.close', |
||||
name: 'Close field', |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'fieldMap.volume', |
||||
name: 'Volume field', |
||||
showIf: (opts) => opts.mode !== MarketTrendMode.Price, |
||||
}); |
||||
|
||||
// commonOptionsBuilder.addTooltipOptions(builder);
|
||||
commonOptionsBuilder.addLegendOptions(builder); |
||||
}) |
||||
.setDataSupport({ annotations: true, alertStates: true }); |
@ -0,0 +1,17 @@ |
||||
{ |
||||
"type": "panel", |
||||
"name": "Market trend", |
||||
"id": "market-trend", |
||||
"state": "alpha", |
||||
|
||||
"info": { |
||||
"author": { |
||||
"name": "Grafana Labs", |
||||
"url": "https://grafana.com" |
||||
}, |
||||
"logos": { |
||||
"small": "img/candlestick.svg", |
||||
"large": "img/candlestick.svg" |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,181 @@ |
||||
import { MarketTrendMode, ColorStrategy, PriceStyle } from './models.gen'; |
||||
import uPlot from 'uplot'; |
||||
import { colorManipulator } from '@grafana/data'; |
||||
|
||||
const { alpha } = colorManipulator; |
||||
|
||||
export type FieldIndices = Record<string, number>; |
||||
|
||||
interface RendererOpts { |
||||
mode: MarketTrendMode; |
||||
priceStyle: PriceStyle; |
||||
fields: FieldIndices; |
||||
colorStrategy: ColorStrategy; |
||||
upColor: string; |
||||
downColor: string; |
||||
flatColor: string; |
||||
volumeAlpha: number; |
||||
flatAsUp: boolean; |
||||
} |
||||
|
||||
export function drawMarkers(opts: RendererOpts) { |
||||
let { mode, priceStyle, fields, colorStrategy, upColor, downColor, flatColor, volumeAlpha, flatAsUp = true } = opts; |
||||
|
||||
let drawPrice = mode !== MarketTrendMode.Volume && fields.high != null && fields.low != null; |
||||
let asCandles = drawPrice && priceStyle === PriceStyle.Candles; |
||||
let drawVolume = mode !== MarketTrendMode.Price && 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 colWidth = u.bbox.width / (idx1 - idx0); |
||||
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.Inter ? interDir : intraDir, |
||||
flatPathVol as Path2D, |
||||
upPathVol as Path2D, |
||||
downPathVol as Path2D, |
||||
i === idx0 && ColorStrategy.Inter ? 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.Inter ? interDir : intraDir, |
||||
flatPath as Path2D, |
||||
upPath as Path2D, |
||||
downPath as Path2D, |
||||
i === idx0 && ColorStrategy.Inter ? 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.Inter) { |
||||
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(); |
||||
}; |
||||
} |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue