1#!/usr/bin/env python
2#  Copyright (c) 2011 The WebRTC project authors. All Rights Reserved.
3#
4#  Use of this source code is governed by a BSD-style license
5#  that can be found in the LICENSE file in the root of the source
6#  tree. An additional intellectual property rights grant can be found
7#  in the file PATENTS.  All contributing project authors may
8#  be found in the AUTHORS file in the root of the source tree.
9
10__author__ = 'kjellander@webrtc.org (Henrik Kjellander)'
11
12class DataHelper(object):
13  """
14  Helper class for managing table data.
15  This class does not verify the consistency of the data tables sent into it.
16  """
17
18  def __init__(self, data_list, table_description, names_list, messages):
19    """ Initializes the DataHelper with data.
20
21    Args:
22      data_list: List of one or more data lists in the format that the
23        Google Visualization Python API expects (list of dictionaries, one
24        per row of data). See the gviz_api.DataTable documentation for more
25        info.
26      table_description: dictionary describing the data types of all
27        columns in the data lists, as defined in the gviz_api.DataTable
28        documentation.
29      names_list: List of strings of what we're going to name the data
30        columns after. Usually different runs of data collection.
31      messages: List of strings we might append error messages to.
32    """
33    self.data_list = data_list
34    self.table_description = table_description
35    self.names_list = names_list
36    self.messages = messages
37    self.number_of_datasets = len(data_list)
38    self.number_of_frames = len(data_list[0])
39
40  def CreateData(self, field_name, start_frame=0, end_frame=0):
41    """ Creates a data structure for a specified data field.
42
43    Creates a data structure (data type description dictionary and a list
44    of data dictionaries) to be used with the Google Visualization Python
45    API. The frame_number column is always present and one column per data
46    set is added and its field name is suffixed by _N where N is the number
47    of the data set (0, 1, 2...)
48
49    Args:
50      field_name: String name of the field, must be present in the data
51        structure this DataHelper was created with.
52      start_frame: Frame number to start at (zero indexed). Default: 0.
53      end_frame: Frame number to be the last frame. If zero all frames
54        will be included. Default: 0.
55
56    Returns:
57      A tuple containing:
58      - a dictionary describing the columns in the data result_data_table below.
59        This description uses the name for each data set specified by
60        names_list.
61
62        Example with two data sets named 'Foreman' and 'Crew':
63        {
64         'frame_number': ('number', 'Frame number'),
65         'ssim_0': ('number', 'Foreman'),
66         'ssim_1': ('number', 'Crew'),
67         }
68      - a list containing dictionaries (one per row) with the frame_number
69        column and one column of the specified field_name column per data
70        set.
71
72        Example with two data sets named 'Foreman' and 'Crew':
73        [
74         {'frame_number': 0, 'ssim_0': 0.98, 'ssim_1': 0.77 },
75         {'frame_number': 1, 'ssim_0': 0.81, 'ssim_1': 0.53 },
76        ]
77    """
78
79    # Build dictionary that describes the data types
80    result_table_description = {'frame_number': ('string', 'Frame number')}
81    for dataset_index in range(self.number_of_datasets):
82      column_name = '%s_%s' % (field_name, dataset_index)
83      column_type = self.table_description[field_name][0]
84      column_description = self.names_list[dataset_index]
85      result_table_description[column_name] = (column_type, column_description)
86
87    # Build data table of all the data
88    result_data_table = []
89    # We're going to have one dictionary per row.
90    # Create that and copy frame_number values from the first data set
91    for source_row in self.data_list[0]:
92      row_dict = {'frame_number': source_row['frame_number']}
93      result_data_table.append(row_dict)
94
95    # Pick target field data points from the all data tables
96    if end_frame == 0:  # Default to all frames
97      end_frame = self.number_of_frames
98
99    for dataset_index in range(self.number_of_datasets):
100      for row_number in range(start_frame, end_frame):
101        column_name = '%s_%s' % (field_name, dataset_index)
102        # Stop if any of the data sets are missing the frame
103        try:
104          result_data_table[row_number][column_name] = \
105          self.data_list[dataset_index][row_number][field_name]
106        except IndexError:
107          self.messages.append("Couldn't find frame data for row %d "
108          "for %s" % (row_number, self.names_list[dataset_index]))
109          break
110    return result_table_description, result_data_table
111
112  def GetOrdering(self, table_description):  # pylint: disable=R0201
113    """ Creates a list of column names, ordered alphabetically except for the
114      frame_number column which always will be the first column.
115
116      Args:
117        table_description: A dictionary of column definitions as defined by the
118          gviz_api.DataTable documentation.
119      Returns:
120        A list of column names, where frame_number is the first and the
121        remaining columns are sorted alphabetically.
122    """
123    # The JSON data representation generated from gviz_api.DataTable.ToJSon()
124    # must have frame_number as its first column in order for the chart to
125    # use it as it's X-axis value series.
126    # gviz_api.DataTable orders the columns by name by default, which will
127    # be incorrect if we have column names that are sorted before frame_number
128    # in our data table.
129    columns_ordering = ['frame_number']
130    # add all other columns:
131    for column in sorted(table_description.keys()):
132      if column != 'frame_number':
133        columns_ordering.append(column)
134    return columns_ordering
135
136  def CreateConfigurationTable(self, configurations):  # pylint: disable=R0201
137    """ Combines multiple test data configurations for display.
138
139    Args:
140      configurations: List of one ore more configurations. Each configuration
141      is required to be a list of dictionaries with two keys: 'name' and
142      'value'.
143      Example of a single configuration:
144      [
145        {'name': 'name', 'value': 'VP8 software'},
146        {'name': 'test_number', 'value': '0'},
147        {'name': 'input_filename', 'value': 'foreman_cif.yuv'},
148      ]
149    Returns:
150      A tuple containing:
151      - a dictionary describing the columns in the configuration table to be
152        displayed. All columns will have string as data type.
153        Example:
154        {
155          'name': 'string',
156          'test_number': 'string',
157          'input_filename': 'string',
158         }
159      - a list containing dictionaries (one per configuration) with the
160        configuration column names mapped to the value for each test run:
161
162        Example matching the columns above:
163        [
164         {'name': 'VP8 software',
165          'test_number': '12',
166          'input_filename': 'foreman_cif.yuv' },
167         {'name': 'VP8 hardware',
168          'test_number': '5',
169          'input_filename': 'foreman_cif.yuv' },
170        ]
171    """
172    result_description = {}
173    result_data = []
174
175    for configuration in configurations:
176      data = {}
177      result_data.append(data)
178      for values in configuration:
179        name = values['name']
180        value = values['value']
181        result_description[name] = 'string'
182        data[name] = value
183    return result_description, result_data
184