tradingview / lightweight-charts

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

Multiple panes support #50

Open nothingalike opened 5 years ago

nothingalike commented 5 years ago

Looking through the documentation i dont see anything along these lines. I want to have my main chart but then add some additional data in a separate chart window. I believe some apps call it an Indicator Window. I was curious if its possible with this library.

mkdudeja commented 2 years ago

Hello @timocov ,

Definitely looking forward to this feature to decide shall we go ahead with lightweight charts or some other.

Do we have any plans to merge the above PR by @ntf or any timeline to have this feature?

Looking forward to your response.

Thanks

tomlister commented 2 years ago

@timocov I've been waiting for this feature for ever. I know it's been highly sought after on the discord too. I'm very keen to be able to use this.

Good job @ntf !

+1 for PR.

ntf commented 2 years ago

@timocov and other folks, could you please help figure out a set of features / APIs that people may need to interact with multiple panes? The sooner we can finalize the APIs, the sooner we have a MR that is merge-able for everyone.

Things I have implemented so far:

You can play around the API on the jsfiddle attached. https://jsfiddle.net/adrianntf/6qea5ytv/

Things I am going to implement:

jleylegian commented 2 years ago

Any eta when this will be available?

allartdo commented 2 years ago

I would also like to subcribe to the list of people waiting for the multiple panes support, if that is of any help :)

Do have to say @timocov, I understand you want this to be a readable and easy understandable thing. But dont you think at some point you just have to implement a working version? Easy/readable or not? Since many people wait for this. Besides, you can from then always improve right?

We are now 2 years in the future since this issue is created, and no V1 of multiple panes yet, if I understand correctly atleast. Only workarounds.

Would like to hear your opinion about this, since I may understand this incorrectly.

ntf commented 2 years ago

Hi all,

I am satisfied with my current implementation of multiple panes and will move on to work on other stuff.

image

The legends on panes are rendered using React Portal with the newly added CharApi#getPaneElements().

I will update the merge request once more time later this week.

josuamanuel commented 2 years ago

Hi all,

I am satisfied with my current implementation of multiple panes and will move on to work on other stuff.

image

The legends on panes are rendered using React Portal with the newly added CharApi#getPaneElements().

I will update the merge request once more time later this week.

Hi, This looks great đź‘Ť . Can you share the jsfiddle of the picture you posted?

josuamanuel commented 2 years ago

Hi all, I am satisfied with my current implementation of multiple panes and will move on to work on other stuff. image The legends on panes are rendered using React Portal with the newly added CharApi#getPaneElements(). I will update the merge request once more time later this week.

Hi, This looks great đź‘Ť . Can you share the jsfiddle of the picture you posted?

@ntf Is there any easy way to specify the height percentage of each pane?

ntf commented 2 years ago

Hi all, I am satisfied with my current implementation of multiple panes and will move on to work on other stuff. image The legends on panes are rendered using React Portal with the newly added CharApi#getPaneElements(). I will update the merge request once more time later this week.

Hi, This looks great đź‘Ť . Can you share the jsfiddle of the picture you posted?

@ntf Is there any easy way to specify the height percentage of each pane?

Need to add another API to specific heights for pane. There’s is no way to do that programmatically as of today. If you have time, you can take a look the PaneSeparator code how it change the height of Panes.

Regarding jsfiddle, you should able reproduce something similar with the jsfiddle I attached earlier. The screenshot use my own custom React binding and backend. I wouldn’t have time to extract logic to a jsfiddle.

I have rebased the branch and mergeable now. I would defer the decision to repo maintainer.

josuamanuel commented 2 years ago

Hi all, I am satisfied with my current implementation of multiple panes and will move on to work on other stuff. image The legends on panes are rendered using React Portal with the newly added CharApi#getPaneElements(). I will update the merge request once more time later this week.

Hi, This looks great đź‘Ť . Can you share the jsfiddle of the picture you posted?

@ntf Is there any easy way to specify the height percentage of each pane?

Need to add another API to specific heights for pane. There’s is no way to do that programmatically as of today. If you have time, you can take a look the PaneSeparator code how it change the height of Panes.

Regarding jsfiddle, you should able reproduce something similar with the jsfiddle I attached earlier. The screenshot use my own custom React binding and backend. I wouldn’t have time to extract logic to a jsfiddle.

