Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setting the crosshair programmatically #438

Closed
gkaindl opened this issue May 15, 2020 · 50 comments
Closed

Setting the crosshair programmatically #438

gkaindl opened this issue May 15, 2020 · 50 comments
Labels
enhancement Feature requests, and general improvements. need more feedback Requires more feedback.
Milestone

Comments

@gkaindl
Copy link
Contributor

gkaindl commented May 15, 2020

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 gkaindl changed the title Settings the crosshair programmatically Setting the crosshair programmatically May 15, 2020
@ghost
Copy link

ghost commented May 18, 2020

@gkaindl That would be great!

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

@timocov
Copy link
Contributor

timocov commented May 18, 2020

Related to #376.

@timocov timocov added enhancement Feature requests, and general improvements. need more feedback Requires more feedback. labels May 18, 2020
@timocov
Copy link
Contributor

timocov commented May 18, 2020

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

@gkaindl
Copy link
Contributor Author

gkaindl commented May 19, 2020

@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!

@b4git
Copy link

b4git commented May 23, 2020

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!

@timocov
Copy link
Contributor

timocov commented May 25, 2020

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.

@gkaindl
Copy link
Contributor Author

gkaindl commented May 25, 2020

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.

@timocov
Copy link
Contributor

timocov commented Jun 1, 2020

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.

@srhtylmz19
Copy link

any update? possible to use crosshair programatically?

@ch4rlesyeo
Copy link

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.

@triorr
Copy link

triorr commented Oct 26, 2020

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


And this

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

here


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

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

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.

@florian-kittel
Copy link

florian-kittel commented Dec 20, 2020

@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();
  }
}

@triorr
Copy link

triorr commented Dec 29, 2020

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.

@timocov timocov added this to the 4.0 milestone Feb 1, 2021
@adgower
Copy link

adgower commented Mar 8, 2021

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);

@cmp-nct
Copy link

cmp-nct commented Mar 20, 2021

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

@timocov timocov modified the milestones: 4.0, Future Jun 14, 2021
@julio899
Copy link

julio899 commented Nov 3, 2021

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
Copy link

julio899 commented Nov 9, 2021

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

@florian-kittel
Copy link

florian-kittel commented Nov 13, 2021

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().

matiasmolleja added a commit to matiasmolleja/lightweight-charts that referenced this issue Apr 4, 2022
difurious pushed a commit to difurious/lightweight-charts that referenced this issue Aug 1, 2022
@0x0tyy
Copy link

0x0tyy commented Sep 9, 2022

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

@0x0tyy
Copy link

0x0tyy commented Sep 11, 2022

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

@triorr
Copy link

triorr commented Sep 12, 2022

@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

@0x0tyy
Copy link

0x0tyy commented Sep 13, 2022

@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.

@adgower
Copy link

adgower commented Oct 26, 2022

@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! > lightweight-charts@3.8.0 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

@0xAskar
Copy link

0xAskar commented Nov 21, 2022

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?

@julio899
Copy link

julio899 commented Nov 21, 2022 via email

@tasteitslight
Copy link

tasteitslight commented Dec 8, 2022

@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
Copy link

tasteitslight commented Dec 9, 2022

Hi all, I was able to get @triorr's code to work with some slight modifications. See below:

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

here:

(it is line 209 in version 3.8.0)

this:

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

here:

(line 193 in version 3.8.0)

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;
      this._setCrosshairPositionNoFire(x, y);
    } else {
      this._state.model().setHoveredSource(null);
      this._clearCrosshairPosition();
    }
  }

private _setCrosshairPositionNoFire(x: Coordinate, y: Coordinate): void {
    this._model().setAndSaveCurrentPositionFire(this._correctXCoord(x), this._correctYCoord(y), false, ensureNotNull(this._state));
}

here:

(line 644 in 3.8.0)

and finally 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:

(line 658 in 3.8.0)

With the following implementation:

this.mainChart.subscribeCrosshairMove((e) => {
        if (e.time !== undefined) {
          var xx = this.midChart.timeScale().timeToCoordinate(e.time);
          this.midChart.setCrosshairXY(xx,100,true);
        } else if (e.point !== undefined){
          this.midChart.setCrosshairXY(e.point.x,100,false);
        }
      });
      this.midChart.subscribeCrosshairMove((e) => {
        if (e.time !== undefined) {
          var xx = this.mainChart.timeScale().timeToCoordinate(e.time);
          this.mainChart.setCrosshairXY(xx,100,true);
        } else if (e.point !== undefined){
          this.mainChart.setCrosshairXY(e.point.x,100,false);
        }
      });

