Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(231)

Side by Side Diff: frontend/croschart/gviz_python/gviz_api.py

Issue 6821082: Integrate dynamic charts into autotest frontend. (Closed) Base URL: ssh://gitrw.chromium.org:9222/autotest.git@master
Patch Set: Make multi-chart pages look like current pages. Created 9 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2009 Google Inc.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 """Converts Python data into data for Google Visualization API clients.
18
19 This library can be used to create a google.visualization.DataTable usable by
20 visualizations built on the Google Visualization API. Output formats are raw
21 JSON, JSON response, and JavaScript.
22
23 See http://code.google.com/apis/visualization/ for documentation on the
24 Google Visualization API.
25 """
26
27 __author__ = "Amit Weinstein, Misha Seltzer"
28
29 import cgi
30 import datetime
31 import types
32
33
34 class DataTableException(Exception):
35 """The general exception object thrown by DataTable."""
36 pass
37
38
39 class DataTable(object):
40 """Wraps the data to convert to a Google Visualization API DataTable.
41
42 Create this object, populate it with data, then call one of the ToJS...
43 methods to return a string representation of the data in the format described.
44
45 You can clear all data from the object to reuse it, but you cannot clear
46 individual cells, rows, or columns. You also cannot modify the table schema
47 specified in the class constructor.
48
49 You can add new data one or more rows at a time. All data added to an
50 instantiated DataTable must conform to the schema passed in to __init__().
51
52 You can reorder the columns in the output table, and also specify row sorting
53 order by column. The default column order is according to the original
54 table_description parameter. Default row sort order is ascending, by column
55 1 values. For a dictionary, we sort the keys for order.
56
57 The data and the table_description are closely tied, as described here:
58
59 The table schema is defined in the class constructor's table_description
60 parameter. The user defines each column using a tuple of
61 (id[, type[, label[, custom_properties]]]). The default value for type is
62 string, label is the same as ID if not specified, and custom properties is
63 an empty dictionary if not specified.
64
65 table_description is a dictionary or list, containing one or more column
66 descriptor tuples, nested dictionaries, and lists. Each dictionary key, list
67 element, or dictionary element must eventually be defined as
68 a column description tuple. Here's an example of a dictionary where the key
69 is a tuple, and the value is a list of two tuples:
70 {('a', 'number'): [('b', 'number'), ('c', 'string')]}
71
72 This flexibility in data entry enables you to build and manipulate your data
73 in a Python structure that makes sense for your program.
74
75 Add data to the table using the same nested design as the table's
76 table_description, replacing column descriptor tuples with cell data, and
77 each row is an element in the top level collection. This will be a bit
78 clearer after you look at the following examples showing the
79 table_description, matching data, and the resulting table:
80
81 Columns as list of tuples [col1, col2, col3]
82 table_description: [('a', 'number'), ('b', 'string')]
83 AppendData( [[1, 'z'], [2, 'w'], [4, 'o'], [5, 'k']] )
84 Table:
85 a b <--- these are column ids/labels
86 1 z
87 2 w
88 4 o
89 5 k
90
91 Dictionary of columns, where key is a column, and value is a list of
92 columns {col1: [col2, col3]}
93 table_description: {('a', 'number'): [('b', 'number'), ('c', 'string')]}
94 AppendData( data: {1: [2, 'z'], 3: [4, 'w']}
95 Table:
96 a b c
97 1 2 z
98 3 4 w
99
100 Dictionary where key is a column, and the value is itself a dictionary of
101 columns {col1: {col2, col3}}
102 table_description: {('a', 'number'): {'b': 'number', 'c': 'string'}}
103 AppendData( data: {1: {'b': 2, 'c': 'z'}, 3: {'b': 4, 'c': 'w'}}
104 Table:
105 a b c
106 1 2 z
107 3 4 w
108 """
109
110 def __init__(self, table_description, data=None, custom_properties=None):
111 """Initialize the data table from a table schema and (optionally) data.
112
113 See the class documentation for more information on table schema and data
114 values.
115
116 Args:
117 table_description: A table schema, following one of the formats described
118 in TableDescriptionParser(). Schemas describe the
119 column names, data types, and labels. See
120 TableDescriptionParser() for acceptable formats.
121 data: Optional. If given, fills the table with the given data. The data
122 structure must be consistent with schema in table_description. See
123 the class documentation for more information on acceptable data. You
124 can add data later by calling AppendData().
125 custom_properties: Optional. A dictionary from string to string that
126 goes into the table's custom properties. This can be
127 later changed by changing self.custom_properties.
128
129 Raises:
130 DataTableException: Raised if the data and the description did not match,
131 or did not use the supported formats.
132 """
133 self.__columns = self.TableDescriptionParser(table_description)
134 self.__data = []
135 self.custom_properties = {}
136 if custom_properties is not None:
137 self.custom_properties = custom_properties
138 if data:
139 self.LoadData(data)
140
141 @staticmethod
142 def _EscapeValueForCsv(v):
143 """Escapes the value for use in a CSV file.
144
145 Puts the string in double-quotes, and escapes any inner double-quotes by
146 doubling them.
147
148 Args:
149 v: The value to escape.
150
151 Returns:
152 The escaped values.
153 """
154 return '"%s"' % v.replace('"', '""')
155
156 @staticmethod
157 def _EscapeValue(v):
158 """Puts the string in quotes, and escapes any inner quotes and slashes."""
159 if isinstance(v, unicode):
160 # Here we use repr as in the usual case, but on unicode strings, it
161 # also escapes the unicode characters (which we want to leave as is).
162 # So, after repr() we decode using raw-unicode-escape, which decodes
163 # only the unicode characters, and leaves all the rest (", ', \n and
164 # more) escaped.
165 # We don't take the first character, because repr adds a u in the
166 # beginning of the string (usual repr output for unicode is u'...').
167 return repr(v).decode("raw-unicode-escape")[1:]
168 # Here we use python built-in escaping mechanism for string using repr.
169 return repr(str(v))
170
171 @staticmethod
172 def _EscapeCustomProperties(custom_properties):
173 """Escapes the custom properties dictionary."""
174 l = []
175 for key, value in custom_properties.iteritems():
176 l.append("%s:%s" % (DataTable._EscapeValue(key),
177 DataTable._EscapeValue(value)))
178 return "{%s}" % ",".join(l)
179
180 @staticmethod
181 def SingleValueToJS(value, value_type, escape_func=None):
182 """Translates a single value and type into a JS value.
183
184 Internal helper method.
185
186 Args:
187 value: The value which should be converted
188 value_type: One of "string", "number", "boolean", "date", "datetime" or
189 "timeofday".
190 escape_func: The function to use for escaping strings.
191
192 Returns:
193 The proper JS format (as string) of the given value according to the
194 given value_type. For None, we simply return "null".
195 If a tuple is given, it should be in one of the following forms:
196 - (value, formatted value)
197 - (value, formatted value, custom properties)
198 where the formatted value is a string, and custom properties is a
199 dictionary of the custom properties for this cell.
200 To specify custom properties without specifying formatted value, one can
201 pass None as the formatted value.
202 One can also have a null-valued cell with formatted value and/or custom
203 properties by specifying None for the value.
204 This method ignores the custom properties except for checking that it is a
205 dictionary. The custom properties are handled in the ToJSon and ToJSCode
206 methods.
207 The real type of the given value is not strictly checked. For example,
208 any type can be used for string - as we simply take its str( ) and for
209 boolean value we just check "if value".
210 Examples:
211 SingleValueToJS(None, "boolean") returns "null"
212 SingleValueToJS(False, "boolean") returns "false"
213 SingleValueToJS((5, "5$"), "number") returns ("5", "'5$'")
214 SingleValueToJS((None, "5$"), "number") returns ("null", "'5$'")
215
216 Raises:
217 DataTableException: The value and type did not match in a not-recoverable
218 way, for example given value 'abc' for type 'number'.
219 """
220 if escape_func is None:
221 escape_func = DataTable._EscapeValue
222 if isinstance(value, tuple):
223 # In case of a tuple, we run the same function on the value itself and
224 # add the formatted value.
225 if (len(value) not in [2, 3] or
226 (len(value) == 3 and not isinstance(value[2], dict))):
227 raise DataTableException("Wrong format for value and formatting - %s." %
228 str(value))
229 if not isinstance(value[1], types.StringTypes + (types.NoneType,)):
230 raise DataTableException("Formatted value is not string, given %s." %
231 type(value[1]))
232 js_value = DataTable.SingleValueToJS(value[0], value_type)
233 if value[1] is None:
234 return (js_value, None)
235 return (js_value, escape_func(value[1]))
236
237 # The standard case - no formatting.
238 t_value = type(value)
239 if value is None:
240 return "null"
241 if value_type == "boolean":
242 if value:
243 return "true"
244 return "false"
245
246 elif value_type == "number":
247 if isinstance(value, (int, long, float)):
248 return str(value)
249 raise DataTableException("Wrong type %s when expected number" % t_value)
250
251 elif value_type == "string":
252 if isinstance(value, tuple):
253 raise DataTableException("Tuple is not allowed as string value.")
254 return escape_func(value)
255
256 elif value_type == "date":
257 if not isinstance(value, (datetime.date, datetime.datetime)):
258 raise DataTableException("Wrong type %s when expected date" % t_value)
259 # We need to shift the month by 1 to match JS Date format
260 return "new Date(%d,%d,%d)" % (value.year, value.month - 1, value.day)
261
262 elif value_type == "timeofday":
263 if not isinstance(value, (datetime.time, datetime.datetime)):
264 raise DataTableException("Wrong type %s when expected time" % t_value)
265 return "[%d,%d,%d]" % (value.hour, value.minute, value.second)
266
267 elif value_type == "datetime":
268 if not isinstance(value, datetime.datetime):
269 raise DataTableException("Wrong type %s when expected datetime" %
270 t_value)
271 return "new Date(%d,%d,%d,%d,%d,%d)" % (value.year,
272 value.month - 1, # To match JS
273 value.day,
274 value.hour,
275 value.minute,
276 value.second)
277 # If we got here, it means the given value_type was not one of the
278 # supported types.
279 raise DataTableException("Unsupported type %s" % value_type)
280
281 @staticmethod
282 def ColumnTypeParser(description):
283 """Parses a single column description. Internal helper method.
284
285 Args:
286 description: a column description in the possible formats:
287 'id'
288 ('id',)
289 ('id', 'type')
290 ('id', 'type', 'label')
291 ('id', 'type', 'label', {'custom_prop1': 'custom_val1'})
292 Returns:
293 Dictionary with the following keys: id, label, type, and
294 custom_properties where:
295 - If label not given, it equals the id.
296 - If type not given, string is used by default.
297 - If custom properties are not given, an empty dictionary is used by
298 default.
299
300 Raises:
301 DataTableException: The column description did not match the RE, or
302 unsupported type was passed.
303 """
304 if not description:
305 raise DataTableException("Description error: empty description given")
306
307 if not isinstance(description, (types.StringTypes, tuple)):
308 raise DataTableException("Description error: expected either string or "
309 "tuple, got %s." % type(description))
310
311 if isinstance(description, types.StringTypes):
312 description = (description,)
313
314 # According to the tuple's length, we fill the keys
315 # We verify everything is of type string
316 for elem in description[:3]:
317 if not isinstance(elem, types.StringTypes):
318 raise DataTableException("Description error: expected tuple of "
319 "strings, current element of type %s." %
320 type(elem))
321 desc_dict = {"id": description[0],
322 "label": description[0],
323 "type": "string",
324 "custom_properties": {}}
325 if len(description) > 1:
326 desc_dict["type"] = description[1].lower()
327 if len(description) > 2:
328 desc_dict["label"] = description[2]
329 if len(description) > 3:
330 if not isinstance(description[3], dict):
331 raise DataTableException("Description error: expected custom "
332 "properties of type dict, current element "
333 "of type %s." % type(description[3]))
334 desc_dict["custom_properties"] = description[3]
335 if len(description) > 4:
336 raise DataTableException("Description error: tuple of length > 4")
337 if desc_dict["type"] not in ["string", "number", "boolean",
338 "date", "datetime", "timeofday"]:
339 raise DataTableException(
340 "Description error: unsupported type '%s'" % desc_dict["type"])
341 return desc_dict
342
343 @staticmethod
344 def TableDescriptionParser(table_description, depth=0):
345 """Parses the table_description object for internal use.
346
347 Parses the user-submitted table description into an internal format used
348 by the Python DataTable class. Returns the flat list of parsed columns.
349
350 Args:
351 table_description: A description of the table which should comply
352 with one of the formats described below.
353 depth: Optional. The depth of the first level in the current description.
354 Used by recursive calls to this function.
355
356 Returns:
357 List of columns, where each column represented by a dictionary with the
358 keys: id, label, type, depth, container which means the following:
359 - id: the id of the column
360 - name: The name of the column
361 - type: The datatype of the elements in this column. Allowed types are
362 described in ColumnTypeParser().
363 - depth: The depth of this column in the table description
364 - container: 'dict', 'iter' or 'scalar' for parsing the format easily.
365 - custom_properties: The custom properties for this column.
366 The returned description is flattened regardless of how it was given.
367
368 Raises:
369 DataTableException: Error in a column description or in the description
370 structure.
371
372 Examples:
373 A column description can be of the following forms:
374 'id'
375 ('id',)
376 ('id', 'type')
377 ('id', 'type', 'label')
378 ('id', 'type', 'label', {'custom_prop1': 'custom_val1'})
379 or as a dictionary:
380 'id': 'type'
381 'id': ('type',)
382 'id': ('type', 'label')
383 'id': ('type', 'label', {'custom_prop1': 'custom_val1'})
384 If the type is not specified, we treat it as string.
385 If no specific label is given, the label is simply the id.
386 If no custom properties are given, we use an empty dictionary.
387
388 input: [('a', 'date'), ('b', 'timeofday', 'b', {'foo': 'bar'})]
389 output: [{'id': 'a', 'label': 'a', 'type': 'date',
390 'depth': 0, 'container': 'iter', 'custom_properties': {}},
391 {'id': 'b', 'label': 'b', 'type': 'timeofday',
392 'depth': 0, 'container': 'iter',
393 'custom_properties': {'foo': 'bar'}}]
394
395 input: {'a': [('b', 'number'), ('c', 'string', 'column c')]}
396 output: [{'id': 'a', 'label': 'a', 'type': 'string',
397 'depth': 0, 'container': 'dict', 'custom_properties': {}},
398 {'id': 'b', 'label': 'b', 'type': 'number',
399 'depth': 1, 'container': 'iter', 'custom_properties': {}},
400 {'id': 'c', 'label': 'column c', 'type': 'string',
401 'depth': 1, 'container': 'iter', 'custom_properties': {}}]
402
403 input: {('a', 'number', 'column a'): { 'b': 'number', 'c': 'string'}}
404 output: [{'id': 'a', 'label': 'column a', 'type': 'number',
405 'depth': 0, 'container': 'dict', 'custom_properties': {}},
406 {'id': 'b', 'label': 'b', 'type': 'number',
407 'depth': 1, 'container': 'dict', 'custom_properties': {}},
408 {'id': 'c', 'label': 'c', 'type': 'string',
409 'depth': 1, 'container': 'dict', 'custom_properties': {}}]
410
411 input: { ('w', 'string', 'word'): ('c', 'number', 'count') }
412 output: [{'id': 'w', 'label': 'word', 'type': 'string',
413 'depth': 0, 'container': 'dict', 'custom_properties': {}},
414 {'id': 'c', 'label': 'count', 'type': 'number',
415 'depth': 1, 'container': 'scalar', 'custom_properties': {}}]
416
417 input: {'a': ('number', 'column a'), 'b': ('string', 'column b')}
418 output: [{'id': 'a', 'label': 'column a', 'type': 'number', 'depth': 0,
419 'container': 'dict', 'custom_properties': {}},
420 {'id': 'b', 'label': 'column b', 'type': 'string', 'depth': 0,
421 'container': 'dict', 'custom_properties': {}}
422
423 NOTE: there might be ambiguity in the case of a dictionary representation
424 of a single column. For example, the following description can be parsed
425 in 2 different ways: {'a': ('b', 'c')} can be thought of a single column
426 with the id 'a', of type 'b' and the label 'c', or as 2 columns: one named
427 'a', and the other named 'b' of type 'c'. We choose the first option by
428 default, and in case the second option is the right one, it is possible to
429 make the key into a tuple (i.e. {('a',): ('b', 'c')}) or add more info
430 into the tuple, thus making it look like this: {'a': ('b', 'c', 'b', {})}
431 -- second 'b' is the label, and {} is the custom properties field.
432 """
433 # For the recursion step, we check for a scalar object (string or tuple)
434 if isinstance(table_description, (types.StringTypes, tuple)):
435 parsed_col = DataTable.ColumnTypeParser(table_description)
436 parsed_col["depth"] = depth
437 parsed_col["container"] = "scalar"
438 return [parsed_col]
439
440 # Since it is not scalar, table_description must be iterable.
441 if not hasattr(table_description, "__iter__"):
442 raise DataTableException("Expected an iterable object, got %s" %
443 type(table_description))
444 if not isinstance(table_description, dict):
445 # We expects a non-dictionary iterable item.
446 columns = []
447 for desc in table_description:
448 parsed_col = DataTable.ColumnTypeParser(desc)
449 parsed_col["depth"] = depth
450 parsed_col["container"] = "iter"
451 columns.append(parsed_col)
452 if not columns:
453 raise DataTableException("Description iterable objects should not"
454 " be empty.")
455 return columns
456 # The other case is a dictionary
457 if not table_description:
458 raise DataTableException("Empty dictionaries are not allowed inside"
459 " description")
460
461 # To differentiate between the two cases of more levels below or this is
462 # the most inner dictionary, we consider the number of keys (more then one
463 # key is indication for most inner dictionary) and the type of the key and
464 # value in case of only 1 key (if the type of key is string and the type of
465 # the value is a tuple of 0-3 items, we assume this is the most inner
466 # dictionary).
467 # NOTE: this way of differentiating might create ambiguity. See docs.
468 if (len(table_description) != 1 or
469 (isinstance(table_description.keys()[0], types.StringTypes) and
470 isinstance(table_description.values()[0], tuple) and
471 len(table_description.values()[0]) < 4)):
472 # This is the most inner dictionary. Parsing types.
473 columns = []
474 # We sort the items, equivalent to sort the keys since they are unique
475 for key, value in sorted(table_description.items()):
476 # We parse the column type as (key, type) or (key, type, label) using
477 # ColumnTypeParser.
478 if isinstance(value, tuple):
479 parsed_col = DataTable.ColumnTypeParser((key,) + value)
480 else:
481 parsed_col = DataTable.ColumnTypeParser((key, value))
482 parsed_col["depth"] = depth
483 parsed_col["container"] = "dict"
484 columns.append(parsed_col)
485 return columns
486 # This is an outer dictionary, must have at most one key.
487 parsed_col = DataTable.ColumnTypeParser(table_description.keys()[0])
488 parsed_col["depth"] = depth
489 parsed_col["container"] = "dict"
490 return ([parsed_col] +
491 DataTable.TableDescriptionParser(table_description.values()[0],
492 depth=depth + 1))
493
494 @property
495 def columns(self):
496 """Returns the parsed table description."""
497 return self.__columns
498
499 def NumberOfRows(self):
500 """Returns the number of rows in the current data stored in the table."""
501 return len(self.__data)
502
503 def SetRowsCustomProperties(self, rows, custom_properties):
504 """Sets the custom properties for given row(s).
505
506 Can accept a single row or an iterable of rows.
507 Sets the given custom properties for all specified rows.
508
509 Args:
510 rows: The row, or rows, to set the custom properties for.
511 custom_properties: A string to string dictionary of custom properties to
512 set for all rows.
513 """
514 if not hasattr(rows, "__iter__"):
515 rows = [rows]
516 for row in rows:
517 self.__data[row] = (self.__data[row][0], custom_properties)
518
519 def LoadData(self, data, custom_properties=None):
520 """Loads new rows to the data table, clearing existing rows.
521
522 May also set the custom_properties for the added rows. The given custom
523 properties dictionary specifies the dictionary that will be used for *all*
524 given rows.
525
526 Args:
527 data: The rows that the table will contain.
528 custom_properties: A dictionary of string to string to set as the custom
529 properties for all rows.
530 """
531 self.__data = []
532 self.AppendData(data, custom_properties)
533
534 def AppendData(self, data, custom_properties=None):
535 """Appends new data to the table.
536
537 Data is appended in rows. Data must comply with
538 the table schema passed in to __init__(). See SingleValueToJS() for a list
539 of acceptable data types. See the class documentation for more information
540 and examples of schema and data values.
541
542 Args:
543 data: The row to add to the table. The data must conform to the table
544 description format.
545 custom_properties: A dictionary of string to string, representing the
546 custom properties to add to all the rows.
547
548 Raises:
549 DataTableException: The data structure does not match the description.
550 """
551 # If the maximal depth is 0, we simply iterate over the data table
552 # lines and insert them using _InnerAppendData. Otherwise, we simply
553 # let the _InnerAppendData handle all the levels.
554 if not self.__columns[-1]["depth"]:
555 for row in data:
556 self._InnerAppendData(({}, custom_properties), row, 0)
557 else:
558 self._InnerAppendData(({}, custom_properties), data, 0)
559
560 def _InnerAppendData(self, prev_col_values, data, col_index):
561 """Inner function to assist LoadData."""
562 # We first check that col_index has not exceeded the columns size
563 if col_index >= len(self.__columns):
564 raise DataTableException("The data does not match description, too deep")
565
566 # Dealing with the scalar case, the data is the last value.
567 if self.__columns[col_index]["container"] == "scalar":
568 prev_col_values[0][self.__columns[col_index]["id"]] = data
569 self.__data.append(prev_col_values)
570 return
571
572 if self.__columns[col_index]["container"] == "iter":
573 if not hasattr(data, "__iter__") or isinstance(data, dict):
574 raise DataTableException("Expected iterable object, got %s" %
575 type(data))
576 # We only need to insert the rest of the columns
577 # If there are less items than expected, we only add what there is.
578 for value in data:
579 if col_index >= len(self.__columns):
580 raise DataTableException("Too many elements given in data")
581 prev_col_values[0][self.__columns[col_index]["id"]] = value
582 col_index += 1
583 self.__data.append(prev_col_values)
584 return
585
586 # We know the current level is a dictionary, we verify the type.
587 if not isinstance(data, dict):
588 raise DataTableException("Expected dictionary at current level, got %s" %
589 type(data))
590 # We check if this is the last level
591 if self.__columns[col_index]["depth"] == self.__columns[-1]["depth"]:
592 # We need to add the keys in the dictionary as they are
593 for col in self.__columns[col_index:]:
594 if col["id"] in data:
595 prev_col_values[0][col["id"]] = data[col["id"]]
596 self.__data.append(prev_col_values)
597 return
598
599 # We have a dictionary in an inner depth level.
600 if not data.keys():
601 # In case this is an empty dictionary, we add a record with the columns
602 # filled only until this point.
603 self.__data.append(prev_col_values)
604 else:
605 for key in sorted(data):
606 col_values = dict(prev_col_values[0])
607 col_values[self.__columns[col_index]["id"]] = key
608 self._InnerAppendData((col_values, prev_col_values[1]),
609 data[key], col_index + 1)
610
611 def _PreparedData(self, order_by=()):
612 """Prepares the data for enumeration - sorting it by order_by.
613
614 Args:
615 order_by: Optional. Specifies the name of the column(s) to sort by, and
616 (optionally) which direction to sort in. Default sort direction
617 is asc. Following formats are accepted:
618 "string_col_name" -- For a single key in default (asc) order.
619 ("string_col_name", "asc|desc") -- For a single key.
620 [("col_1","asc|desc"), ("col_2","asc|desc")] -- For more than
621 one column, an array of tuples of (col_name, "asc|desc").
622
623 Returns:
624 The data sorted by the keys given.
625
626 Raises:
627 DataTableException: Sort direction not in 'asc' or 'desc'
628 """
629 if not order_by:
630 return self.__data
631
632 proper_sort_keys = []
633 if isinstance(order_by, types.StringTypes) or (
634 isinstance(order_by, tuple) and len(order_by) == 2 and
635 order_by[1].lower() in ["asc", "desc"]):
636 order_by = (order_by,)
637 for key in order_by:
638 if isinstance(key, types.StringTypes):
639 proper_sort_keys.append((key, 1))
640 elif (isinstance(key, (list, tuple)) and len(key) == 2 and
641 key[1].lower() in ("asc", "desc")):
642 proper_sort_keys.append((key[0], key[1].lower() == "asc" and 1 or -1))
643 else:
644 raise DataTableException("Expected tuple with second value: "
645 "'asc' or 'desc'")
646
647 def SortCmpFunc(row1, row2):
648 """cmp function for sorted. Compares by keys and 'asc'/'desc' keywords."""
649 for key, asc_mult in proper_sort_keys:
650 cmp_result = asc_mult * cmp(row1[0].get(key), row2[0].get(key))
651 if cmp_result:
652 return cmp_result
653 return 0
654
655 return sorted(self.__data, cmp=SortCmpFunc)
656
657 def ToJSCode(self, name, columns_order=None, order_by=()):
658 """Writes the data table as a JS code string.
659
660 This method writes a string of JS code that can be run to
661 generate a DataTable with the specified data. Typically used for debugging
662 only.
663
664 Args:
665 name: The name of the table. The name would be used as the DataTable's
666 variable name in the created JS code.
667 columns_order: Optional. Specifies the order of columns in the
668 output table. Specify a list of all column IDs in the order
669 in which you want the table created.
670 Note that you must list all column IDs in this parameter,
671 if you use it.
672 order_by: Optional. Specifies the name of the column(s) to sort by.
673 Passed as is to _PreparedData.
674
675 Returns:
676 A string of JS code that, when run, generates a DataTable with the given
677 name and the data stored in the DataTable object.
678 Example result:
679 "var tab1 = new google.visualization.DataTable();
680 tab1.addColumn('string', 'a', 'a');
681 tab1.addColumn('number', 'b', 'b');
682 tab1.addColumn('boolean', 'c', 'c');
683 tab1.addRows(10);
684 tab1.setCell(0, 0, 'a');
685 tab1.setCell(0, 1, 1, null, {'foo': 'bar'});
686 tab1.setCell(0, 2, true);
687 ...
688 tab1.setCell(9, 0, 'c');
689 tab1.setCell(9, 1, 3, '3$');
690 tab1.setCell(9, 2, false);"
691
692 Raises:
693 DataTableException: The data does not match the type.
694 """
695 if columns_order is None:
696 columns_order = [col["id"] for col in self.__columns]
697 col_dict = dict([(col["id"], col) for col in self.__columns])
698
699 # We first create the table with the given name
700 jscode = "var %s = new google.visualization.DataTable();\n" % name
701 if self.custom_properties:
702 jscode += "%s.setTableProperties(%s);\n" % (
703 name, DataTable._EscapeCustomProperties(self.custom_properties))
704
705 # We add the columns to the table
706 for i, col in enumerate(columns_order):
707 jscode += "%s.addColumn('%s', %s, %s);\n" % (
708 name,
709 col_dict[col]["type"],
710 DataTable._EscapeValue(col_dict[col]["label"]),
711 DataTable._EscapeValue(col_dict[col]["id"]))
712 if col_dict[col]["custom_properties"]:
713 jscode += "%s.setColumnProperties(%d, %s);\n" % (
714 name, i, DataTable._EscapeCustomProperties(
715 col_dict[col]["custom_properties"]))
716 jscode += "%s.addRows(%d);\n" % (name, len(self.__data))
717
718 # We now go over the data and add each row
719 for (i, (row, cp)) in enumerate(self._PreparedData(order_by)):
720 # We add all the elements of this row by their order
721 for (j, col) in enumerate(columns_order):
722 if col not in row or row[col] is None:
723 continue
724 cell_cp = ""
725 if isinstance(row[col], tuple) and len(row[col]) == 3:
726 cell_cp = ", %s" % DataTable._EscapeCustomProperties(row[col][2])
727 value = self.SingleValueToJS(row[col], col_dict[col]["type"])
728 if isinstance(value, tuple):
729 # We have a formatted value or custom property as well
730 if value[1] is None:
731 value = (value[0], "null")
732 jscode += ("%s.setCell(%d, %d, %s, %s%s);\n" %
733 (name, i, j, value[0], value[1], cell_cp))
734 else:
735 jscode += "%s.setCell(%d, %d, %s);\n" % (name, i, j, value)
736 if cp:
737 jscode += "%s.setRowProperties(%d, %s);\n" % (
738 name, i, DataTable._EscapeCustomProperties(cp))
739 return jscode
740
741 def ToHtml(self, columns_order=None, order_by=()):
742 """Writes the data table as an HTML table code string.
743
744 Args:
745 columns_order: Optional. Specifies the order of columns in the
746 output table. Specify a list of all column IDs in the order
747 in which you want the table created.
748 Note that you must list all column IDs in this parameter,
749 if you use it.
750 order_by: Optional. Specifies the name of the column(s) to sort by.
751 Passed as is to _PreparedData.
752
753 Returns:
754 An HTML table code string.
755 Example result (the result is without the newlines):
756 <html><body><table border='1'>
757 <thead><tr><th>a</th><th>b</th><th>c</th></tr></thead>
758 <tbody>
759 <tr><td>1</td><td>"z"</td><td>2</td></tr>
760 <tr><td>"3$"</td><td>"w"</td><td></td></tr>
761 </tbody>
762 </table></body></html>
763
764 Raises:
765 DataTableException: The data does not match the type.
766 """
767 table_template = "<html><body><table border='1'>%s</table></body></html>"
768 columns_template = "<thead><tr>%s</tr></thead>"
769 rows_template = "<tbody>%s</tbody>"
770 row_template = "<tr>%s</tr>"
771 header_cell_template = "<th>%s</th>"
772 cell_template = "<td>%s</td>"
773
774 if columns_order is None:
775 columns_order = [col["id"] for col in self.__columns]
776 col_dict = dict([(col["id"], col) for col in self.__columns])
777
778 columns_list = []
779 for col in columns_order:
780 columns_list.append(header_cell_template %
781 cgi.escape(col_dict[col]["label"]))
782 columns_html = columns_template % "".join(columns_list)
783
784 rows_list = []
785 # We now go over the data and add each row
786 for row, unused_cp in self._PreparedData(order_by):
787 cells_list = []
788 # We add all the elements of this row by their order
789 for col in columns_order:
790 # For empty string we want empty quotes ("").
791 value = ""
792 if col in row and row[col] is not None:
793 value = self.SingleValueToJS(row[col], col_dict[col]["type"])
794 if isinstance(value, tuple):
795 # We have a formatted value and we're going to use it
796 cells_list.append(cell_template % cgi.escape(value[1]))
797 else:
798 cells_list.append(cell_template % cgi.escape(value))
799 rows_list.append(row_template % "".join(cells_list))
800 rows_html = rows_template % "".join(rows_list)
801
802 return table_template % (columns_html + rows_html)
803
804 def ToCsv(self, columns_order=None, order_by=(), separator=", "):
805 """Writes the data table as a CSV string.
806
807 Args:
808 columns_order: Optional. Specifies the order of columns in the
809 output table. Specify a list of all column IDs in the order
810 in which you want the table created.
811 Note that you must list all column IDs in this parameter,
812 if you use it.
813 order_by: Optional. Specifies the name of the column(s) to sort by.
814 Passed as is to _PreparedData.
815 separator: Optional. The separator to use between the values.
816
817 Returns:
818 A CSV string representing the table.
819 Example result:
820 'a', 'b', 'c'
821 1, 'z', 2
822 3, 'w', ''
823
824 Raises:
825 DataTableException: The data does not match the type.
826 """
827 if columns_order is None:
828 columns_order = [col["id"] for col in self.__columns]
829 col_dict = dict([(col["id"], col) for col in self.__columns])
830
831 columns_list = []
832 for col in columns_order:
833 columns_list.append(DataTable._EscapeValueForCsv(col_dict[col]["label"]))
834 columns_line = separator.join(columns_list)
835
836 rows_list = []
837 # We now go over the data and add each row
838 for row, unused_cp in self._PreparedData(order_by):
839 cells_list = []
840 # We add all the elements of this row by their order
841 for col in columns_order:
842 value = '""'
843 if col in row and row[col] is not None:
844 value = self.SingleValueToJS(row[col], col_dict[col]["type"],
845 DataTable._EscapeValueForCsv)
846 if isinstance(value, tuple):
847 # We have a formatted value. Using it only for date/time types.
848 if col_dict[col]["type"] in ["date", "datetime", "timeofday"]:
849 cells_list.append(value[1])
850 else:
851 cells_list.append(value[0])
852 else:
853 # We need to quote date types, because they contain commas.
854 if (col_dict[col]["type"] in ["date", "datetime", "timeofday"] and
855 value != '""'):
856 value = '"%s"' % value
857 cells_list.append(value)
858 rows_list.append(separator.join(cells_list))
859 rows = "\n".join(rows_list)
860
861 return "%s\n%s" % (columns_line, rows)
862
863 def ToTsvExcel(self, columns_order=None, order_by=()):
864 """Returns a file in tab-separated-format readable by MS Excel.
865
866 Returns a file in UTF-16 little endian encoding, with tabs separating the
867 values.
868
869 Args:
870 columns_order: Delegated to ToCsv.
871 order_by: Delegated to ToCsv.
872
873 Returns:
874 A tab-separated little endian UTF16 file representing the table.
875 """
876 return self.ToCsv(
877 columns_order, order_by, separator="\t").encode("UTF-16LE")
878
879 def ToJSon(self, columns_order=None, order_by=()):
880 """Writes a JSON string that can be used in a JS DataTable constructor.
881
882 This method writes a JSON string that can be passed directly into a Google
883 Visualization API DataTable constructor. Use this output if you are
884 hosting the visualization HTML on your site, and want to code the data
885 table in Python. Pass this string into the
886 google.visualization.DataTable constructor, e.g,:
887 ... on my page that hosts my visualization ...
888 google.setOnLoadCallback(drawTable);
889 function drawTable() {
890 var data = new google.visualization.DataTable(_my_JSon_string, 0.6);
891 myTable.draw(data);
892 }
893
894 Args:
895 columns_order: Optional. Specifies the order of columns in the
896 output table. Specify a list of all column IDs in the order
897 in which you want the table created.
898 Note that you must list all column IDs in this parameter,
899 if you use it.
900 order_by: Optional. Specifies the name of the column(s) to sort by.
901 Passed as is to _PreparedData().
902
903 Returns:
904 A JSon constructor string to generate a JS DataTable with the data
905 stored in the DataTable object.
906 Example result (the result is without the newlines):
907 {cols: [{id:'a',label:'a',type:'number'},
908 {id:'b',label:'b',type:'string'},
909 {id:'c',label:'c',type:'number'}],
910 rows: [{c:[{v:1},{v:'z'},{v:2}]}, c:{[{v:3,f:'3$'},{v:'w'},{v:null}]}],
911 p: {'foo': 'bar'}}
912
913 Raises:
914 DataTableException: The data does not match the type.
915 """
916 if columns_order is None:
917 columns_order = [col["id"] for col in self.__columns]
918 col_dict = dict([(col["id"], col) for col in self.__columns])
919
920 # Creating the columns jsons
921 cols_jsons = []
922 for col_id in columns_order:
923 d = dict(col_dict[col_id])
924 d["id"] = DataTable._EscapeValue(d["id"])
925 d["label"] = DataTable._EscapeValue(d["label"])
926 d["cp"] = ""
927 if col_dict[col_id]["custom_properties"]:
928 d["cp"] = ",p:%s" % DataTable._EscapeCustomProperties(
929 col_dict[col_id]["custom_properties"])
930 cols_jsons.append(
931 "{id:%(id)s,label:%(label)s,type:'%(type)s'%(cp)s}" % d)
932
933 # Creating the rows jsons
934 rows_jsons = []
935 for row, cp in self._PreparedData(order_by):
936 cells_jsons = []
937 for col in columns_order:
938 # We omit the {v:null} for a None value of the not last column
939 value = row.get(col, None)
940 if value is None and col != columns_order[-1]:
941 cells_jsons.append("")
942 else:
943 value = self.SingleValueToJS(value, col_dict[col]["type"])
944 if isinstance(value, tuple):
945 # We have a formatted value or custom property as well
946 if len(row.get(col)) == 3:
947 if value[1] is None:
948 cells_jsons.append("{v:%s,p:%s}" % (
949 value[0],
950 DataTable._EscapeCustomProperties(row.get(col)[2])))
951 else:
952 cells_jsons.append("{v:%s,f:%s,p:%s}" % (value + (
953 DataTable._EscapeCustomProperties(row.get(col)[2]),)))
954 else:
955 cells_jsons.append("{v:%s,f:%s}" % value)
956 else:
957 cells_jsons.append("{v:%s}" % value)
958 if cp:
959 rows_jsons.append("{c:[%s],p:%s}" % (
960 ",".join(cells_jsons), DataTable._EscapeCustomProperties(cp)))
961 else:
962 rows_jsons.append("{c:[%s]}" % ",".join(cells_jsons))
963
964 general_custom_properties = ""
965 if self.custom_properties:
966 general_custom_properties = (
967 ",p:%s" % DataTable._EscapeCustomProperties(self.custom_properties))
968
969 # We now join the columns jsons and the rows jsons
970 json = "{cols:[%s],rows:[%s]%s}" % (",".join(cols_jsons),
971 ",".join(rows_jsons),
972 general_custom_properties)
973 return json
974
975 def ToJSonResponse(self, columns_order=None, order_by=(), req_id=0,
976 response_handler="google.visualization.Query.setResponse"):
977 """Writes a table as a JSON response that can be returned as-is to a client.
978
979 This method writes a JSON response to return to a client in response to a
980 Google Visualization API query. This string can be processed by the calling
981 page, and is used to deliver a data table to a visualization hosted on
982 a different page.
983
984 Args:
985 columns_order: Optional. Passed straight to self.ToJSon().
986 order_by: Optional. Passed straight to self.ToJSon().
987 req_id: Optional. The response id, as retrieved by the request.
988 response_handler: Optional. The response handler, as retrieved by the
989 request.
990
991 Returns:
992 A JSON response string to be received by JS the visualization Query
993 object. This response would be translated into a DataTable on the
994 client side.
995 Example result (newlines added for readability):
996 google.visualization.Query.setResponse({
997 'version':'0.6', 'reqId':'0', 'status':'OK',
998 'table': {cols: [...], rows: [...]}});
999
1000 Note: The URL returning this string can be used as a data source by Google
1001 Visualization Gadgets or from JS code.
1002 """
1003 table = self.ToJSon(columns_order, order_by)
1004 return ("%s({'version':'0.6', 'reqId':'%s', 'status':'OK', "
1005 "'table': %s});") % (response_handler, req_id, table)
1006
1007 def ToResponse(self, columns_order=None, order_by=(), tqx=""):
1008 """Writes the right response according to the request string passed in tqx.
1009
1010 This method parses the tqx request string (format of which is defined in
1011 the documentation for implementing a data source of Google Visualization),
1012 and returns the right response according to the request.
1013 It parses out the "out" parameter of tqx, calls the relevant response
1014 (ToJSonResponse() for "json", ToCsv() for "csv", ToHtml() for "html",
1015 ToTsvExcel() for "tsv-excel") and passes the response function the rest of
1016 the relevant request keys.
1017
1018 Args:
1019 columns_order: Optional. Passed as is to the relevant response function.
1020 order_by: Optional. Passed as is to the relevant response function.
1021 tqx: Optional. The request string as received by HTTP GET. Should be in
1022 the format "key1:value1;key2:value2...". All keys have a default
1023 value, so an empty string will just do the default (which is calling
1024 ToJSonResponse() with no extra parameters).
1025
1026 Returns:
1027 A response string, as returned by the relevant response function.
1028
1029 Raises:
1030 DataTableException: One of the parameters passed in tqx is not supported.
1031 """
1032 tqx_dict = {}
1033 if tqx:
1034 tqx_dict = dict(opt.split(":") for opt in tqx.split(";"))
1035 if tqx_dict.get("version", "0.6") != "0.6":
1036 raise DataTableException(
1037 "Version (%s) passed by request is not supported."
1038 % tqx_dict["version"])
1039
1040 if tqx_dict.get("out", "json") == "json":
1041 response_handler = tqx_dict.get("responseHandler",
1042 "google.visualization.Query.setResponse")
1043 return self.ToJSonResponse(columns_order, order_by,
1044 req_id=tqx_dict.get("reqId", 0),
1045 response_handler=response_handler)
1046 elif tqx_dict["out"] == "html":
1047 return self.ToHtml(columns_order, order_by)
1048 elif tqx_dict["out"] == "csv":
1049 return self.ToCsv(columns_order, order_by)
1050 elif tqx_dict["out"] == "tsv-excel":
1051 return self.ToTsvExcel(columns_order, order_by)
1052 else:
1053 raise DataTableException(
1054 "'out' parameter: '%s' is not supported" % tqx_dict["out"])
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698