I have rebased the branch and mergeable now. I would defer the decision to repo maintainer.

@ntf Thanks for the advice. I reproduced it and the result is quite nice. Seeing the results, I am happy with the current fix proportions. I would love to help making the pane proportion changeable though I am not confident enough to make these deep changes. Something that it impacts me more (maybe others) is that I don't know if it is possible to control the rightPriceScale (normal, percentage, logarithmic) specifically for each pane. Changing the value for the chart object applies the changes for all panel.

image

jleylegian commented 2 years ago

Is there an ETA?

thezentrader commented 2 years ago

This feature would be much appreciated!

tungyingwaltz commented 2 years ago

@ntf Thank you. Can add pane index to the crosshair event? because I want to use series.createPriceLine() to other pane.

ntf commented 2 years ago

@ntf Thank you. Can add pane index to the crosshair event?

https://github.com/tradingview/lightweight-charts/pull/824/commits/fce7aa3552ffd724fb7e3b9dad4a37a8b0dad905

Added paneIndex to MouseEventParams. Both crosshair move and mouse click event should work. (I only tested the crosshair move event).

    function handleCrosshairMoved(param) {
        if (!param.point) {
            return;
        }

        console.log(`A user moved the crosshair to (${param.point.x}, ${param.point.y}) point of pane ${param.paneIndex}, the time is ${param.time}`);
    }
       chart.subscribeCrosshairMove(handleCrosshairMoved);

@josuamanuel

It is too much work for me to make each pane's price scale customizable. However, I can add a new option nonPrimaryPriceScale such that pane other than the first one uses this option instead of the rightPriceScale.

https://github.com/tradingview/lightweight-charts/pull/824/commits/468de85efc45a2c9569e478fd8e80e0a4202e71c

const chartOptions = {
    rightPriceScale: {
    autoScale: true,
    mode: 2, // first pane use this 
    scaleMargins: {
        top: 0.1,
        bottom: 0.08,
    }
    },
    nonPrimaryPriceScale: {
    autoScale: true,
    mode: 0, // other pane use this
    scaleMargins: {
        top: 0.1,
        bottom: 0.08,
    }
    }
};

image

josuamanuel commented 2 years ago

such that pane other than the first one uses this option instead of the rightPriceScale.

@ntf I solved the problem by changing the scale at Serie level instead of the parent chart level.

const candleSerie = chart.addCandlestickSeries()
candleSeries.priceScale().applyOptions({
    mode: LightweightCharts.PriceScaleMode[scaleValue]
})
jleylegian commented 2 years ago

Is there an ETA?

Enivel commented 2 years ago

need this feature

jleylegian commented 2 years ago

Will this be available by christmas?

ntf commented 2 years ago

Hi @timocov, what are the remaining to have this to be merged?

ddegroot1985 commented 2 years ago

This feature would be very much appreciated

MeyssamB commented 2 years ago

Dose anybody know how to fill between line series? (I know it is not related to this topic, but it seems you are in the garden)

ntf commented 2 years ago

Dose anybody know how to fill between line series? (I know it is not related to this topic, but it seems you are in the garden)

You meant something like filling the area between the lower and upper bound of a Bollinger band?

MeyssamB commented 2 years ago

Dose anybody know how to fill between line series? (I know it is not related to this topic, but it seems you are in the garden)

You meant something like filling the area between the lower and upper bound of a Bollinger band?

Exactly, or something like Ichmoku.

moderncodes commented 2 years ago

@ntf really great job for handling it. It's been a while since you completed it, and now who knows how much longer the process might take to merge in to master, would you have time just create your own extension? And maybe have @timocov add it to the TradingView repository as an optional module. That way people can actually start using it today, plus dev's can have all time they need to implement in to the main.

Majchrzak commented 2 years ago

@ntf really great job for handling it.

It's been a while since you completed it, and now who knows how much longer the process might take to merge in to master, would you have time just create your own extension? And maybe have @timocov add it to the TradingView repository as an optional module. That way people can actually start using it today, plus dev's can have all time they need to implement in to the main.

You should try to install package directly from feature branch. I mean, you can use npm or yarn and give them an address of git repo and hash of specific commit to install.

ntf commented 2 years ago

@ntf really great job for handling it.

