lightweight-charts icon indicating copy to clipboard operation
lightweight-charts copied to clipboard

Setting the crosshair programmatically

Open gkaindl opened this issue 4 years ago • 26 comments

Describe the solution you'd like

Setting the crosshair programmatically would be useful in some circumstances. Right now, the crosshair can only be controlled via mouse or touch interactions, but not programmatically, I think (excluding the solution of synthesizing events).

Other user interactions, such as scrolling/panning and changing the scales (once #416 is merged), are already enabled to be controlled programmatically, but control of the crosshair is missing to fully manage the erverything in the UI via client code.

Additional context

Ideally, methods to move the crosshair to a given (canvas) coordinate set or to hide it would be good. If #435 gets implemented too, moving it to a specific time point would also be doable then.

Some areas where this might be useful:

  • Moving the crosshair in a synchronized fashion on multiple charts as the user brushes over one of them.
  • Implementing custom crosshair behaviors with non-standard input devices (like, say, on an iPad, the crosshair could be moved directly with the pen, whereas touch is used to scroll/pan/zoom)
  • Highlighting specific bars by letting the user select items in another UI element, like a list of events
  • Serializing and restoring UI state, like when live-mirroring the state completely between multiple users, using web sockets or webRTC.

This is already doable to certain extent by synthesizing events (see the gif below), but it doesn't work on touch devices (due to the way events are handled in this case), and deriving the coordinates reliably (if the charts involved aren't sized the same) requires a lot of code, using your dataset, the visible time- and logical ranges, and so on. A proper API would make this way easier.

charts-short

gkaindl avatar May 15 '20 20:05 gkaindl

@gkaindl That would be great!

Also is it possible to share codes of the events that you made in the gif (synthesizing events) ?

ghost avatar May 18 '20 10:05 ghost

Related to #376.

timocov avatar May 18 '20 16:05 timocov

It looks like we need to add something like CrossHair API (or kind of). Let's wait more feedback.

timocov avatar May 18 '20 16:05 timocov

@lejyoner-ds My current way of doing it with synthetic events is really more of a hack and works possibly only for my use-case, since I also synchronize the visible range between charts, but I've written some details together in a gist, so that we're not cluttering up this issue thread. I hope you find it useful!

@timocov So regarding how the crosshair API could work, it would be ideal for my use-case(s) to have the following API additions to chart-api:

showCrosshairAtPoint(point: Point): Shows (or moves) the crosshair to the given point in the chart's coordinate system (e.g. same coordinate system as what is currently supplied to MouseEventHandler). If the point falls outside the coordinate system of the chart, the crosshair gets hidden (e.g. it has been moved outside the chart’s area).

hideCrosshair(): Hides the crosshair if it is currently visible, or does nothing if the crosshair is currently not visible.

pointForTime(time: Time): Point | null: Returns the point for a given time value in this chart. The x-coordinate should be the center of the bar, the y-coordinate should be the coordinate the crosshair would snap to if it was set to “magnet” mode. If the time value falls outside the current visibleRange, null is returned. This has its own issue #435 (not opened by me, but fits together nicely).

timeForPoint(point: Point): Time | null: Just the inverse of the pointForTime(), e.g returns the time value for given chart coordinates, or null if the point is either outside the chart’s bounds, or if there is no bar at the given point in the chart. This method (together with the inverse) would be great to be able to easily move the crosshair to the same bar time in multiple different charts (e.g. I get the time for the point in the original chart, then get the point for this time in the other chart, and show the crosshair there).

Optionally, these three methods could be added for some use-cases, e.g. those where people want to implement their own handling of the crosshair (for all three of these, I’m unsure about the best naming):

claimCrosshair(): Disables the “internal” management of the crosshair, e.g. after this is called, the library no longer manages the crosshair, so it doesn’t appear on mouseenter, move on mousemove, disappear on mouseleave, and so on. After calling this, the crosshair is completely controlled by the user only.

