tradingview / lightweight-charts

Performant financial charts built with HTML5 canvas
https://www.tradingview.com/lightweight-charts/
Apache License 2.0
9.2k stars 1.59k forks source link

Setting the crosshair programmatically #438

Closed gkaindl closed 11 months ago

gkaindl commented 4 years ago

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:

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

ghost commented 4 years ago

@gkaindl That would be great!

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

timocov commented 4 years ago

Related to #376.

timocov commented 4 years ago

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

gkaindl commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

any update? possible to use crosshair programatically?

ch4rlesyeo commented 3 years ago

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 commented 3 years ago

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.

florian-kittel commented 3 years ago

@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 commented 3 years ago

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.

adgower commented 3 years ago

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 commented 3 years ago

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

julio899 commented 2 years ago

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 commented 2 years ago

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

florian-kittel commented 2 years ago

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

0x0tyy commented 2 years ago

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

florian-kittel commented 2 years ago

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 commented 2 years ago

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

0x0tyy commented 2 years ago

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

florian-kittel commented 2 years ago

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)

0x0tyy commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

@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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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

tasteitslight commented 1 year ago

@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 commented 1 year ago

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:

https://github.com/tradingview/lightweight-charts/blob/e263d724aa135da699f744b152422b94f094aa28/src/api/ichart-api.ts#L212

(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:

https://github.com/tradingview/lightweight-charts/blob/e263d724aa135da699f744b152422b94f094aa28/src/api/chart-api.ts#L147

(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:

https://github.com/tradingview/lightweight-charts/blob/e263d724aa135da699f744b152422b94f094aa28/src/gui/pane-widget.ts#L659

(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:

https://github.com/tradingview/lightweight-charts/blob/e263d724aa135da699f744b152422b94f094aa28/src/model/chart-model.ts#L659

(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 commented 1 year ago

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:

https://github.com/tradingview/lightweight-charts/blob/e263d724aa135da699f744b152422b94f094aa28/src/api/ichart-api.ts#L212

(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:

https://github.com/tradingview/lightweight-charts/blob/e263d724aa135da699f744b152422b94f094aa28/src/api/chart-api.ts#L147

(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:

https://github.com/tradingview/lightweight-charts/blob/e263d724aa135da699f744b152422b94f094aa28/src/gui/pane-widget.ts#L659

(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:

https://github.com/tradingview/lightweight-charts/blob/e263d724aa135da699f744b152422b94f094aa28/src/model/chart-model.ts#L659

(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 commented 1 year ago

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:

https://github.com/tradingview/lightweight-charts/blob/e263d724aa135da699f744b152422b94f094aa28/src/api/ichart-api.ts#L212

(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:

https://github.com/tradingview/lightweight-charts/blob/e263d724aa135da699f744b152422b94f094aa28/src/api/chart-api.ts#L147

(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:

https://github.com/tradingview/lightweight-charts/blob/e263d724aa135da699f744b152422b94f094aa28/src/gui/pane-widget.ts#L659

(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:

https://github.com/tradingview/lightweight-charts/blob/e263d724aa135da699f744b152422b94f094aa28/src/model/chart-model.ts#L659

(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 commented 1 year ago

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

https://github.com/tradingview/lightweight-charts/commit/fadc845745b568baef5d9ecb5470e00814f6b5ff

Let me know how that works for you

tasteitslight commented 1 year ago

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);
  }
SlicedSilver commented 11 months ago

Version 4.1 includes a method for setting the crosshair position.

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

ChristopherJohnson25 commented 10 months ago

@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 commented 10 months ago

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 commented 9 months ago

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

https://github.com/tradingview/lightweight-charts/assets/61302262/0153658f-cc6a-4b78-ba30-23759b1179d9

SlicedSilver commented 9 months ago

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 commented 4 months ago

@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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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

ReactJS13 commented 4 months ago

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 commented 4 months ago

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

SlicedSilver commented 4 months ago

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 commented 4 months ago

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

https://github.com/tradingview/lightweight-charts/issues/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 commented 4 months ago

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.

https://github.com/tradingview/lightweight-charts/issues/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 commented 4 months ago

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