Skip to content

Commit

Permalink
update hovered source on all crosshair changes (#1727)
Browse files Browse the repository at this point in the history
* feature: set hovered source on all crosshair changes

* add interaction test

* remove comment
  • Loading branch information
illetid authored Nov 22, 2024
1 parent 40c8cc5 commit 31c601f
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 14 deletions.
6 changes: 1 addition & 5 deletions src/gui/pane-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { IDataSourcePaneViews } from '../model/idata-source';
import { InvalidationLevel } from '../model/invalidate-mask';
import { KineticAnimation } from '../model/kinetic-animation';
import { Pane } from '../model/pane';
import { hitTestPane, HitTestResult } from '../model/pane-hit-test';
import { Point } from '../model/point';
import { TimePointIndex } from '../model/time-data';
import { TouchMouseEventData } from '../model/touch-mouse-event-data';
Expand All @@ -31,7 +32,6 @@ import { createBoundCanvas, releaseCanvas } from './canvas-utils';
import { IChartWidgetBase } from './chart-widget';
import { drawBackground, drawForeground, DrawFunction, drawSourceViews, ViewsGetter } from './draw-functions';
import { MouseEventHandler, MouseEventHandlerEventBase, MouseEventHandlerMouseEvent, MouseEventHandlers, MouseEventHandlerTouchEvent, Position, TouchMouseEvent } from './mouse-event-handler';
import { hitTestPane, HitTestResult } from './pane-hit-test';
import { PriceAxisWidget, PriceAxisWidgetSide } from './price-axis-widget';

const enum KineticScrollConstants {
Expand Down Expand Up @@ -263,13 +263,9 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers {
return;
}
this._onMouseEvent();

const x = event.localX;
const y = event.localY;
this._setCrosshairPosition(x, y, event);
const hitTest = this.hitTest(x, y);
this._chart.setCursorStyle(hitTest?.cursorStyle ?? null);
this._model().setHoveredSource(hitTest && { source: hitTest.source, object: hitTest.object });
}

public mouseClickEvent(event: MouseEventHandlerMouseEvent): void {
Expand Down
15 changes: 14 additions & 1 deletion src/model/chart-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ColorType, LayoutOptions } from './layout-options';
import { LocalizationOptions, LocalizationOptionsBase } from './localization-options';
import { Magnet } from './magnet';
import { DEFAULT_STRETCH_FACTOR, MIN_PANE_HEIGHT, Pane } from './pane';
import { hitTestPane } from './pane-hit-test';
import { Point } from './point';
import { PriceScale, PriceScaleOptions } from './price-scale';
import { Series } from './series';
Expand Down Expand Up @@ -508,12 +509,16 @@ export class ChartModel<HorzScaleItem> implements IDestroyable, IChartModelBase
}

public setHoveredSource(source: HoveredSource | null): void {
if (this._hoveredSource?.source === source?.source && this._hoveredSource?.object?.externalId === source?.object?.externalId) {
return;
}
const prevSource = this._hoveredSource;
this._hoveredSource = source;
if (prevSource !== null) {
this.updateSource(prevSource.source);
}
if (source !== null) {
// additional check to prevent unnecessary updates of same source
if (source !== null && source.source !== prevSource?.source) {
this.updateSource(source.source);
}
}
Expand Down Expand Up @@ -791,6 +796,7 @@ export class ChartModel<HorzScaleItem> implements IDestroyable, IChartModelBase

this.cursorUpdate();
if (!skipEvent) {
this._updateHoveredSourceOnChange(pane, x, y);
this._crosshairMoved.fire(this._crosshair.appliedIndex(), { x, y }, event);
}
}
Expand Down Expand Up @@ -1049,6 +1055,13 @@ export class ChartModel<HorzScaleItem> implements IDestroyable, IChartModelBase
return this._colorParser;
}

private _updateHoveredSourceOnChange(pane: Pane, x: Coordinate, y: Coordinate): void {
if (pane) {
const hitTest = hitTestPane(pane, x, y);
this.setHoveredSource(hitTest && { source: hitTest.source, object: hitTest.object });
}
}