unclaimCrosshair(): Symmetric to claimCrosshair(), calling this restores the normal library handling of the crosshair, e.g. the crosshair is no longer managed by the user alone. I think it would also make sense for claim/unclaim calls needing to be balanced, e.g. if I call claimCrosshair() x times, I also need to call unclaimCrosshair() x times to restore normal operation.

chartEventElement(): ChartEventElement: This should return the canvas element for the actual chart, so that users can attach their own event handlers to it. So if I want to implement special handling for the iPad pencil (as an example), I could attach my own handlers for touch events and handle the pencil specially. If I want to take full control of the crosshair, I use this together with claimCrosshair() to disable the default handlers. Since I don’t think it would be good if people expect this method to actually return the canvas (and start to rely on this implementation detail), ChartEventElement could be an interface that only contains addEventListener() and removeEventListener() methods, which behave exactly like the normal DOM methods.

Of course, one could think of allowing multiple crosshairs within the same chart, so that there’s the library-controlled one, and one or more user-controlled one, but I personally don’t think that’s needed.

So that would be my idea – Maybe the others who would be interested in a crosshair API have some feedback/requirements, too!

gkaindl avatar May 19 '20 22:05 gkaindl

It would be useful in many cases to support moving the cross-hair smoothly across candles using interpolation (or extrapolation?) when two charts have different time frames.

For example: if one chart uses hourly time scale (hourly candles) and another chart uses daily time scale (daily candles), then ideally syncing crosshairs between these two charts would allow showing or moving the crosshairs smoothly in both charts (if not snapped to center of the candle) as the user moves moves the mouse in one chart. If user moves mouse in the hourly chart from left to right, it would also move the crosshair in the daily chart as well by the corresponding time scale interval in the daily chart.

At the moment, I do not have access to any example gif or animation of what I am describing, but hope the meaning is clear enough!

b4git avatar May 23 '20 05:05 b4git

Also we need to worry about #50 (crosshair might be on several panes, has different prices/coordinates and so on).

At the moment, I do not have access to any example gif or animation of what I am describing, but hope the meaning is clear enough!

Yeah, we have the same feature on charting library/trading terminal/tradingview's chart itself.

timocov avatar May 25 '20 17:05 timocov

Also we need to worry about #50 (crosshair might be on several panes, has different prices/coordinates and so on).

Oh, good point! I wasn't aware that a public pane API is in the works, I've only seen the concept of a pane used internally so far – Is there a branch that already has a work-in-progress public pane API to look at?

Maybe it would be sufficient then to put the proposed methods on the pane-api, rather than the chart-api then (I suppose the subscriptions for clicks and crosshair updates would also move there).

Yeah, we have the same feature on charting library/trading terminal/tradingview's chart itself.

I think it's also a question of how the "lightweight" in lightweight-charts is interpreted: For use-cases like interpolating the crosshair position from a shorter-timeframe chart/pane over a longer-timeframe chart/pane, one valid approach would be to only provide the most basic primitives and let users implement the actual interpolation behavior themselves, or to provide more "convenience methods" and treat the "lightweight" aspect as "If a representative amount of users need/want the behavior, it gets added to the library natively". So as an example, if there was only the pointForTime() method from my earlier post, the interpolation could be achieved by taking the crosshair time from the source chart, then searching my data in the target chart for the two times that this time falls in between, use pointForTime() for these two times to get the coordinates in the chart, interpolate between these coordinates, and just use showCrosshairAtPoint() to draw the crosshair. It's more work, but it would be doable.

Even if there are more "powerful" methods available, having the primitives around is still great, because people might want to customize certain aspects to fit their particular use-case, which would either require these methods to be rather complex and customizable (like, say, for the interpolation use-case, somebody needed a different interpolation method than linear).

Anyway, since there are already quite a few issues related to crosshair features, we can check them to see if there are use-cases that can't be built using the proposed primitives, and maybe extend/amend them accordingly.

