amcharts / amcharts5

The newest, fastest, and most advanced amCharts charting library for JavaScript and TypeScript apps.
324 stars 90 forks source link

Chart freezes on scroll/stretched #683

Closed tinegaCollins closed 1 year ago

tinegaCollins commented 1 year ago

I have a standard am 5 chart that takes data from a web socket. the chart is working fine but scrolling on the page just freezes . at first i thought it was websocket issue but the freezing only happens when the page is scrolled this is a peice of code from the component

useEffect(async () => { if (symbol != '' && props.exchangeSource != '' && props?.priceDecimal != '') { socket.io.on("reconnect", (_socket) => { initAllSocket('join_kline'); }); initAllSocket('join_kline');

            socket.on("RECEIVE_DERIVATIVES_KLINE", (data) => {
                // console.log(data);
                // console.log(symbol);
                data.pair = data?.pair == 'TRONUSDT' ? 'TRXUSDT' : data?.pair;
                if (data?.result && data.pair == symbol && data?.event == 'DERIVATIVES_KLINE') {
                    setChartData(data?.result)
                }
            });
        root = am5.Root.new("chartdiv");

        root.setThemes([
            am5themes_Animated.new(root),
            (mode == 'dark' ? am5themes_Dark.new(root) : am5themes_Dataviz.new(root))
        ]);
        root.interfaceColors.set("negative", DownColor);
        root.interfaceColors.set("positive", UpColor);

        stockChart = root.container.children.push(am5stock.StockChart.new(root, {
            stockPositiveColor: UpColor,
            stockNegativeColor: DownColor,
            volumePositiveColor: UpColor,
            volumeNegativeColor: DownColor,

            percentScaleSeriesSettings: {
                valueYShow: "valueYChangePercent",
                openValueYShow: "openValueYChangePercent",
                highValueYShow: "highValueYChangePercent",
                lowValueYShow: "lowValueYChangePercent",
            },
            percentScaleValueAxisSettings: {
                numberFormat: "#.##'%'"
            },
            autoSetPercentScale: true
        }));

        // price decimal point
        let nFormat = '#,###'
        if (props?.priceDecimal > 0) {
            nFormat = `#,###.${"0".repeat(props?.priceDecimal)}`
        }
        // root.numberFormatter.set("numberFormat", nFormat);

        mainPanel = stockChart.panels.push(am5stock.StockPanel.new(root, {
            panX: true,
            panY: false,
            wheelX: "panX",
            wheelY: "zoomX",
            layout: root.verticalLayout,
        }));

        valueAxis = mainPanel.yAxes.push(am5xy.ValueAxis.new(root, {
            renderer: am5xy.AxisRendererY.new(root, {
                pan: "zoom"
            }),
            extraMax: 0.1, // adds some space for for main series
            tooltip: am5.Tooltip.new(root, {}),
            numberFormat: "#,###.00",
            extraTooltipPrecision: 2,
            strictMinMax: false,
        }));

        dateAxis = mainPanel.xAxes.push(am5xy.DateAxis.new(root, {
            baseInterval: {
                timeUnit: "hour",
                count: 1
            },
            groupIntervals: [

                { timeUnit: "minute", count: 1 },
                { timeUnit: "hour", count: 1 },
                { timeUnit: "month", count: 1 },
                { timeUnit: "year", count: 1 }
            ],
            renderer: am5xy.AxisRendererX.new(root, {}),
            tooltip: am5.Tooltip.new(root, {}),
            groupData: true,
            gridIntervals: [
                { timeUnit: "second", count: 1 },
                { timeUnit: "second", count: 5 },
                { timeUnit: "second", count: 10 },
                { timeUnit: "second", count: 30 },
                { timeUnit: "minute", count: 1 },
                { timeUnit: "minute", count: 5 },
                { timeUnit: "minute", count: 10 },
                { timeUnit: "minute", count: 15 },
                { timeUnit: "minute", count: 30 },
                { timeUnit: "hour", count: 1 },
                { timeUnit: "hour", count: 4 },
                { timeUnit: "hour", count: 8 },
                { timeUnit: "day", count: 1 },
                { timeUnit: "week", count: 1 },
            ],
            extraMax: 0.1,//////////
        }));

        // add range which will show current value
        currentValueDataItem = valueAxis.createAxisRange(valueAxis.makeDataItem({ value: 0 }));
        currentLabel = currentValueDataItem.get("label");
        if (currentLabel) {
            currentLabel.setAll({
                fill: am5.color(0xffffff),
                background: am5.Rectangle.new(root, { fill: am5.color(0x000000) })
            })
        }

        currentGrid = currentValueDataItem.get("grid");
        if (currentGrid) {
            currentGrid.setAll({ strokeOpacity: 0.5, strokeDasharray: [2, 5] });
        }

        valueSeries = mainPanel.series.push(am5xy.CandlestickSeries.new(root, {
            name: symbol,
            clustered: false,
            height: am5.percent(70),
            valueXField: "date",
            valueYField: "close",
            highValueYField: "high",
            lowValueYField: "low",
            openValueYField: "open",
            calculateAggregates: true,
            xAxis: dateAxis,
            yAxis: valueAxis,
            valueYGrouped: "close",
            legendValueText: "Open: [bold]{openValueY}[/] High: [bold]{highValueY}[/] Low: [bold]{lowValueY}[/] Close: [bold]{valueY}[/]",
            legendRangeValueText: "{valueYClose}",
            tooltipText: "Open: [bold]{openValueY}[/]\high: [bold]{highValueY}[/]\Low: [bold]{lowValueY}[/]\Close: [bold]{valueY}[/]\n",
            legendRangeValueText: "",
            fill: am5.color(0x000000),
            stroke: am5.color(0x000000),
            ariaLive: true,
        }));

        valueSeries.columns.template.states.create("riseFromOpen", {
            fill: UpColor,
            stroke: UpColor

        });

        valueSeries.columns.template.states.create("dropFromOpen", {
            fill: DownColor,
            stroke: DownColor
        });

        var valueTooltip = valueSeries.set("tooltip", am5.Tooltip.new(root, {
            getFillFromSprite: false,
            getStrokeFromSprite: true,
            getLabelFillFromSprite: true,
            autoTextColor: false,
            pointerOrientation: "horizontal",
            labelText: "Open: [bold]{openValueY}[/]\nHigh: [bold]{highValueY}[/]\nLow: [bold]{lowValueY}[/]\nClose: [bold]{valueY}[/]",
        }));

        valueTooltip.get("background").setAll({
            fill: am5.color(0xffffff)
        })

        stockChart.set("stockSeries", valueSeries);

        valueLegend = mainPanel.plotContainer.children.push(am5stock.StockLegend.new(root, {
            stockChart: stockChart,
            fill: UpColor,
            stroke: DownColor
        }));

        // Create a volume panel (chart)
        // -------------------------------------------------------------------------------
        // https://www.amcharts.com/docs/v5/charts/stock-chart/#Adding_panels
        var volumePanel = stockChart.panels.push(am5stock.StockPanel.new(root, {
            wheelY: "zoomX",
            panX: true,
            panY: false,
            height: am5.percent(30),
            // paddingTop: 6
        }));

        // hide close button as we don't want this panel to be closed
        volumePanel.panelControls.closeButton.set("forceHidden", true);

        var volumeDateAxis = volumePanel.xAxes.push(am5xy.GaplessDateAxis.new(root, {
            baseInterval: {
                timeUnit: "hour",
                count: 1
            },
            groupData: true,
            groupCount: 150,
            renderer: am5xy.AxisRendererX.new(root, {}),
            tooltip: am5.Tooltip.new(root, {
                forceHidden: true
            }),
            height: am5.percent(30),
        }));

        // we don't need it to be visible
        volumeDateAxis.get("renderer").labels.template.set("forceHidden", true);

        // Create volume axis
        // -------------------------------------------------------------------------------
        // https://www.amcharts.com/docs/v5/charts/xy-chart/axes/
        var volumeAxisRenderer = am5xy.AxisRendererY.new(root, {
            inside: true,
            fill: UpColor,
            stroke: DownColor
        });

        // volumeAxisRenderer.labels.template.set("forceHidden", true);
        // volumeAxisRenderer.grid.template.set("forceHidden", true);

        var volumeValueAxis = volumePanel.yAxes.push(am5xy.ValueAxis.new(root, {
            numberFormat: "#.#a",
            height: am5.percent(30),
            y: am5.percent(100),
            centerY: am5.percent(100),
            renderer: volumeAxisRenderer
        }));

        // Add series
        volumeSeries = volumePanel.series.push(am5xy.ColumnSeries.new(root, {
            name: "Volume",
            clustered: false,
            valueXField: "date",
            valueYField: "volume",
            xAxis: dateAxis,
            yAxis: volumeValueAxis,
            calculateAggregates: true,
            legendValueText: "[bold]{valueY}[/]",
            fill: UpColor,
            stroke: DownColor,
            tooltip: am5.Tooltip.new(root, {
                labelText: "{valueY}"
            })
        }));

        volumeSeries.columns.template.setAll({
            strokeOpacity: 0,
            fillOpacity: 0.5
        });

        // color columns by stock rules
        volumeSeries.columns.template.adapters.add("fill", function (fill, target) {
            var dataItem = target.dataItem;
            if (dataItem) {
                return stockChart.getVolumeColor(dataItem);
            }
            return fill;
        })

        // Add a stock legend
        // -------------------------------------------------------------------------------
        // https://www.amcharts.com/docs/v5/charts/stock-chart/stock-legend/
        var volumeLegend = volumePanel.plotContainer.children.push(am5stock.StockLegend.new(root, {
            stockChart: stockChart
        }));
        // Set main series
        // -------------------------------------------------------------------------------
        // https://www.amcharts.com/docs/v5/charts/stock-chart/#Setting_main_series
        stockChart.set("volumeSeries", volumeSeries);
        valueLegend.data.setAll([valueSeries]);
        volumeLegend.data.setAll([volumeSeries]);

        // Add cursor(s)
        // -------------------------------------------------------------------------------
        // https://www.amcharts.com/docs/v5/charts/xy-chart/cursor/
        mainPanel.set("cursor", am5xy.XYCursor.new(root, {
            yAxis: valueAxis,
            xAxis: dateAxis,
            snapToSeries: [valueSeries],
            snapToSeriesBy: "y!"
        }));

        var volumeCursor = volumePanel.set("cursor", am5xy.XYCursor.new(root, {
            yAxis: volumeValueAxis,
            xAxis: volumeDateAxis,
            snapToSeries: [volumeSeries],
            snapToSeriesBy: "y!"
        }));

        volumeCursor.lineY.set("forceHidden", true);

        // Add scrollbar
        // -------------------------------------------------------------------------------
        // https://www.amcharts.com/docs/v5/charts/xy-chart/scrollbars/
        var scrollbar = mainPanel.set("scrollbarX", am5xy.XYChartScrollbar.new(root, {
            orientation: "horizontal",
            forceHidden: true,  // hide scrolebar
            height: am5.percent(0),  // hide scrolebar
            // visible: false, // hide scrolebar
            height: 50
        }));
        stockChart.toolsContainer.children.push(scrollbar);

        sbDateAxis = scrollbar.chart.xAxes.push(am5xy.GaplessDateAxis.new(root, {
            baseInterval: intervalData,
            renderer: am5xy.AxisRendererX.new(root, {})
        }));

        var sbValueAxis = scrollbar.chart.yAxes.push(am5xy.ValueAxis.new(root, {
            renderer: am5xy.AxisRendererY.new(root, {})
        }));

        sbSeries = scrollbar.chart.series.push(am5xy.LineSeries.new(root, {
            valueYField: "close",
            valueXField: "date",
            xAxis: sbDateAxis,
            yAxis: sbValueAxis
        }));

        sbSeries.fills.template.setAll({
            visible: true,
            fillOpacity: 0.3
        });

        // Set up series type switcher
        // -------------------------------------------------------------------------------
        // https://www.amcharts.com/docs/v5/charts/stock/toolbar/series-type-control/
        let seriesSwitcher = am5stock.SeriesTypeControl.new(root, {
            stockChart: stockChart
        });

        seriesSwitcher.events.on("selected", function (ev) {
            setSeriesType(ev.item.id);
        });

        function getNewSettings(series) {
            let newSettings = [];
            am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "stroke", "fill"], function (setting) {
                newSettings[setting] = series.get(setting);
            });
            return newSettings;
        }

        function setSeriesType(seriesType) {
            // Get current series and its settings
            let currentSeries = stockChart.get("stockSeries");
            let newSettings = getNewSettings(currentSeries);

            // Remove previous series
            let data = currentSeries.data.values;
            mainPanel.series.removeValue(currentSeries);

            // Create new series
            let series;
            switch (seriesType) {
                case "line":
                    series = mainPanel.series.push(am5xy.LineSeries.new(root, newSettings));
                    break;
                case "candlestick":
                case "procandlestick":
                    newSettings.clustered = false;
                    series = mainPanel.series.push(am5xy.CandlestickSeries.new(root, newSettings));
                    if (seriesType == "procandlestick") {
                        series.columns.template.get("themeTags").push("pro");
                    }
                    break;
                case "ohlc":
                    newSettings.clustered = false;
                    series = mainPanel.series.push(am5xy.OHLCSeries.new(root, newSettings));
                    break;
            }

            // Set new series as stockSeries
            if (series) {
                valueLegend.data.removeValue(currentSeries);
                series.data.setAll(data);
                stockChart.set("stockSeries", series);
                let cursor = mainPanel.get("cursor");
                if (cursor) {
                    cursor.set("snapToSeries", [series]);
                }
                valueLegend.data.insertIndex(0, series);
            }
        }

        var intervalControl = am5stock.IntervalControl.new(root, {
            stockChart: stockChart,
            currentItem: "1 hour",
            items: [
                { id: "1 minute", label: "1 minute", interval: { timeUnit: "minute", count: 1, name: "1m" } },
                { id: "15 minute", label: "15 minutes", interval: { timeUnit: "minute", count: 15, name: "15m" } },
                { id: "1 hour", label: "1 hour", interval: { timeUnit: "hour", count: 1, name: "1h" } },
                { id: "4 hour", label: "4 hours", interval: { timeUnit: "hour", count: 4, name: "4h" } },
                { id: "1 day", label: "1 day", interval: { timeUnit: "day", count: 1, name: "1d" } },
                { id: "1 week", label: "1 week", interval: { timeUnit: "week", count: 1, name: "1w" } },
                { id: "1 month", label: "1 month", interval: { timeUnit: "month", count: 1, name: "1M" } },
            ]
        });

        intervalControl.events.on("selected", function (ev) {

            counter = 0;
            // setNoData(false)
            // Get series
            var valueSeries = stockChart.get("stockSeries");
            // Set up zoomout
            valueSeries.events.once("datavalidated", function () {
                mainPanel.zoomOut();
            });

            intervalData = ev.item.interval;
            min = currentDate.getTime() - am5.time.getDuration(intervalData.timeUnit, intervalData.count * limit);

            var promises = [];
            promises.push(loadData(intervalData?.name, min, max, "none"));

            // Once data loading is done, set `baseInterval` on the DateAxis
            Promise.all(promises).then(function () {
                dateAxis.set("baseInterval", ev.item.interval);
                sbDateAxis.set("baseInterval", ev.item.interval);
            });
            // intervalvalue = ev.item.interval;
            // console.log("intervalvalue:", intervalvalue)
            // setDataInterval(ev.item.interval);
        });

        async function setDataInterval(interval) {
            // Load external data
            dateAxis.set("baseInterval", { timeUnit: interval?.timeUnit, count: interval?.count });
            sbDateAxis.set("baseInterval", { timeUnit: interval?.timeUnit, count: interval?.count });

            // console.log(interval)
            loadData("hour", min, max, "none");//////////////////////////////////////////
            // let result = await getKlineData(interval?.name);
            // valueSeries.data.setAll(result?.chartData);
            // volumeSeries?.data.setAll(result?.chartData);
            // sbSeries?.data.setAll(result?.chartData);
            // setIntervalData(interval);

        }

        // Stock toolbar
        // -------------------------------------------------------------------------------
        document.getElementById("chartcontrols").innerHTML = '';
        var toolbar = am5stock.StockToolbar.new(root, {
            container: document.getElementById("chartcontrols"),
            stockChart: stockChart,
            controls: [
                // am5stock.IndicatorControl.new(root, {
                //     stockChart: stockChart,
                //     legend: valueLegend
                // }),
                am5stock.PeriodSelector.new(root, {
                    stockChart: stockChart,
                    periods: [
                        { timeUnit: "minute", count: 1, name: "1m" },
                        { timeUnit: "minute", count: 30, name: "30m" },
                        { timeUnit: "hour", count: 1, name: "1h" },
                        { timeUnit: "hour", count: 10, name: "10h" },
                        { timeUnit: "day", count: 1, name: "1D" },
                        { timeUnit: "month", count: 1, name: "1M" },
                        { timeUnit: "week", count: 1, name: "1w" },
                        { timeUnit: "year", count: 1, name: "1Y" },
                        { timeUnit: "max", name: "Max" },
                    ]
                }),
                intervalControl,
                // seriesSwitcher,
                // am5stock.ResetControl.new(root, {
                //     stockChart: stockChart
                // }),
                // am5stock.SettingsControl.new(root, {
                //     stockChart: stockChart
                // })
            ]
        })

        async function loadData(unit, min, max, side, unit_count = 1) {
            // round min so that selected unit would be included
            min = am5.time.round(new Date(min), unit, 1).getTime();
            let unit_short = '1m';
            if (unit == 'minute') {
                if (unit_count == 1) {
                    unit_short = '1m';
                }
            } else {
                unit_short = unit;
            }
            currentUnit = unit
            var data = [];
                let res = await usdt_perpetual.derivativeKline({
                    symbol: symbol.toUpperCase() == 'TRXUSDT' ? 'TRONUSDT' : symbol.toUpperCase(),
                    interval: unit_short,
                    limit: limit,
                    startTime: min,
                    endTime: max
                })
                let klines_data = res?.data?.result;
                for (var i = 0; i < klines_data.length; i++) {
                    data.push({
                        date: klines_data[i]?.ot,
                        close: Number(klines_data[i]?.c),
                        open: Number(klines_data[i]?.o),
                        low: Number(klines_data[i]?.l),
                        high: Number(klines_data[i]?.h),
                        volume: Number(klines_data[i]?.v)
                    });
                }

            // Process data (convert dates and values)
            var processor = am5.DataProcessor.new(root, {
                numericFields: ["date", "open", "high", "low", "close", "volume"]
            });
            // console.log(side, "datadata", data)
            processor.processMany(data);
            var start = dateAxis.get("start");
            var end = dateAxis.get("end");
            // will hold first/last dates of each series
            var seriesFirst = {};
            var seriesLast = {};

            lastValue = valueSeries.data.length && valueSeries.data.getIndex(valueSeries.data.length - 1).close;

            // Set data
            if (side == "none") {
                if (data.length > 0) {
                    // change base interval if it's different
                    // if (dateAxis.get("baseInterval").timeUnit != unit) {
                    //     dateAxis.set("baseInterval", { timeUnit: unit, count: 1 });
                    //     sbDateAxis.set("baseInterval", { timeUnit: unit, count: 1 });
                    // }

                    // dateAxis.set("min", min);
                    // dateAxis.set("max", max);
                    // dateAxis.setPrivate("min", min);   // needed in order not to animate
                    // dateAxis.setPrivate("max", max);   // needed in order not to animate     

                    valueSeries.data.setAll(data);
                    volumeSeries.data.setAll(data);
                    sbSeries.data.setAll(data);

                    // dateAxis.zoom(0, 1, 0);

                }
            }
            else if (side == "left") {
                // save dates of first items so that duplicates would not be added
                seriesFirst[valueSeries.uid] = valueSeries.data.getIndex(0).date;
                seriesFirst[volumeSeries.uid] = volumeSeries.data.getIndex(0).date;
                seriesFirst[sbSeries.uid] = sbSeries.data.getIndex(0).date;

                for (var i = data.length - 1; i >= 0; i--) {
                    var date = data[i].date;
                    // only add if first items date is bigger then newly added items date
                    if (seriesFirst[valueSeries.uid] > date) {
                        valueSeries.data.unshift(data[i]);
                    }
                    if (seriesFirst[volumeSeries.uid] > date) {
                        volumeSeries.data.unshift(data[i]);
                    }
                    if (seriesFirst[sbSeries.uid] > date) {
                        sbSeries.data.unshift(data[i]);
                    }
                }

                // update axis min
                min = Math.max(min, absoluteMin);
                dateAxis.set("min", min);
                dateAxis.setPrivate("min", min); // needed in order not to animate
                // recalculate start and end so that the selection would remain
                dateAxis.set("start", 0);
                dateAxis.set("end", (end - start) / (1 - start));
            } else if (side == "right") {
                // save dates of last items so that duplicates would not be added
                // seriesLast[valueSeries.uid] = valueSeries.data.getIndex(valueSeries.data.length - 1).date;
                // seriesLast[volumeSeries.uid] = volumeSeries.data.getIndex(volumeSeries.data.length - 1).date;
                // seriesLast[sbSeries.uid] = sbSeries.data.getIndex(sbSeries.data.length - 1).date;

                // for (var i = 0; i < data.length; i++) {
                //     var date = data[i].date;
                //     // only add if last items date is smaller then newly added items date
                //     if (seriesLast[valueSeries.uid] < date) {
                //         valueSeries.data.push(data[i]);
                //     }
                //     if (seriesLast[volumeSeries.uid] < date) {
                //         volumeSeries.data.push(data[i]);
                //     }
                //     if (seriesLast[sbSeries.uid] < date) {
                //         sbSeries.data.push(data[i]);
                //     }
                // }
                // // update axis max
                // max = Math.min(max, absoluteMax);
                // dateAxis.set("max", max);
                // dateAxis.setPrivate("max", max); // needed in order not to animate

                // // recalculate start and end so that the selection would remain
                // dateAxis.set("start", start / end);
                // dateAxis.set("end", 1);
            }

            current_market = valueSeries.data.length && valueSeries.data.getIndex(valueSeries.data.length - 1).close;
            lastValue = valueSeries.data.length && valueSeries.data.getIndex(valueSeries.data.length - 1).open;
            if (current_market && currentLabel) {

                currentLabel.set("text", stockChart.getNumberFormatter().format(current_market));
                currentValueDataItem?.animate({ key: "value", to: current_market, duration: 500, easing: easing });
                bg = currentLabel?.get("background");
                // let mark_color = root.interfaceColors?.get(lastValue > current_market ? "primaryButton" : "negative");
                // console.log("lastValue", lastValue)
                // console.log("current_market", current_market)

                let mark_color = (lastValue > current_market) ? DownColor : UpColor
                // console.log("default bg:", mark_color)
                if (bg) {
                    bg.set("fill", am5.color(mark_color));
                }
                currentGrid = currentValueDataItem?.get("grid");
                if (currentGrid) {
                    currentGrid.setAll({ strokeOpacity: 0.5, strokeDasharray: [2, 5], stroke: am5.color(mark_color) });
                }
            }
        }

        function loadSomeData() {
            var start = dateAxis.get("start");
            var end = dateAxis.get("end");

            var selectionMin = Math.max(dateAxis.getPrivate("selectionMin"), absoluteMin);
            var selectionMax = Math.min(dateAxis.getPrivate("selectionMax"), absoluteMax);
            var min = dateAxis.getPrivate("min");
            var max = dateAxis.getPrivate("max");

            // if start is less than 0, means we are panning to the right, need to load data to the left (earlier days)
            if (start < 0) {
                loadData(currentUnit, selectionMin, min, "left");
            }
            // if end is bigger than 1, means we are panning to the left, need to load data to the right (later days)
            if (end > 1) {
                loadData(currentUnit, max, selectionMax, "right");
            }
        }

        mainPanel.events.on("panended", async () => {
            counter = counter + 1;
            loadSomeData(counter);
        });

        var wheelTimeout
        stockChart.events.on("wheel", async () => {
            if (wheelTimeout) {
                wheelTimeout.dispose();
            }

            wheelTimeout = await stockChart.setTimeout(async () => {
                counter = counter + 1;
                loadSomeData(counter);
            }, 500);
        });

        loadData(currentUnit, min, max, "none");
    }

    return () => {
        if (root) {
            root.dispose();
            // socket.emit('leave_kline', {
            //     event: `SPOT_KLINE`,
            // });
        }
    }
}, [symbol, mode, props.exchangeSource, props?.priceDecimal])

any help is appreciated

tinegaCollins commented 1 year ago

image i don't know if this helps

martynasma commented 1 year ago

Would you be able to post your whole chart and data on CodePen/jsFiddle so that we can test?