tradingview / lightweight-charts

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

React Component - integration #200

Closed jurepetrovic closed 2 years ago

jurepetrovic commented 5 years ago

How can I integrate this with React? Do I have to create custom react component?

Regards, Jure

AoDev commented 5 years ago

You need to render an element with a ref that you can use to get a reference to the dom element and mount the chart.

jurepetrovic commented 5 years ago

I did a custom react component for now. Just for test, but it seems ok for now. Obviously, I will need to add some props and state.

import * as React from 'react';
import { createChart } from 'lightweight-charts';

export class LightweightChart extends React.PureComponent {

    static defaultProps = {
        containerId: 'lightweight_chart_container',
    };

    chart = null;

    componentDidMount() {

        const chart = createChart(this.props.containerId, { width: 800, height: 600 });
        this.chart = chart;

        const lineSeries = chart.addLineSeries();

        lineSeries.setData([
            { time: '2019-04-10', value: 60.01 },
            { time: '2019-04-11', value: 80.01 },
        ]);

        const barSeries = chart.addBarSeries({
            thinBars: false,
        });

        // set the data
        barSeries.setData([
            { time: "2019-04-10", open: 141.77, high: 170.39, low: 120.25, close: 145.72 },
            { time: "2019-04-11", open: 145.72, high: 147.99, low: 100.11, close: 108.19 },
                 ]);
    }

    componentWillUnmount() {
        if (this.chart !== null) {
            this.chart.remove();
            this.chart = null;
        }
    }

    render() {
        return (
            <div
                id={ this.props.containerId }
                className={ 'LightweightChart' }
            />
        );
    }
}

Do you see any problems with that?

AoDev commented 5 years ago

Personally I don't code React apps that way. I always keep the state outside of the component. That said, your approach is fine, except that personally I would use a ref instead of a static id. (If you have a static id, you can't put more than one chart in your screen by the way.)

jurepetrovic commented 5 years ago

Yes, I noticed that while reading your "refs" link. Since I don't need more than one for now, I'll keep it simple ;)

How do you mean state outside component? You make extra component for the state or you keep it in parent component?

Thanks for your help!

ackimwilliams commented 4 years ago

@jurepetrovic were you able to resolve this issue? Still having this issue: lightweight-charts.esm.production.js:7 Uncaught (in promise) Error: Value is null

jurepetrovic commented 4 years ago

Yes, it worked with code above. Then I just included component where needed.

hypo-thesis commented 4 years ago

Yea this is not a solution as it creates another chart upon introducing state data.

hypo-thesis commented 4 years ago

I tackled this issue by using hooks in 2 separate useEffects as such :

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

let chart 
let candlestickSeries

export default ({lastCandle}) => {
    const myRef = React.useRef()
    // console.log('candles' , lastCandle)
    React.useEffect(() => {
         chart = createChart(myRef.current, { width: 1000, height: 800 });
        chart.applyOptions({
            timeScale: {
                rightOffset: 45,
                barSpacing: 15,
                lockVisibleTimeRangeOnResize: true,
                rightBarStaysOnScroll: true,
            },
            priceScale: {
                position: 'right',
                // mode: 1,
                autoScale: false,
                // invertScale: true,
                alignLabels: true,
                borderVisible: false,
                borderColor: '#555ffd',
                scaleMargins: {
                    top: 0.65,
                    bottom: 0.25,
                },
                crosshair: {
                    vertLine: {
                        color: '#6A5ACD',
                        width: 0.5,
                        style: 1,
                        visible: true,
                        labelVisible: false,
                    },
                    horzLine: {
                        color: '#6A5ACD',
                        width: 0.5,
                        style: 0,
                        visible: true,
                        labelVisible: true,
                    },
                    mode: 1,
                },
                grid: {
                    vertLines: {
                        color: 'rgba(70, 130, 180, 0.5)',
                        style: 1,
                        visible: true,
                    },
                    horzLines: {
                        color: 'rgba(70, 130, 180, 0.5)',
                        style: 1,
                        visible: true,
                    },
                },

            },
        });
         candlestickSeries = chart.addCandlestickSeries({
            upColor: '#0B6623',
            downColor: '#FF6347',
            borderVisible: false,
            wickVisible: true,
            borderColor: '#000000',
            wickColor: '#000000',
            borderUpColor: '#4682B4',
            borderDownColor: '#A52A2A',
            wickUpColor: "#4682B4",
            wickDownColor: "#A52A2A",
        });
    }, []);

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

    return (
        <>
            <div ref={myRef} id='chart' />
        </>
    );
}
jopfre commented 3 years ago

