2

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!

Leon Gaban
  • 36,509
  • 115
  • 332
  • 529
  • 1
    Could you replicate a minimal, working example of the problem in a live demo? To determinate if this is a plugin or Highcharts problem it would be useful if you could recreate the issue without use of the plugin. Right now your question looks more like a debugging problem. – Kacper Madej Feb 12 '16 at 16:36
  • I will as soon as I can! I'll have to host our exact HighCharts code somewhere and link to it from a plunkr, will comment here once that is done. – Leon Gaban Feb 12 '16 at 17:17
  • If you prefer to use another form of support, that allows more confidential exchange of data, you could contact Highcharts support using forum (you could use PM there) or via e-mail - http://www.highcharts.com/support. When the problem will be resolved we could post a solution here and the question will be answered. I hope that what I am writing in this comment is not forbidden on SO. – Kacper Madej Feb 15 '16 at 10:16
  • @KacperMadej Hi, I'm starting work on a Plnkr to replicate this bug, however stuck on another one at the moment, mind a look? http://stackoverflow.com/questions/35420784/typeerror-highchartsh-is-not-a-function – Leon Gaban Feb 15 '16 at 23:11
  • I see that [that question](http://stackoverflow.com/questions/35420784/typeerror-highchartsh-is-not-a-function) is resolved. Is this problem resolved too? – Kacper Madej Feb 17 '16 at 11:25
  • No not yet, and actually I didn't have time to finish that plnkr out yet. I did come up with a hack that works however. What I do is once the series are added then redrawn. I remove the Price line series, then readd it using cached data. That actually works, but of course it isn't ideal. One area we are looking at too is that the drawChart function gets called multiple times, because when you select a price, sometimes the timespan is reselected (refresh ui flow) So possibly the chart getting draw multiple times is the cause of this. Other dev is working on the problem now, will report back. – Leon Gaban Feb 17 '16 at 15:02

1 Answers1

2

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/>';
}
Leon Gaban
  • 36,509
  • 115
  • 332
  • 529