tradingview / lightweight-charts

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

Auto fitting to the height of pane1 histograms #1709

Open vishnuc opened 1 month ago

vishnuc commented 1 month ago

hi , I am testing out v5-candidate with multpane , I added volume series to new pane via

   const volumeSeries = chart.addHistogramSeries({
            color: '#26a69a', // Default color (used for volume bars)
            priceFormat: {
                type: 'volume',
            },
        }, 1); // Add to second pane (pane index 1)

now if i want to scale this volumeseries , it is also scaling the pane 0 price series.

volumeSeries.priceScale().applyOptions({
    autoScale: false, // disables auto scaling based on visible content
    scaleMargins: {
        top: 0.9,
        bottom: 0,
    },
});
Screenshot 2024-10-14 at 1 56 53 PM Screenshot 2024-10-14 at 1 57 43 PM

Its expected to fit / scale only pane 1 as the series is in pane1

safaritrader commented 1 month ago

image

use an id for price scale :

    const volumeSeries = chart.addHistogramSeries({
    color: '#26a69a',
    priceFormat: {
        type: 'volume',
    },
    priceScaleId: 'rightVolume',
}, 1);
chart.priceScale('rightVolume').applyOptions({
    autoScale: false,
    scaleMargins: {
        top: 0.9,
        bottom: 0,
    },
});
vishnuc commented 1 month ago

If i set priceScaleId: 'rightVolume', , I am not able to scale by dragging its pricescale.. also when i hover the volume its not displaying price label on right side.

safaritrader commented 1 month ago

If i set priceScaleId: 'rightVolume', , I am not able to scale by dragging its pricescale.. also when i hover the volume its not displaying price label on right side.

As I understand it, if you want to have full control over the volume, the best approach is to build a plugin, or you can use a secondary overlay canvas or a second price scale at the left

vishnuc commented 1 month ago

sorry , I am not a professional to build plugin..Also i want both price and volume scale on the right, thats why using the pane.

so can you tell me about secondary overlay canvas ?

safaritrader commented 1 month ago

sorry , I am not a professional to build plugin..Also i want both price and volume scale on the right, thats why using the pane.

so can you tell me about secondary overlay canvas ?

as i am a fan of pure js i created a plugin for lightweight chart. you can see the concept and use it : https://github.com/safaritrader/lightweight-chart-plugin

vishnuc commented 1 month ago

wow , thanks i was thinking to create volume profile for my project , but did u extend version 4 or 5 ..i need multi pane support.

safaritrader commented 1 month ago

wow , thanks i was thinking to create volume profile for my project , but did u extend version 4 or 5 ..i need multi pane support.

your welcome. i am expanding that plugin frequently. it's based on v 4.2.0

vishnuc commented 1 month ago

ok , then I want to build plugin for my v5 , can you guide me with any basic tutorials ? there is no documentation in TV how to create a plugin from scratch.

I am coding vanilla js for past 8 years.. since there was no use for constructor , canvas I did not learn , but I am willing to learn now .. can you tell me what should I learn ? because I am new to constructor , canvas etc ..

is there a skeleton or template that i must follow to create plugin from scratch and how it works.

safaritrader commented 1 month ago

ok , then I want to build plugin for my v5 , can you guide me with any basic tutorials ? there is no documentation in TV how to create a plugin from scratch.

I am coding vanilla js for past 8 years.. since there was no use for constructor , canvas I did not learn , but I am willing to learn now .. can you tell me what should I learn ? because I am new to constructor , canvas etc ..

is there a skeleton or template that i must follow to create plugin from scratch and how it works.

As i know the last version for lightweight charts is 4.2

vishnuc commented 1 month ago

ok there is another branch named 5 , you can use that to checkout multi pane feature - https://github.com/tradingview/lightweight-charts/tree/v5-candidate

btw please tell me what should I learn , or any tutorials from scratch to create one simple plugin

safaritrader commented 1 month ago

ok there is another branch named 5 , you can use that to checkout multi pane feature - https://github.com/tradingview/lightweight-charts/tree/v5-candidate

btw please tell me what should I learn , or any tutorials from scratch to create one simple plugin

Official Library suggest to do that : https://github.com/tradingview/lightweight-charts/tree/master/packages/create-lwc-plugin which i am working with pure js and its not fitting my requirements and im using overlay canvas which i described in my Lightwight charts plugin. its very simple when you read you will learn!. the main approach of mine is to use another canvas over the lwc canvas and do every thing you want

