timeseries.py 9.48 KB
Newer Older
Alan Mitchell's avatar
Alan Mitchell committed
1
import numpy as np, pandas as pd
Ian Moore's avatar
Ian Moore committed
2
from datetime import datetime
3
import pytz
4
import textwrap
5
import bmsapp.models, bmsapp.data_util
6
from . import basechart
7 8 9 10

class TimeSeries(basechart.BaseChart):
    """Chart class for creating Time Series graph.
    """
11

12
    # see BaseChart for definition of these constants
13
    CTRLS = 'refresh, ctrl_sensor, ctrl_avg, ctrl_use_rolling_averaging, ctrl_occupied, time_period_group, get_embed_link'
14 15
    MULTI_SENSOR = 1

16 17 18 19 20 21
    def result(self):
        """
        Returns the HTML and configuration for a Time Series chart.  
        """

        # Determine the sensors to plot. This creates a list of Sensor objects to plot.
22
        sensor_list = [ bmsapp.models.Sensor.objects.get(pk=id) for id in self.request_params.getlist('select_sensor_multi') ]
23 24 25

        # determine the Y axes that will be needed to cover the the list of sensor, based on the labels
        # of the units
Ian Moore's avatar
Ian Moore committed
26
        y_axes = {label:index for index, label in enumerate(list(set([sensor.unit.label for sensor in sensor_list])), start=1)}
27 28

        # get the requested averaging interval in hours
Ian Moore's avatar
Ian Moore committed
29 30
        if 'averaging_time' in self.request_params:
            averaging_hours = float(self.request_params['averaging_time'])
31 32 33 34
            if 'use_rolling_averaging' in self.request_params:
                use_rolling_averaging = True
            else:
                use_rolling_averaging = False
Ian Moore's avatar
Ian Moore committed
35 36
        else:
            averaging_hours = 0
37 38 39
            use_rolling_averaging = False


40 41 42 43 44

        # determine the start time for selecting records and loop through the selected
        # records to get the needed dataset
        st_ts, end_ts = self.get_ts_range()

45 46 47
        # Get the timezone for the building
        tz = pytz.timezone(self.timezone)

48 49 50 51 52 53
        # Create the series to plot and add up total points to plot and the points
        # in the longest series.
        series = []
        # determine suitable line width
        line_width = 1 if len(sensor_list) > 1 else 2
        for sensor in sensor_list:
54 55

            # get the database records
Ian Moore's avatar
Ian Moore committed
56
            df = self.reading_db.dataframeForOneID(sensor.sensor_id, st_ts, end_ts,tz)
57

Ian Moore's avatar
Ian Moore committed
58 59
            if not df.empty:
                # perform average (if requested)
60
                if averaging_hours:
61
                    df = bmsapp.data_util.resample_timeseries(df,averaging_hours,use_rolling_averaging)
62 63

                # create lists for plotly
64 65 66 67
                if np.absolute(df.val.values).max() < 10000:
                    values = np.char.mod('%.4g',df.val.values).astype(float).tolist()
                else:
                    values = np.round(df.val.values).tolist()
68 69 70 71 72
                times = df.index.strftime('%Y-%m-%d %H:%M:%S').tolist()
            else:
                times = []
                values = []

Ian Moore's avatar
Ian Moore committed
73 74 75 76
            series_opt = {'x': times,
                          'y': values,
                          'type': 'scatter',
                          'mode': 'lines', 
77
                          'name': sensor.title, 
Ian Moore's avatar
Ian Moore committed
78 79 80 81
                          'yaxis': 'y'+(str(y_axes[sensor.unit.label]) if y_axes[sensor.unit.label] > 1 else ''),
                          'line': {'width': line_width},
                         }

82 83
            # if the sensor has defined states, make the series a Step type series.
            if sensor.unit.measure_type == 'state':
84
                series_opt['line']['shape'] = 'hv'
85 86
            series.append( series_opt )

Ian Moore's avatar
Ian Moore committed
87 88 89 90 91 92 93
        # Set the basic chart options
        chart_type = 'plotly'
        opt = self.get_chart_options(chart_type)

        # set the chart data
        opt['data'] = series

94 95
        # If there there are more than 20 sensors, hide the legend
        if len(sensor_list) > 20:
96
            opt['layout']['showlegend'] = False
97

98
        opt['layout']['xaxis']['title'] =  "Date/Time (%s)" % self.timezone
Ian Moore's avatar
Ian Moore committed
99
        opt['layout']['xaxis']['type'] =  'date'
100
        opt['layout']['xaxis']['hoverformat'] = '%a %m/%d %H:%M'
101

102 103 104
        opt['layout']['annotations'] = []
        opt['layout']['shapes'] = []

105
        # Make the chart y axes configuration objects
Ian Moore's avatar
Ian Moore committed
106 107 108
        if len(y_axes) == 1:
            opt['layout']['margin']['l'] = 60
            opt['layout']['margin']['r'] = 20