Here's the simplest implementation I could come up with using hooks.

import React, { useEffect } from 'react';
import { createChart } from 'lightweight-charts';

function Chart() {
  const ref = React.useRef();

  useEffect(() => {
    const chart = createChart(ref.current, { width: 400, height: 300 });
    const lineSeries = chart.addLineSeries();
    lineSeries.setData([
      { 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 },
    ]);
  }, []);

  return (
    <>
      <div ref={ref} />
    </>
  );
}

export default Chart;
jtdaugh commented 3 years ago

Here's a fairly good react wrapper: https://github.com/Kaktana/kaktana-react-lightweight-charts#readme

IbrahimSam96 commented 3 years ago

Anyone figured out how to do it using auto height and auto width ? Thanks

roalmeida commented 3 years ago

Here's the simplest implementation I could come up with using hooks.

import React, { useEffect } from 'react';
import { createChart } from 'lightweight-charts';

function Chart() {
  const ref = React.useRef();

  useEffect(() => {
    const chart = createChart(ref.current, { width: 400, height: 300 });
    const lineSeries = chart.addLineSeries();
    lineSeries.setData([
      { 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 },
    ]);
  }, []);

  return (
    <>
      <div ref={ref} />
    </>
  );
}

export default Chart;

For anyone having problem with duplicated chart when saving, just add a return inside useEffect

useEffect(() =>  {
  //... code 

    return () => {
      chart.remove()
    } 
}, [])
roalmeida commented 3 years ago

Anyone figured out how to do it using auto height and auto width ? Thanks

@IbrahimSam96 In my case I just omit the "with" to get 100%

IbrahimSam96 commented 3 years ago

Thanks for your response! Here is another way doing it by attaching an event listener "resize" and removing it in the useEfect clean up.

const  LivePortfolioGraph = (props) => {

const [width, setWidth] = useState(0);
const [zobe, setZobe] = useState([]);

useEffect( ()=> {

const handleResize = _debounce(() => {chart.applyOptions({width: props.g.current.clientWidth,   });  } ,100 ); 

window.addEventListener("resize", handleResize); 

const chart = createChart(props.g.current, {
        width: width,
        height: 450,
        layout: {
            textColor: '#d1d4dc',
            backgroundColor: '#131722',
        },
        rightPriceScale: {
            scaleMargins: {
                top: 0.3,
                bottom: 0.25,
            },
        },
        timeScale: {
            visible: true,
        },
        crosshair: {
            vertLine: {
                width: 5,
                color: 'rgba(224, 227, 235, 0.1)',
                style: 0,
            },
            horzLine: {
                visible: false,
                labelVisible: true,
            },
        },
        localization: {
            priceFormatter: price => {
            const nf = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
             return   nf.format(price)
            },

        },    
        grid: {
            vertLines: {
                color: 'rgba(42, 46, 57, 0)',
            },
            horzLines: {
                color: 'rgba(42, 46, 57, 0)',
            },
        },
});

var areaSeries = chart.addAreaSeries({
topColor: 'rgba(38, 198, 218, 0.04)',
bottomColor: 'rgba(38, 198, 218, 0.04)',
lineColor: 'rgba(38, 198, 218, 1)',
lineWidth: 2,
crossHairMarkerVisible: false,
});

// Clean Up
return () => {
window.removeEventListener('resize', handleResize);

}

}, [zobe])

return (

<div style={{position:"relative", left:"5%", height:"100%", width:"100%", display:"none"}}>

</div>

)

}

export default LivePortfolioGraph;
cryptofish7 commented 3 years ago

