时间对比的错位问题

如果某个数据系列以某个时间为固定周期波动变化,我们通常会对其进行纵向的时间对比,比如某个生产服务器的带宽数据等等。

用echarts可以简单地实现多条数据系列同时显示,进行横向对比。但是在用其实现时间对比的时候遇到了麻烦,比如下图

两条数据虽然同时显示了,但是相邻位置的数据并不是一个相近的时间区间内的,显然这样的对比毫无意义。

先来分析一下数据内容。这两条数据是同一台服务器上相邻两天的带宽数据,这些数据每5分钟获取一次存入数据库。一般情况下,定时脚本执行插入的时间是固定的,插入的次数也是固定的,这样每天都数据量都相同,可以简单的进行对比。不过,现实的情况是,因为网络情况不稳定或者其他原因,每次插入数据库的时间并不相同,今天是00:01分插入数据,明天可能是00:03分插入数据。更糟糕的是,插入次数也是不稳定的,可能有些时间段内1小时插入12条数据,而另一个时间段只有3条/小时。在进行小范围的时间对比时这些影响不大,而如果需要以星期,月为区间进行对比,到后期同一位置的数据误差可能达到数个小时之久,我们需要一种更合理的方式处理这些数据的显示。

解决思路

在上面echarts图中,x轴的刻度是按照存储数据的时间定义的,类似这样

xAxis : [
        {
            type : 'category',
        }
]

在进行时间对比时,我们需要两条x轴分别代表两个时间序列。因为数据存入时间不稳定,两条x轴在图表中不能精确的对应上。因此,要解决时间错位的问题,就必须采用绝对时间轴,用类似散点的方式来展示数据,让数据适应标准轴而不是数据生成时间轴。

在highcharts中,可以指定x轴的类型为时间,自动生成x轴。如下

xAxis: [
    {
        type: 'datetime',
        minRange:3600000,
    }
],

需要注意的是应提前限定对比区间长度的一致,才能让同等长度的时间轴单位长度代表的时间等长。

然而图表并不随人愿,总是让数据平铺到整个时间轴上,让数据量不相等的两个系列占用同样的宽度。因此我们需要指定开始时间和结束时间,并告诉highcharts,让那些没有数据对应的时间点空着吧。

series: [
    {
        name: '{{key}}',
        visible: true,
        xAxis: {{num}},
        pointStart: Date.parse("{{start_point[key]}}"),
        pointInterval:3600000 * 24 * {{start_point['days']}} - 3600000 * 8,
        data: [null,{%for i in value %}[Date.parse("{{str(i['time'])[:-3]}}"),{{data}}],{%end%}null]
    },
]

这个数据系列与echarts的不同在于,这里data的数据类似散点图,指定了该数据对应的时间点。注意这里x轴和数据的时间是用js函数生成的timestamp,并且在数据首尾都有null占位。

这个图表的效果如下:

即使该时间点无数据,图表也会让它空在那,以保证同一时间的数据一一对应

解决方案

总结一下,解决对比数据时间错位的问题 1. 采用绝对时间轴 2. 指定每个数据对应的时间 3. 双x轴时间等长 4. 声明无数据位置留空

遇到的问题

  1. highcharts时间显示错误 如果x轴和数据的时间是用date对象的UTC方法产生的,需要注意这个方法获取的月份参数应该比实际月份少一。。。因为date对象里一月份对应的是0,二月份对应的是1。因此如果数据原始时间格式是%Y-%m-%d的话,不推荐用UTC方法。 而用上面写到的parse方法的话,在中国会存在时间错位8小时的问题,因为parse是根据当前时区(东8)的时间生成的timestamp,而highcharts是用标准时来解析的。解决方法是给highcharts一个全局变量指定时区偏移8小时,或者关闭UTC设置。详见下面代码

附完整代码(tornado框架)

$(function () {
    Highcharts.setOptions({
        global: {
            useUTC: false,
            // timezoneOffset: -8 * 60,
        }
    });
    $('#highcharts').highcharts({
        chart: {
            zoomType: 'x',
        },
        title: {
            text: false
        },
        subtitle: {
            text: '频道带宽(Mbps)'
        },
        xAxis: [
            {
                type: 'datetime',
                minRange:3600000,
            },
            {% if comp_time %}
            {   
                type: 'datetime',
                minRange:3600000,
                opposite: true
            },
            {% end %}
        ],
        yAxis: {
            title: {
                text: '带宽(Mbps)'
            }
        },
        legend: {
            enabled: {% if compchannel or comp_time %}true{%else%}false{%end%}
        },
        plotOptions: {
            area: {
                fillColor: {
                    linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1},
                    stops: [
                        [0, Highcharts.getOptions().colors[0]],
                        [1, Highcharts.Color(Highcharts.getOptions().colors[0]).setOpacity(0).get('rgba')]
                    ]
                },
                marker: {
                    radius: 2
                },
                lineWidth: 1,
                states: {
                    hover: {
                        lineWidth: 1
                    }
                },
                threshold: null,
            },
            series:{
                events: {
                  mouseOver: function(event){
                  }
                },
                cursor: 'pointer',
            }
        },
        {% if compchannel %}
        tooltip:{
            shared: true,
        },
        {% end %}
        {% if comp_time %}
        tooltip:{
            // shared: true,
            formatter:function(){
                return '<strong>'+Highcharts.dateFormat('%Y-%m-%d %H:%M',this.x)+'</strong><br />'
                +'带宽: '+this.y+' Mbps/s';
            }
        },
        {% end %}
        series: [
            {% if comp_time %}
                {% for num,(key,value) in enumerate(band.items()) %}
                    {
                        name: '{{key}}',
                        visible: true,
                        xAxis: {{num}},
                        pointStart: Date.parse("{{start_point[key]}}"),
                        pointInterval:3600000 * 24 * {{start_point['days']}} - 3600000 * 8,
                        data: [null,{%for i in value %}[Date.parse("{{str(i['time'])[:-3]}}"),{{float('%.2f'%(i['bandbit']/1000000))}}],{%end%}null]
                    },
                {% end %}
            {% else %}
                {% if compchannel %}
                    {% for key,value in band.items() %}
                        {% if key != 'total'%}
                            {
                                name: '{{key}}',
                                visible: true,
                                data: [{%for i in value %}[Date.parse("{{str(i['time'])[:-3]}}"),{{float('%.2f'%(i['bandbit']/1000000))}}],{%end%}]
                            },
                        {% end %}
                    {% end %}
                {% else %}
                    {% for key,value in band.items() %}
                        {% if key == 'total'%}
                            {
                                type: 'area',
                                name: '带宽总和',
                                pointStart: Date.parse("{{str(value[0]['time'])}}"),
                                data: [{%for i in value %}[Date.parse("{{str(i['time'])[:-3]}}"),{{float('%.2f'%(i['bandbit']/1000000))}}],{%end%}]
                            },
                        {% end %}
                    {% end %}
                {% end %}
            {% end %}
        ]
    });
});