Search code examples
vue.jslightweight-charts

Time in marker wrong (TImezone?)


I have created this beautiful chart with Lightweight Charts. enter image description here

The problem that I now face is that in the marker on the x-axis (grey tooltip) the time displayed seems to be off by 2 hours.

I get the feeling that this is a timezone issue, as I am CEST which is UTC+2, but I could not figure out how and where this happened and how to fix it.

This is the code of the Vue SFC which generates the chart part

<template>
    <v-container
        fluid
        id="chart"
    >
        <v-btn-toggle
            v-model="interval"
            tile
            group
            @change="refresh"
        >
            <v-btn value="day">
                Intraday
            </v-btn>

            <v-btn value="week">
                Woche
            </v-btn>

            <v-btn value="month">
                Monat
            </v-btn>

            <v-btn value="year">
                Jahr
            </v-btn>

            <v-btn value="fiveyears">
                5 Jahre
            </v-btn>
        </v-btn-toggle>
        <Alert
            :message="errorMessage"
            type="error"
        ></Alert>
    </v-container>
</template>

<script>
    import { createChart, TickMarkType } from 'lightweight-charts';
    import ChartService from '@/service/ChartService'
    import Alert from '@/components/Alert'
    import FormatterService from "../../service/FormatterService";

    export default {
        name: "ShareChart",

        components: {
            Alert
        },

        props: [
          'quote',
          'bus'
        ],

        data() {
            return {
                errorMessage: '',
                chart: null,
                interval: 'day',
                series: null,
                format: null
            }
        },

        methods: {
            create() {
                const chart = createChart(
                    document.getElementById('chart'),
                    {
                        height: 200,
                        localization: {
                            locale: 'de-DE',
                            dateFormat: 'dd.MM.yyyy',
                            priceFormatter: price => this.format.eur(price)
                        },
                        handleScale: false,
                        handleScroll: false
                    });

                chart.applyOptions({
                    timeScale: {
                        timeVisible: true,
                        tickMarkFormatter: (time, tickMarkType) => {
                            time = new Date(time*1000)
                            let opt = {}

                            switch(tickMarkType) {
                                case TickMarkType.Year:
                                    opt = {
                                       year: 'numeric'
                                    }
                                    break
                                case TickMarkType.Month:
                                    opt = {
                                        month: 'short'
                                    }
                                    break
                                case TickMarkType.DayOfMonth:
                                    opt = {
                                        day: '2-digit'
                                    }
                                    break
                                case TickMarkType.Time:
                                    opt = {
                                        hour: '2-digit',
                                        minute: '2-digit'
                                    }
                                    break
                                case TickMarkType.TimeWithSeconds:
                                    opt = {
                                        hour: '2-digit',
                                        minute: '2-digit',
                                        seconds: '2-digit'
                                    }
                            }

                            return time.toLocaleString('de-DE', opt)
                        },
                    },
                })

                this.chart = chart
            },

            async getData() {
                let res = null

                try {
                    res = await ChartService.get(
                        this.interval,
                        {
                            isin: this.quote.isin,
                            date: new Date().toISOString(),
                            currency: this.quote.currency,
                            mic: this.quote.mic
                        }
                    )
                } catch (e) {
                    this.errorMessage = e
                    return null
                }

                return res.quotes
            },

            draw(datasets) {
                if(!this.chart) return
                if(!datasets) return

                if (this.series) this.chart.removeSeries(this.series);

                this.series = this.chart.addAreaSeries()

                let chartData = []

                for(let ds of datasets) {
                    chartData.push({
                        time: new Date(ds.date).getTime()/1000,
                        value: ds.value
                    })
                }

                this.series.setData(chartData)
                this.setAreaColors(datasets[0].value, datasets[datasets.length-1].value)
                this.setTimeRange()
            },

            setAreaColors(open, close) {
                let red = 0
                let green = 200

                if(open > close) {
                    red = 250
                    green = 0
                }

                this.series.applyOptions({
                    lineColor: 'rgb('+red+', '+green+', 0)',
                    topColor: 'rgba('+red+', '+green+', 0, 1)',
                    bottomColor: 'rgba('+red+', '+green+', 0, 0)'
                });
            },

            setTimeRange() {
                if(this.interval != 'day') {
                    return this.chart.timeScale().fitContent()
                }

                const fromTime = new Date()
                fromTime.setHours(8, 0, 0)

                const toTime = new Date()
                toTime.setHours(20, 0, 0)
                // TODO: Ignores timezones

                this.chart.timeScale().setVisibleRange({
                    from: fromTime.getTime() / 1000,
                    to: toTime.getTime() / 1000,
                });
            },

            async refresh() {
                const quotes = await this.getData()
                this.draw(quotes)
            }
        },

        mounted() {
            this.format = new FormatterService({
                currency: this.quote.currency
            })

            this.create()
            this.refresh()
        },

        watch: {
            quote: function () {
                this.refresh()
            }
        }
    }
</script>

<style scoped>

</style>

The purpose of the setTimeRange() function was to set the visible time range to 8am to 8pm local time for the 'intraday' chart. If there was no data anywhere in that timeframe, it should display white space, which does not work either.

Can someone assist?


Solution

  • Thanks to @timocov to pointing me to a related issue on the projects github. Turns out this is a known bug.

    This is how I worked around it:

    chart.applyOptions({
        timeScale: {
            timeVisible: true,
            tickMarkFormatter: (time, tickMarkType) => {
                time = this.getTimezoneCorrectedTime(time)
                    [
            ...]
            }
        }
    })
    

    getTimezoneCorrectedTime(utcTime, returnAsUnixTimestamp = false) {
        if(utcTime instanceof Date) {
            utcTime = utcTime.getTime()/1000
        }
    
        const timezoneOffsetMinutes = new Date().getTimezoneOffset()
        const correctedTime = utcTime+(timezoneOffsetMinutes*60)
    
        if(returnAsUnixTimestamp) return correctedTime
    
        return new Date(correctedTime*1000)
    }