vishnuc commented 1 month ago

Ok I am reading your code now. along with chatgpt help

On Wed, 16 Oct, 2024, 4:17 pm Hassan Safari, @.***> wrote:

ok there is another branch named 5 , you can use that to checkout multi pane feature - https://github.com/tradingview/lightweight-charts/tree/v5-candidate

btw please tell me what should I learn , or any tutorials from scratch to create one simple plugin

Official Library suggest to do that :

https://github.com/tradingview/lightweight-charts/tree/master/packages/create-lwc-plugin which i am working with pure js and its not fitting my requirements and im using overlay canvas which i described in my Lightwight charts plugin. its very simple when you read you will learn!. the main approach of mine is to use another canvas over the lwc canvas and do every thing you want

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

vishnuc commented 1 month ago

Hey @safaritrader I saw plugin use fancy-canvas etc , how did u overcome all those things using vanilla js.

safaritrader commented 1 month ago

Hey @safaritrader I saw plugin use fancy-canvas etc , how did u overcome all those things using vanilla js.

Hi Dear @vishnuc Sometimes a simple solution is faster for achieving the desired outcome, plus I tested that plugin on 3 million data points for volume profile, and it worked perfectly. I try not to get too caught up in complexities and aim to reach my goal with the simplest approach. However, there are still some small bugs that I'll fix in the next update. Since I work with large datasets, I need to make sure the code can handle at least 10 million data points. One of the good features of this plugin is that it doesn’t require extensive expertise to modify or rewrite.

vishnuc commented 1 month ago

hi , but I compiled and run your example.. its not fluid when i scale price axis .. native plugins are way smoother...also when time axis is scaled I think you redraw after certain interval.

safaritrader commented 1 month ago

hi , but I compiled and run your example.. its not fluid when i scale price axis .. native plugins are way smoother...also when time axis is scaled I think you redraw after certain interval.

its depend on your data scale. for now its loop through all data but im replacing a simple algorithm to not loop through all data in next version and its depend on your browser also it take 5 sec to build a volume profile from 3m data (tested in firefox). other plugins are using lightweight chart built-in feature's which is great but not fit my requirements

vishnuc commented 1 month ago

hi , i succeeded creating primitive with pure js , its smooth . now working on series , will update you.

    <script >

        class AnchoredTextRenderer {
            constructor(options) {
                this._data = options;
            }

            draw(target) {
                target.useMediaCoordinateSpace((scope) => {
                    const ctx = scope.context;
                    ctx.font = this._data.font;
                    const textWidth = ctx.measureText(this._data.text).width;
                    const horzMargin = 20;
                    let x = horzMargin;
                    const width = scope.mediaSize.width;
                    const height = scope.mediaSize.height;

                    switch (this._data.horzAlign) {
                        case 'right':
                            x = width - horzMargin - textWidth;
                            break;
                        case 'middle':
                            x = width / 2 - textWidth / 2;
                            break;
                    }

                    const vertMargin = 10;
                    const lineHeight = this._data.lineHeight;
                    let y = vertMargin + lineHeight;

                    switch (this._data.vertAlign) {
                        case 'middle':
                            y = height / 2 + lineHeight / 2;
                            break;
                        case 'bottom':
                            y = height - vertMargin;
                            break;
                    }

                    ctx.fillStyle = this._data.color;
                    ctx.fillText(this._data.text, x, y);
                });
            }
        }

        class AnchoredTextPaneView {
            constructor(source) {
                this._source = source;
            }

            update() {}

            renderer() {
                return new AnchoredTextRenderer(this._source._data);
            }
        }

        class AnchoredText {
            constructor(options) {
                this._data = options;
                this._paneViews = [new AnchoredTextPaneView(this)];
            }

            updateAllViews() {
                this._paneViews.forEach(pw => pw.update());
            }

            paneViews() {
                return this._paneViews;
            }

            attached({ requestUpdate }) {
                this.requestUpdate = requestUpdate;
            }

            detached() {
                this.requestUpdate = undefined;
            }

            applyOptions(options) {
                this._data = { ...this._data, ...options };
                if (this.requestUpdate) this.requestUpdate();
            }
        }

        // Create the chart
        const chart = LightweightCharts.createChart(document.getElementById('chart'), {
            width: window.innerWidth,
            height: 500,
        });

          // Add a line series
          const lineSeries1 = chart.addLineSeries({},0);
        const data1 = [
            { time: '2024-01-01', value: 120 },
            { time: '2024-01-02', value: 130 },
            { time: '2024-01-03', value: 125 },
            { time: '2024-01-04', value: 135 },
            { time: '2024-01-05', value: 140 },
        ];
        lineSeries1.setData(data1);

        // Add a line series
        const lineSeries = chart.addLineSeries({},1);
        const data = [
            { time: '2024-01-01', value: 120 },
            { time: '2024-01-02', value: 130 },
            { time: '2024-01-03', value: 125 },
            { time: '2024-01-04', value: 135 },
            { time: '2024-01-05', value: 140 },
        ];
        lineSeries.setData(data);

        // Add Anchored Text
        const anchoredText = new AnchoredText({
            vertAlign: 'middle',
            horzAlign: 'middle',
            text: 'Anchored Text',
            lineHeight: 54,
            font: 'italic bold 54px Arial',
            color: 'green',
        });
        lineSeries.attachPrimitive(anchoredText);

        // Change the text after 2 seconds
        setTimeout(() => {
            anchoredText.applyOptions({
                text: 'New Text',
            });
        }, 2000);

        // Update chart size on window resize
        window.addEventListener('resize', () => {
            chart.applyOptions({ width: window.innerWidth });
        });
    </script>
