Skip to content

Commit

Permalink
Merge pull request #1505 from tradingview/fix-line-markers-positioning
Browse files Browse the repository at this point in the history
fix series crosshair marker x positioning #1504
  • Loading branch information
SlicedSilver committed Feb 6, 2024
2 parents 6daf134 + 8b82918 commit 32be070
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 58 deletions.
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ module.exports = [
{
name: 'Standalone',
path: 'dist/lightweight-charts.standalone.production.js',
limit: '49.68 KB',
limit: '49.67 KB',
},
];
20 changes: 13 additions & 7 deletions src/renderers/marks-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { MediaCoordinatesRenderingScope } from 'fancy-canvas';
import { BitmapCoordinatesRenderingScope } from 'fancy-canvas';

import { SeriesItemsIndexesRange } from '../model/time-data';

import { BitmapCoordinatesPaneRenderer } from './bitmap-coordinates-pane-renderer';
import { LineItemBase } from './line-renderer-base';
import { MediaCoordinatesPaneRenderer } from './media-coordinates-pane-renderer';

export interface MarksRendererData {
items: LineItemBase[];
Expand All @@ -14,28 +14,34 @@ export interface MarksRendererData {
visibleRange: SeriesItemsIndexesRange | null;
}

export class PaneRendererMarks extends MediaCoordinatesPaneRenderer {
export class PaneRendererMarks extends BitmapCoordinatesPaneRenderer {
protected _data: MarksRendererData | null = null;

public setData(data: MarksRendererData): void {
this._data = data;
}

protected _drawImpl({ context: ctx }: MediaCoordinatesRenderingScope): void {
protected _drawImpl({ context: ctx, horizontalPixelRatio, verticalPixelRatio }: BitmapCoordinatesRenderingScope): void {
if (this._data === null || this._data.visibleRange === null) {
return;
}

const visibleRange = this._data.visibleRange;
const data = this._data;

const draw = (radius: number) => {
const tickWidth = Math.max(1, Math.floor(horizontalPixelRatio));
const correction = (tickWidth % 2) / 2;

const draw = (radiusMedia: number) => {
ctx.beginPath();

for (let i = visibleRange.to - 1; i >= visibleRange.from; --i) {
const point = data.items[i];
ctx.moveTo(point.x, point.y);
ctx.arc(point.x, point.y, radius, 0, Math.PI * 2);
const centerX = Math.round(point.x * horizontalPixelRatio) + correction; // correct x coordinate only
const centerY = point.y * verticalPixelRatio;
const radius = radiusMedia * verticalPixelRatio + correction;
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
}

ctx.fill();
Expand Down
37 changes: 18 additions & 19 deletions src/renderers/series-markers-arrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,36 @@ import { ceiledOdd } from '../helpers/mathex';
import { Coordinate } from '../model/coordinate';

import { hitTestSquare } from './series-markers-square';
import { shapeSize } from './series-markers-utils';
import { BitmapShapeItemCoordinates, shapeSize } from './series-markers-utils';

export function drawArrow(
up: boolean,
ctx: CanvasRenderingContext2D,
centerX: Coordinate,
centerY: Coordinate,
coords: BitmapShapeItemCoordinates,
size: number
): void {
const arrowSize = shapeSize('arrowUp', size);
const halfArrowSize = (arrowSize - 1) / 2;
const halfArrowSize = ((arrowSize - 1) / 2) * coords.pixelRatio;
const baseSize = ceiledOdd(size / 2);
const halfBaseSize = (baseSize - 1) / 2;
const halfBaseSize = ((baseSize - 1) / 2) * coords.pixelRatio;

ctx.beginPath();
if (up) {
ctx.moveTo(centerX - halfArrowSize, centerY);
ctx.lineTo(centerX, centerY - halfArrowSize);
ctx.lineTo(centerX + halfArrowSize, centerY);
ctx.lineTo(centerX + halfBaseSize, centerY);
ctx.lineTo(centerX + halfBaseSize, centerY + halfArrowSize);
ctx.lineTo(centerX - halfBaseSize, centerY + halfArrowSize);
ctx.lineTo(centerX - halfBaseSize, centerY);
ctx.moveTo(coords.x - halfArrowSize, coords.y);
ctx.lineTo(coords.x, coords.y - halfArrowSize);
ctx.lineTo(coords.x + halfArrowSize, coords.y);
ctx.lineTo(coords.x + halfBaseSize, coords.y);
ctx.lineTo(coords.x + halfBaseSize, coords.y + halfArrowSize);
ctx.lineTo(coords.x - halfBaseSize, coords.y + halfArrowSize);
ctx.lineTo(coords.x - halfBaseSize, coords.y);
} else {
ctx.moveTo(centerX - halfArrowSize, centerY);
ctx.lineTo(centerX, centerY + halfArrowSize);
ctx.lineTo(centerX + halfArrowSize, centerY);
ctx.lineTo(centerX + halfBaseSize, centerY);
ctx.lineTo(centerX + halfBaseSize, centerY - halfArrowSize);
ctx.lineTo(centerX - halfBaseSize, centerY - halfArrowSize);
ctx.lineTo(centerX - halfBaseSize, centerY);
ctx.moveTo(coords.x - halfArrowSize, coords.y);
ctx.lineTo(coords.x, coords.y + halfArrowSize);
ctx.lineTo(coords.x + halfArrowSize, coords.y);
ctx.lineTo(coords.x + halfBaseSize, coords.y);
ctx.lineTo(coords.x + halfBaseSize, coords.y - halfArrowSize);
ctx.lineTo(coords.x - halfBaseSize, coords.y - halfArrowSize);
ctx.lineTo(coords.x - halfBaseSize, coords.y);
}

ctx.fill();
Expand Down
7 changes: 3 additions & 4 deletions src/renderers/series-markers-circle.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { Coordinate } from '../model/coordinate';

import { shapeSize } from './series-markers-utils';
import { BitmapShapeItemCoordinates, shapeSize } from './series-markers-utils';

export function drawCircle(
ctx: CanvasRenderingContext2D,
centerX: Coordinate,
centerY: Coordinate,
coords: BitmapShapeItemCoordinates,
size: number
): void {
const circleSize = shapeSize('circle', size);
const halfSize = (circleSize - 1) / 2;

ctx.beginPath();
ctx.arc(centerX, centerY, halfSize, 0, 2 * Math.PI, false);
ctx.arc(coords.x, coords.y, halfSize * coords.pixelRatio, 0, 2 * Math.PI, false);

ctx.fill();
}
Expand Down
37 changes: 24 additions & 13 deletions src/renderers/series-markers-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MediaCoordinatesRenderingScope } from 'fancy-canvas';
import { BitmapCoordinatesRenderingScope } from 'fancy-canvas';

import { ensureNever } from '../helpers/assertions';
import { makeFont } from '../helpers/make-font';
Expand All @@ -9,11 +9,12 @@ import { SeriesMarkerShape } from '../model/series-markers';
import { TextWidthCache } from '../model/text-width-cache';
import { SeriesItemsIndexesRange, TimedValue } from '../model/time-data';

import { MediaCoordinatesPaneRenderer } from './media-coordinates-pane-renderer';
import { BitmapCoordinatesPaneRenderer } from './bitmap-coordinates-pane-renderer';
import { drawArrow, hitTestArrow } from './series-markers-arrow';
import { drawCircle, hitTestCircle } from './series-markers-circle';
import { drawSquare, hitTestSquare } from './series-markers-square';
import { drawText, hitTestText } from './series-markers-text';
import { BitmapShapeItemCoordinates } from './series-markers-utils';

export interface SeriesMarkerText {
content: string;
Expand All @@ -38,7 +39,7 @@ export interface SeriesMarkerRendererData {
visibleRange: SeriesItemsIndexesRange | null;
}

export class SeriesMarkersRenderer extends MediaCoordinatesPaneRenderer {
export class SeriesMarkersRenderer extends BitmapCoordinatesPaneRenderer {
private _data: SeriesMarkerRendererData | null = null;
private _textWidthCache: TextWidthCache = new TextWidthCache();
private _fontSize: number = -1;
Expand Down Expand Up @@ -76,7 +77,7 @@ export class SeriesMarkersRenderer extends MediaCoordinatesPaneRenderer {
return null;
}

protected _drawImpl({ context: ctx }: MediaCoordinatesRenderingScope, isHovered: boolean, hitTestData?: unknown): void {
protected _drawImpl({ context: ctx, horizontalPixelRatio, verticalPixelRatio }: BitmapCoordinatesRenderingScope, isHovered: boolean, hitTestData?: unknown): void {
if (this._data === null || this._data.visibleRange === null) {
return;
}
Expand All @@ -91,38 +92,48 @@ export class SeriesMarkersRenderer extends MediaCoordinatesPaneRenderer {
item.text.height = this._fontSize;
item.text.x = item.x - item.text.width / 2 as Coordinate;
}
drawItem(item, ctx);
drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio);
}
}
}

function drawItem(item: SeriesMarkerRendererDataItem, ctx: CanvasRenderingContext2D): void {
function bitmapShapeItemCoordinates(item: SeriesMarkerRendererDataItem, horizontalPixelRatio: number, verticalPixelRatio: number): BitmapShapeItemCoordinates {
const tickWidth = Math.max(1, Math.floor(horizontalPixelRatio));
const correction = (tickWidth % 2) / 2;
return {
x: Math.round(item.x * horizontalPixelRatio) + correction,
y: item.y * verticalPixelRatio,
pixelRatio: horizontalPixelRatio,
};
}

function drawItem(item: SeriesMarkerRendererDataItem, ctx: CanvasRenderingContext2D, horizontalPixelRatio: number, verticalPixelRatio: number): void {
ctx.fillStyle = item.color;

if (item.text !== undefined) {
drawText(ctx, item.text.content, item.text.x, item.text.y);
drawText(ctx, item.text.content, item.text.x, item.text.y, horizontalPixelRatio, verticalPixelRatio);
}

drawShape(item, ctx);
drawShape(item, ctx, bitmapShapeItemCoordinates(item, horizontalPixelRatio, verticalPixelRatio));
}

function drawShape(item: SeriesMarkerRendererDataItem, ctx: CanvasRenderingContext2D): void {
function drawShape(item: SeriesMarkerRendererDataItem, ctx: CanvasRenderingContext2D, coordinates: BitmapShapeItemCoordinates): void {
if (item.size === 0) {
return;
}

switch (item.shape) {
case 'arrowDown':
drawArrow(false, ctx, item.x, item.y, item.size);
drawArrow(false, ctx, coordinates, item.size);
return;
case 'arrowUp':
drawArrow(true, ctx, item.x, item.y, item.size);
drawArrow(true, ctx, coordinates, item.size);
return;
case 'circle':
drawCircle(ctx, item.x, item.y, item.size);
drawCircle(ctx, coordinates, item.size);
return;
case 'square':
drawSquare(ctx, item.x, item.y, item.size);
drawSquare(ctx, coordinates, item.size);
return;
}

Expand Down
13 changes: 6 additions & 7 deletions src/renderers/series-markers-square.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { Coordinate } from '../model/coordinate';

import { shapeSize } from './series-markers-utils';
import { BitmapShapeItemCoordinates, shapeSize } from './series-markers-utils';

export function drawSquare(
ctx: CanvasRenderingContext2D,
centerX: Coordinate,
centerY: Coordinate,
coords: BitmapShapeItemCoordinates,
size: number
): void {
const squareSize = shapeSize('square', size);
const halfSize = (squareSize - 1) / 2;
const left = centerX - halfSize;
const top = centerY - halfSize;
const halfSize = ((squareSize - 1) * coords.pixelRatio) / 2;
const left = coords.x - halfSize;
const top = coords.y - halfSize;

ctx.fillRect(left, top, squareSize, squareSize);
ctx.fillRect(left, top, squareSize * coords.pixelRatio, squareSize * coords.pixelRatio);
}

export function hitTestSquare(
Expand Down
7 changes: 6 additions & 1 deletion src/renderers/series-markers-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ export function drawText(
ctx: CanvasRenderingContext2D,
text: string,
x: number,
y: number
y: number,
horizontalPixelRatio: number,
verticalPixelRatio: number
): void {
ctx.save();
ctx.scale(horizontalPixelRatio, verticalPixelRatio);
ctx.fillText(text, x, y);
ctx.restore();
}

export function hitTestText(
Expand Down
6 changes: 6 additions & 0 deletions src/renderers/series-markers-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ export function calculateShapeHeight(barSpacing: number): number {
export function shapeMargin(barSpacing: number): number {
return Math.max(size(barSpacing, 0.1), Constants.MinShapeMargin);
}

export interface BitmapShapeItemCoordinates {
x: number;
y: number;
pixelRatio: number;
}
19 changes: 13 additions & 6 deletions tests/e2e/graphics/helpers/screenshoter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,14 @@ export class Screenshoter {
return (window as unknown as TestCaseWindow).testCaseReady;
});

// move mouse to top-left corner
await page.mouse.move(0, 0);
const shouldIgnoreMouseMove = await page.evaluate(() => {
return Boolean((window as unknown as TestCaseWindow).ignoreMouseMove);
});

if (!shouldIgnoreMouseMove) {
// move mouse to top-left corner
await page.mouse.move(0, 0);
}

const waitForMouseMove = page.evaluate(() => {
if ((window as unknown as TestCaseWindow).ignoreMouseMove) { return Promise.resolve(); }
Expand All @@ -93,10 +99,11 @@ export class Screenshoter {
});
});

// to avoid random cursor position
await page.mouse.move(viewportWidth / 2, viewportHeight / 2);

await waitForMouseMove;
if (!shouldIgnoreMouseMove) {
// to avoid random cursor position
await page.mouse.move(viewportWidth / 2, viewportHeight / 2);
await waitForMouseMove;
}

// let's wait until the next af to make sure that everything is repainted
await page.evaluate(() => {
Expand Down
44 changes: 44 additions & 0 deletions tests/e2e/graphics/test-cases/crosshair-marker-position.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Ignore the mouse movement because we are using setCrosshairPosition
window.ignoreMouseMove = true;

function runTestCase(container) {
const chart = (window.chart = LightweightCharts.createChart(container));

const mainSeries = chart.addLineSeries({
pointMarkersVisible: true,
pointMarkersRadius: 8,
});

mainSeries.setData([
{
time: '2024-01-01',
value: 100,
},
{
time: '2024-01-02',
value: 200,
},
{
time: '2024-01-03',
value: 150,
},
{
time: '2024-01-04',
value: 170,
},
]);

chart.timeScale().applyOptions({ barSpacing: 27.701, fixRightEdge: true, rightOffset: 0 });
return new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
chart.setCrosshairPosition(
200,
{ year: 2024, month: 1, day: 2 },
mainSeries
);
resolve();
});
});
});
}

0 comments on commit 32be070

Please sign in to comment.