Search code examples
javascriptangularjschartshighchartshighcharts-ng

Price line disappears in HighChart


I'm using HighCharts and the Angular highcharts-ng directive. And most actions I take have a high likely hood of causing the price line vanish, or rather actually, not display.

  • Resizing the browser window, thus resizing chart (line vanishes 90%)
  • Changing the timespan (line vanishes about 20% of the time)
  • Changing the price line data (line vanishes about 10% of the time)
  • Adding or removing Area graphs to chart
  • No errors

^ The above % are based on feeling :(

enter image description here

^ Also note that the line graph inside of the focus timeline navigator also disappears. Also if you hover over the chart, the points for the missing line show up.


Below is what the chart should look like (however the focus line is gone below as well):

enter image description here

The chart I'm using is a type of multi chart where I have 1 line graph(stock price) and up to 3 area graphs charting other data. I'm also using highstocks 4.2.0. Our app is quite complicated and hard to replicate in plunkr.

enter image description here

Here is my chartConfig

generateChartObject();

function generateChartObject() {
    // Create the chart
    vs.chartConfig = {
        options: {
            ignoreHiddenSeries: false,
            credits: { enabled: true, text: 'tickertags' },
            legend: {
                itemStyle: {
                    color: "#333333",
                    cursor: "pointer",
                    fontSize: "10px",
                    fontWeight: "normal"
                },
                enabled: true,
                floating: true,
                align: 'left',
                verticalAlign: 'top',
                x: 60
            },
            chart : {
                title: { text: '' },
                subtitle: { text: '' },
                renderTo: 'chart1',
                zoomType: 'x',
                events: {
                    load: function () {
                        broadcastChartloaded();
                        vs.chartObject.hideLoading();
                    }
                }
            },
            scrollbar: {
                enabled: false,
                liveRedraw: false
            },
            navigator : {
                enabled: true,
                adaptToUpdatedData: true,
                series : {
                    // data : quote_data
                }
            },
            rangeSelector: {
                enabled: false,
            },
            tooltip: {
                pointFormatter: tooltipFormatter,
                shared: true
            },
            exporting: { enabled: false },
            plotOptions: {
                series: {
                    point: {
                        events: {
                            click: afterClick,
                        }
                    }
                },
                area: {
                    stacking: 'normal',
                },
                column: {
                    stacking: 'normal',
                }
            }
        },
        exporting: { enabled: false },
        useHighStocks: true,
        xAxis : {
            dateTimeLabelFormats : {
                hour: '%I %p',
                minute: '%I:%M %p'
            },
            events : {
                afterSetExtremes : afterSetExtremes,
                setExtremes : setExtremes
            },
            minRange: 3600 * 1000 // one hour
        },
        yAxis: [{ // Primary yAxis
            labels: {
                format: '${value:.2f}',
                style: {
                    color: '#4C73FF',
                }
            },
            title: {
                text: 'Price',
                style: {
                    color: '#4C73FF',
                }
            }
        },
        { // Secondary yAxis
            gridLineWidth: 0,
            title: {
                text: 'Mentions',
                style: {
                    color: '#FDE18D'
                }
            },
            labels: {
                formatter: volumeFormatter,
                style: {
                    color: '#FDE18D'
                }
            },
            opposite: false
        }],
        func: function(chart) {
            vs.chartObject = chart;
        }
    };
}

How my chart is generated:

Step 1 Chart a price line

Here in my tickersController I broadcast which ticker to graph a line for out to the chartController:

$scope.$emit("display.chart", vm.ticker.ticker);

My highChartsController receives the event and starts it's work:

$scope.$on("display.chart", function(event, ticker) {
    displayChart(ticker)
        .then(function (data) {
            // console.log('"displayChart TICKER data returned"',data);
            vs.chartObject.addSeries({
                zIndex: 1000,
                showInLegend: true,
                yAxis: 0,
                type: 'line',
                name: ticker,
                color: '#4C73FF',
                data: data,
                dataGrouping: {
                    enabled: true
                }
            }, true);

            checkForTags();

        }).catch(function(error) {
            console.error('error', error);
        });
    }
);

Here is the displayChart function with a Promise waiting on an API GET (The promise returns with the data in quote_data):

function displayChart(ticker) {
    console.log('displayChart()');

    var deferred = $q.defer(),
        promise;

    if (ticker === undefined) ticker = 'SPY';

    vs.ticker  = ticker; // Bugged, sometimes ticker is an Object
    vs._ticker = ticker; // Quick fix
    symbolUrls = {};

    var url = '/app/api/tickers/quotes/'+ticker;
    symbolUrls[ticker] = url;

    promise = ApiFactory.quotes(buildFullUrl('quotes', url)).then(function (data) {
        var quote_data = formatQuotes(data, 'quotes');

        // Remove any old chart series:
        if (!_.isEmpty(vs.chartObject)) {
            while(vs.chartObject.series.length > 0)
                vs.chartObject.series[0].remove(true);
        }

        deferred.resolve(quote_data);
    });

    return deferred.promise;
}

Now back in my eventListener $scope.$on("display.chart", function(event, ticker) {

After displayChart returns with the Promise and the data, I add the series, then call checkForTags():

displayChart(ticker)
    .then(function (data) {
        // console.log('"displayChart TICKER data returned"',data);
        vs.chartObject.addSeries({
            zIndex: 1000,
            showInLegend: true,
            yAxis: 0,
            type: 'line',
            name: ticker,
            color: '#4C73FF',
            data: data,
            dataGrouping: {
                enabled: true
            }
        }, true);

        checkForTags();

Step 2, GET and graph the area graphs:

function checkForTags() {
    var tags = TagFactory.retrieveTickerTags('all');
    // Up to 3 tags in the tags Array (The Area graphs):
    if (tags.length) {
        getTickerTags(tags);
    }
}

function getTickerTags(tags) {
    prepareTags(tags).then(function(data) {
        // Once data comes back from prepareTags, the loop below,
        // generates up to 3 tag Area Graphs in the chart:
        for (var i = data.length - 1; i >= 0; i--) {
            vs.chartObject.addSeries({
                showInLegend: true,
                zIndex: 900,
                yAxis: 1,
                type: 'area',
                // name: term,
                color: lookupColors[i],
                data: data[i],
                dataGrouping: {
                    enabled: false
                }
            }, true);

            if (i === 0) {
                handleRedraw(); // function below
            }
        }

    })
    .catch(function(error) {
        console.error('error', error);
    });
}

function prepareTags(tags) {
    var deferred        = $q.defer();
    var tagsComplete    = 0;
    var allTags         = [];

    // Request ALL Tag series quote data:
    for (var i=0; i<tags.length; i++) {
        var url = '/app/api/twitter/' + tags[i].term_id;

        getTagData(url, tags[i].term, term_positions[tags[i].term_id]).then(function(data) {
            // Push tag data into allTags Array:
            allTags.push( data );
            tagsComplete++;

            // When loop is complete, resolve all the Promises:
            if (tagsComplete === tags.length) {
                deferred.resolve(allTags);
            }
        });
    }

    return deferred.promise;
}

function getTagData(url, term, i) {
    var e = vs.chartObject.xAxis[0].getExtremes();
    var final_url = buildFullUrl(url, Math.round(e.min/1000), Math.round(e.max/1000));
    // Response returned:
    return ApiFactory.quotes(final_url).then(function(data) {
        // Formatted data returned:
        return formatQuotes(data);
    });
}

// Finally the handlRedraw function is called at the end:
function handleRedraw() {
    vs.chartObject.hideLoading();
    vs.chartObject.redraw();
}

Step 3, Resize the browser window.

Below is the window resize function with broadcasts an event to the chartController, then restoreChartSize is hit then finally changePeriodicity:

window.onresize = function(event) {
    $scope.$emit("window.resize");
    $scope.$emit("restore.chart");
};

function restoreChartSize() {
    if (!vs.chartObject.reflowNow) {
        vs.chartObject.reflowNow = vs.chartObject.reflowNow = function() {
            this.containerHeight = this.options.chart.height || $(this.renderTo).height();
            this.containerWidth  = this.options.chart.width  || $(this.renderTo).width();
            this.setSize(this.containerWidth, this.containerHeight, true);
            this.hasUserSize = null;
        }
    }

    vs.chartObject.reflowNow();
    changePeriodicity(vs.interval);
}

function changePeriodicity(newInterval) {
    vs.interval   = newInterval;
    var rightDate = new Date();
    var leftDate  = new Date();

    if ( newInterval == "hour" ) {
        leftDate.setHours(rightDate.getHours()-1);
    }
    else if ( newInterval == "day" ) {
        leftDate.setDate(rightDate.getDate()-1);
    }
    else if ( newInterval == "week" ) {
        leftDate.setDate(rightDate.getDate()-7);
    }
    else if ( newInterval == "month" ) {
        leftDate.setDate(rightDate.getDate()-30);
    }
    else if ( newInterval == "year" ) {
        leftDate.setDate(rightDate.getDate()-365);
    }
    if (vs.chartObject.xAxis) {
        vs.chartObject.showLoading('Loading data...');
        vs.chartObject.xAxis[0].setExtremes(leftDate.getTime(), rightDate.getTime());

    }
    // if (vs.chartObject.loadingShown) {
    //     handleRedraw();
    // }
}

Any thoughts or directions here are appreciated!


Solution

  • We finally frigging figured it out!!!

    It was this tooltipFormatter function that was screwing everything up for some reason.

    enter image description here

    After commenting it out, we can't get the Price line to disappear anymore! Still no idea why the bug happened in the first place though :o here is the tooltipFormatter function:

    function tooltipFormatter(that) {
        if (this.color === "#4C73FF" && this.y > 0) {
            this.y = '$' + this.y.formatMoney(2);
        } else if (this.y > 999) {
            this.y = this.y.formatCommas();
        }
        return '<span style="color:' + this.color + '">\u25CF</span> ' + this.series.name + ': <b>' + this.y + '</b><br/>';
    }