PS: I'm not arguing that there shouldn't be more advanced/powerful methods available that implement entire behaviors, I'm rather just describing my viewpoint/feedback. I'm always a bit afraid to come across as demanding/opinionated in threads like this.

gkaindl avatar May 25 '20 21:05 gkaindl

Is there a branch that already has a work-in-progress public pane API to look at?

Not so far, but we need to keep it in mind to avoid huge breaking changes or even API conflicts.

Also, some converters (like from time/price to coordinate and vice versa) should be done in price/time scale API (actually some of them are already done there - see #435 for instance, but it looks like for price scale we have converters in series but I don't remember exactly reason for that).

Anyway, we'll keep in mind this request, if anybody has additional/specific request for that, leave a comment. If you need it as it - just put your 👍 at the topic message.

timocov avatar Jun 01 '20 20:06 timocov

any update? possible to use crosshair programatically?

srhtylmz19 avatar Jul 24 '20 12:07 srhtylmz19

We have a specific use case in mobile devices where we need to close the crosshair when user released their touch from the screen. Apparently some users dislike the the way crosshair sticks and need another tab to close it.

Therefore able to close the crosshair manually/programmatically will come in handy as we can just bind it with onTouchEnd event. Would be even better if we could introduce another mode (stick/non-stick) for crosshair that could achieve to above use case.

ch4rlesyeo avatar Oct 08 '20 19:10 ch4rlesyeo

Here is my hack/solution/workaround to sync the crosshair between two charts that involve modification of the source code. It involve a slight modification of 4 files ichart-api.ts, chart-api.ts, pane-widget.ts and chart-model.ts. Add this

setCrossHairXY(x: number,y: number,visible: boolean): void;

here https://github.com/tradingview/lightweight-charts/blob/faa3295f026966cdee84a01c107d090c57b73f6f/src/api/ichart-api.ts#L114 And this

public setCrossHairXY(x: number,y: number,visible: boolean): void{
    this._chartWidget.paneWidgets()[0].setCrossHair(x,y,visible);
}

here https://github.com/tradingview/lightweight-charts/blob/faa3295f026966cdee84a01c107d090c57b73f6f/src/api/chart-api.ts#L176 And this

public setCrossHair(xx: number,yy: number,visible: boolean): void {
	if (!this._state) {
		return;
	}
	if (visible){
		const x = xx as Coordinate;
		const y = yy as Coordinate;

		if (!mobileTouch) {
			this._setCrosshairPositionNoFire(x, y);
		}
	}else{
		this._state.model().setHoveredSource(null);
		if (!isMobile) {
			this._clearCrosshairPosition();
		}
	}
}
private _setCrosshairPositionNoFire(x: Coordinate, y: Coordinate): void {
	this._model().setAndSaveCurrentPositionFire(this._correctXCoord(x), this._correctYCoord(y), false, ensureNotNull(this._state));
}

here https://github.com/tradingview/lightweight-charts/blob/faa3295f026966cdee84a01c107d090c57b73f6f/src/gui/pane-widget.ts#L693

And this

public setAndSaveCurrentPositionFire(x: Coordinate, y: Coordinate, fire: boolean, pane: Pane): void {
	this._crosshair.saveOriginCoord(x, y);
	let price = NaN;
	let index = this._timeScale.coordinateToIndex(x);

	const visibleBars = this._timeScale.visibleStrictRange();
	if (visibleBars !== null) {
		index = Math.min(Math.max(visibleBars.left(), index), visibleBars.right()) as TimePointIndex;
	}

	const priceScale = pane.defaultPriceScale();
	const firstValue = priceScale.firstValue();
	if (firstValue !== null) {
		price = priceScale.coordinateToPrice(y, firstValue);
	}
	price = this._magnet.align(price, index, pane);

	this._crosshair.setPosition(index, price, pane);
	this._cursorUpdate();
	if (fire) {
		this._crosshairMoved.fire(this._crosshair.appliedIndex(), { x, y });
	}
}

here https://github.com/tradingview/lightweight-charts/blob/faa3295f026966cdee84a01c107d090c57b73f6f/src/model/chart-model.ts#L446

And now you can do something like this .

chart.subscribeCrosshairMove(crosssyncHandler);
function crosssyncHandler(e) {
  if (e.time !== undefined) {
    var xx = chart2.timeScale().timeToCoordinate(e.time);
    chart2.setCrossHairXY(xx,50,true);
  } else if (e.point !== undefined){
    chart2.setCrossHairXY(e.point.x,10,false);
  }
}

chart2.subscribeCrosshairMove(crossSyncHandler2);
function crossSyncHandler2(e) {
  if (e.time !== undefined) {
    var xx = chart.timeScale().timeToCoordinate(e.time);
    chart.setCrossHairXY(xx,200,true);
  } else if (e.point !== undefined){
    chart.setCrossHairXY(e.point.x,100,false);
  }
}

Here is a jsfiddle that shows the result https://jsfiddle.net/trior/y1vcxtqw/

Almost all the code listed above is just a refactored version of already existing code. Hope it helps a bit.

triorr avatar Oct 26 '20 04:10 triorr

@triorr Thank you very much for that suggestion and the code.

You miss to post your custom function this._setCrosshairPositionNoFire(x, y); in the lightweight-charts/src/gui/pane-widget.ts. I assum you disabled the setAndSaveCurrentPosition on lightweight-charts/src/model/chart-model.ts. So I did that as well an it works fine.

I added a clearCrossHair function for leaving a chart, that the cross hair will also remove on the ohter.

public clearCrossHair(): void {
	this._chartWidget.paneWidgets()[0].clearCrossHair();
}

in lightweight-charts/src/api/chart-api.ts under your suggested setCrossHairXY method.

And reister in as well under setCrossHairXY

clearCrossHair(): void;

in lightweight-charts/src/api/ichart-api.ts

When using your code example it can be extend by:

chart.subscribeCrosshairMove(crosssyncHandler);
let mouseOverChart = false;
function crosssyncHandler(e) {
  if (e.time !== undefined) {
     var xx = chart2.timeScale().timeToCoordinate(e.time);
     chart2.setCrossHairXY(xx,50,true);
   } else if (e.point !== undefined){
     chart2.setCrossHairXY(e.point.x,10,false);
   }  else if(mouseOverChart) {
     mouseOverChart = false;
     chart.clearCrossHair();
   }
}
 
chart2.subscribeCrosshairMove(crossSyncHandler2);
let mouseOverChart2 = false;
function crossSyncHandler2(e) {
  if (e.time !== undefined) {
    var xx = chart.timeScale().timeToCoordinate(e.time);
    chart.setCrossHairXY(xx,200,true);
  } else if (e.point !== undefined){
    chart.setCrossHairXY(e.point.x,100,false);
  } else if(mouseOverChart2) {
    mouseOverChart2 = false;
    chart.clearCrossHair();
  }
}

florian-kittel avatar Dec 20 '20 09:12 florian-kittel

Thanks @florian-kittel, That's true I forgot the _setCrosshairPositionNoFire function . I modified my post above to correct the error.

My solution is far from complete. You can add all sorts of stuff to make it function correctly in a production area depending on your use case.

triorr avatar Dec 29 '20 10:12 triorr

I have a problem where I have 4 charts using same data, but deriving multiple timeframes for example: daily, weekly, monthly, and yearly. I want to sync the crosshair horizontal line to the price scale axis.

@triorr what is the best way to do this? I was able to get the charts syncing on the timescale like you showed above.

Thanks

SOLVED

var yy = candlestickSeries1.priceToCoordinate(candlestickSeries4.coordinateToPrice(e.point.y));

chart1.setCrossHairXY(xx,yy,true);

adgower avatar Mar 08 '21 16:03 adgower

It's working well, that should be added into the library.

cmp-nct avatar Mar 20 '21 04:03 cmp-nct

Hi Everyone, I get a question & sorry if it's obvious, but what's the branch? where I can support for testing. I'm testing replique the code of @florian-kittel (example) but only that the function this._cursorUpdate() don't exist. I could support for testing, that's a pleasure for me.

julio899 avatar Nov 03 '21 05:11 julio899

Thanks I was can tested It's nice, it would be add in some feature soon? image

julio899 avatar Nov 09 '21 06:11 julio899

Hi Everyone, I get a question & sorry if it's obvious, but what's the branch? where I can support for testing. I'm testing replique the code of @florian-kittel (example) but only that the function this._cursorUpdate() don't exist. I could support for testing, that's a pleasure for me.

Hi, my changes based on v3.4.0. I the later versions this._cursorUpdate() was renamed to this.cursorUpdate().

florian-kittel avatar Nov 13 '21 13:11 florian-kittel

Using trior's patch to sync crosshairs work in desktop-browsers as shown in the video.

But it does not work inside a mobile-based browser.

Can this limitation be bypassed? @triorr @florian-kittel

https://user-images.githubusercontent.com/108040259/189456995-dfc26244-19fd-4e83-8a97-b86aac19d60e.mp4

0x0tyy avatar Sep 09 '22 23:09 0x0tyy

Using trior's patch to sync crosshairs work in desktop-browsers as shown in the video.

But it does not work inside a mobile-based browser.

Can this limitation be bypassed? @triorr @florian-kittel

screen-20220910-015331.mp4

If you refer to the commit from august, I can not confirm. I still use an older version. But it works on my side for mobile browsers.

florian-kittel avatar Sep 10 '22 20:09 florian-kittel

https://user-images.githubusercontent.com/10675435/189500879-c3c71d95-01ad-45a6-a922-3ffb8f9d5a27.MOV

florian-kittel avatar Sep 10 '22 20:09 florian-kittel

FullSizeRender.MOV

Are you using a bluetooth mouse here? I just tried it on 3.2 and 3.4. Both versions prevent me from moving the crosshair using the cursor (bluetooth mouse).

I can activate the crosshair with a longtap using fingers but the crosshairs didn't sync like yours in the video.

this video is using 3.4 comparing desktop firefox vs mobile browser:

https://user-images.githubusercontent.com/108040259/189521135-d15a1e35-ab14-47fa-8177-98e598d56557.mp4

0x0tyy avatar Sep 11 '22 09:09 0x0tyy

FullSizeRender.MOV

Are you using a bluetooth mouse here? I just tried it on 3.2 and 3.4. Both versions prevent me from moving the crosshair using the cursor (bluetooth mouse).

I can activate the crosshair with a longtap using fingers but the crosshairs didn't sync like yours in the video.

this video is using 3.4 comparing desktop firefox vs mobile browser: screen-20220911-123950.mp4

Hi, no I have recoreded the video directly in my iphone 12. It is my PWA and I am using the chart completly with touch. I never tried to use a mouse for the iPhone so I can not say if a mouse hover on mobile device will work. But touch works on my side. (Same goes for iPad, works with touch, never tried mouse on iPad too)

florian-kittel avatar Sep 11 '22 09:09 florian-kittel

FullSizeRender.MOV

Are you using a bluetooth mouse here? I just tried it on 3.2 and 3.4. Both versions prevent me from moving the crosshair using the cursor (bluetooth mouse). I can activate the crosshair with a longtap using fingers but the crosshairs didn't sync like yours in the video. this video is using 3.4 comparing desktop firefox vs mobile browser: screen-20220911-123950.mp4

Hi, no I have recoreded the video directly in my iphone 12. It is my PWA and I am using the chart completly with touch. I never tried to use a mouse for the iPhone so I can not say if a mouse hover on mobile device will work. But touch works on my side. (Same goes for iPad, works with touch, never tried mouse on iPad too)

I see, I went thru all the versions from the releases and none of them seem to be working for me. Could I try out your lightweight-charts.standalone.production.js dist? If that doesn't work, I have another idea for the crosshair but would like to check it just in case. If you don't mind. Thanks

0x0tyy avatar Sep 11 '22 15:09 0x0tyy

@0x0tyy If I had to guess. I think it's something to do with mobileTouch and isMobile in the setCrossHair function. Also you maybe want to take a look at this file support-touch.ts

triorr avatar Sep 12 '22 05:09 triorr

@0x0tyy If I had to guess. I think it's something to do with mobileTouch and isMobile in the setCrossHair function. Also you maybe want to take a look at this file support-touch.ts

yeah that was the culprit. It is working perfectly in mobile now.

0x0tyy avatar Sep 13 '22 20:09 0x0tyy

@0x0tyy If I had to guess. I think it's something to do with mobileTouch and isMobile in the setCrossHair function. Also you maybe want to take a look at this file support-touch.ts

how come this file is removed in 3.8.0?

I forked the repo:

made the changes then tried to npm install from my public repo on my account

It failed saying git build tools. Any tips? Trying to import into vue 3 project

C:\WINDOWS\system32\cmd.exe /d /s /c npm run install-hooks
npm ERR! > [email protected] install-hooks
npm ERR! > node scripts/githooks/install.js
npm ERR! node:internal/modules/cjs/loader:998
npm ERR!   throw err;
npm ERR!   ^
npm ERR!
npm ERR! Error: Cannot find module 'C:\****\node_modules\lightweight-charts\scripts\githooks\install.js'
npm ERR!     at Module._resolveFilename (node:internal/modules/cjs/loader:995:15)
npm ERR!     at Module._load (node:internal/modules/cjs/loader:841:27)
npm ERR!     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
npm ERR!     at node:internal/main/run_main_module:23:47 {
npm ERR!   code: 'MODULE_NOT_FOUND',
npm ERR!   requireStack: []
npm ERR! }
npm ERR!
npm ERR! Node.js v18.12.0

adgower avatar Oct 26 '22 00:10 adgower

Thanks I was can tested It's nice, it would be add in some feature soon? image

Hi, I was wondering how you were able to get these technical analysis on these charts?

0xAskar avatar Nov 21 '22 16:11 0xAskar

You can tested in https://topacio.trade

On Mon, Nov 21, 2022, 12:26 PM Askar @.***> wrote:

Thanks I was can tested It's nice, it would be add in some feature soon? [image: image] https://user-images.githubusercontent.com/2575745/140875019-9f58cc17-4580-4182-935a-652189202f88.png

Hi, I was wondering how you were able to get these technical analysis on these charts?

— Reply to this email directly, view it on GitHub https://github.com/tradingview/lightweight-charts/issues/438#issuecomment-1322329404, or unsubscribe https://github.com/notifications/unsubscribe-auth/AATU3AMNTHSNXTWL5SBSPZLWJOPEFANCNFSM4NCQWYKQ . You are receiving this because you are subscribed to this thread.Message ID: @.***>

julio899 avatar Nov 21 '22 17:11 julio899

@florian-kittel could you please share your clearCrossHair function that should be included in pane-widget.ts ?

@triorr and @0x0tyy - I'm looking to implement this solution/hackaround on lightweight-charts 3.8. Could you share what you used to set these variables: isMobile mobileTouch? support-touch.ts is no longer present in the build. I see isTouch is present in mouse-event-handler.ts. Would this single variable be sufficient, or do isMobile and mobileTouch need to be distinct?

tasteitslight avatar Dec 08 '22 02:12 tasteitslight