You can see i made the trivial change renaming setCrossHair to `setCrosshair' to follow the lightweight-chart naming convention.

Also, I removed isMobile and mobileTouch as it seems these are no longer part of the build. As a novice coder, I am uncertain exactly what these were meant to accomplish. It is working well enough for our uses, although there are some things worth changing, such as @florian-kittel's suggestion which I have yet to incorporate (I'm still unsure how!)

Thank you everyone for your help. Hopefully this can assist anyone else looking for this functionality.

@NomNomCameron
Copy link

Hi all, I was able to get @triorr's code to work with some slight modifications. See below:

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

here:

(it is line 209 in version 3.8.0)

this:

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

here:

(line 193 in version 3.8.0)

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;
      this._setCrosshairPositionNoFire(x, y);
    } else {
      this._state.model().setHoveredSource(null);
      this._clearCrosshairPosition();
    }
  }

private _setCrosshairPositionNoFire(x: Coordinate, y: Coordinate): void {
    this._model().setAndSaveCurrentPositionFire(this._correctXCoord(x), this._correctYCoord(y), false, ensureNotNull(this._state));
}

here:

(line 644 in 3.8.0)

and finally 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:

(line 658 in 3.8.0)

With the following implementation:

this.mainChart.subscribeCrosshairMove((e) => {
        if (e.time !== undefined) {
          var xx = this.midChart.timeScale().timeToCoordinate(e.time);
          this.midChart.setCrosshairXY(xx,100,true);
        } else if (e.point !== undefined){
          this.midChart.setCrosshairXY(e.point.x,100,false);
        }
      });
      this.midChart.subscribeCrosshairMove((e) => {
        if (e.time !== undefined) {
          var xx = this.mainChart.timeScale().timeToCoordinate(e.time);
          this.mainChart.setCrosshairXY(xx,100,true);
        } else if (e.point !== undefined){
          this.mainChart.setCrosshairXY(e.point.x,100,false);
        }
      });

You can see i made the trivial change renaming setCrossHair to `setCrosshair' to follow the lightweight-chart naming convention.

Also, I removed isMobile and mobileTouch as it seems these are no longer part of the build. As a novice coder, I am uncertain exactly what these were meant to accomplish. It is working well enough for our uses, although there are some things worth changing, such as @florian-kittel's suggestion which I have yet to incorporate (I'm still unsure how!)

Thank you everyone for your help. Hopefully this can assist anyone else looking for this functionality.

Thanks so much for posting your changes, I implemented them and it achieved exactly what I needed! You saved me so much time figuring the changes out myself! Hopefully this api (or something similar) gets merged into the official lib

@silsuer
Copy link

silsuer commented Aug 14, 2023

Hi all, I was able to get @triorr's code to work with some slight modifications. See below:

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

here:

(it is line 209 in version 3.8.0)

this:

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

here:

(line 193 in version 3.8.0)

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;
      this._setCrosshairPositionNoFire(x, y);
    } else {
      this._state.model().setHoveredSource(null);
      this._clearCrosshairPosition();
    }
  }

private _setCrosshairPositionNoFire(x: Coordinate, y: Coordinate): void {
    this._model().setAndSaveCurrentPositionFire(this._correctXCoord(x), this._correctYCoord(y), false, ensureNotNull(this._state));
}

here:

(line 644 in 3.8.0)

and finally 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:

(line 658 in 3.8.0)

With the following implementation:

this.mainChart.subscribeCrosshairMove((e) => {
        if (e.time !== undefined) {
          var xx = this.midChart.timeScale().timeToCoordinate(e.time);
          this.midChart.setCrosshairXY(xx,100,true);
        } else if (e.point !== undefined){
          this.midChart.setCrosshairXY(e.point.x,100,false);
        }
      });
      this.midChart.subscribeCrosshairMove((e) => {
        if (e.time !== undefined) {
          var xx = this.mainChart.timeScale().timeToCoordinate(e.time);
          this.mainChart.setCrosshairXY(xx,100,true);
        } else if (e.point !== undefined){
          this.mainChart.setCrosshairXY(e.point.x,100,false);
        }
      });