vishnuc commented 1 month ago

ok series too works fine with pure js , why dont u use like this in ur plugin,.. so even when u scale it will look good.


   <script type="module">
        import { createChart } from '/tv1.mjs';

        // Renderer for the Lollipop series
        class LollipopSeriesRenderer {
            constructor() {
                this._data = null;
                this._options = null;
            }

            draw(target, priceConverter) {
                target.useBitmapCoordinateSpace(scope => this._drawImpl(scope, priceConverter));
            }

            update(data, options) {
                this._data = data;
                this._options = options;
            }

            _drawImpl(scope, priceToCoordinate) {
                if (!this._data || !this._options || this._data.bars.length === 0 || !this._data.visibleRange) {
                    return;
                }

                const bars = this._data.bars.map(bar => ({
                    x: bar.x,
                    y: priceToCoordinate(bar.originalData.value) ?? 0,
                }));

                const lineWidth = Math.min(this._options.lineWidth, this._data.barSpacing)*3;
                const radius = Math.min(Math.floor(this._data.barSpacing / 2), 5);  // Max radius for the circles
                const zeroY = priceToCoordinate(0);

                for (let i = this._data.visibleRange.from; i < this._data.visibleRange.to; i++) {
                    const bar = bars[i];
                    const xPosition = bar.x * scope.horizontalPixelRatio;
                    const yPosition = bar.y * scope.verticalPixelRatio;

                    scope.context.beginPath();
                    scope.context.fillStyle = this._options.color;

                    // Draw the line from zero to the value
                    scope.context.fillRect(xPosition - lineWidth / 2, zeroY * scope.verticalPixelRatio, lineWidth, yPosition - zeroY * scope.verticalPixelRatio);

                    // Draw the circle on top
                    scope.context.arc(xPosition, yPosition, radius * scope.horizontalPixelRatio, 0, Math.PI * 2);
                    scope.context.fill();
                }
            }
        }

        // Lollipop Series class
        class LollipopSeries {
            constructor() {
                this._renderer = new LollipopSeriesRenderer();
            }

            priceValueBuilder(plotRow) {
                return [0, plotRow.value];
            }

            isWhitespace(data) {
                return data.value === undefined;
            }

            renderer() {
                return this._renderer;
            }

            update(data, options) {
                this._renderer.update(data, options);
            }

            defaultOptions() {
                return {
                    lineWidth: 2,
                    color: 'rgb(0, 100, 255)',
                };
            }
        }

        // Create the chart
        const chart = createChart(document.getElementById('chart'), {
            width: window.innerWidth,
            height: 500,
        });

        // Add Lollipop series to chart
        const customSeriesView = new LollipopSeries();
        const myCustomSeries = chart.addCustomSeries(customSeriesView, {
            lineWidth: 1,   // Make lines thinner
            color: 'rgb(0, 200, 255)',  // Lollipop color
        });

        // Generate random data for the Lollipop series
        function generateLollipopData() {
            const data = [];
            let baseTime = new Date('2021-01-01').getTime() / 1000; // Unix timestamp in seconds

            for (let i = 0; i < 50; i++) {
                const value = Math.random() * 300;  // Use a wider value range
                data.push({
                    time: baseTime + i * 60 * 60 * 24, // Increment by 1 day
                    value: value,
                });
            }
            return data;
        }

        // Set Lollipop data
        const lollipopData = generateLollipopData();
        myCustomSeries.setData(lollipopData);

        // Update chart size on window resize
        window.addEventListener('resize', () => {
            chart.applyOptions({ width: window.innerWidth });
        });
    </script>