private _getOrCreatePane(index: number): Pane {
assert(index >= 0, 'Index should be greater or equal to 0');
index = Math.min(this._panes.length, index);
Expand Down
11 changes: 6 additions & 5 deletions src/gui/pane-hit-test.ts → src/model/pane-hit-test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { HoveredObject } from '../model/chart-model';
import { Coordinate } from '../model/coordinate';
import { IDataSource, IPrimitiveHitTestSource } from '../model/idata-source';
import { PrimitiveHoveredItem, PrimitivePaneViewZOrder } from '../model/ipane-primitive';
import { Pane } from '../model/pane';
import { IPaneView } from '../views/pane/ipane-view';

import { HoveredObject } from './chart-model';
import { Coordinate } from './coordinate';
import { IDataSource, IPrimitiveHitTestSource } from './idata-source';
import { PrimitiveHoveredItem, PrimitivePaneViewZOrder } from './ipane-primitive';
import { Pane } from './pane';

export interface HitTestResult {
source: IPrimitiveHitTestSource;
object?: HoveredObject;
Expand Down
41 changes: 38 additions & 3 deletions src/plugins/series-markers/series-markers-arrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { ceiledOdd } from '../../helpers/mathex';

import { Coordinate } from '../../model/coordinate';

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

export function drawArrow(
Expand Down Expand Up @@ -46,6 +45,42 @@ export function hitTestArrow(
x: Coordinate,
y: Coordinate
): boolean {
// TODO: implement arrow hit test
return hitTestSquare(centerX, centerY, size, x, y);
const arrowSize = shapeSize('arrowUp', size);
const halfArrowSize = (arrowSize - 1) / 2;
const baseSize = ceiledOdd(size / 2);
const halfBaseSize = (baseSize - 1) / 2;

const triangleTolerance = 3;
const rectTolerance = 2;

const baseLeft = centerX - halfBaseSize - rectTolerance;
const baseRight = centerX + halfBaseSize + rectTolerance;
const baseTop = up ? centerY : centerY - halfArrowSize;
const baseBottom = up ? centerY + halfArrowSize : centerY;

if (x >= baseLeft && x <= baseRight &&
y >= baseTop - rectTolerance && y <= baseBottom + rectTolerance) {
return true;
}

const isInTriangleBounds = (): boolean => {
const headLeft = centerX - halfArrowSize - triangleTolerance;
const headRight = centerX + halfArrowSize + triangleTolerance;
const headTop = up ? centerY - halfArrowSize - triangleTolerance : centerY;
const headBottom = up ? centerY : centerY + halfArrowSize + triangleTolerance;

if (x < headLeft || x > headRight ||
y < headTop || y > headBottom) {
return false;
}

const dx = Math.abs(x - centerX);
const dy = up
? Math.abs(y - centerY) // up arrow
: Math.abs(y - centerY); // down arrow

return dy + triangleTolerance >= dx / 2;
};

return isInTriangleBounds();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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 initialInteractionsToPerform() {
return [];
}

let markerX = 0;
let markerY = 0;
function finalInteractionsToPerform() {
return [{
action: 'clickXY',
options: {
// set the cursor aside from the marker
x: markerX - 30,
y: markerY + 5,
},
}];
}

let chart;
let lastHoveredObjectId = null;
function beforeInteractions(container) {
chart = LightweightCharts.createChart(container);

const mainSeries = chart.addSeries(LightweightCharts.LineSeries);

const mainSeriesData = generateData();
const markerTime = mainSeriesData[450].time;
const price = mainSeriesData[450].value;

mainSeries.setData(mainSeriesData);
LightweightCharts.createSeriesMarkers(
mainSeries,
[
{
time: markerTime,
position: 'inBar',
color: '#2196F3',
size: 3,
shape: 'circle',
text: '',
id: 'TEST',
},
]
);
mainSeries.createPriceLine({
price: price,
color: '#000',
lineWidth: 2,
lineStyle: 2,
axisLabelVisible: false,
title: '',
id: 'LINE',
});
chart.subscribeCrosshairMove(params => {
if (!params) {
return;
}
lastHoveredObjectId = params.hoveredObjectId;
});
return new Promise(resolve => {
requestAnimationFrame(() => {
// get coordinates for marker bar
markerX = chart.timeScale().timeToCoordinate(markerTime);
markerY = mainSeries.priceToCoordinate(price);

resolve();
});
});
}

function afterInitialInteractions() {
return new Promise(resolve => {
requestAnimationFrame(resolve);
});
}

function afterFinalInteractions() {
// scroll to the marker
chart.timeScale().scrollToPosition(chart.timeScale().scrollPosition() + 5, false);
return new Promise(resolve => {
requestAnimationFrame(() => {
const pass = lastHoveredObjectId === 'TEST';
if (!pass) {
throw new Error("Expected hoveredObjectId to be equal to 'TEST'.");
}
resolve();
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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 initialInteractionsToPerform() {
return [];
}

let markerX = 0;
let markerY = 0;
function finalInteractionsToPerform() {
return [
{
action: 'clickXY',
options: {
x: markerX,
y: markerY + 5,
},
},
];
}

let chart;
let lastHoveredObjectId = null;
function beforeInteractions(container) {
chart = LightweightCharts.createChart(container);

const mainSeries = chart.addSeries(LightweightCharts.LineSeries);

const mainSeriesData = generateData();
const markerTime = mainSeriesData[450].time;
const price = mainSeriesData[450].value;

mainSeries.setData(mainSeriesData);
LightweightCharts.createSeriesMarkers(
mainSeries,
[
{
time: markerTime,
position: 'inBar',
color: '#2196F3',
size: 3,
shape: 'circle',
text: '',
id: 'TEST',
},
]
);
mainSeries.createPriceLine({
price: price,
color: '#000',
lineWidth: 2,
lineStyle: 2,
axisLabelVisible: false,
title: '',
id: 'LINE',
});
chart.subscribeClick(mouseParams => {
if (!mouseParams) {
return;
}
lastHoveredObjectId = mouseParams.hoveredObjectId;
});

return new Promise(resolve => {
requestAnimationFrame(() => {
// get coordinates for marker bar
markerX = chart.timeScale().timeToCoordinate(markerTime);
markerY = mainSeries.priceToCoordinate(price);
resolve();
});
});
}

function afterInitialInteractions() {
return new Promise(resolve => {
requestAnimationFrame(resolve);
});
}

function afterFinalInteractions() {
const pass = lastHoveredObjectId === 'TEST';
if (!pass) {
throw new Error("Expected hoveredObjectId to be equal to 'TEST'.");
}

return Promise.resolve();
}

0 comments on commit 31c601f

Please sign in to comment.