Kaktana / kaktana-react-lightweight-charts

A simple react wrapper for the tradingview lightweight charts module
MIT License
106 stars 55 forks source link

Realtime update Series #4

Open kentdev92 opened 4 years ago

kentdev92 commented 4 years ago

Thank you so much for your work hard sir. I'm currently using your lib to create my product. But i cannot found any function like series.update() to update realtime series data. So could you please help me to do this and update your repo

AurelReb commented 4 years ago

Hi, There is currently no relative function for update(). However, you can just change the series values and it will automatically update the chart in realtime.

I'll look into the update function to maybe implement it.

jaredpeck commented 3 years ago

Hi, There is currently no relative function for update(). However, you can just change the series values and it will automatically update the chart in realtime.

I'll look into the update function to maybe implement it.

Hi @AurelReb,

My partner and I are working to making a live lineseries chart but were unable to simply add objects to the lineseries data variable to get the chart update. How do you recommend we add new data to the lineseries without the update() function from the original library?

Cheers, Jared

AurelReb commented 3 years ago

Hi, Here is an example:

​class​ ​App​ ​extends​ ​Component​ ​{​
  ​constructor​(​props​)​ ​{​
    ​super​(​props​)​;​

    ​this​.​state​ ​=​ ​{​
      ​options​: ​{​
        ​alignLabels​: ​true​,​
        ​timeScale​: ​{​
          ​rightOffset​: ​12​,​
          ​barSpacing​: ​3​,​
          ​fixLeftEdge​: ​true​,​
          ​lockVisibleTimeRangeOnResize​: ​true​,​
          ​rightBarStaysOnScroll​: ​true​,​
          ​borderVisible​: ​false​,​
          ​borderColor​: ​"#fff000"​,​
          ​visible​: ​true​,​
          ​timeVisible​: ​true​,​
          ​secondsVisible​: ​false​
        ​}​
      ​}​,​
      lineSeries: ​[​{​
        ​data​: ​[
          { time: '2019-04-11', value: 80.01 },
          { time: '2019-04-12', value: 96.63 },
          { time: '2019-04-13', value: 76.64 },
          { time: '2019-04-14', value: 81.89 },
          { time: '2019-04-15', value: 74.43 },
          { time: '2019-04-16', value: 80.01 },
          { time: '2019-04-17', value: 96.63 },
          { time: '2019-04-18', value: 76.64 },
          { time: '2019-04-19', value: 81.89 },
          { time: '2019-04-20', value: 74.43 },
        ]
      ​}​]​
    ​}​
  ​}​

  addValue = () => {
    let newlineSeries = [...this.state.lineSeries, {time: '2019-04-21', value: 100}];
    this.setState({lineSeries: newlineSeries});

  ​render​(​)​ ​{​
    ​return​ ​(
      <>
      ​  <​Chart​ ​options​=​{​this​.​state​.​options​}​ ​lineSeries=​{​this​.​state​.​lineSeries}​ ​autoWidth​ ​height​=​{​320​}​ /​>​
        <button onClick={this.addValue}>Add</button>
    </>
    ​)​
  ​}​
​}

When you click on the button, the value 100 will automatically be added to the series. You can do the same for your live update.

burnside commented 3 years ago

This issue was why I had to transition away from kaktana charts. I found a way to do it with React.useEffect() (Requires react > 16.8 I believe?) directly using lightweight-charts:

import React from 'react';
import { createChart } from 'lightweight-charts';
import PropTypes from 'prop-types';

const HEIGHT = 300;

let chart;
let candlestickSeries;

const CandleChart = ({legend, initCandles, lastCandle, decimals}) => {
    const chartRef = React.useRef();
    const legendRef = React.useRef();

    React.useEffect(() => {
        chart = createChart(chartRef.current, {
            width: chartRef.current.offsetWidth,
            height: HEIGHT,
            alignLabels: true,
            timeScale: {
                rightOffset: 0,
                barSpacing: 15,
                fixLeftEdge: false,
                lockVisibleTimeRangeOnResize: true,
                rightBarStaysOnScroll: true,
                borderVisible: false,
                borderColor: '#fff000',
                visible: true,
                timeVisible: true,
                secondsVisible: false
            },
            rightPriceScale: {
                scaleMargins: {
                    top: 0.3,
                    bottom: 0.25,
                },
                borderVisible: false,
            },
            priceScale: {
                autoScale: true,
            },
            watermark: {
                color: 'rgba(0, 0, 0, 0.7)',
                visible: true,
                text: 'TxQuick',
                fontSize: 18,
                horzAlign: 'left',
                vertAlign: 'bottom',
            },
        });

        candlestickSeries = chart.addCandlestickSeries({
            priceScaleId: 'right',
            upColor: '#00AA00',
            downColor: '#AA0000',
            borderVisible: false,
            wickVisible: true,
            borderColor: '#000000',
            wickColor: '#000000',
            borderUpColor: '#00AA00',
            borderDownColor: '#AA0000',
            wickUpColor: '#00AA00',
            wickDownColor: '#AA0000',
            priceFormat: {
                type: 'custom',
                minMove: '0.00000001',
                formatter: (price) => {
                    return parseFloat(price).toFixed(decimals);
                }
            },
        });

        candlestickSeries.setData(initCandles);
    }, []);

    React.useEffect(() => {
        candlestickSeries.update(lastCandle);
    }, [lastCandle]);

    React.useEffect(() => {
        const handler = () => {
            chart.resize(chartRef.current.offsetWidth, HEIGHT);
        };
        window.addEventListener('resize', handler);
        return () => {
            window.removeEventListener('resize', handler);
        };
    }, []);

    return (
        <div ref={chartRef} id='chart' style={{'position': 'relative', 'width': '100%'}}>
            <div
                ref={legendRef}
                style={{
                    position: 'absolute',
                    zIndex: 2,
                    color: '#333',
                    padding: 10,
                }}
            >
                {legend}
            </div>
        </div>
    );
};

CandleChart.propTypes = {
    legend: PropTypes.string,
    initCandles: PropTypes.array,
    lastCandle: PropTypes.object,
    decimals: PropTypes.number,
};

export default CandleChart;

My data was coming in at 1s intervals, so you couldn't navigate around the chart at all because it would render the chart fresh with every update. This fixes that issue. This also auto-resizes and implements a legend.

Maybe this approach could be worked into kaktana charts?

Regardless, I hope this helps someone out there. :)