It's been a while since you completed it, and now who knows how much longer the process might take to merge in to master, would you have time just create your own extension? And maybe have @timocov add it to the TradingView repository as an optional module. That way people can actually start using it today, plus dev's can have all time they need to implement in to the main.

You should try to install package directly from feature branch. I mean, you can use npm or yarn and give them an address of git repo and hash of specific commit to install.

Please use the feature branch instead, I wouldn't have time to make it possible to load extension and maintain an extension.

To evaluate the feature, you can install from my feature branch. Otherwise you can merge my merge request to your own forked repo and use npm/yarn to install from git repo directly.

For the fill color on area between two lines, I may take a look if I need that feature, no ETA.

martinsSmith commented 2 years ago

how i plot a tradingview with my own data using javascript like $.getJSON(BDTASK.getSiteAction('tradecharthistory?market=' + market), function(response) {} as the data??

arqex commented 2 years ago

Hey! I'm very interested in this feature, but I see that there has been more than 2 years since it's open and it seems that there is no plans to merge @ntf solution or a proposal to implements it differently.

Is it because of the ntf's API proposal lacks something?

I can understand that it maybe doesn't fit 100% percent with the current API, but maybe we can discuss how to adapt it to make it work.

I'll give it a try and mock up an API alternative. Maybe if we update the IChartApi interface with a couple of methods:

interface IChartApi {
  //...
  addPane: (panelId: string, options: PaneOptions ) => IPaneApi
  removePane: (paneOrId: string | IPaneApi ) => void
}

Here we would have 2 new interfaces:

Both interfaces would be reduced versions of their sibling interfaces for the whole chart. E.g. IPaneApi:

interface IPaneApi {
  remove(): void;
  resize(height: number, forceRepaint?: boolean): void;
  addAreaSeries(areaOptions?: AreaSeriesPartialOptions): ISeriesApi<'Area'>;
  addBarSeries(barOptions?: BarSeriesPartialOptions): ISeriesApi<'Bar'>;
  addCandlestickSeries(candlestickOptions?: CandlestickSeriesPartialOptions): ISeriesApi<'Candlestick'>;
  addHistogramSeries(histogramOptions?: HistogramSeriesPartialOptions): ISeriesApi<'Histogram'>;
  addLineSeries(lineOptions?: LineSeriesPartialOptions): ISeriesApi<'Line'>;
  removeSeries(seriesApi: ISeriesApi<SeriesType>): void;
  subscribeClick(handler: MouseEventHandler): void;
  unsubscribeClick(handler: MouseEventHandler): void;
  priceScale(priceScaleId?: string): IPriceScaleApi;
  applyOptions(options: DeepPartial<PaneOptions>): void;
}

The missing attributes - if we compare it with the IChartApi - are not there because they wouldn't be responsibility of the pane, but responsibility of the Chart. E.g: there is no chance of changing the timeScale, because the timeScale belong to the Chart.

The same would happen to the PaneOptions that would be a reduced version of the `ChartOptions:

interface ChartOptions {
    height: number;
    layout: LayoutOptions;
    leftPriceScale: VisiblePriceScaleOptions;
    rightPriceScale: VisiblePriceScaleOptions;
    overlayPriceScales: OverlayPriceScaleOptions;
}

With an API like this, if we don't need to use multiple panels we can keep using Lightweight Charts exactly the way we are using them right now.

If we want to have extra panes we can do something like:

const chart: IChartApi = createChart(container);
const pane: IPaneApi = chart.addPanel('paneId');
pane.addLineSeries( lineSeriesData );

There are no indices involved and it's a bit more similar to how the current API works with the series for example. Is this a direction that may have more chances to be merged? cc @ntf @timocov

ntf commented 2 years ago

Hey! I'm very interested in this feature, but I see that there has been more than 2 years since it's open and it seems that there is no plans to merge @ntf solution or a proposal to implements it differently.

Is it because of the ntf's API proposal lacks something?

I can understand that it maybe doesn't fit 100% percent with the current API, but maybe we can discuss how to adapt it to make it work.

I'll give it a try and mock up an API alternative. Maybe if we update the IChartApi interface with a couple of methods:


interface IChartApi {

  //...

  addPane: (panelId: string, options: PaneOptions ) => IPaneApi

  removePane: (paneOrId: string | IPaneApi ) => void

}

Here we would have 2 new interfaces:

  • PaneOptions it's a subset of the ChartOptions

  • IPaneApi also a subset of the IChartApi

Both interfaces would be reduced versions of their sibling interfaces for the whole chart. E.g. IPaneApi:


interface IPaneApi {

  remove(): void;

  resize(height: number, forceRepaint?: boolean): void;

  addAreaSeries(areaOptions?: AreaSeriesPartialOptions): ISeriesApi<'Area'>;

  addBarSeries(barOptions?: BarSeriesPartialOptions): ISeriesApi<'Bar'>;

  addCandlestickSeries(candlestickOptions?: CandlestickSeriesPartialOptions): ISeriesApi<'Candlestick'>;

  addHistogramSeries(histogramOptions?: HistogramSeriesPartialOptions): ISeriesApi<'Histogram'>;

  addLineSeries(lineOptions?: LineSeriesPartialOptions): ISeriesApi<'Line'>;

  removeSeries(seriesApi: ISeriesApi<SeriesType>): void;

  subscribeClick(handler: MouseEventHandler): void;

  unsubscribeClick(handler: MouseEventHandler): void;

  priceScale(priceScaleId?: string): IPriceScaleApi;

  applyOptions(options: DeepPartial<PaneOptions>): void;

}

The missing attributes - if we compare it with the IChartApi - are not there because they wouldn't be responsibility of the pane, but responsibility of the Chart. E.g: there is no chance of changing the timeScale, because the timeScale belong to the Chart.

The same would happen to the PaneOptions that would be a reduced version of the `ChartOptions:


interface ChartOptions {

  height: number;

  layout: LayoutOptions;

  leftPriceScale: VisiblePriceScaleOptions;

  rightPriceScale: VisiblePriceScaleOptions;

  overlayPriceScales: OverlayPriceScaleOptions;

}

With an API like this, if we don't need to use multiple panels we can keep using Lightweight Charts exactly the way we are using them right now.

If we want to have extra panes we can do something like:


const chart: IChartApi = createChart(container);

const pane: IPaneApi = chart.addPanel('paneId');

pane.addLineSeries( lineSeriesData );

There are no indices involved and it's a bit more similar to how the current API works with the series for example. Is this a direction that may have more chances to be merged? cc @ntf @timocov

Thanks for your idea and this seems there quite a lot of work to achieve what you described.

I am not going to comment about the design as my implementation just enough to suit my need with a React wrapper I built on top of that.

I guess completing this feature doesn't align with TradingView's interest and therefore we wouldn't see it being merged or being implemented by the maintainers in foreseeable future.

arqex commented 2 years ago

Yeh, it would require some work. I could even give it a try based on the work you did on your branch, (it works great!) but only if it is going to be merged. I don't have time to maintain a fork or something similar either.

I would understand if TradingView doesn't want this free, open-source library, to have features that might compete with their full-featured paid library. If it's that way, maybe this thread should be closed.

If multi-panes is still something that can be added, it would be nice to have some feedback on how they imagine it working, so community can help.

ManuelLevi commented 2 years ago

Having multiple panes would be great and a lot of projects would benefit from it.

It's a shame not to see this implemented and merged yet, the PR #824 seems to be failing on two of the automatic checks: "coverage" and "dts-changes".

Could this be the reason why?

ntf commented 2 years ago

@ManuelLevi , of course the code coverage would be lowered if we don't implement new testcases. and I guess dts-changes basically means the API has changed and need maintainer to signoff.

teneon commented 2 years ago

This would be very useful. Any ETA on this feautre?

davidxiao commented 2 years ago

hi, @ntf , @josuamanuel , looks great can you pls provide the demo or jsfiddle links? sorry i didn't find from this thread, i am planning use it with React as well. Thanks

davidxiao commented 2 years ago

hi, @ntf , @josuamanuel , looks great can you pls provide the demo or jsfiddle links? sorry i didn't find from this thread, i am planning use it with React as well. Thanks

Got one from PR, thanks

it'd be great if can be merged, it's long time since created

ntf commented 2 years ago

@davidxiao I guess repo maintainer may not be interested in merging this feature.

Feel free to grab the changes from https://github.com/ntf/lightweight-charts/tree/master or help raising a new merge request to /tradingview/lightweight-charts .

davidxiao commented 2 years ago

hi, @ntf , Thanks, Yeah, seems the maintainer is not interested in merging it. BTW, how to install and use from your repo with React? seems you didn't publish it in npm?

David

ntf commented 2 years ago

@davidxiao

For simplicity or evaluation you can do:

    yarn add git+https://github.com/ntf/lightweight-charts.git --scripts-prepend-node-path

If you have your own folk, you can merge my commits from my repo.

I haven't open sourced my React wrapper yet due to lack of time to isolate those code from my project. You can read below snippet for the general idea how to embed the chart in React and how to place React Portal at those pane elements to draw legends.

import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { BarData, ChartOptions, createChart, CrosshairMode, DeepPartial, IChartApi, ISeriesApi, LineData, SeriesType } from 'lightweight-charts';
import { ChartContextProvider } from "./ChartContext";
import { ChartLegends } from "./ChartLegends";
import { ChartDefinition, SeriesDefinition, SeriesOptions } from "./interfaces";

interface SimpleChartProps extends DeepPartial<ChartOptions> {
    className?: string
    children?: ReactNode
    def?: ChartDefinition
    data?: Record<string, LineData[] | BarData[]>
}

interface ChartState {
    chart: IChartApi,
    sources: Record<string, ISeriesApi<SeriesType>>
}

export const ChartRenderer: React.FunctionComponent<SimpleChartProps> = (props) => {

    // https://github.com/tradingview/lightweight-charts/blob/master/docs/customization.md
    const { className, children, data = {}, ...options } = props;
    const containerRef = useRef<HTMLDivElement>();

    const [state, setState] = useState<ChartState>({ chart: null, sources: {} });

    const [paneRefs, setPaneRefs] = useState<HTMLElement[]>([]);

    useEffect(() => {
        const _chart = createChart(containerRef.current, {
            width: containerRef.current.offsetWidth,
            height: containerRef.current.offsetHeight,
            grid: {
                vertLines: {
                    color: "rgba(213,213,213,0.5)"
                },
                horzLines: {
                    color: "rgba(213,213,213,0.5)"
                }
            },
            timeScale: {
                rightOffset: 10,
                barSpacing: 15,
                // fixLeftEdge: true,
                // fixRightEdge: true,
                lockVisibleTimeRangeOnResize: true,
                rightBarStaysOnScroll: true,
                borderVisible: false,
                visible: true,
                timeVisible: true,
                secondsVisible: true,
                // tickMarkFormatter: (time, tickMarkType, locale) => {
                //     console.log(time, tickMarkType, locale);
                //     const year = isBusinessDay(time) ? time.year : new Date(time * 1000).getUTCFullYear();
                //     return String(year);
                // },
            },
            overlayPriceScales: {
                scaleMargins: {
                    top: 0.6,
                    bottom: 0,
                }
            },
            rightPriceScale: {
                autoScale: true,
                scaleMargins: {
                    top: 0.1,
                    bottom: 0.08,
                }
            },
            crosshair: {
                mode: CrosshairMode.Normal
            }
        });

        setState({ chart: _chart, sources: {} });
        return () => {
            setState({ chart: null, sources: {} });
            _chart?.remove();
        };
    }, []);

    // FIX IT
    // useEffect(() => {
    //     state.chart?.applyOptions(options);
    // }, [state.chart, options]);

    useEffect(() => {
        const resizeHandler = () => {
            state.chart?.resize(containerRef.current.offsetWidth, containerRef.current.offsetHeight);
        };
        window.addEventListener('resize', resizeHandler);
        return () => {
            window.removeEventListener('resize', resizeHandler);
        };
    }, [state.chart]);

    const addSeries: (options: (SeriesDefinition & SeriesOptions), paneIndex: number) => [string, ISeriesApi<SeriesType>] = useCallback((options: SeriesDefinition & SeriesOptions, paneIndex: number) => {
        const key = options.key;
        const chart = state.chart;
        const seriesOptions = {
            ...options,
            pane: paneIndex
        };

        let series;
        switch (options.type) {
            case "Bar":
                series = chart.addBarSeries(seriesOptions);
                break;
            case "Candlestick":
                series = chart.addCandlestickSeries(seriesOptions);
                break;
            case "Area":
                series = chart.addAreaSeries(seriesOptions);
                break;
            case "Line":
                series = chart.addLineSeries(seriesOptions);
                break;
            case "Histogram":
                series = chart.addHistogramSeries(seriesOptions);
                break;
            default:
        }

        return [key, series];
    }, [state.chart]);

    useEffect(() => {
        if (!state.chart) {
            return null;
        }

        setState((prev) => {
            const nextState = {
                ...prev,
                sources: { ...prev.sources }
            };

            const sources = nextState.sources || {};
            props.def.layout.forEach((pane, paneIndex) => {
                pane.sources.forEach((source, sourceIndex) => {
                    if (source.type === "SeriesGroup") {
                        source.sources.forEach((source, sourceIndex) => {
                            const seriesOptions = {
                                ...source,
                                pane: paneIndex
                            };
                            if (!sources[source.key]) {
                                const [key, series] = addSeries(seriesOptions, paneIndex);
                                sources[key] = series;
                            }
                        });
                    } else {
                        const seriesOptions = {
                            ...source,
                            pane: paneIndex
                        };
                        if (!sources[source.key]) {
                            const [key, series] = addSeries(seriesOptions, paneIndex);
                            sources[key] = series;
                        }
                    }
                });
            });

            return nextState;
        });

    }, [state.chart, props.def]);

    useEffect(() => {
        if (!state.chart) {
            return null;
        }
        setPaneRefs(state.chart.getPaneElements());
    }, [state.chart, state.sources]);

    useEffect(() => {
        if (!state.chart) {
            return null;
        }

        Object.entries(data).forEach(([key, data]) => {
            if (state.sources[key] && data) {
                state.sources[key].setData(data);
            }
        });
    }, [state.sources, data]);

    return (
        <div className={props.className} ref={containerRef} style={{ position: "relative", height: (250 + 160 * (props.def.layout.length - 1)) }}>
            {state.chart && (
                <ChartContextProvider
                    chart={state.chart}
                    def={props.def}
                    data={props.data}
                    sources={state.sources}
                    paneRefs={paneRefs}
                >
                    <ChartLegends/>
                    {props.children}
                </ChartContextProvider>
            )}
        </div>
    );
};
export const ChartLegends: React.FunctionComponent<ChartLegendsProps> = (props) => {
    const { crosshair, sources, paneRefs, def, data = {} } = useChart();

    const styles = useStyles();

    return (<>
        {def.layout.flatMap((pane, paneIndex) => {
                if (!paneRefs[paneIndex]) {
                    return null;
                }

                return ReactDOM.createPortal(
                    <div className={styles.legend}>
                        {pane.sources.map((sourceDef, index) => {
                            const _sources: ReadonlyArray<SeriesDefinition> = sourceDef.type === "SeriesGroup" ? sourceDef.sources : [sourceDef];
                            const values: CrosshairState[] = _sources.map((subSourceDef) =>
                                getBar(sources[subSourceDef.key], crosshair, getLastPrice(data[subSourceDef.key]))
                            );

                            let legend: React.ReactElement;
                            switch (sourceDef.type) {
                                // code to build the legend component
                            }

                            return (
                                <p key={sourceDef.key}>
                                    {legend}
                                </p>
                            );
                        })}
                    </div>, paneRefs[paneIndex]);
            }
        )}
    </>);
};
davidxiao commented 2 years ago

hi, @ntf Awesome, thanks very much,

David Xiao

tomlister commented 2 years ago

Clearly they won't merge it. I'm sick of my inbox being spammed with the same comments.

Just fork it... Simple, if you want multi-pane, just do it yourself.

Why would they merge a feature that's available in the commercial version?

TombolaShepless commented 2 years ago

Why would they merge a feature that's available in the commercial version?

Exactly this ^^^

romfrancois commented 2 years ago

Hi guys!

Thanks for being so active on this project. Given the traction gained on this ticket let me shed some light on the matter as it's not as straightforward and simple as it seems.

Back in the days when we were working on the development of LWC, we looked through similar charts and use cases and we implemented almost everything our competitors or individual projects had. None of them had panes!

Why would they merge a feature that's available in the commercial version?

Let me correct this one as it's not entirely true. Yes panes are available in the TAC version and we want to avoid as much as possible cannibalisation between our different projects. However TAC is free and you don't have to pay to access & use it. The only condition for using it for free is to be willing to keep our logo on the chart but it's not very invasive. You simply have to fill out a form and we'll grant you access to the library, documentation, etc. Maybe even some of your scenarios could be fulfilled using either or both libraries.

Last but not least the main concern we would have in bringing such feature would be a considerable increase in the size of the bundle. As the name stands, this library has to remain "lightweight" therefore we can't bring all the features we'd like to it.

allartdo commented 2 years ago

@romfrancois Thanks for the info!

Does this mean the multiple panes function will never be added because it would mean the amount of functions added to lightweight would become too big? Or are you saying the current code used in TAC is too big for the lightweight one?

If it's the first, then it basicly means you will never add it right? Making this issue not open but closed. If it's the second, then what is the ideal size you are looking for? Is what @ntf created good enough?

With size, do you mean in actuall KB/MB etc? Or size as amount of functionalities in the library? Basicly im asking, what is it your searching for exactly to make this implementable? Or will it never be implemented?

ntf commented 2 years ago

@davidxiao I might isolate these from my codebase and publish to npm one day but I don't have the time yet. Meanwhile, just grab these and good luck:

import React, { useCallback, useContext, useEffect, useState } from 'react';
import {
    BarData,
    IChartApi,
    ISeriesApi, LineData,
    MouseEventHandler,
    MouseEventParams,
    SeriesDataItemTypeMap,
    SeriesType
} from "lightweight-charts";
import { ChartDefinition } from "./interfaces";

export type ChartContextType = {
    def?: ChartDefinition,
    data?: Record<string, LineData[] | BarData[]>,
    chart: IChartApi,
    sources: Record<string, ISeriesApi<SeriesType>>,
    paneRefs: HTMLElement[],
    crosshair: MouseEventParams,
    //  seriesManager: SeriesManager
}

export const ChartContext = React.createContext<ChartContextType>(null);

interface ChartContextProviderProps {
    def?: ChartDefinition
    data?: Record<string, LineData[] | BarData[]>
    chart: IChartApi
    sources: Record<string, ISeriesApi<SeriesType>>
    paneRefs: HTMLElement[]
}

export const ChartContextProvider: React.FunctionComponent<ChartContextProviderProps> = (props) => {
    const { chart, def, data , sources = {} } = props;

    const [crosshair, setCrosshairState] = useState<MouseEventParams>(null);

    const handleCrosshairMoved: MouseEventHandler = useCallback((params) => {
        if (!params.point || !params.time) {
            return;
        }
        setCrosshairState(params);
    }, [setCrosshairState]);

    useEffect(() => {
        chart?.subscribeCrosshairMove(handleCrosshairMoved);
        return () => {
            chart?.unsubscribeCrosshairMove(handleCrosshairMoved);
        };
    }, [chart, handleCrosshairMoved]);

    return (
        <ChartContext.Provider value={{
            def: props.def,
            data: props.data,
            chart,
            sources,
            paneRefs: props.paneRefs,
            crosshair
        }}>
            {props.children}
        </ChartContext.Provider>
    );
};

export const useChart = (): ChartContextType => {
    return useContext(ChartContext);
};
import {
    AreaSeriesPartialOptions,
    BarPrice,
    BarSeriesPartialOptions,
    BusinessDay,
    CandlestickSeriesPartialOptions,
    DeepPartial,
    HistogramSeriesPartialOptions,
    LineSeriesPartialOptions, OhlcData,
    SeriesOptionsCommon,
    SeriesType,
    UTCTimestamp
} from "lightweight-charts";
import React from "react";
import { SubscriptionDatasource } from "./interfaces/datasource";

export interface MetadataDefinition {
    name: string
    symbol: string
}

export interface CrosshairState {
    time?: UTCTimestamp | BusinessDay | string,
    value?: BarPrice
    prices?: Omit<OhlcData, 'time'>
}

export interface LegendFormatterProps {
    metadata?: MetadataDefinition
    value?: CrosshairState

    datasource?: SubscriptionDatasource,
    // child data sources
    sources?: ReadonlyArray<SeriesDefinition>,
    values?: ReadonlyArray<CrosshairState>
    title?: string
}

export type LegendFormatter = React.FunctionComponent<LegendFormatterProps>

export interface SourceBaseDefinition {
    type: SeriesType | "SeriesGroup"
    /**
     * title of the series group, to be placed in the legend
     */
    title?: string,
    legend?: LegendFormatter,

    datasource?: SubscriptionDatasource
}

export interface SeriesDefinition extends SourceBaseDefinition, DeepPartial<SeriesOptionsCommon> {
    key: string
    /**
     * title of the series
     */
    title?: string;
    color?: string;
    type: SeriesType
    params?: Record<string, any>
}

export interface SeriesGroupDefinition extends SourceBaseDefinition {
    type: "SeriesGroup"
    key?: string,
    sources?: ReadonlyArray<SeriesDefinition>
}

export type SeriesOptions =
    AreaSeriesPartialOptions
    | BarSeriesPartialOptions
    | CandlestickSeriesPartialOptions
    | HistogramSeriesPartialOptions
    | LineSeriesPartialOptions;
export type SourceDefinition = SeriesDefinition & SeriesOptions | SeriesGroupDefinition;

export interface PaneDefinition {
    sources: ReadonlyArray<SourceDefinition>
}

export type ChartLayout = ReadonlyArray<PaneDefinition>;

export interface ChartDefinition {
    metadata: MetadataDefinition
    timeframe: "PT24H"
    layout: ChartLayout
}

I was a paid tradingview user but the features they provided via Pine Script wasn't something I need. FYI, this is a graph rendered with my forked version on my application with indicators computed in the server side. image

justin-pierce commented 2 years ago

@romfrancois

Let me correct this one as it's not entirely true. Yes panes are available in the TAC version and we want to avoid as much as possible cannibalisation between our different projects. However TAC is free and you don't have to pay to access & use it. The only condition for using it for free is to be willing to keep our logo on the chart but it's not very invasive. You simply have to fill out a form and we'll grant you access to the library, documentation, etc. Maybe even some of your scenarios could be fulfilled using either or both libraries.

Last I checked, TAC was not permitted for private usage (like a personal trading bot not accessible to the public). Is that still the case?

allartdo commented 2 years ago

@justin-pierce He probably ain't lying? Works for the company. Check again I guess...

justin-pierce commented 2 years ago

@justin-pierce He probably ain't lying? Works for the company. Check again I guess...

Not accusing him of lying, asking for clarification. It says this on the page:

Can I get charting library for personal use?

At this time, we do not provide Technical Analysis Charts for personal use, hobbies, studies, or testing. The FREE Technical Analysis Charts license can be provided only to companies and/or individuals for use in public web projects and/or applications.

For my usecase, it seems I'm limited to LWC with no pane support.

edit: will likely just switch to Plotly

romfrancois commented 2 years ago

@allartdo I'd say it's a bit of both. As I said before we want to avoid cannibalisation meaning we can't offer parity in terms of functionalities across our products therefore it's very unlikely this feature will land on LWC. Having said that we aren't totally against the concept but not in this form. To be transparent we are thinking about bringing in a plugin-system where LWC will remain lightweight but with the ability of enhancing the existing. This way us as TradingView or anyone else for that matter could develop such feature and bring it to the library.

@justin-pierce you're right; Im afraid TAC usage is allowed for public project only.

john-wallace-dev commented 2 years ago

It's pretty common to have indicators below the charts. Charting sites that do it include: finance.yahoo.com, thinkorswim, and of course tradingview.com among many others.

I've filled out the forms to try and get the Technical Analysis Charts, but no dice since I want to use it for a proprietary project which isn't allowed. So although listed as "free", TAC is not freely available. (BTW, why isn't TAC available for proprietary projects? That's a very strange limitation, I'm assuming imposed by your business people. I think they are missing an opportunity there.)

Thank you ntf for offering a solution to the community! Even without multiple panes, LWC is a great solution as far as it goes, but it's disappointing that it's impossible for me to get this feature in a supported version of LWC and impossible for me to get TAC.

kayazinc commented 2 years ago

Just wondering is there any reason why the maintainers are unwilling to merge to PR?

Beside this PR, there are other useful features PR eg. 1/ set crosshair programmatically (https://github.com/tradingview/lightweight-charts/issues/438) 2/ draggable priceline (https://github.com/tradingview/lightweight-charts/issues/1086)

if possbile, i am willing to be a contributor to the wonderful charting lib, thus i hope to understand the rational of tradingview merging process.

Thanks for your efforts and sharing, tradingview developers and maintainers.