Here's the simplest implementation I could come up with using hooks.

import React, { useEffect } from 'react';
import { createChart } from 'lightweight-charts';

function Chart() {
  const ref = React.useRef();

  useEffect(() => {
    const chart = createChart(ref.current, { width: 400, height: 300 });
    const lineSeries = chart.addLineSeries();
    lineSeries.setData([
      { 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 },
    ]);
  }, []);

  return (
    <>
      <div ref={ref} />
    </>
  );
}

export default Chart;

Anyone managed to get this working in typescript? For me it doesn't like the fact that ref.current is of type undefined:

Argument of type 'undefined' is not assignable to parameter of type 'string | HTMLElement'.  TS2345

    30 |
    31 |   useEffect(() => {
  > 32 |     const chart = createChart(ref.current, { width: 400, height: 300 })
       |                               ^
    33 |     const candlestickSeries = chart.addCandlestickSeries()
    34 |     candlestickSeries.setData(candleData)
    35 |   }, [])
ryancwalsh commented 3 years ago

@jopfre Thanks for https://github.com/tradingview/lightweight-charts/issues/200#issuecomment-748493961! I was running into bugs with the React wrapper https://github.com/Kaktana/kaktana-react-lightweight-charts that I'm not encountering with your approach.

acrabb commented 3 years ago

Here's the simplest implementation I could come up with using hooks.

Thanks for this @jopfre !

I'm still getting an error...how do we then use this in another component's render function? Note: I passed in the ref trying to fix the issue and its still happening...

render() {
        return (
            <>
                {Chart(React.useRef())}
            </>
        );
    }

?? TIA!

Side note...I'm a mobile developer learning react...why are hooks needed for this at all?

UPDATE: Got it working...~really don't know how~ 😅 I tried some stuff...then hit Ctl+Z until I was back to a certain point...and then it worked. Typical 😂

here's how

render() {
        return (
            <>
                <Chart />
            </>
        );
    }
acrabb commented 3 years ago

I noticed that useEffect isn't getting called after the first render. Is anyone else seeing this? Problem here is the chart isn't receiving updated params when the state changes in the parent component.

UPDATE If I add the arrays of data I use to the dependency list in useEffect then the chart WILL update. However, a new/updated chart will get added, BUT the old one will not get removed. So I end up with N number of charts where N is the number of times render was called in the wrapping component. Halp.

UPDATE 2 Used an approach based on this guy's code...not using hooks...and it works :) https://github.com/Kaktana/kaktana-react-lightweight-charts/blob/master/src/kaktana-react-lightweight-charts.js

acrabb commented 3 years ago
import React from "react";
import { createChart } from 'lightweight-charts';

class ChartWrapper extends React.Component {
    state = {
    }

    constructor(props) {
        super(props);
        this.chartDiv = React.createRef();
    }

    componentDidMount() {
        this.chart = createChart(this.chartDiv.current, {
            width: 800, height: 400,
            timeScale: {
                timeVisible: true,
                secondsVisible: false,
            },
            watermark: {
                visible: true,
                fontSize: 34,
                color: 'rgba(0, 0, 0, 0.25)',
            },
        });

        window.addEventListener("resize", this.resizeHandler);
        this.resizeHandler();
    }

    componentDidUpdate() {
        this.addPriceData(this.chart, this.props.priceData, this.props.tradeData)

        // "Setting state" for the chart
        var options = {
            watermark: {
                text: this.props.waterData
            }
        }
        this.chart.applyOptions(options)
    }

    render() {
        return (
            <div ref={this.chartDiv}
                style={{ position: "relative" }}>
            </div>
        );
    }

    // ------------------------------------------------------------------------
    resizeHandler = () => {
        this.chart.resize(this.chartDiv.current.parentNode.clientWidth, this.chartDiv.current.parentNode.clientHeight);
    }

    addLineData(chart, data) {
        const lineSeries = chart.addLineSeries();
        lineSeries.setData(data)
        console.log('added data to chart', data)
        return lineSeries
    }