jaredpeck commented 3 years ago

Thanks to the both of you for your help. I will go back to using the original library.

CalebEverett commented 3 years ago

Thank you @burnside for the nice solution. I was trying to get the real time update hooked up to a websocket and found as part of of the initial rendering that candlestickSeires.update(lastCandle) was getting called with an empty object, which it didn't like.

Here was my solution:

useEffect(() => {
  if (!(lastCandle && Object.keys(lastCandle).length === 0 && lastCandle.constructor === Object)) {
      candlestickSeries.update(lastCandle);
  }

}, [lastCandle]);

Also found that adding

return () => chart.remove()

after

candlestickSeries.setData(initCandles);

eliminated an initial rendering of a blank chart.

CalebEverett commented 3 years ago

I'm actually having another issue now, which is that when I try to add two chart components to the same page, only the second stays rendered. It also seems like the second one is hooked up to the websocket from the first one. Can anybody see where I might be going wrong:

const CandleChart = ({ market, symbol, decimals }) => {
    // https://github.com/tradingview/lightweight-charts/blob/master/docs/customization.md

    const classes = useStyles();
    const chartRef = useRef();
    const [initCandles, setInitCandles] = useState([])
    const [lastCandle, setLastCandle] = useState({})
    const wsRef = useRef()
    const theme = useTheme();

    useEffect(() => {
        const apiUrl = `http://localhost:8000/klines/${market}/${symbol}`

        async function getInitCandles() {
            const response = await fetch(apiUrl);
            const data = await response.json();
            setInitCandles(data);
        };

        getInitCandles();
    }
        , [market, symbol]);

    useEffect(() => {
        wsRef.current = new WebSocket(`ws://localhost:8000/ws/${market}/${symbol}`)
        wsRef.current.onmessage = (event) => {
            console.log(market, event.data)
            setLastCandle(JSON.parse(event.data))

        };
        return () => wsRef.current.close()
    }
        , [market, symbol]);

    useEffect(() => {
        chart = createChart(chartRef.current, {
            width: chartRef.current.offsetWidth,
            height: HEIGHT,
            alignLabels: true,
            timeScale: {
                rightOffset: 0,
                barSpacing: 15,
                fixLeftEdge: false,
                lockVisibleTimeRangeOnResize: true,
                rightBarStaysOnScroll: true,
                borderVisible: false,
                visible: true,
                timeVisible: true,
                secondsVisible: true
            },
            rightPriceScale: {
                scaleMargins: {
                    top: 0.3,
                    bottom: 0.25,
                },
                borderVisible: false,
            },
            priceScale: {
                autoScale: true,
            },
            grid: {
                vertLines: {
                    color: theme.palette.action.secondary,
                    style: 4

                },
                horzLines: {
                    color: theme.palette.action.secondary,
                    style: 4
                },
            },

            layout: {
                fontFamily: theme.typography.fontFamily,
                backgroundColor: theme.palette.background.paper,
                textColor: theme.palette.text.secondary
            }
        });

        candlestickSeries = chart.addCandlestickSeries({
            priceScaleId: 'right',
            upColor: theme.palette.success.main,
            downColor: theme.palette.error.main,
            borderVisible: false,
            wickVisible: true,
            priceFormat: {
                type: 'custom',
                minMove: '0.00000001',
                formatter: (price) => {
                    return parseFloat(price).toFixed(decimals);
                }
            },
        });

        candlestickSeries.setData(initCandles);

        return () => chart.remove()

    }, [initCandles, decimals]);

    useEffect(() => {
        if (!(lastCandle && Object.keys(lastCandle).length === 0 && lastCandle.constructor === Object)) {
            candlestickSeries.update(lastCandle);
        }

    }, [lastCandle]);

    useEffect(() => {
        const handler = () => {
            chart.resize(chartRef.current.offsetWidth, HEIGHT);
        };
        window.addEventListener('resize', handler);
        return () => {
            window.removeEventListener('resize', handler);
        };
    }, []);

    return (
        <div ref={chartRef} id={`${symbol}${market}chart`} style={{ 'position': 'relative', 'width': '100%' }}></div>
    );
};