safaritrader commented 1 month ago

ok series too works fine with pure js , why dont u use like this in ur plugin,.. so even when u scale it will look good.


   <script type="module">
        import { createChart } from '/tv1.mjs';

        // Renderer for the Lollipop series
        class LollipopSeriesRenderer {
            constructor() {
                this._data = null;
                this._options = null;
            }

            draw(target, priceConverter) {
                target.useBitmapCoordinateSpace(scope => this._drawImpl(scope, priceConverter));
            }

            update(data, options) {
                this._data = data;
                this._options = options;
            }

            _drawImpl(scope, priceToCoordinate) {
                if (!this._data || !this._options || this._data.bars.length === 0 || !this._data.visibleRange) {
                    return;
                }

                const bars = this._data.bars.map(bar => ({
                    x: bar.x,
                    y: priceToCoordinate(bar.originalData.value) ?? 0,
                }));

                const lineWidth = Math.min(this._options.lineWidth, this._data.barSpacing)*3;
                const radius = Math.min(Math.floor(this._data.barSpacing / 2), 5);  // Max radius for the circles
                const zeroY = priceToCoordinate(0);

                for (let i = this._data.visibleRange.from; i < this._data.visibleRange.to; i++) {
                    const bar = bars[i];
                    const xPosition = bar.x * scope.horizontalPixelRatio;
                    const yPosition = bar.y * scope.verticalPixelRatio;

                    scope.context.beginPath();
                    scope.context.fillStyle = this._options.color;

                    // Draw the line from zero to the value
                    scope.context.fillRect(xPosition - lineWidth / 2, zeroY * scope.verticalPixelRatio, lineWidth, yPosition - zeroY * scope.verticalPixelRatio);

                    // Draw the circle on top
                    scope.context.arc(xPosition, yPosition, radius * scope.horizontalPixelRatio, 0, Math.PI * 2);
                    scope.context.fill();
                }
            }
        }

        // Lollipop Series class
        class LollipopSeries {
            constructor() {
                this._renderer = new LollipopSeriesRenderer();
            }

            priceValueBuilder(plotRow) {
                return [0, plotRow.value];
            }

            isWhitespace(data) {
                return data.value === undefined;
            }

            renderer() {
                return this._renderer;
            }

            update(data, options) {
                this._renderer.update(data, options);
            }

            defaultOptions() {
                return {
                    lineWidth: 2,
                    color: 'rgb(0, 100, 255)',
                };
            }
        }

        // Create the chart
        const chart = createChart(document.getElementById('chart'), {
            width: window.innerWidth,
            height: 500,
        });

        // Add Lollipop series to chart
        const customSeriesView = new LollipopSeries();
        const myCustomSeries = chart.addCustomSeries(customSeriesView, {
            lineWidth: 1,   // Make lines thinner
            color: 'rgb(0, 200, 255)',  // Lollipop color
        });

        // Generate random data for the Lollipop series
        function generateLollipopData() {
            const data = [];
            let baseTime = new Date('2021-01-01').getTime() / 1000; // Unix timestamp in seconds

            for (let i = 0; i < 50; i++) {
                const value = Math.random() * 300;  // Use a wider value range
                data.push({
                    time: baseTime + i * 60 * 60 * 24, // Increment by 1 day
                    value: value,
                });
            }
            return data;
        }

        // Set Lollipop data
        const lollipopData = generateLollipopData();
        myCustomSeries.setData(lollipopData);

        // Update chart size on window resize
        window.addEventListener('resize', () => {
            chart.applyOptions({ width: window.innerWidth });
        });
    </script>

that using for loop for redraw too which cant make a different for my current plugin. its executing redraw on resize and changing scales too it just using less code to attach custom shapes or series which is good

vishnuc commented 1 month ago

No yours is not giving native feel , I am checking in my macbook retina..Its easy I think u can create lots of plugin with native way with vanilla js.

also if i scale my pricescale up and down urs is hiding the volume profile etc