    addPriceData(chart, data, tradeData) {
        if (!data) {
            return
        }

        if (data[0].value) {
            const lineSeries = this.addLineData(chart, data)
            this.addTradeMarkers(lineSeries, tradeData)
        } else if (data[0].open) {
            const lineSeries = chart.addCandlestickSeries();
            lineSeries.setData(data)
            this.addTradeMarkers(lineSeries, tradeData)
        }
    }

    addTradeMarkers(series, tradeData) {
        if (!series || !tradeData) {
            return
        }

        var markers = []
        tradeData.forEach((trade) => {
            if (trade.action === 'sell') {
                markers.push({ time: trade.time, position: 'aboveBar', color: '#e91e63', shape: 'arrowDown', text: 'Sell ' + trade.quantity });
            } else {
                markers.push({ time: trade.time, position: 'belowBar', color: '#2196F3', shape: 'arrowUp', text: 'Buy ' + trade.quantity });
            }
        })
        series.setMarkers(markers);
    }
}

export default ChartWrapper
andrewgarrison commented 3 years ago

useEffect(() => { const chart = createChart(ref.current, { width: 400, height: 300 }); const lineSeries = chart.addLineSeries(); lineSeries.setData([ { 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 }, ]); }, []);

I was able to get this working in TS by wrapping everything in the useEffect with a ref.current null check

useEffect(() => {
    if (ref.current) {
      const chart = createChart(ref.current, { width: 400, height: 300 });
      const lineSeries = chart.addLineSeries();
      lineSeries.setData([
        { 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 },
      ]);
    }
  }, [ref]);
iamgamelover commented 3 years ago

Anyone managed to get this working in typescript? For me it doesn't like the fact that ref.current is of type undefined:

Argument of type 'undefined' is not assignable to parameter of type 'string | HTMLElement'.  TS2345

    30 |
    31 |   useEffect(() => {
  > 32 |     const chart = createChart(ref.current, { width: 400, height: 300 })
       |                               ^
    33 |     const candlestickSeries = chart.addCandlestickSeries()
    34 |     candlestickSeries.setData(candleData)
    35 |   }, [])

@cryptofish7 I fixed it by passing a parameter:

function Chart(this: any) {
   const ref = React.useRef(this);
   ...

   return (....);

And using in the way:

<Flex>
   <Chart />
</Flex>
justin-pierce commented 2 years ago

Here's the simplest implementation I could come up with using hooks.

import React, { useEffect } from 'react';
import { createChart } from 'lightweight-charts';

function Chart() {
  const ref = React.useRef();

  useEffect(() => {
    const chart = createChart(ref.current, { width: 400, height: 300 });
    const lineSeries = chart.addLineSeries();
    lineSeries.setData([
      { 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 },
    ]);
  }, []);

  return (
    <>
      <div ref={ref} />
    </>
  );
}

export default Chart;

Anybody have tips on how to do legends or floating tooltips with this setup? They use document.body to place the chart and then add stuff to that -- not sure how to work with the useRef().current in the same way.

burgossrodrigo commented 2 years ago
import React, { useEffect, useRef } from 'react';
import { gql, useQuery  } from '@apollo/client';
import { createChart } from "lightweight-charts";
import { CircularProgress } from '@mui/material';
//import styled from 'styled-components'
/*
const StyledChart = styled.div`

  background-color: red;

`*/

const Bitquery = () =>{

  const chartRef = useRef(null);

  const CHART_DATA =  gql`
  query chart{
ethereum(network: bsc) {
dexTrades(
options: {limit: 1000, asc: "timeInterval.minute"}
date: {since: "2021-10-26"}
exchangeName: {is: "Pancake"}
baseCurrency: {is: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"}
quoteCurrency: {is: "0xe9e7cea3dedca5984780bafc599bd69add087d56"}
) {
timeInterval {
  minute(count: 5)
}
baseCurrency {
  symbol
  address
}
baseAmount
quoteCurrency {
  symbol
  address
}
quoteAmount
trades: count
quotePrice
maximum_price: quotePrice(calculate: maximum)
minimum_price: quotePrice(calculate: minimum)
open_price: minimum(of: block, get: quote_price)
close_price: maximum(of: block, get: quote_price)
}
}
}
`;

useEffect(()=> {
  if(chartRef.current){
    const chart = createChart(chartRef.current, {
      width: 600,
      height: 300,
      localization: {
        dateFormat: 'yyyy-mm-dd HH:mm:ss',
    }
    });

    prepareChart(chart);
  }
}, [])

        const { loading, data } = useQuery(CHART_DATA);

        console.log(data);

         function prepareChart(chart){

          let changedData = [];

          var areaSeries  = chart.addAreaSeries();

           data.ethereum.dexTrades.map((chart_) => {
            changedData.push({
              time: chart_.timeInterval.minute,
              value: chart_.maximum_price
            });

            console.log(changedData);
            return areaSeries.setData(changedData);

          })

        }

  if (loading){ return (<> < CircularProgress /></>) }
    else{
     return (<><div ref={chartRef} /></>);
  }

}

export default Bitquery;

This code only return the chart one time. After the refresh this stop working.

justin-pierce commented 2 years ago

This code only return the chart one time. After the refresh this stop working.

I'm new to React but my understanding is that in useEffect, you have an empty array at the end so it will only load once -- basically, you want to put something in there that you want to trigger a refresh (I use the data object used to populate the chart)

burgossrodrigo commented 2 years ago

This code only return the chart one time. After the refresh this stop working.

I'm new to React but my understanding is that in useEffect, you have an empty array at the end so it will only load once -- basically, you want to put something in there that you want to trigger a refresh (I use the data object used to populate the chart)

Thank you just the despair didn't allowed me to perceive that.

VIEWVIEWVIEW commented 2 years ago

Here's the simplest implementation I could come up with using hooks.

import React, { useEffect } from 'react';
import { createChart } from 'lightweight-charts';

function Chart() {
  const ref = React.useRef();

  useEffect(() => {
    const chart = createChart(ref.current, { width: 400, height: 300 });
    const lineSeries = chart.addLineSeries();
    lineSeries.setData([
      { 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 },
    ]);
  }, []);

  return (
    <>
      <div ref={ref} />
    </>
  );
}

export default Chart;

Anyone managed to get this working in typescript? For me it doesn't like the fact that ref.current is of type undefined:

Argument of type 'undefined' is not assignable to parameter of type 'string | HTMLElement'.  TS2345

    30 |
    31 |   useEffect(() => {
  > 32 |     const chart = createChart(ref.current, { width: 400, height: 300 })
       |                               ^
    33 |     const candlestickSeries = chart.addCandlestickSeries()
    34 |     candlestickSeries.setData(candleData)
    35 |   }, [])

I have to admit that the following type assertion is not beautiful, but it does its job:

  const ref = useRef() as React.MutableRefObject<HTMLDivElement>;

Here's a whole chart component.

import * as React from 'react';
import { createChart } from 'lightweight-charts';
import { useEffect, useRef, useState } from 'react'

interface ILineChart {
}

const LineChart: React.FunctionComponent<ILineChart> = (props) => {
  const ref = useRef() as React.MutableRefObject<HTMLDivElement>;

  useEffect(() => {
    const chart = createChart(ref.current, { width: 400, height: 300 });
    const lineSeries = chart.addLineSeries();
    lineSeries.setData([
      { 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 },
    ]);
    return () => {
      chart.remove()
    }
  }, []);

  return (
    <>
      <div ref={ref} />
    </>
  )
};

export default LineChart;
timocov commented 2 years ago

Btw we're going to prepare a tutorial how to make lightweight-charts work with react app so if anybody we'll have a time to look at #958 - feel free to leave any suggestions/ideas there.

timocov commented 2 years ago

The tutorials are deployed here now https://tradingview.github.io/lightweight-charts/tutorials. If you'll have any questions/suggestions/feedback - please create a discussion/issue.

jlubeck commented 3 months ago

Can you add a tutorial on how to add a tooltip with react? thank you!

The tutorials are deployed here now https://tradingview.github.io/lightweight-charts/tutorials. If you'll have any questions/suggestions/feedback - please create a discussion/issue.