CandleChart.propTypes = {
    market: PropTypes.string,
    symbol: PropTypes.string,
    decimals: PropTypes.number,
};

export default CandleChart;
burnside commented 3 years ago

@CalebEverett - When reading your description of your issue I thought you might have an overlapping div id, but I see in your code you caught the div id issue. I had a similar problem displaying multiple charts and used this to do the div id's randomly:

const DIVID = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10);

That's all I can think of off the top of my head though. I haven't tried to hook up separate websockets per chart.

CalebEverett commented 3 years ago

@burnside Thanks so much - Ok, I'll give that a shot. I think it's close. When I'm console.logging the separate streams in the websocket useEffect, they are both showing up, so seems likely a reference issue.

CalebEverett commented 3 years ago

@burnside I think I got is sorted. The global chart and candlestickSeries variables were the issue. I moved those down inside the CandleChart, using useRef for good measure and things seem to be working.

CalebEverett commented 3 years ago

here's the modified component in case it helps somebody - thanks again for getting this going!:

import { Card, CardContent, Grid } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import { useTheme } from '@material-ui/core/styles'
import React, { useEffect, useRef, useState } from 'react';
import { createChart } from 'lightweight-charts';
import PropTypes from 'prop-types';

const HEIGHT = 300;

const useStyles = makeStyles({
    root: {
        width: "100%",
    }
});

