Skip to content

Commit 32be070

Browse files
authored
Merge pull request #1505 from tradingview/fix-line-markers-positioning
fix series crosshair marker x positioning #1504
2 parents 6daf134 + 8b82918 commit 32be070

10 files changed

+134
-58
lines changed

.size-limit.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ module.exports = [
1919
{
2020
name: 'Standalone',
2121
path: 'dist/lightweight-charts.standalone.production.js',
22-
limit: '49.68 KB',
22+
limit: '49.67 KB',
2323
},
2424
];

src/renderers/marks-renderer.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { MediaCoordinatesRenderingScope } from 'fancy-canvas';
1+
import { BitmapCoordinatesRenderingScope } from 'fancy-canvas';
22

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

5+
import { BitmapCoordinatesPaneRenderer } from './bitmap-coordinates-pane-renderer';
56
import { LineItemBase } from './line-renderer-base';
6-
import { MediaCoordinatesPaneRenderer } from './media-coordinates-pane-renderer';
77

88
export interface MarksRendererData {
99
items: LineItemBase[];
@@ -14,28 +14,34 @@ export interface MarksRendererData {
1414
visibleRange: SeriesItemsIndexesRange | null;
1515
}
1616

17-
export class PaneRendererMarks extends MediaCoordinatesPaneRenderer {
17+
export class PaneRendererMarks extends BitmapCoordinatesPaneRenderer {
1818
protected _data: MarksRendererData | null = null;
1919

2020
public setData(data: MarksRendererData): void {
2121
this._data = data;
2222
}
2323

24-
protected _drawImpl({ context: ctx }: MediaCoordinatesRenderingScope): void {
24+
protected _drawImpl({ context: ctx, horizontalPixelRatio, verticalPixelRatio }: BitmapCoordinatesRenderingScope): void {
2525
if (this._data === null || this._data.visibleRange === null) {
2626
return;
2727
}
2828

2929
const visibleRange = this._data.visibleRange;
3030
const data = this._data;
3131

32-
const draw = (radius: number) => {
32+
const tickWidth = Math.max(1, Math.floor(horizontalPixelRatio));
33+
const correction = (tickWidth % 2) / 2;
34+
35+
const draw = (radiusMedia: number) => {
3336
ctx.beginPath();
3437

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

4147
ctx.fill();

src/renderers/series-markers-arrow.ts

+18-19
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,36 @@ import { ceiledOdd } from '../helpers/mathex';
33
import { Coordinate } from '../model/coordinate';
44

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

88
export function drawArrow(
99
up: boolean,
1010
ctx: CanvasRenderingContext2D,
11-
centerX: Coordinate,
12-
centerY: Coordinate,
11+
coords: BitmapShapeItemCoordinates,
1312
size: number
1413
): void {
1514
const arrowSize = shapeSize('arrowUp', size);
16-
const halfArrowSize = (arrowSize - 1) / 2;
15+
const halfArrowSize = ((arrowSize - 1) / 2) * coords.pixelRatio;
1716
const baseSize = ceiledOdd(size / 2);
18-
const halfBaseSize = (baseSize - 1) / 2;
17+
const halfBaseSize = ((baseSize - 1) / 2) * coords.pixelRatio;
1918

2019
ctx.beginPath();
2120
if (up) {
22-
ctx.moveTo(centerX - halfArrowSize, centerY);
23-
ctx.lineTo(centerX, centerY - halfArrowSize);
24-
ctx.lineTo(centerX + halfArrowSize, centerY);
25-
ctx.lineTo(centerX + halfBaseSize, centerY);
26-
ctx.lineTo(centerX + halfBaseSize, centerY + halfArrowSize);
27-
ctx.lineTo(centerX - halfBaseSize, centerY + halfArrowSize);
28-
ctx.lineTo(centerX - halfBaseSize, centerY);
21+
ctx.moveTo(coords.x - halfArrowSize, coords.y);
22+
ctx.lineTo(coords.x, coords.y - halfArrowSize);
23+
ctx.lineTo(coords.x + halfArrowSize, coords.y);
24+
ctx.lineTo(coords.x + halfBaseSize, coords.y);
25+
ctx.lineTo(coords.x + halfBaseSize, coords.y + halfArrowSize);
26+
ctx.lineTo(coords.x - halfBaseSize, coords.y + halfArrowSize);
27+
ctx.lineTo(coords.x - halfBaseSize, coords.y);
2928
} else {
30-
ctx.moveTo(centerX - halfArrowSize, centerY);
31-
ctx.lineTo(centerX, centerY + halfArrowSize);
32-
ctx.lineTo(centerX + halfArrowSize, centerY);
33-
ctx.lineTo(centerX + halfBaseSize, centerY);
34-
ctx.lineTo(centerX + halfBaseSize, centerY - halfArrowSize);
35-
ctx.lineTo(centerX - halfBaseSize, centerY - halfArrowSize);
36-
ctx.lineTo(centerX - halfBaseSize, centerY);
29+
ctx.moveTo(coords.x - halfArrowSize, coords.y);
30+
ctx.lineTo(coords.x, coords.y + halfArrowSize);
31+
ctx.lineTo(coords.x + halfArrowSize, coords.y);
32+
ctx.lineTo(coords.x + halfBaseSize, coords.y);
33+
ctx.lineTo(coords.x + halfBaseSize, coords.y - halfArrowSize);
34+
ctx.lineTo(coords.x - halfBaseSize, coords.y - halfArrowSize);
35+
ctx.lineTo(coords.x - halfBaseSize, coords.y);
3736
}
3837

3938
ctx.fill();

src/renderers/series-markers-circle.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import { Coordinate } from '../model/coordinate';
22

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

55
export function drawCircle(
66
ctx: CanvasRenderingContext2D,
7-
centerX: Coordinate,
8-
centerY: Coordinate,
7+
coords: BitmapShapeItemCoordinates,
98
size: number
109
): void {
1110
const circleSize = shapeSize('circle', size);
1211
const halfSize = (circleSize - 1) / 2;
1312

1413
ctx.beginPath();
15-
ctx.arc(centerX, centerY, halfSize, 0, 2 * Math.PI, false);
14+
ctx.arc(coords.x, coords.y, halfSize * coords.pixelRatio, 0, 2 * Math.PI, false);
1615

1716
ctx.fill();
1817
}

src/renderers/series-markers-renderer.ts

+24-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MediaCoordinatesRenderingScope } from 'fancy-canvas';
1+
import { BitmapCoordinatesRenderingScope } from 'fancy-canvas';
22

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

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

1819
export interface SeriesMarkerText {
1920
content: string;
@@ -38,7 +39,7 @@ export interface SeriesMarkerRendererData {
3839
visibleRange: SeriesItemsIndexesRange | null;
3940
}
4041

41-
export class SeriesMarkersRenderer extends MediaCoordinatesPaneRenderer {
42+
export class SeriesMarkersRenderer extends BitmapCoordinatesPaneRenderer {
4243
private _data: SeriesMarkerRendererData | null = null;
4344
private _textWidthCache: TextWidthCache = new TextWidthCache();
4445
private _fontSize: number = -1;
@@ -76,7 +77,7 @@ export class SeriesMarkersRenderer extends MediaCoordinatesPaneRenderer {
7677
return null;
7778
}
7879

79-
protected _drawImpl({ context: ctx }: MediaCoordinatesRenderingScope, isHovered: boolean, hitTestData?: unknown): void {
80+
protected _drawImpl({ context: ctx, horizontalPixelRatio, verticalPixelRatio }: BitmapCoordinatesRenderingScope, isHovered: boolean, hitTestData?: unknown): void {
8081
if (this._data === null || this._data.visibleRange === null) {
8182
return;
8283
}
@@ -91,38 +92,48 @@ export class SeriesMarkersRenderer extends MediaCoordinatesPaneRenderer {
9192
item.text.height = this._fontSize;
9293
item.text.x = item.x - item.text.width / 2 as Coordinate;
9394
}
94-
drawItem(item, ctx);
95+
drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio);
9596
}
9697
}
9798
}
9899

99-
function drawItem(item: SeriesMarkerRendererDataItem, ctx: CanvasRenderingContext2D): void {
100+
function bitmapShapeItemCoordinates(item: SeriesMarkerRendererDataItem, horizontalPixelRatio: number, verticalPixelRatio: number): BitmapShapeItemCoordinates {
101+
const tickWidth = Math.max(1, Math.floor(horizontalPixelRatio));
102+
const correction = (tickWidth % 2) / 2;
103+
return {
104+
x: Math.round(item.x * horizontalPixelRatio) + correction,
105+
y: item.y * verticalPixelRatio,
106+
pixelRatio: horizontalPixelRatio,
107+
};
108+
}
109+
110+
function drawItem(item: SeriesMarkerRendererDataItem, ctx: CanvasRenderingContext2D, horizontalPixelRatio: number, verticalPixelRatio: number): void {
100111
ctx.fillStyle = item.color;
101112

102113
if (item.text !== undefined) {
103-
drawText(ctx, item.text.content, item.text.x, item.text.y);
114+
drawText(ctx, item.text.content, item.text.x, item.text.y, horizontalPixelRatio, verticalPixelRatio);
104115
}
105116

106-
drawShape(item, ctx);
117+
drawShape(item, ctx, bitmapShapeItemCoordinates(item, horizontalPixelRatio, verticalPixelRatio));
107118
}
108119

109-
function drawShape(item: SeriesMarkerRendererDataItem, ctx: CanvasRenderingContext2D): void {
120+
function drawShape(item: SeriesMarkerRendererDataItem, ctx: CanvasRenderingContext2D, coordinates: BitmapShapeItemCoordinates): void {
110121
if (item.size === 0) {
111122
return;
112123
}
113124

114125
switch (item.shape) {
115126
case 'arrowDown':
116-
drawArrow(false, ctx, item.x, item.y, item.size);
127+
drawArrow(false, ctx, coordinates, item.size);
117128
return;
118129
case 'arrowUp':
119-
drawArrow(true, ctx, item.x, item.y, item.size);
130+
drawArrow(true, ctx, coordinates, item.size);
120131
return;
121132
case 'circle':
122-
drawCircle(ctx, item.x, item.y, item.size);
133+
drawCircle(ctx, coordinates, item.size);
123134
return;
124135
case 'square':
125-
drawSquare(ctx, item.x, item.y, item.size);
136+
drawSquare(ctx, coordinates, item.size);
126137
return;
127138
}
128139

src/renderers/series-markers-square.ts

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import { Coordinate } from '../model/coordinate';
22

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

55
export function drawSquare(
66
ctx: CanvasRenderingContext2D,
7-
centerX: Coordinate,
8-
centerY: Coordinate,
7+
coords: BitmapShapeItemCoordinates,
98
size: number
109
): void {
1110
const squareSize = shapeSize('square', size);
12-
const halfSize = (squareSize - 1) / 2;
13-
const left = centerX - halfSize;
14-
const top = centerY - halfSize;
11+
const halfSize = ((squareSize - 1) * coords.pixelRatio) / 2;
12+
const left = coords.x - halfSize;
13+
const top = coords.y - halfSize;
1514

16-
ctx.fillRect(left, top, squareSize, squareSize);
15+
ctx.fillRect(left, top, squareSize * coords.pixelRatio, squareSize * coords.pixelRatio);
1716
}
1817

1918
export function hitTestSquare(

src/renderers/series-markers-text.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ export function drawText(
44
ctx: CanvasRenderingContext2D,
55
text: string,
66
x: number,
7-
y: number
7+
y: number,
8+
horizontalPixelRatio: number,
9+
verticalPixelRatio: number
810
): void {
11+
ctx.save();
12+
ctx.scale(horizontalPixelRatio, verticalPixelRatio);
913
ctx.fillText(text, x, y);
14+
ctx.restore();
1015
}
1116

1217
export function hitTestText(

src/renderers/series-markers-utils.ts

+6
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,9 @@ export function calculateShapeHeight(barSpacing: number): number {
3232
export function shapeMargin(barSpacing: number): number {
3333
return Math.max(size(barSpacing, 0.1), Constants.MinShapeMargin);
3434
}
35+
36+
export interface BitmapShapeItemCoordinates {
37+
x: number;
38+
y: number;
39+
pixelRatio: number;
40+
}

tests/e2e/graphics/helpers/screenshoter.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,14 @@ export class Screenshoter {
7373
return (window as unknown as TestCaseWindow).testCaseReady;
7474
});
7575

76-
// move mouse to top-left corner
77-
await page.mouse.move(0, 0);
76+
const shouldIgnoreMouseMove = await page.evaluate(() => {
77+
return Boolean((window as unknown as TestCaseWindow).ignoreMouseMove);
78+
});
79+
80+
if (!shouldIgnoreMouseMove) {
81+
// move mouse to top-left corner
82+
await page.mouse.move(0, 0);
83+
}
7884

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

96-
// to avoid random cursor position
97-
await page.mouse.move(viewportWidth / 2, viewportHeight / 2);
98-
99-
await waitForMouseMove;
102+
if (!shouldIgnoreMouseMove) {
103+
// to avoid random cursor position
104+
await page.mouse.move(viewportWidth / 2, viewportHeight / 2);
105+
await waitForMouseMove;
106+
}
100107

101108
// let's wait until the next af to make sure that everything is repainted
102109
await page.evaluate(() => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Ignore the mouse movement because we are using setCrosshairPosition
2+
window.ignoreMouseMove = true;
3+
4+
function runTestCase(container) {
5+
const chart = (window.chart = LightweightCharts.createChart(container));
6+
7+
const mainSeries = chart.addLineSeries({
8+
pointMarkersVisible: true,
9+
pointMarkersRadius: 8,
10+
});
11+
12+
mainSeries.setData([
13+
{
14+
time: '2024-01-01',
15+
value: 100,
16+
},
17+
{
18+
time: '2024-01-02',
19+
value: 200,
20+
},
21+
{
22+
time: '2024-01-03',
23+
value: 150,
24+
},
25+
{
26+
time: '2024-01-04',
27+
value: 170,
28+
},
29+
]);
30+
31+
chart.timeScale().applyOptions({ barSpacing: 27.701, fixRightEdge: true, rightOffset: 0 });
32+
return new Promise(resolve => {
33+
requestAnimationFrame(() => {
34+
requestAnimationFrame(() => {
35+
chart.setCrosshairPosition(
36+
200,
37+
{ year: 2024, month: 1, day: 2 },
38+
mainSeries
39+
);
40+
resolve();
41+
});
42+
});
43+
});
44+
}

0 commit comments

Comments
 (0)