109
            opt['layout']['yaxis']['title'] = list(y_axes.keys())[0]
Ian Moore's avatar
Ian Moore committed
110 111 112
        elif len(y_axes) == 2:
            opt['layout']['margin']['l'] = 60
            opt['layout']['margin']['r'] = 60
113
            y_axes_by_id = {v: k for k, v in y_axes.items()}
Ian Moore's avatar
Ian Moore committed
114 115 116 117 118 119 120 121 122
            opt['layout']['yaxis']['title'] = y_axes_by_id[1]
            opt['layout']['yaxis2'] = {'title': y_axes_by_id[2],
                                              'overlaying':'y',
                                              'side': 'right',
                                              'titlefont': opt['layout']['yaxis']['titlefont']}
        else:
            opt['layout']['margin']['l'] = 60
            opt['layout']['margin']['r'] = 20           
            opt['layout']['xaxis']['domain'] = [0.10 * (len(y_axes) - 1), 1]          
123
            for label, id in list(y_axes.items()):
Ian Moore's avatar
Ian Moore committed
124 125 126 127 128 129 130 131 132 133 134 135
                if id == 1:
                    opt['layout']['yaxis']['title'] = label
                else:
                    opt['layout']['yaxis'+str(id)] = {'title': label,
                                                      'overlaying':'y',
                                                      'side': 'left',
                                                      'anchor': 'free',
                                                      'position': 0.10 * (id - 2),
                                                      'ticks': 'outside',
                                                      'ticklen': 8,
                                                      'tickwidth': 1,
                                                      'titlefont': opt['layout']['yaxis']['titlefont']}
Ian Moore's avatar
Ian Moore committed
136

137

138 139
        # If occupied period shading is requested, do it, as long as data
        # is averaged over 1 day or less
140 141 142 143 144 145
        if ('show_occupied' in self.request_params):

            # get resolution to use in determining whether a timestamp is
            # occupied.
            resolution = self.occupied_resolution()  

146
            # determine the occupied periods
147
            if (self.schedule is None) or (resolution is None):
148
                # no schedule or data doesn't lend itself to classifying
149 150 151 152 153
                periods = [(st_ts, end_ts)]
            else:
                periods = self.schedule.occupied_periods(st_ts, end_ts, resolution=resolution)

            for occ_start, occ_stop in periods:
154 155 156 157 158 159 160 161 162 163 164 165
                band = {'type': 'rect',
                        'xref': 'x',
                        'layer': 'below',
                        'yref': 'paper',
                        'fillcolor': '#d0f5dd',
                        'opacity': 0.75,
                        'line': {'width': 0},
                        'x0': datetime.fromtimestamp(occ_start,tz).strftime('%Y-%m-%d %H:%M:%S'),
                        'y0': 0,
                        'x1': datetime.fromtimestamp(occ_stop,tz).strftime('%Y-%m-%d %H:%M:%S'),
                        'y1': 1
                        }
166
                opt['layout']['shapes'].append(band)
167
        
168
        # If the building has timeline annotations, add them to the chart
169 170 171 172
        if self.building.timeline_annotations:
            # Parse the timeline_annotations string
            t_a_list = self.building.timeline_annotations.splitlines()
            for t_a in t_a_list:
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
                try:
                    t_a_text, t_a_datetimestring = t_a.split(":",1)
                    t_a_ts = bmsapp.data_util.datestr_to_ts(t_a_datetimestring, tz)
                    if t_a_ts >= st_ts and t_a_ts <= end_ts:
                        # Add the text annotation to the top of the chart
                        opt['layout']['annotations'].append({
                                                             'x': datetime.fromtimestamp(t_a_ts,tz).strftime('%Y-%m-%d %H:%M:%S'),
                                                             'y': 1,
                                                             'xref': 'x',
                                                             'yref': 'paper',
                                                             'text': "<br>".join(textwrap.wrap(t_a_text,8,break_long_words=False)),
                                                             'showarrow': False,
                                                             'bgcolor': 'white'
                                                          })
                        # Add a vertical dotted line to the chart
                        opt['layout']['shapes'].append({
                                                        'type': 'line',
                                                        'xref': 'x',
                                                        'yref': 'paper',
                                                        'line': {'width': 1.25, 'color': 'black', 'dash': 'dot'},
                                                        'x0': datetime.fromtimestamp(t_a_ts,tz).strftime('%Y-%m-%d %H:%M:%S'),
                                                        'y0': 0,
                                                        'x1': datetime.fromtimestamp(t_a_ts,tz).strftime('%Y-%m-%d %H:%M:%S'),
                                                        'y1': 1
                                                        })
                except:
                    # ignore annotations that create errors
                    pass
201

202
        html = basechart.chart_config.chart_container_html(opt['layout']['title'])
203 204 205

        return {'html': html, 'objects': [(chart_type, opt)]}