const CandleChart = ({ market, symbol, decimals }) => {
    // https://github.com/tradingview/lightweight-charts/blob/master/docs/customization.md

    const classes = useStyles();
    const elRef = useRef();
    const chartRef = useRef()
    const candlestickSeriesRef = useRef()
    const [initCandles, setInitCandles] = useState([])
    const [lastCandle, setLastCandle] = useState({})
    const theme = useTheme();

    useEffect(() => {
        const apiUrl = `http://localhost:8000/klines/${market}/${symbol}`

        async function getInitCandles() {
            const response = await fetch(apiUrl);
            const data = await response.json();
            setInitCandles(data);
        };

        getInitCandles();
    }
        , [market, symbol]);

    useEffect(() => {
        let ws = new WebSocket(`ws://localhost:8000/ws/${market}/${symbol}`)
        ws.onmessage = (event) => {
            setLastCandle(JSON.parse(event.data))
        };
        return () => ws.close()
    }
        , [market, symbol]);

    useEffect(() => {
        chartRef.current = createChart(elRef.current, {
            width: elRef.current.offsetWidth,
            height: HEIGHT,
            alignLabels: true,
            timeScale: {
                rightOffset: 0,
                barSpacing: 15,
                fixLeftEdge: false,
                lockVisibleTimeRangeOnResize: true,
                rightBarStaysOnScroll: true,
                borderVisible: false,
                visible: true,
                timeVisible: true,
                secondsVisible: true
            },
            rightPriceScale: {
                scaleMargins: {
                    top: 0.3,
                    bottom: 0.25,
                },
                borderVisible: false,
            },
            priceScale: {
                autoScale: true,
            },
            grid: {
                vertLines: {
                    color: theme.palette.action.secondary,
                    style: 4

                },
                horzLines: {
                    color: theme.palette.action.secondary,
                    style: 4
                },
            },

            layout: {
                fontFamily: theme.typography.fontFamily,
                backgroundColor: theme.palette.background.paper,
                textColor: theme.palette.text.secondary
            }
        });

        candlestickSeriesRef.current = chartRef.current.addCandlestickSeries({
            priceScaleId: 'right',
            upColor: theme.palette.success.main,
            downColor: theme.palette.error.main,
            borderVisible: false,
            wickVisible: true,
            priceFormat: {
                type: 'custom',
                minMove: '0.00000001',
                formatter: (price) => {
                    return parseFloat(price).toFixed(decimals);
                }
            },
        });

        candlestickSeriesRef.current.setData(initCandles);

        return () => chartRef.current.remove()

    }, [initCandles]);

    useEffect(() => {
        if (!(lastCandle && Object.keys(lastCandle).length === 0 && lastCandle.constructor === Object)) {
            candlestickSeriesRef.current.update(lastCandle);
        }

    }, [lastCandle]);

    useEffect(() => {
        const handler = () => {
            chartRef.current.resize(elRef.current.offsetWidth, HEIGHT);
        };
        window.addEventListener('resize', handler);
        return () => {
            window.removeEventListener('resize', handler);
        };
    }, []);

    return (
        <Grid item xs={12} lg={6}>
            <Card className={classes.root}>
                <CardContent>
                    <Typography variant="h5" color="textSecondary">
                        {symbol} {market}
                    </Typography>
                    <div ref={elRef} style={{ 'position': 'relative', 'width': '100%' }}></div>
                </CardContent>
            </Card>
        </Grid>
    );
};

CandleChart.propTypes = {
    market: PropTypes.string,
    symbol: PropTypes.string,
    decimals: PropTypes.number,
};

export default CandleChart;