diff --git a/.size-limit.js b/.size-limit.js index 6028dbcd3e..7512fb3e78 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -4,21 +4,21 @@ module.exports = [ { name: 'CJS', path: 'dist/lightweight-charts.production.cjs', - limit: '46.83 KB', + limit: '46.94 KB', }, { name: 'ESM', path: 'dist/lightweight-charts.production.mjs', - limit: '46.74 KB', + limit: '46.86 KB', }, { name: 'Standalone-ESM', path: 'dist/lightweight-charts.standalone.production.mjs', - limit: '48.45 KB', + limit: '48.56 KB', }, { name: 'Standalone', path: 'dist/lightweight-charts.standalone.production.js', - limit: '48.50 KB', + limit: '48.61 KB', }, ]; diff --git a/src/api/options/series-options-defaults.ts b/src/api/options/series-options-defaults.ts index ec8c2d512a..0b57034123 100644 --- a/src/api/options/series-options-defaults.ts +++ b/src/api/options/series-options-defaults.ts @@ -37,12 +37,14 @@ export const lineStyleDefaults: LineStyleOptions = { lineStyle: LineStyle.Solid, lineWidth: 3, lineType: LineType.Simple, + lineVisible: true, crosshairMarkerVisible: true, crosshairMarkerRadius: 4, crosshairMarkerBorderColor: '', crosshairMarkerBorderWidth: 2, crosshairMarkerBackgroundColor: '', lastPriceAnimation: LastPriceAnimationMode.Disabled, + pointMarkersVisible: false, }; export const areaStyleDefaults: AreaStyleOptions = { @@ -53,12 +55,14 @@ export const areaStyleDefaults: AreaStyleOptions = { lineStyle: LineStyle.Solid, lineWidth: 3, lineType: LineType.Simple, + lineVisible: true, crosshairMarkerVisible: true, crosshairMarkerRadius: 4, crosshairMarkerBorderColor: '', crosshairMarkerBorderWidth: 2, crosshairMarkerBackgroundColor: '', lastPriceAnimation: LastPriceAnimationMode.Disabled, + pointMarkersVisible: false, }; export const baselineStyleDefaults: BaselineStyleOptions = { @@ -78,6 +82,7 @@ export const baselineStyleDefaults: BaselineStyleOptions = { lineWidth: 3, lineStyle: LineStyle.Solid, lineType: LineType.Simple, + lineVisible: true, crosshairMarkerVisible: true, crosshairMarkerRadius: 4, @@ -86,6 +91,7 @@ export const baselineStyleDefaults: BaselineStyleOptions = { crosshairMarkerBackgroundColor: '', lastPriceAnimation: LastPriceAnimationMode.Disabled, + pointMarkersVisible: false, }; export const histogramStyleDefaults: HistogramStyleOptions = { diff --git a/src/model/series-options.ts b/src/model/series-options.ts index 9a856b94cd..2bb0a2b1c6 100644 --- a/src/model/series-options.ts +++ b/src/model/series-options.ts @@ -174,6 +174,26 @@ export interface LineStyleOptions { */ lineType: LineType; + /** + * Show series line. + * + * @defaultValue `true` + */ + lineVisible: boolean; + + /** + * Show circle markers on each point. + * + * @defaultValue `false` + */ + pointMarkersVisible: boolean; + /** + * Circle markers radius in pixels. + * + * @defaultValue `undefined` + */ + pointMarkersRadius?: number; + /** * Show the crosshair marker. * @@ -187,13 +207,13 @@ export interface LineStyleOptions { */ crosshairMarkerRadius: number; /** - * Crosshair marker border color. An empty string falls back to the the color of the series under the crosshair. + * Crosshair marker border color. An empty string falls back to the color of the series under the crosshair. * * @defaultValue `''` */ crosshairMarkerBorderColor: string; /** - * The crosshair marker background color. An empty string falls back to the the color of the series under the crosshair. + * The crosshair marker background color. An empty string falls back to the color of the series under the crosshair. * * @defaultValue `''` */ @@ -266,6 +286,26 @@ export interface AreaStyleOptions { */ lineType: LineType; + /** + * Show series line. + * + * @defaultValue `true` + */ + lineVisible: boolean; + + /** + * Show circle markers on each point. + * + * @defaultValue `false` + */ + pointMarkersVisible: boolean; + /** + * Circle markers radius in pixels. + * + * @defaultValue `undefined` + */ + pointMarkersRadius?: number; + /** * Show the crosshair marker. * @@ -279,13 +319,13 @@ export interface AreaStyleOptions { */ crosshairMarkerRadius: number; /** - * Crosshair marker border color. An empty string falls back to the the color of the series under the crosshair. + * Crosshair marker border color. An empty string falls back to the color of the series under the crosshair. * * @defaultValue `''` */ crosshairMarkerBorderColor: string; /** - * The crosshair marker background color. An empty string falls back to the the color of the series under the crosshair. + * The crosshair marker background color. An empty string falls back to the color of the series under the crosshair. * * @defaultValue `''` */ @@ -393,6 +433,26 @@ export interface BaselineStyleOptions { */ lineType: LineType; + /** + * Show series line. + * + * @defaultValue `true` + */ + lineVisible: boolean; + + /** + * Show circle markers on each point. + * + * @defaultValue `false` + */ + pointMarkersVisible: boolean; + /** + * Circle markers radius in pixels. + * + * @defaultValue `undefined` + */ + pointMarkersRadius?: number; + /** * Show the crosshair marker. * @@ -406,13 +466,13 @@ export interface BaselineStyleOptions { */ crosshairMarkerRadius: number; /** - * Crosshair marker border color. An empty string falls back to the the color of the series under the crosshair. + * Crosshair marker border color. An empty string falls back to the color of the series under the crosshair. * * @defaultValue `''` */ crosshairMarkerBorderColor: string; /** - * The crosshair marker background color. An empty string falls back to the the color of the series under the crosshair. + * The crosshair marker background color. An empty string falls back to the color of the series under the crosshair. * * @defaultValue `''` */ diff --git a/src/renderers/area-renderer-base.ts b/src/renderers/area-renderer-base.ts index 4c85768540..149a8156f4 100644 --- a/src/renderers/area-renderer-base.ts +++ b/src/renderers/area-renderer-base.ts @@ -1,11 +1,11 @@ -import { MediaCoordinatesRenderingScope } from 'fancy-canvas'; +import { BitmapCoordinatesRenderingScope } from 'fancy-canvas'; import { Coordinate } from '../model/coordinate'; import { PricedValue } from '../model/price-scale'; import { SeriesItemsIndexesRange, TimedValue } from '../model/time-data'; +import { BitmapCoordinatesPaneRenderer } from './bitmap-coordinates-pane-renderer'; import { LinePoint, LineStyle, LineType, LineWidth, setLineStyle } from './draw-line'; -import { MediaCoordinatesPaneRenderer } from './media-coordinates-pane-renderer'; import { walkLine } from './walk-line'; export type AreaFillItemBase = TimedValue & PricedValue & LinePoint; @@ -25,26 +25,27 @@ export interface PaneRendererAreaDataBase extends MediaCoordinatesPaneRenderer { +export abstract class PaneRendererAreaBase extends BitmapCoordinatesPaneRenderer { protected _data: TData | null = null; public setData(data: TData): void { this._data = data; } - protected _drawImpl(renderingScope: MediaCoordinatesRenderingScope): void { + protected _drawImpl(renderingScope: BitmapCoordinatesRenderingScope): void { if (this._data === null) { return; } @@ -71,5 +72,5 @@ export abstract class PaneRendererAreaBase { } -interface AreaFillCache extends Record { - fillStyle: CanvasRenderingContext2D['fillStyle']; - bottom: Coordinate; -} - export class PaneRendererArea extends PaneRendererAreaBase { - private _fillCache: AreaFillCache | null = null; - - protected override _fillStyle(renderingScope: MediaCoordinatesRenderingScope, item: AreaFillItem): CanvasRenderingContext2D['fillStyle'] { - const { context: ctx, mediaSize } = renderingScope; - - const { topColor, bottomColor } = item; - const bottom = mediaSize.height as Coordinate; - - if ( - this._fillCache !== null && - this._fillCache.topColor === topColor && - this._fillCache.bottomColor === bottomColor && - this._fillCache.bottom === bottom - ) { - return this._fillCache.fillStyle; - } - - const fillStyle = ctx.createLinearGradient(0, 0, 0, bottom); - fillStyle.addColorStop(0, topColor); - fillStyle.addColorStop(1, bottomColor); - - this._fillCache = { topColor, bottomColor, fillStyle, bottom }; - - return fillStyle; + private readonly _fillCache: GradientStyleCache = new GradientStyleCache(); + + protected override _fillStyle(renderingScope: BitmapCoordinatesRenderingScope, item: AreaFillItem): CanvasRenderingContext2D['fillStyle'] { + return this._fillCache.get( + renderingScope, + { + topColor1: item.topColor, + topColor2: '', + bottomColor1: '', + bottomColor2: item.bottomColor, + bottom: renderingScope.bitmapSize.height as Coordinate, + } + ); } } diff --git a/src/renderers/baseline-renderer-area.ts b/src/renderers/baseline-renderer-area.ts index a22c24e401..369bbc8ca0 100644 --- a/src/renderers/baseline-renderer-area.ts +++ b/src/renderers/baseline-renderer-area.ts @@ -1,63 +1,31 @@ -import { MediaCoordinatesRenderingScope } from 'fancy-canvas'; - -import { clamp } from '../helpers/mathex'; +import { BitmapCoordinatesRenderingScope } from 'fancy-canvas'; import { Coordinate } from '../model/coordinate'; import { BaselineFillColorerStyle } from '../model/series-bar-colorer'; import { AreaFillItemBase, PaneRendererAreaBase, PaneRendererAreaDataBase } from './area-renderer-base'; +import { GradientStyleCache } from './gradient-style-cache'; export type BaselineFillItem = AreaFillItemBase & BaselineFillColorerStyle; export interface PaneRendererBaselineData extends PaneRendererAreaDataBase { } - -interface BaselineFillCache extends Record { - fillStyle: CanvasRenderingContext2D['fillStyle']; - baseLevelCoordinate: Coordinate; - bottom: Coordinate; -} export class PaneRendererBaselineArea extends PaneRendererAreaBase { - private _fillCache: BaselineFillCache | null = null; + private readonly _fillCache: GradientStyleCache = new GradientStyleCache(); - protected override _fillStyle(renderingScope: MediaCoordinatesRenderingScope, item: BaselineFillItem): CanvasRenderingContext2D['fillStyle'] { - const { context: ctx, mediaSize } = renderingScope; + protected override _fillStyle(renderingScope: BitmapCoordinatesRenderingScope, item: BaselineFillItem): CanvasRenderingContext2D['fillStyle'] { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const data = this._data!; - const { topFillColor1, topFillColor2, bottomFillColor1, bottomFillColor2 } = item; - const baseLevelCoordinate = data.baseLevelCoordinate ?? mediaSize.height as Coordinate; - const bottom = mediaSize.height as Coordinate; - - if ( - this._fillCache !== null && - this._fillCache.topFillColor1 === topFillColor1 && - this._fillCache.topFillColor2 === topFillColor2 && - this._fillCache.bottomFillColor1 === bottomFillColor1 && - this._fillCache.bottomFillColor2 === bottomFillColor2 && - this._fillCache.baseLevelCoordinate === baseLevelCoordinate && - this._fillCache.bottom === bottom - ) { - return this._fillCache.fillStyle; - } - - const fillStyle = ctx.createLinearGradient(0, 0, 0, bottom); - const baselinePercent = clamp(baseLevelCoordinate / bottom, 0, 1); - - fillStyle.addColorStop(0, topFillColor1); - fillStyle.addColorStop(baselinePercent, topFillColor2); - fillStyle.addColorStop(baselinePercent, bottomFillColor1); - fillStyle.addColorStop(1, bottomFillColor2); - - this._fillCache = { - topFillColor1, - topFillColor2, - bottomFillColor1, - bottomFillColor2, - fillStyle, - baseLevelCoordinate, - bottom, - }; - - return fillStyle; + return this._fillCache.get( + renderingScope, + { + topColor1: item.topFillColor1, + topColor2: item.topFillColor2, + bottomColor1: item.bottomFillColor1, + bottomColor2: item.bottomFillColor2, + bottom: renderingScope.bitmapSize.height as Coordinate, + baseLevelCoordinate: data.baseLevelCoordinate, + } + ); } } diff --git a/src/renderers/baseline-renderer-line.ts b/src/renderers/baseline-renderer-line.ts index 6772ce9051..9ca3026f4f 100644 --- a/src/renderers/baseline-renderer-line.ts +++ b/src/renderers/baseline-renderer-line.ts @@ -1,10 +1,9 @@ -import { MediaCoordinatesRenderingScope } from 'fancy-canvas'; - -import { clamp } from '../helpers/mathex'; +import { BitmapCoordinatesRenderingScope } from 'fancy-canvas'; import { Coordinate } from '../model/coordinate'; import { BaselineStrokeColorerStyle } from '../model/series-bar-colorer'; +import { GradientStyleCache } from './gradient-style-cache'; import { LineItemBase as LineStrokeItemBase, PaneRendererLineBase, PaneRendererLineDataBase } from './line-renderer-base'; export type BaselineStrokeItem = LineStrokeItemBase & BaselineStrokeColorerStyle; @@ -12,50 +11,23 @@ export interface PaneRendererBaselineLineData extends PaneRendererLineDataBase { - strokeStyle: CanvasRenderingContext2D['strokeStyle']; - baseLevelCoordinate: Coordinate; - bottom: Coordinate; -} - export class PaneRendererBaselineLine extends PaneRendererLineBase { - private _strokeCache: BaselineStrokeCache | null = null; + private readonly _strokeCache: GradientStyleCache = new GradientStyleCache(); - protected override _strokeStyle(renderingScope: MediaCoordinatesRenderingScope, item: BaselineStrokeItem): CanvasRenderingContext2D['strokeStyle'] { - const { context: ctx, mediaSize } = renderingScope; + protected override _strokeStyle(renderingScope: BitmapCoordinatesRenderingScope, item: BaselineStrokeItem): CanvasRenderingContext2D['strokeStyle'] { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const data = this._data!; - const { topLineColor, bottomLineColor } = item; - const { baseLevelCoordinate } = data; - const bottom = mediaSize.height as Coordinate; - - if ( - this._strokeCache !== null && - this._strokeCache.topLineColor === topLineColor && - this._strokeCache.bottomLineColor === bottomLineColor && - this._strokeCache.baseLevelCoordinate === baseLevelCoordinate && - this._strokeCache.bottom === bottom - ) { - return this._strokeCache.strokeStyle; - } - - const strokeStyle = ctx.createLinearGradient(0, 0, 0, bottom); - const baselinePercent = clamp(baseLevelCoordinate / bottom, 0, 1); - - strokeStyle.addColorStop(0, topLineColor); - strokeStyle.addColorStop(baselinePercent, topLineColor); - strokeStyle.addColorStop(baselinePercent, bottomLineColor); - strokeStyle.addColorStop(1, bottomLineColor); - - this._strokeCache = { - topLineColor, - bottomLineColor, - strokeStyle, - baseLevelCoordinate, - bottom, - }; - - return strokeStyle; + return this._strokeCache.get( + renderingScope, + { + topColor1: item.topLineColor, + topColor2: item.topLineColor, + bottomColor1: item.bottomLineColor, + bottomColor2: item.bottomLineColor, + bottom: renderingScope.bitmapSize.height as Coordinate, + baseLevelCoordinate: data.baseLevelCoordinate, + } + ); } } diff --git a/src/renderers/draw-line.ts b/src/renderers/draw-line.ts index 8be951893e..655d887ac9 100644 --- a/src/renderers/draw-line.ts +++ b/src/renderers/draw-line.ts @@ -58,7 +58,7 @@ export const enum LineStyle { */ LargeDashed = 3, /** - * A dottled line with more space between dots. + * A dotted line with more space between dots. */ SparseDotted = 4, } diff --git a/src/renderers/draw-series-point-markers.ts b/src/renderers/draw-series-point-markers.ts new file mode 100644 index 0000000000..5afc10da87 --- /dev/null +++ b/src/renderers/draw-series-point-markers.ts @@ -0,0 +1,46 @@ +import { BitmapCoordinatesRenderingScope } from 'fancy-canvas'; + +import { SeriesItemsIndexesRange } from '../model/time-data'; + +import { LinePoint } from './draw-line'; + +export function drawSeriesPointMarkers( + renderingScope: BitmapCoordinatesRenderingScope, + items: readonly TItem[], + pointMarkersRadius: number, + visibleRange: SeriesItemsIndexesRange, + // the values returned by styleGetter are compared using the operator !==, + // so if styleGetter returns objects, then styleGetter should return the same object for equal styles + styleGetter: (renderingScope: BitmapCoordinatesRenderingScope, item: TItem) => TStyle +): void { + const { horizontalPixelRatio, verticalPixelRatio, context } = renderingScope; + let prevStyle: TStyle | null = null; + + const tickWidth = Math.max(1, Math.floor(horizontalPixelRatio)); + const correction = (tickWidth % 2) / 2; + + const radius = pointMarkersRadius * verticalPixelRatio + correction; + for (let i = visibleRange.to - 1; i >= visibleRange.from; --i) { + const point = items[i]; + if (point) { + const style = styleGetter(renderingScope, point); + if (style !== prevStyle) { + context.beginPath(); + if (prevStyle !== null) { + context.fill(); + } + + context.fillStyle = style; + prevStyle = style; + } + + const centerX = Math.round(point.x * horizontalPixelRatio) + correction; // correct x coordinate only + const centerY = point.y * verticalPixelRatio; + + context.moveTo(centerX, centerY); + context.arc(centerX, centerY, radius, 0, Math.PI * 2); + } + } + + context.fill(); +} diff --git a/src/renderers/gradient-style-cache.ts b/src/renderers/gradient-style-cache.ts new file mode 100644 index 0000000000..01d344f792 --- /dev/null +++ b/src/renderers/gradient-style-cache.ts @@ -0,0 +1,52 @@ +import { BitmapCoordinatesRenderingScope } from 'fancy-canvas'; + +import { clamp } from '../helpers/mathex'; + +import { Coordinate } from '../model/coordinate'; + +export interface GradientCacheParams { + topColor1: string; + topColor2: string; + bottomColor1: string; + bottomColor2: string; + baseLevelCoordinate?: Coordinate | null; + bottom: Coordinate; +} + +export class GradientStyleCache { + private _params?: GradientCacheParams; + private _cachedValue?: CanvasGradient; + + public get(scope: BitmapCoordinatesRenderingScope, params: GradientCacheParams): CanvasGradient { + const cachedParams = this._params; + const { topColor1, topColor2, bottomColor1, bottomColor2, bottom, baseLevelCoordinate } = params; + + if ( + this._cachedValue === undefined || + cachedParams === undefined || + cachedParams.topColor1 !== topColor1 || + cachedParams.topColor2 !== topColor2 || + cachedParams.bottomColor1 !== bottomColor1 || + cachedParams.bottomColor2 !== bottomColor2 || + cachedParams.baseLevelCoordinate !== baseLevelCoordinate || + cachedParams.bottom !== bottom + ) { + const gradient = scope.context.createLinearGradient(0, 0, 0, bottom); + + gradient.addColorStop(0, topColor1); + + if (baseLevelCoordinate != null) { + const baselinePercent = clamp(baseLevelCoordinate * scope.verticalPixelRatio / bottom, 0, 1); + gradient.addColorStop(baselinePercent, topColor2); + gradient.addColorStop(baselinePercent, bottomColor1); + } + + gradient.addColorStop(1, bottomColor2); + + this._cachedValue = gradient; + this._params = params; + } + + return this._cachedValue; + } +} diff --git a/src/renderers/line-renderer-base.ts b/src/renderers/line-renderer-base.ts index 7fc07cfbda..26f248057c 100644 --- a/src/renderers/line-renderer-base.ts +++ b/src/renderers/line-renderer-base.ts @@ -1,16 +1,17 @@ -import { MediaCoordinatesRenderingScope } from 'fancy-canvas'; +import { BitmapCoordinatesRenderingScope } from 'fancy-canvas'; import { PricedValue } from '../model/price-scale'; import { SeriesItemsIndexesRange, TimedValue } from '../model/time-data'; +import { BitmapCoordinatesPaneRenderer } from './bitmap-coordinates-pane-renderer'; import { LinePoint, LineStyle, LineType, LineWidth, setLineStyle } from './draw-line'; -import { MediaCoordinatesPaneRenderer } from './media-coordinates-pane-renderer'; +import { drawSeriesPointMarkers } from './draw-series-point-markers'; import { walkLine } from './walk-line'; export type LineItemBase = TimedValue & PricedValue & LinePoint; export interface PaneRendererLineDataBase { - lineType: LineType; + lineType?: LineType; items: TItem[]; @@ -20,26 +21,29 @@ export interface PaneRendererLineDataBase extends MediaCoordinatesPaneRenderer { +export abstract class PaneRendererLineBase extends BitmapCoordinatesPaneRenderer { protected _data: TData | null = null; public setData(data: TData): void { this._data = data; } - protected _drawImpl(renderingScope: MediaCoordinatesRenderingScope): void { + protected _drawImpl(renderingScope: BitmapCoordinatesRenderingScope): void { if (this._data === null) { return; } - const { items, visibleRange, barWidth, lineType, lineWidth, lineStyle } = this._data; + const { items, visibleRange, barWidth, lineType, lineWidth, lineStyle, pointMarkersRadius } = this._data; if (visibleRange === null) { return; @@ -48,14 +52,22 @@ export abstract class PaneRendererLineBase( - renderingScope: MediaCoordinatesRenderingScope, +export function walkLine( + renderingScope: BitmapCoordinatesRenderingScope, items: readonly TItem[], lineType: LineType, visibleRange: SeriesItemsIndexesRange, barWidth: number, // the values returned by styleGetter are compared using the operator !==, // so if styleGetter returns objects, then styleGetter should return the same object for equal styles - styleGetter: (renderingScope: MediaCoordinatesRenderingScope, item: TItem) => TStyle, - finishStyledArea: (ctx: CanvasRenderingContext2D, style: TStyle, areaFirstItem: LinePoint, newAreaFirstItem: LinePoint) => void + styleGetter: (renderingScope: BitmapCoordinatesRenderingScope, item: TItem) => TStyle, + finishStyledArea: (renderingScope: BitmapCoordinatesRenderingScope, style: TStyle, areaFirstItem: LinePoint, newAreaFirstItem: LinePoint) => void ): void { if (items.length === 0 || visibleRange.from >= items.length || visibleRange.to <= 0) { return; } - const ctx = renderingScope.context; + const { context: ctx, horizontalPixelRatio, verticalPixelRatio } = renderingScope; const firstItem = items[visibleRange.from]; let currentStyle = styleGetter(renderingScope, firstItem); @@ -35,61 +35,66 @@ export function walkLine( const item1: LinePoint = { x: firstItem.x - halfBarWidth as Coordinate, y: firstItem.y }; const item2: LinePoint = { x: firstItem.x + halfBarWidth as Coordinate, y: firstItem.y }; - ctx.moveTo(item1.x, item1.y); - ctx.lineTo(item2.x, item2.y); + ctx.moveTo(item1.x * horizontalPixelRatio, item1.y * verticalPixelRatio); + ctx.lineTo(item2.x * horizontalPixelRatio, item2.y * verticalPixelRatio); - finishStyledArea(ctx, currentStyle, item1, item2); + finishStyledArea(renderingScope, currentStyle, item1, item2); + } else { + const changeStyle = (newStyle: TStyle, currentItem: TItem) => { + finishStyledArea(renderingScope, currentStyle, currentStyleFirstItem, currentItem); - return; - } + ctx.beginPath(); + currentStyle = newStyle; + currentStyleFirstItem = currentItem; + }; - const changeStyle = (newStyle: TStyle, currentItem: TItem) => { - finishStyledArea(ctx, currentStyle, currentStyleFirstItem, currentItem); + let currentItem = currentStyleFirstItem; ctx.beginPath(); - currentStyle = newStyle; - currentStyleFirstItem = currentItem; - }; - - let currentItem = currentStyleFirstItem; - - ctx.beginPath(); - ctx.moveTo(firstItem.x, firstItem.y); - - for (let i = visibleRange.from + 1; i < visibleRange.to; ++i) { - currentItem = items[i]; - const itemStyle = styleGetter(renderingScope, currentItem); - - switch (lineType) { - case LineType.Simple: - ctx.lineTo(currentItem.x, currentItem.y); - break; - case LineType.WithSteps: - ctx.lineTo(currentItem.x, items[i - 1].y); - - if (itemStyle !== currentStyle) { - changeStyle(itemStyle, currentItem); - ctx.lineTo(currentItem.x, items[i - 1].y); + ctx.moveTo(firstItem.x * horizontalPixelRatio, firstItem.y * verticalPixelRatio); + + for (let i = visibleRange.from + 1; i < visibleRange.to; ++i) { + currentItem = items[i]; + const itemStyle = styleGetter(renderingScope, currentItem); + + switch (lineType) { + case LineType.Simple: + ctx.lineTo(currentItem.x * horizontalPixelRatio, currentItem.y * verticalPixelRatio); + break; + case LineType.WithSteps: + ctx.lineTo(currentItem.x * horizontalPixelRatio, items[i - 1].y * verticalPixelRatio); + + if (itemStyle !== currentStyle) { + changeStyle(itemStyle, currentItem); + ctx.lineTo(currentItem.x * horizontalPixelRatio, items[i - 1].y * verticalPixelRatio); + } + + ctx.lineTo(currentItem.x * horizontalPixelRatio, currentItem.y * verticalPixelRatio); + break; + case LineType.Curved: { + const [cp1, cp2] = getControlPoints(items, i - 1, i); + ctx.bezierCurveTo( + cp1.x * horizontalPixelRatio, + cp1.y * verticalPixelRatio, + cp2.x * horizontalPixelRatio, + cp2.y * verticalPixelRatio, + currentItem.x * horizontalPixelRatio, + currentItem.y * verticalPixelRatio + ); + break; } + } - ctx.lineTo(currentItem.x, currentItem.y); - break; - case LineType.Curved: { - const [cp1, cp2] = getControlPoints(items, i - 1, i); - ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, currentItem.x, currentItem.y); - break; + if (lineType !== LineType.WithSteps && itemStyle !== currentStyle) { + changeStyle(itemStyle, currentItem); + ctx.moveTo(currentItem.x * horizontalPixelRatio, currentItem.y * verticalPixelRatio); } } - if (lineType !== LineType.WithSteps && itemStyle !== currentStyle) { - changeStyle(itemStyle, currentItem); - ctx.moveTo(currentItem.x, currentItem.y); + if (currentStyleFirstItem !== currentItem || currentStyleFirstItem === currentItem && lineType === LineType.WithSteps) { + finishStyledArea(renderingScope, currentStyle, currentStyleFirstItem, currentItem); } } - - if (currentStyleFirstItem !== currentItem || currentStyleFirstItem === currentItem && lineType === LineType.WithSteps) { - finishStyledArea(ctx, currentStyle, currentStyleFirstItem, currentItem); - } } const curveTension = 6; diff --git a/src/views/pane/area-pane-view.ts b/src/views/pane/area-pane-view.ts index 6593abb213..27daba1b38 100644 --- a/src/views/pane/area-pane-view.ts +++ b/src/views/pane/area-pane-view.ts @@ -27,26 +27,27 @@ export class SeriesAreaPaneView extends LinePaneViewBase<'Area', AreaFillItem & } protected _prepareRendererData(): void { - const areaStyleProperties = this._series.options(); + const options = this._series.options(); this._areaRenderer.setData({ - lineType: areaStyleProperties.lineType, + lineType: options.lineType, items: this._items, - lineStyle: areaStyleProperties.lineStyle, - lineWidth: areaStyleProperties.lineWidth, + lineStyle: options.lineStyle, + lineWidth: options.lineWidth, baseLevelCoordinate: null, - invertFilledArea: areaStyleProperties.invertFilledArea, + invertFilledArea: options.invertFilledArea, visibleRange: this._itemsVisibleRange, barWidth: this._model.timeScale().barSpacing(), }); this._lineRenderer.setData({ - lineType: areaStyleProperties.lineType, + lineType: options.lineVisible ? options.lineType : undefined, items: this._items, - lineStyle: areaStyleProperties.lineStyle, - lineWidth: areaStyleProperties.lineWidth, + lineStyle: options.lineStyle, + lineWidth: options.lineWidth, visibleRange: this._itemsVisibleRange, barWidth: this._model.timeScale().barSpacing(), + pointMarkersRadius: options.pointMarkersVisible ? (options.pointMarkersRadius || options.lineWidth / 2 + 2) : undefined, }); } } diff --git a/src/views/pane/baseline-pane-view.ts b/src/views/pane/baseline-pane-view.ts index f14fb7d16f..d86499c49f 100644 --- a/src/views/pane/baseline-pane-view.ts +++ b/src/views/pane/baseline-pane-view.ts @@ -32,17 +32,17 @@ export class SeriesBaselinePaneView extends LinePaneViewBase<'Baseline', Baselin return; } - const baselineProps = this._series.options(); + const options = this._series.options(); - const baseLevelCoordinate = this._series.priceScale().priceToCoordinate(baselineProps.baseValue.price, firstValue.value); + const baseLevelCoordinate = this._series.priceScale().priceToCoordinate(options.baseValue.price, firstValue.value); const barWidth = this._model.timeScale().barSpacing(); this._baselineAreaRenderer.setData({ items: this._items, - lineWidth: baselineProps.lineWidth, - lineStyle: baselineProps.lineStyle, - lineType: baselineProps.lineType, + lineWidth: options.lineWidth, + lineStyle: options.lineStyle, + lineType: options.lineType, baseLevelCoordinate, invertFilledArea: false, @@ -54,9 +54,10 @@ export class SeriesBaselinePaneView extends LinePaneViewBase<'Baseline', Baselin this._baselineLineRenderer.setData({ items: this._items, - lineWidth: baselineProps.lineWidth, - lineStyle: baselineProps.lineStyle, - lineType: baselineProps.lineType, + lineWidth: options.lineWidth, + lineStyle: options.lineStyle, + lineType: options.lineVisible ? options.lineType : undefined, + pointMarkersRadius: options.pointMarkersVisible ? (options.pointMarkersRadius || options.lineWidth / 2 + 2) : undefined, baseLevelCoordinate, diff --git a/src/views/pane/line-pane-view.ts b/src/views/pane/line-pane-view.ts index 6a1d730025..2c72f14f65 100644 --- a/src/views/pane/line-pane-view.ts +++ b/src/views/pane/line-pane-view.ts @@ -16,13 +16,14 @@ export class SeriesLinePaneView extends LinePaneViewBase<'Line', LineStrokeItem, } protected _prepareRendererData(): void { - const lineStyleProps = this._series.options(); + const options = this._series.options(); const data: PaneRendererLineData = { items: this._items, - lineStyle: lineStyleProps.lineStyle, - lineType: lineStyleProps.lineType, - lineWidth: lineStyleProps.lineWidth, + lineStyle: options.lineStyle, + lineType: options.lineVisible ? options.lineType : undefined, + lineWidth: options.lineWidth, + pointMarkersRadius: options.pointMarkersVisible ? (options.pointMarkersRadius || options.lineWidth / 2 + 2) : undefined, visibleRange: this._itemsVisibleRange, barWidth: this._model.timeScale().barSpacing(), }; diff --git a/tests/e2e/graphics/test-cases/series/area-with-point-markers.js b/tests/e2e/graphics/test-cases/series/area-with-point-markers.js new file mode 100644 index 0000000000..c50aa59ee2 --- /dev/null +++ b/tests/e2e/graphics/test-cases/series/area-with-point-markers.js @@ -0,0 +1,25 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function runTestCase(container) { + const chart = (window.chart = LightweightCharts.createChart(container, { + timeScale: { + barSpacing: 12, + }, + })); + + const mainSeries = chart.addAreaSeries({ pointMarkersVisible: true }); + + mainSeries.setData(generateData()); +} diff --git a/tests/e2e/graphics/test-cases/series/baseline-with-point-markers.js b/tests/e2e/graphics/test-cases/series/baseline-with-point-markers.js new file mode 100644 index 0000000000..c2fec958e5 --- /dev/null +++ b/tests/e2e/graphics/test-cases/series/baseline-with-point-markers.js @@ -0,0 +1,57 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 10; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i * (-1), + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + + for (let i = 0; i < 20; ++i) { + res.push({ + time: time.getTime() / 1000, + value: -10 + i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + + for (let i = 0; i < 20; ++i) { + res.push({ + time: time.getTime() / 1000, + value: 10 - i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + + for (let i = 0; i < 20; ++i) { + res.push({ + time: time.getTime() / 1000, + value: -10 + i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addBaselineSeries({ + lineWidth: 1, + baseValue: { + type: 'price', + price: 0, + }, + pointMarkersVisible: true, + }); + + chart.timeScale().fitContent(); + + mainSeries.setData(generateData()); +} diff --git a/tests/e2e/graphics/test-cases/series/line-series-with-point-markers-and-hidden-line.js b/tests/e2e/graphics/test-cases/series/line-series-with-point-markers-and-hidden-line.js new file mode 100644 index 0000000000..66915e7632 --- /dev/null +++ b/tests/e2e/graphics/test-cases/series/line-series-with-point-markers-and-hidden-line.js @@ -0,0 +1,30 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function runTestCase(container) { + const chart = (window.chart = LightweightCharts.createChart(container, { + timeScale: { + barSpacing: 12, + }, + })); + + const mainSeries = chart.addLineSeries({ + lineWidth: 1, + color: '#ff0000', + pointMarkersVisible: true, + lineVisible: false, + }); + + mainSeries.setData(generateData()); +} diff --git a/tests/e2e/graphics/test-cases/series/line-with-point-markers.js b/tests/e2e/graphics/test-cases/series/line-with-point-markers.js new file mode 100644 index 0000000000..8b6acecc45 --- /dev/null +++ b/tests/e2e/graphics/test-cases/series/line-with-point-markers.js @@ -0,0 +1,29 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function runTestCase(container) { + const chart = (window.chart = LightweightCharts.createChart(container, { + timeScale: { + barSpacing: 12, + }, + })); + + const mainSeries = chart.addLineSeries({ + lineWidth: 1, + color: '#ff0000', + pointMarkersVisible: true, + }); + + mainSeries.setData(generateData()); +}