You can see i made the trivial change renaming setCrossHair to `setCrosshair' to follow the lightweight-chart naming convention.

Also, I removed isMobile and mobileTouch as it seems these are no longer part of the build. As a novice coder, I am uncertain exactly what these were meant to accomplish. It is working well enough for our uses, although there are some things worth changing, such as @florian-kittel's suggestion which I have yet to incorporate (I'm still unsure how!)

Thank you everyone for your help. Hopefully this can assist anyone else looking for this functionality.

Thank you for the method you provided. I am using version 4.0, and I cannot use your code of version 3.8. I don’t understand the internal principle of lightweight, and I don’t know how to modify it. Is there a corresponding repair method in version 4.0?

@tasteitslight
Copy link

@silsuer I'm using it with 4.0 via a branch using these changes

fadc845

Let me know how that works for you

@tasteitslight
Copy link

Below is how I'm using this in the js file. It assumes an object charts that contains three charts: main, aux1, and aux2


syncCrosshairs() {
    this.charts.main.subscribeCrosshairMove(this.mainCrosshairHandler);
    Object.keys(this.charts).filter(chart => chart !== 'main').forEach(chart => {
      this.charts[chart].subscribeCrosshairMove(this.auxCrosshairHandler);
    });
	}

  desyncCrosshairs() {
    this.charts.main.unsubscribeCrosshairMove(this.mainCrosshairHandler);
    Object.keys(this.charts).filter(chart => chart !== 'main').forEach(chart => {
      this.charts[chart].unsubscribeCrosshairMove(this.auxCrosshairHandler);
    });
	}

  resyncCrosshairs() {
    this.desyncCrosshairs();
    this.syncCrosshairs()
  }

  mainCrosshairHandler = (e) => {
    if (e.time !== undefined) {
      Object.keys(this.charts).filter(chart => chart !== 'main').forEach((chart) => {
        this.charts[chart].setCrosshairXY(this.charts.main.timeScale().timeToCoordinate(e.time), 10000, true);
      });
    } else if (e.point !== undefined) {
      Object.keys(this.charts).filter(chart => chart !== 'main').forEach((chart) => {
        this.charts[chart].setCrosshairXY(e.point.x, 10000, false);
      });
    }
    this.subLegend(e);
  }

  auxCrosshairHandler = (e) => {
    if (e.time !== undefined) {
      Object.keys(this.charts).forEach(chart => {
        this.charts[chart].setCrosshairXY(this.charts.aux1.timeScale().timeToCoordinate(e.time), 10000, true);
      });
    } else if (e.point !== undefined) {
      Object.keys(this.charts).forEach(chart => {
        this.charts[chart].setCrosshairXY(e.point.x, 10000, false);
      });
    }
    this.subLegend(e);
  }

@edew edew mentioned this issue Oct 2, 2023
3 tasks
@SlicedSilver
Copy link
Contributor

Version 4.1 includes a method for setting the crosshair position.

https://tradingview.github.io/lightweight-charts/tutorials/how_to/set-crosshair-position

@ChristopherJohnson25
Copy link

@SlicedSilver - jumping off this as to not create extra noise and extra issues.

Can setCrosshairPosition be used to persist a crosshair even after user unhovers/ineracts with chart? I need crosshair to persist for a number of seconds.

@SlicedSilver
Copy link
Contributor

Can setCrosshairPosition be used to persist a crosshair even after user unhovers/ineracts with chart? I need crosshair to persist for a number of seconds.

Yes, this would be possible. It allows you to set the crosshair location independently from any touch or mouse event.

@dc-thanh
Copy link

@SlicedSilver I've tried syncing two charts with 'Set crosshair position,' but it seems that it's not synchronizing properly.

Screen.Recording.2023-12-14.at.10.32.03.AM.mp4

@SlicedSilver
Copy link
Contributor

It appears to be syncing correctly. The date and time within the timescale labels (for the crosshair) are showing very similar times. It may not be identical times if the two series / charts don't have the exact same timestamps present within their datasets.

I assume that you many concern is that the crosshair line doesn't appear to be continuous across both charts. I would suggest that you try match the visible time range in both charts (using setVisibleRange or setVisibleLogicalRange). Ideally if both datasets had the same timestamps then you would get even better results.

If this doesn't help or answer your query then could you describe the issue in a bit more detail, and if possible provide a code sample?

@ReactJS13
Copy link

ReactJS13 commented May 8, 2024

@SlicedSilver - I have multiple charts and each chart has multi lines

From this, https://tradingview.github.io/lightweight-charts/tutorials/how_to/set-crosshair-position I can able to sync only 2 charts.

Actual: I can able to sync two charts as per doc. But not able to sync tooltip. And not able to zoom once its sync

Expected: I wants to sync cross hair and tooltip more than 2 charts dynamically. Expected like below

image

@SlicedSilver
Copy link
Contributor

The example only shows 2 charts but the concept can be extended to any number of charts. Also it is possible to use zoom and scrolling when using this example.

Did you add this code?

chart1.timeScale().subscribeVisibleLogicalRangeChange(timeRange => {
    chart2.timeScale().setVisibleLogicalRange(timeRange);
});

chart2.timeScale().subscribeVisibleLogicalRangeChange(timeRange => {
    chart1.timeScale().setVisibleLogicalRange(timeRange);
});

For all of this to work nicely it is recommended that both charts have the same number of data points (and the same timestamps).

@ReactJS13
Copy link

yes, I have used same concept, and able to sync it. but after syncing zoom is not working (scroll works fine).

But my concern here how to handle it dynamically? if tmrw Have to add 4th chart means then we want to manually add it and sync it here. instead if we based on dataset have to create multi charts like that reusuable component am trying.

import { ASIA_KOLKATA_UTC, DATE_FORMAT_1, NUMERICAL_CONVERSION } from "utils/constants";
import { chartListType, chartRecordType } from "types/Interfaces";
import { ColorType, createChart } from "lightweight-charts";
import { getFormatDate, getPrecision, getTimeStamp, isEmpty, styleVariable } from "utils/UtilityFunc";
import React, { useEffect, useState } from "react";

interface MultichartsProps {
chartName: string
dataList: chartListType[]
showLeftPriceScale?: boolean
timeVisible?: boolean
}

const MultiCharts = (props: MultichartsProps) => {

const { chartName, dataList } = props;

const [
    totalSet, setTotalSet
] = useState<any>({});

const [
    chartOneDataSet, setChartOneDataSet
] = useState<any[]>([
]);

const [
    chartTwoDataSet, setChartTwoDataSet
] = useState<any[]>([
]);

const [
    chartThreeDataSet, setChartThreeDataSet
] = useState<any[]>([
]);

useEffect(() => {
    console.log("chartName :", chartName, dataList);

    const combinedData = [
        ...dataList
    ];

    const combinedDataObject = combinedData.reduce((acc: any, obj) => {
        const newObj = { ...obj };
        newObj.records = obj.records.reduce((recordAcc: any, record: any) => {
            recordAcc[ record.time ] = { value: record.value, time: record.time };
            return recordAcc;
        }, {});

        acc[ obj.key as string ] = newObj;

        return acc;
    }, {});

    setTotalSet(combinedDataObject);
    setChartTwoDataSet([
        combinedDataObject.IVP
    ]);
    setChartOneDataSet([
        combinedDataObject.IV, combinedDataObject.HV10, combinedDataObject.HV30, combinedDataObject[ "IV-RV" ]
    ]);
    setChartThreeDataSet([
        combinedDataObject.PRICE
    ]);
    
}, [
    dataList
]);

let chart1: any = null;
let lineSeries1: any = null;

let chart2: any = null;
let lineSeries2: any = null;

let chart3: any = null;
let lineSeries3: any = null;

const getTooltipTime = (time: number) => {
    const date = getTimeStamp(time - ASIA_KOLKATA_UTC);
    return getFormatDate(date, DATE_FORMAT_1);
};

function getTooltip(elemContainer: any, itemList: any, chartId: string, param: any) {
    console.log("elemContainer :", elemContainer, itemList, chartId, param);
    let toolTipWidth = 100, toolTipHeight = 100;
    const toolTipMargin = 15;

    const toolTip = document.createElement("div");
    toolTip.id = `light-tooltip-${chartId}`;
    toolTip.className = "light-tooltip";
    const toolTipElement = document.getElementById(`light-tooltip-${chartId}`) as HTMLElement;
    if (toolTipElement) elemContainer.removeChild(toolTipElement);
    elemContainer.appendChild(toolTip);

    if (isEmpty(param.point)) {
        toolTip.style.display = "none";
        return;
    }

    const positionX = param.point.x;
    const positionY = param.point.y;

    if (isEmpty(param.point) || !param.time ||
        positionX < 0 || positionX > elemContainer.clientWidth ||
        positionY < 0 || positionY > elemContainer.clientHeight) {
        toolTip.style.display = "none";
    } else {
        // time will be in the same format that we supplied to setData.
        // thus it will be YYYY-MM-DD
        toolTip.style.display = "block";

        toolTip.innerHTML = `<div class="time">${getTooltipTime(param.time)}</div>
        ${itemList.map((item: any) => {
    
    if (!item.visible)
        return "";
    console.log("item.key", item.key, totalSet[ item.key ]);
      
    if (!totalSet[ item.key ].records[ param.time - ASIA_KOLKATA_UTC ]) 
        return "";
    
    return `<div class="record">
    <div class="tooltip-label"> ${item.label}: </div>
    <div class="tooltip-value" style="color: ${item.props.color}">
    ${getPrecision(
    totalSet[ item.key ].records[ param.time - ASIA_KOLKATA_UTC ].value,
    item.precision ? item.precision : 0
)} 
</div>
    </div>`;
}).join(" ")}`;

        const toolTipEle = document.getElementById(`light-tooltip-${chartId}`) as HTMLElement;
        toolTipWidth = toolTipEle.offsetWidth;
        toolTipHeight = toolTipEle.offsetHeight;

        let leftPriceScaleWidth = 0;
        const showLeftPriceScale = false;

        if (chart1 && showLeftPriceScale) {
            leftPriceScaleWidth = chart1.priceScale("left").width();
        }

        let leftPosition = positionX + leftPriceScaleWidth + toolTipMargin;
        let rightPosition: string | number = "auto";

        if (leftPosition > elemContainer.clientWidth - toolTipWidth) {
            rightPosition = elemContainer.clientWidth - positionX - leftPriceScaleWidth + toolTipMargin;
            leftPosition = "auto";
        }

        let topPosition = positionY + toolTipMargin;
        if (topPosition > elemContainer.clientHeight - toolTipHeight) {
            topPosition = positionY - toolTipMargin - toolTipHeight;
        }
        toolTip.style.left = leftPosition === "auto" ? "auto" : `${leftPosition}px`;
        toolTip.style.top = `${topPosition}px`;
        toolTip.style.right = rightPosition === "auto" ? "auto" : `${rightPosition}px`;
    }
}

function getCrosshairDataPoint(series: any, param: any) {
    if (!param.time) {
        return null;
    }
    const dataPoint = param.seriesData.get(series);
    return dataPoint || null;
}

function syncCrosshair(chart: any, series: any, dataPoint: any) {
    if (dataPoint && chart && chart.setCrosshairPosition) {
        chart.setCrosshairPosition(dataPoint.value, dataPoint.time, series);
        return;
    }

    if (chart && chart.clearCrosshairPosition)
        chart.clearCrosshairPosition();
}

const removeDuplicate = (record: chartRecordType[]) => {
    return record.filter((value: chartRecordType, inx: number, list: chartRecordType[]) => {
        return inx === list.findIndex((ele) => {
            return ele.time === value.time;
        });
    });
};

function getOnMouseValue(time: number, type: string) {
    if (totalSet[ type ].records[ time - ASIA_KOLKATA_UTC ])
        return totalSet[ type ].records[ time - ASIA_KOLKATA_UTC ].value;
    return "";
}

useEffect(() => {

    const container1 = document.getElementById("chartOne") as HTMLElement;
    const container2 = document.getElementById("chartTwo") as HTMLElement;
    const container3 = document.getElementById("chartThree") as HTMLElement;

    if (chartOneDataSet.length && chartTwoDataSet.length && chartThreeDataSet.length) {
        chart1 = createChart(container1, {
            height: 400
        });

        chart1.applyOptions({
            autoSize: true,
            layout: {
                textColor: styleVariable("--header-text"),
                background: {
                    type: "solid" as ColorType.Solid,
                    color: "transparent"
                },
                fontSize: 13
            },
            grid: {
                vertLines: { color: styleVariable("--chart-grid-vert") },
                horzLines: { color: styleVariable("--chart-grid-horz") }
            },
            crosshair: {
                mode: 0,
                horzLine: {
                    color: styleVariable("--primary-color"),
                    labelBackgroundColor: styleVariable("--primary-color"),
                },
                vertLine: {
                    labelBackgroundColor: styleVariable("--primary-color"),
                }
            },
            rightPriceScale: {
                visible: false,
            },
            leftPriceScale: {
                visible: true,
            },
            timeScale: {
                timeVisible: false,
                secondsVisible: false
            },
            handleScale: false
        });

        chartOneDataSet.forEach((item: any, index: any) => {
            lineSeries1 = chart1[ item.type ]({
                visible: item.visible,
                lineWidth: 1,
                lastValueVisible: false, priceLineVisible: false,
                priceFormat: {
                    type: item.formatType,
                    precision: item.formatType === "" ? 0 : item.precision ? item.precision : 2,
                    minMove: item.formatType === "" ? 1 : 0.01,
                    formatter: function (price: number) {
                        return `${(price / 100000).toFixed(2) } ${NUMERICAL_CONVERSION.LAKH}`; 
                    },
                },
                ...item.props,
                priceScaleId: item.props.priceScaleId
                    ? item.props.priceScaleId :
                    index === 0 ? "left" : index === 1 ? "right" : ""
            });

            const updated = removeDuplicate(Object.values(item.records).map((row: any) => {
                row.time = row.time + ASIA_KOLKATA_UTC;
                return row;
            }));

            lineSeries1.setData(updated);
            console.log("updated :", updated);

            item.seriesKey = lineSeries1;

        });

        chart1.timeScale().subscribeVisibleLogicalRangeChange((timeRange: any) => {
            if (chart2)
                chart2.timeScale().setVisibleLogicalRange(timeRange as any);
            if (chart3)
                chart3.timeScale().setVisibleLogicalRange(timeRange as any);
        });

        chart1.subscribeCrosshairMove((param: any) => {
            const dataPoint = getCrosshairDataPoint(lineSeries1, param);
            syncCrosshair(chart2, lineSeries2, dataPoint);
            syncCrosshair(chart3, lineSeries3, dataPoint);
            if (dataPoint && dataPoint.time && lineSeries1 && lineSeries2 && chart1 && chart2 && chart3) {
                const y1 = lineSeries1.priceToCoordinate(dataPoint.value);
                const y2 = lineSeries2.priceToCoordinate(getOnMouseValue(dataPoint.time, "IVP" ));
                const y3 = lineSeries3.priceToCoordinate(getOnMouseValue(dataPoint.time, "PRICE" ));
                console.log("y3 :", y3);
                const x1 = chart1.timeScale().timeToCoordinate(dataPoint.time);
                const x2 = chart2.timeScale().timeToCoordinate(dataPoint.time);
                const x3 = chart2.timeScale().timeToCoordinate(dataPoint.time);
                const params1: any = {
                    point: {
                        x: x1,
                        y: y1
                    },
                    time: dataPoint.time
                };

                const params2: any = {
                    point: {
                        x: x2,
                        y: y2
                    },
                    time: dataPoint.time
                };

                const params3: any = {
                    point: {
                        x: x3,
                        y: y3
                    },
                    time: dataPoint.time
                };

                getTooltip(container1, chartOneDataSet, "chartOne", params1);
                getTooltip(container2, chartTwoDataSet, "chartTwo", params2);
                getTooltip(container3, chartThreeDataSet, "chartThree", params3);
            } else {
                const params1: any = {
                };

                const params2: any = {
                };

                const params3: any = {
                };

                getTooltip(container1, chartOneDataSet, "chartOne", params1);
                getTooltip(container2, chartTwoDataSet, "chartTwo", params2);
                getTooltip(container3, chartThreeDataSet, "chartThree", params3);
            }
 
        });


        chart2 = createChart(container2, {
            height: 400,
        });

        chart2.applyOptions({
            autoSize: true,
            layout: {
                textColor: styleVariable("--chart-text"),
                fontSize: 12,
                fontFamily: "NxtOption-Regular",
                background: {
                    type: "solid" as ColorType.Solid,
                    color: "transparent"
                }
            },
            grid: {
                vertLines: { color: styleVariable("--chart-grid-vert") },
                horzLines: { color: styleVariable("--chart-grid-horz") }
            },
            crosshair: {
                mode: 0,
                horzLine: {
                    color: styleVariable("--primary-color"),
                    labelBackgroundColor: styleVariable("--primary-color"),
                },
                vertLine: {
                    labelBackgroundColor: styleVariable("--primary-color"),
                }
            },
            rightPriceScale: {
                visible: false,
            },
            leftPriceScale: {
                visible: true,
            },
            timeScale: {
                timeVisible: false,
                secondsVisible: false
            },
            handleScale: false
        });

        chartTwoDataSet.forEach((item: any, index: any) => {
            lineSeries2 = chart2[ item.type ]({
                visible: item.visible,
                lineWidth: 1,
                lastValueVisible: false, priceLineVisible: false,
                priceFormat: {
                    type: item.formatType,
                    precision: item.formatType === "" ? 0 : item.precision ? item.precision : 2,
                    minMove: item.formatType === "" ? 1 : 0.01,
                    formatter: function (price: number) {
                        return `${(price / 100000).toFixed(2) } ${NUMERICAL_CONVERSION.LAKH}`; 
                    },
                },
                ...item.props,
                priceScaleId: item.props.priceScaleId
                    ? item.props.priceScaleId :
                    index === 0 ? "left" : index === 1 ? "right" : ""
            });

            const updated = removeDuplicate(Object.values(item.records).map((row: any) => {
                row.time = row.time + ASIA_KOLKATA_UTC;
                return row;
            }));

            lineSeries2.setData(updated);

            item.seriesKey = lineSeries2;
        });

        chart2.timeScale().subscribeVisibleLogicalRangeChange((timeRange: any) => {
            if (chart1)
                chart1.timeScale().setVisibleLogicalRange(timeRange as any);
            if (chart3)
                chart3.timeScale().setVisibleLogicalRange(timeRange as any);
        });

        chart2.subscribeCrosshairMove((param: any) => {
            const dataPoint = getCrosshairDataPoint(lineSeries2, param);
            console.log("lineSeries2 :", lineSeries2);
            syncCrosshair(chart1, lineSeries1, dataPoint);
            syncCrosshair(chart3, lineSeries3, dataPoint);
            if (dataPoint && dataPoint.time && lineSeries1 && lineSeries2 && chart1 && chart2 && chart3) {
                const y1 = lineSeries1.priceToCoordinate(getOnMouseValue(dataPoint.time, "IV" ));
                const y2 = lineSeries2.priceToCoordinate(dataPoint.value);
                const y3 = lineSeries3.priceToCoordinate(getOnMouseValue(dataPoint.time, "PRICE" ));
                const x1 = chart1.timeScale().timeToCoordinate(dataPoint.time);
                const x2 = chart2.timeScale().timeToCoordinate(dataPoint.time);
                const x3 = chart2.timeScale().timeToCoordinate(dataPoint.time);

                const params1: any = {
                    point: {
                        x: x1,
                        y: y1
                    },
                    time: dataPoint.time
                };

                const params2: any = {
                    point: {
                        x: x2,
                        y: y2
                    },
                    time: dataPoint.time
                };

                const params3: any = {
                    point: {
                        x: x3,
                        y: y3
                    },
                    time: dataPoint.time
                };

                getTooltip(container1, chartOneDataSet, "chartOne", params1);
                getTooltip(container2, chartTwoDataSet, "chartTwo", params2);
                getTooltip(container3, chartThreeDataSet, "chartThree", params3);
            } else {
                const params1: any = {
                };

                const params2: any = {
                };

                const params3: any = {
                };

                getTooltip(container1, chartOneDataSet, "chartOne", params1);
                getTooltip(container2, chartTwoDataSet, "chartTwo", params2);
                getTooltip(container3, chartThreeDataSet, "chartThree", params3);
            }
        });
        

        chart3 = createChart(container3, {
            height: 400
        });

        chart3.applyOptions({
            autoSize: true,
            layout: {
                textColor: styleVariable("--header-text"),
                background: {
                    type: "solid" as ColorType.Solid,
                    color: "transparent"
                },
                fontSize: 13
            },
            grid: {
                vertLines: { color: styleVariable("--chart-grid-vert") },
                horzLines: { color: styleVariable("--chart-grid-horz") }
            },
            crosshair: {
                mode: 0,
                horzLine: {
                    color: styleVariable("--primary-color"),
                    labelBackgroundColor: styleVariable("--primary-color"),
                },
                vertLine: {
                    labelBackgroundColor: styleVariable("--primary-color"),
                }
            },
            rightPriceScale: {
                visible: false,
            },
            leftPriceScale: {
                visible: true,
            },
            timeScale: {
                timeVisible: false,
                secondsVisible: false
            },
            handleScale: false
        });

        console.log("chartThreeDataSet :", chartThreeDataSet);
        chartThreeDataSet.forEach((item: any, index: any) => {
            console.log("chartThreeDataSet item :", item);
            lineSeries3 = chart3[ item.type ]({
                visible: item.visible,
                lineWidth: 1,
                lastValueVisible: false, priceLineVisible: false,
                priceFormat: {
                    type: item.formatType,
                    precision: item.formatType === "" ? 0 : item.precision ? item.precision : 2,
                    minMove: item.formatType === "" ? 1 : 0.01,
                    formatter: function (price: number) {
                        return `${(price / 100000).toFixed(2) } ${NUMERICAL_CONVERSION.LAKH}`; 
                    },
                },
                ...item.props,
                priceScaleId: item.props.priceScaleId
                    ? item.props.priceScaleId :
                    index === 0 ? "left" : index === 1 ? "right" : ""
            });

            const updated = removeDuplicate(Object.values(item.records).map((row: any) => {
                row.time = row.time + ASIA_KOLKATA_UTC;
                return row;
            }));

            lineSeries3.setData(updated);

            item.seriesKey = lineSeries3;

        });

        chart3.timeScale().subscribeVisibleLogicalRangeChange((timeRange: any) => {
            if (chart2)
                chart2.timeScale().setVisibleLogicalRange(timeRange as any);
            if (chart1)
                chart1.timeScale().setVisibleLogicalRange(timeRange as any);
        });

        chart3.subscribeCrosshairMove((param: any) => {
            const dataPoint = getCrosshairDataPoint(lineSeries3, param);
            syncCrosshair(chart2, lineSeries2, dataPoint);
            syncCrosshair(chart1, lineSeries1, dataPoint);
            if (dataPoint && dataPoint.time && lineSeries1 && lineSeries2 && lineSeries3 &&
                 chart1 && chart2 && chart3) {
                const y1 = lineSeries1.priceToCoordinate(getOnMouseValue(dataPoint.time, "IV"));
                const y2 = lineSeries2.priceToCoordinate(getOnMouseValue(dataPoint.time, "IVP" ));
                const y3 = lineSeries3.priceToCoordinate(dataPoint.value);
                const x1 = chart1.timeScale().timeToCoordinate(dataPoint.time);
                const x2 = chart2.timeScale().timeToCoordinate(dataPoint.time);
                const x3 = chart2.timeScale().timeToCoordinate(dataPoint.time);
                const params1: any = {
                    point: {
                        x: x1,
                        y: y1
                    },
                    time: dataPoint.time
                };

                const params2: any = {
                    point: {
                        x: x2,
                        y: y2
                    },
                    time: dataPoint.time
                };

                const params3: any = {
                    point: {
                        x: x3,
                        y: y3
                    },
                    time: dataPoint.time
                };

                getTooltip(container1, chartOneDataSet, "chartOne", params1);
                getTooltip(container2, chartTwoDataSet, "chartTwo", params2);
                getTooltip(container3, chartThreeDataSet, "chartThree", params3);
            } else {
                const params1: any = {
                };

                const params2: any = {
                };

                const params3: any = {
                };

                getTooltip(container1, chartOneDataSet, "chartOne", params1);
                getTooltip(container2, chartTwoDataSet, "chartTwo", params2);
                getTooltip(container3, chartThreeDataSet, "chartThree", params3);
            }
 
        });

        chart1.timeScale().fitContent();
        chart2.timeScale().fitContent();
        chart3.timeScale().fitContent();
    }


    return () => {
        if (chart1) 
            chart1.remove();
        if (chart2) 
            chart2.remove();  
        if (chart3) 
            chart3.remove();            
    };
}, [
    chartTwoDataSet, chartOneDataSet, chartThreeDataSet
]);

return (
    <>
        <div className="lightweight-multiple-charts" id={"chartOne"}></div>
        <div className="lightweight-multiple-charts2" id={"chartTwo"}></div>
        <div className="lightweight-multiple-charts3" id={"chartThree"}></div>
    </>

);

};

export default MultiCharts;

@SlicedSilver
Copy link
Contributor

Could you please create an example on an online code editor like JSfiddle / Codepen / CodeSandbox / ...?

@ReactJS13
Copy link

Here is the link https://codesandbox.io/p/sandbox/chart-js-react-typescript-forked-mrzchx?file=%2Fsrc%2Fstyles.css%3A4%2C1&layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clvy0gjzm0006356l46dfd8ab%2522%252C%2522sizes%2522%253A%255B100%252C0%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clvy0gjzm0002356lmlg67iut%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clvy0gjzm0003356lisrmn50m%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clvy0gjzm0005356ldq9iogin%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B47.01092163249665%252C52.98907836750335%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clvy0gjzm0002356lmlg67iut%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clvy0gjzl0001356lz3lbadka%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252Fsrc%252Findex.tsx%2522%252C%2522state%2522%253A%2522IDLE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A2%252C%2522startColumn%2522%253A12%252C%2522endLineNumber%2522%253A2%252C%2522endColumn%2522%253A12%257D%255D%257D%252C%257B%2522id%2522%253A%2522clvy0huwz003i356lnmb86y3i%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A3%252C%2522startColumn%2522%253A22%252C%2522endLineNumber%2522%253A3%252C%2522endColumn%2522%253A22%257D%255D%252C%2522filepath%2522%253A%2522%252Fpackage.json%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%252C%257B%2522id%2522%253A%2522clvy24a610002356lsbig26ld%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A683%252C%2522startColumn%2522%253A1%252C%2522endLineNumber%2522%253A683%252C%2522endColumn%2522%253A1%257D%255D%252C%2522filepath%2522%253A%2522%252Fsrc%252FMultiCharts.tsx%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%252C%257B%2522id%2522%253A%2522clvy27gcv0002356l66b34scu%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A9%252C%2522startColumn%2522%253A1%252C%2522endLineNumber%2522%253A9%252C%2522endColumn%2522%253A1%257D%255D%252C%2522filepath%2522%253A%2522%252Fsrc%252FApp.tsx%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%252C%257B%2522id%2522%253A%2522clvy2fmzb0002356l280xl0tr%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A4%252C%2522startColumn%2522%253A1%252C%2522endLineNumber%2522%253A4%252C%2522endColumn%2522%253A1%257D%255D%252C%2522filepath%2522%253A%2522%252Fsrc%252Fstyles.css%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522id%2522%253A%2522clvy0gjzm0002356lmlg67iut%2522%252C%2522activeTabId%2522%253A%2522clvy2fmzb0002356l280xl0tr%2522%257D%252C%2522clvy0gjzm0005356ldq9iogin%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clvy0gjzm0004356lslo14a3d%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A0%252C%2522path%2522%253A%2522%252F%2522%257D%255D%252C%2522id%2522%253A%2522clvy0gjzm0005356ldq9iogin%2522%252C%2522activeTabId%2522%253A%2522clvy0gjzm0004356lslo14a3d%2522%257D%252C%2522clvy0gjzm0003356lisrmn50m%2522%253A%257B%2522tabs%2522%253A%255B%255D%252C%2522id%2522%253A%2522clvy0gjzm0003356lisrmn50m%2522%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Afalse%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D

@ReactJS13
Copy link

@SlicedSilver - Any update on this? as suggested have created a codesandbox, plz provide a solution.

@SlicedSilver
Copy link
Contributor

Here is an example with multiple charts which are synced together. You can adjust the number of charts in the code.
https://glitch.com/edit/#!/neighborly-cuboid-card

Since your code is using React, could you check that you are only creating the expected number of charts, and that you are removing charts when they are no longer needed with the remove method on the ChartApi

@ReactJS13
Copy link

@SlicedSilver - I would like to thank you for your support.

I have Observed in the above example, each chart have only one series. But our case is multiple charts and each charts has multple series (may the series has any type(line, area, grouped bar charts)).

Please provide example for tooltip functionality for multi charts multi series data.

As per the example I have tried to create multi series but am not able to get expected solution.

#1589 this ticket also I have raised for the inclusion of grouped bar charts in multi series charts. For this case only. Unfortunately unable to create grouped Bar charts which has positive and negative bars.

We have used TradingView and Light weight charts in our projects, we were already integrated couple of screens. But we got stucked multi charts with multi series where the series has grouped Bar charts. It is showstopper for a long time. So, requesting you to please provide all solutions at one place if posible. Thanks for an advance.

Here is my trail
https://codesandbox.io/p/sandbox/frosty-star-h2jtj3?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clw50tevc0006356lsacswzy0%2522%252C%2522sizes%2522%253A%255B100%252C0%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clw50tevc0002356ly4ebxte2%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clw50tevc0003356l7576h87s%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clw50tevc0005356lyxheupt7%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B50%252C50%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clw50tevc0002356ly4ebxte2%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clw50tevc0001356lg9dm72aj%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252Findex.html%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522id%2522%253A%2522clw50tevc0002356ly4ebxte2%2522%252C%2522activeTabId%2522%253A%2522clw50tevc0001356lg9dm72aj%2522%257D%252C%2522clw50tevc0005356lyxheupt7%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clw50tevc0004356lkzasyawb%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A0%252C%2522path%2522%253A%2522%252F%2522%257D%255D%252C%2522id%2522%253A%2522clw50tevc0005356lyxheupt7%2522%252C%2522activeTabId%2522%253A%2522clw50tevc0004356lkzasyawb%2522%257D%252C%2522clw50tevc0003356l7576h87s%2522%253A%257B%2522tabs%2522%253A%255B%255D%252C%2522id%2522%253A%2522clw50tevc0003356l7576h87s%2522%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Afalse%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D

@SlicedSilver
Copy link
Contributor

I have Observed in the above example, each chart have only one series. But our case is multiple charts and each charts has multple series (may the series has any type(line, area, grouped bar charts)).

Having multiple series on the chart shouldn't make any difference. I've updated the example to have a second series on each chart.

Please provide example for tooltip functionality for multi charts multi series data.

We have some tutorials on adding tooltips. Besides what is shown in the documentation, we wouldn't create an custom example for just a single request.

#1589 this ticket also I have raised for the inclusion of grouped bar charts in multi series charts. For this case only. Unfortunately unable to create grouped Bar charts which has positive and negative bars.

If the plugin doesn't support negative bars then I would suggest that you take the source code of the plugin as a starting point and develop an updated plugin. The example plugin is only an example instead of a full-featured official plugin.

So, requesting you to please provide all solutions at one place if posible.

We don't build example implementations as that would be consultation work and we don't offer paid services. For this open source project, we are happy to assist with bug reports and general queries, and consider feature requests but not to develop full solutions or perform code review on large files (beyond minimal bug reproductions).

@MarvinMiles
Copy link

@SlicedSilver I've tried syncing two charts with 'Set crosshair position,' but it seems that it's not synchronizing properly.

Screen.Recording.2023-12-14.at.10.32.03.AM.mp4

Hello @dc-thanh

Did you managed this out? Have a similar issue with multiple charts with different timestamps on each, which leads to different timeScale section width.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Feature requests, and general improvements. need more feedback Requires more feedback.
Projects
None yet
Development

No branches or pull requests