def set_default_hcoptions(self): self.hcoptions = HCOptions({}) # series and terms dss = self.datasource.series terms = self.series_options.keys() # legend by lgby_dict = dict(((t, dss[t]['legend_by']) for t in terms)) lgby_vname_lists = [[ dss[t]['field_aliases'].get(lgby, lgby) for lgby in lgby_tuple ] for (t, lgby_tuple) in lgby_dict.items()] lgby_titles = (':'.join(lgby_vname_list).title() for lgby_vname_list in lgby_vname_lists) # chart title term_titles = (t.title() for t in terms) title = '' for t, lg in zip(term_titles, lgby_titles): if not lg: title += "%s, " % t else: title += "%s (lgnd. by %s), " % (t, lg) categories = dss[terms[0]]['categories'] categories_vnames = [ dss[terms[0]]['field_aliases'][c].title() for c in categories ] category_title = ':'.join(categories_vnames) chart_title = "%s vs. %s" % (title[:-2], category_title) self.hcoptions['title']['text'] = chart_title
def _set_default_hcoptions(self, chart_options): """Set some default options, like xAxis title, yAxis title, chart title, etc. """ so = self.series_options dss = self.datasource.series self.hcoptions = HCOptions({}) if chart_options is not None: self.hcoptions.update(chart_options) self.hcoptions["series"] = [] # Set title title = "" for x_axis_num, vqs_group in self.x_axis_vqs_groups.items(): for vqs_num, x_y_terms in vqs_group.items(): for x_term, y_terms in x_y_terms.items(): title += ", ".join([dss[y_term]["field_alias"].title() for y_term in y_terms]) title += " vs. " title += dss[x_term]["field_alias"].title() title += " & " if not self.hcoptions["title"]["text"]: self.hcoptions["title"]["text"] = title[:-3] # if xAxis and yAxis are supplied as a dict, embed it in a list # (needed for multiple axes) xAxis, yAxis = self.hcoptions["xAxis"], self.hcoptions["yAxis"] if isinstance(xAxis, dict): self.hcoptions["xAxis"] = [xAxis] if isinstance(yAxis, dict): self.hcoptions["yAxis"] = [yAxis] # set renderTo if not self.hcoptions["chart"]["renderTo"]: self.hcoptions["chart"]["renderTo"] = "container" term_x_axis = [(dss[d["_x_axis_term"]]["field_alias"].title(), d.get("xAxis", 0)) for (k, d) in so.items()] term_y_axis = [(dss[k]["field_alias"].title(), d.get("xAxis", 0)) for (k, d) in so.items()] max_x_axis = max(t[1] for t in term_x_axis) max_y_axis = max(t[1] for t in term_y_axis) x_axis_len = len(self.hcoptions["xAxis"]) y_axis_len = len(self.hcoptions["yAxis"]) if max_x_axis >= x_axis_len: self.hcoptions["xAxis"].extend([HCOptions({})] * (max_x_axis + 1 - x_axis_len)) for i, x_axis in enumerate(self.hcoptions["xAxis"]): if not x_axis["title"]["text"]: axis_title = set(t[0] for t in term_x_axis if t[1] == i) x_axis["title"]["text"] = " & ".join(axis_title) if max_x_axis == 1: if self.hcoptions["xAxis"][1]["opposite"] != False: self.hcoptions["xAxis"][1]["opposite"] = True if max_y_axis >= y_axis_len: self.hcoptions["yAxis"].extend([HCOptions({})] * (max_y_axis + 1 - y_axis_len)) for i, y_axis in enumerate(self.hcoptions["yAxis"]): if not y_axis["title"]["text"]: axis_title = set(t[0] for t in term_y_axis if t[1] == i) y_axis["title"]["text"] = " & ".join(axis_title) if max_y_axis == 1: if self.hcoptions["yAxis"][1]["opposite"] != False: self.hcoptions["yAxis"][1]["opposite"] = True
def _set_default_hcoptions(self, chart_options): """Set some default options, like xAxis title, yAxis title, chart title, etc. """ so = self.series_options dss = self.datasource.series self.hcoptions = HCOptions({}) if chart_options is not None: self.hcoptions.update(chart_options) self.hcoptions['series'] = [] # Set title title = '' for x_axis_num, vqs_group in self.x_axis_vqs_groups.items(): for vqs_num, x_y_terms in vqs_group.items(): for x_term, y_terms in x_y_terms.items(): title += ', '.join([dss[y_term]['field_alias'].title() for y_term in y_terms]) title += ' vs. ' title += dss[x_term]['field_alias'].title() title += ' & ' if not self.hcoptions['title']['text']: self.hcoptions['title']['text'] = title[:-3] # if xAxis and yAxis are supplied as a dict, embed it in a list # (needed for multiple axes) xAxis, yAxis = self.hcoptions['xAxis'], self.hcoptions['yAxis'] if isinstance(xAxis, dict): self.hcoptions['xAxis'] = [xAxis] if isinstance(yAxis, dict): self.hcoptions['yAxis'] = [yAxis] # set renderTo if not self.hcoptions['chart']['renderTo']: self.hcoptions['chart']['renderTo'] = 'container' term_x_axis = [(dss[d['_x_axis_term']]['field_alias'].title(), d.get('xAxis', 0)) for (k, d) in so.items()] term_y_axis = [(dss[k]['field_alias'].title(), d.get('xAxis', 0)) for (k, d) in so.items()] max_x_axis = max(t[1] for t in term_x_axis) max_y_axis = max(t[1] for t in term_y_axis) x_axis_len = len(self.hcoptions['xAxis']) y_axis_len = len(self.hcoptions['yAxis']) if max_x_axis >= x_axis_len: self.hcoptions['xAxis']\ .extend([HCOptions({})]*(max_x_axis+1-x_axis_len)) for i, x_axis in enumerate(self.hcoptions['xAxis']): if not x_axis['title']['text']: axis_title = set(t[0] for t in term_x_axis if t[1] == i) x_axis['title']['text'] = ' & '.join(axis_title) if max_x_axis == 1: if self.hcoptions['xAxis'][1]['opposite'] is not False: self.hcoptions['xAxis'][1]['opposite'] = True if max_y_axis >= y_axis_len: self.hcoptions['yAxis']\ .extend([HCOptions({})]*(max_y_axis+1-y_axis_len)) for i, y_axis in enumerate(self.hcoptions['yAxis']): if not y_axis['title']['text']: axis_title = set(t[0] for t in term_y_axis if t[1] == i) y_axis['title']['text'] = ' & '.join(axis_title) if max_y_axis == 1: if self.hcoptions['yAxis'][1]['opposite'] is not False: self.hcoptions['yAxis'][1]['opposite'] = True
class Chart(object): def __init__(self, datasource, series_options, chart_options=None, x_sortf_mapf_mts=None): """Chart accept the datasource and some options to create the chart and creates it. **Arguments**: - **datasource** (**required**) - a ``DataPool`` object that holds the terms and other information to plot the chart from. - **series_options** (**required**) - specifies the options to plot the terms on the chart. It is of the form :: [{'options': { #any items from HighChart series. For ex., 'type': 'column' }, 'terms': { 'x_name': ['y_name', {'other_y_name': { #overriding options}}, ...], ... }, }, ... #repeat dicts with 'options' & 'terms' ] Where - - **options** (**required**) - a ``dict``. Any of the parameters from the `Highcharts options object - series array <http://www.highcharts.com/ref/#series>`_ are valid as entries in the ``options`` dict except ``data`` (because data array is generated from your datasource by chartit). For example, ``type``, ``xAxis``, etc. are all valid entries here. .. note:: The items supplied in the options dict are not validated to make sure that Highcharts actually supports them. Any invalid options are just passed to Highcharts JS which silently ignores them. - **terms** (**required**) - a ``dict``. keys are the x-axis terms and the values are lists of y-axis terms for that particular x-axis term. Both x-axis and y-axis terms must be present in the corresponding datasource, otherwise an APIInputError is raised. The entries in the y-axis terms list must either be a ``str`` or a ``dict``. If entries are dicts, the keys need to be valid y-term names and the values need to be any options to override the default options. For example, :: [{'options': { 'type': 'column', 'yAxis': 0}, 'terms': { 'city': [ 'temperature', {'rainfall': { 'type': 'line', 'yAxis': 1}}]}}] plots a column chart of city vs. temperature as a line chart on yAxis: 0 and city vs. rainfall as a line chart on yAxis: 1. This can alternatively be expressed as two separate entries: :: [{'options': { 'type': 'column', 'yAxis': 0}, 'terms': { 'city': [ 'temperature']}}, {'options': { 'type': 'line', 'yAxis': 1}, 'terms': { 'city': [ 'rainfall']}}] - **chart_options** (*optional*) - a ``dict``. Any of the options from the `Highcharts options object <http://www.highcharts.com/ref/>`_ are valid (except the options in the ``series`` array which are passed in the ``series_options`` argument. The following ``chart_options`` for example, set the chart title and the axes titles. :: {'chart': { 'title': { 'text': 'Weather Chart'}}, 'xAxis': { 'title': 'month'}, 'yAxis': { 'title': 'temperature'}} .. note:: The items supplied in the ``chart_options`` dict are not validated to make sure that Highcharts actually supports them. Any invalid options are just passed to Highcharts JS which silently ignores them. **Raises**: - ``APIInputError`` if any of the terms are not present in the corresponding datasource or if the ``series_options`` cannot be parsed. """ self.user_input = locals() if not isinstance(datasource, DataPool): raise APIInputError("%s must be an instance of DataPool." % datasource) self.datasource = datasource self.series_options = clean_cso(series_options, self.datasource) self.x_sortf_mapf_mts = clean_x_sortf_mapf_mts(x_sortf_mapf_mts) self.x_axis_vqs_groups = self._groupby_x_axis_and_vqs() self._set_default_hcoptions(chart_options) self.generate_plot() def _groupby_x_axis_and_vqs(self): """Returns a list of list of lists where each list has the term and option dict with the same xAxis and within each list with same xAxis, all items in same sub-list have items with same ValueQuerySet. Here is an example of what this function would return. :: [ [[(term-1-A-1, opts-1-A-1), (term-1-A-2, opts-1-A-2), ...], [(term-1-B-1, opts-1-B-1), (term-1-B-2, opts-1-B-2), ...], ...], [[term-2-A-1, opts-2-A-1), (term-2-A-2, opts-2-A-2), ...], [term-2-B-2, opts-2-B-2), (term-2-B-2, opts-2-B-2), ...], ...], ... ] In the above example, - term-1-*-* all have same xAxis. - term-*-A-* all are from same ValueQuerySet (table) """ dss = self.datasource.series x_axis_vqs_groups = defaultdict(dict) sort_fn = lambda (tk, td): td.get('xAxis', 0) so = sorted(self.series_options.items(), key=sort_fn) x_axis_groups = groupby(so, sort_fn) for (x_axis, itr1) in x_axis_groups: sort_fn = lambda (tk, td): dss[td['_x_axis_term']]['_data'] itr1 = sorted(itr1, key=sort_fn) for _vqs_num, (_data, itr2) in enumerate(groupby(itr1, sort_fn)): x_axis_vqs_groups[x_axis][_vqs_num] = _x_vqs = {} for tk, td in itr2: _x_vqs.setdefault(td['_x_axis_term'], []).append(tk) return x_axis_vqs_groups def _set_default_hcoptions(self, chart_options): """Set some default options, like xAxis title, yAxis title, chart title, etc. """ so = self.series_options dss = self.datasource.series self.hcoptions = HCOptions({}) if chart_options is not None: self.hcoptions.update(chart_options) self.hcoptions['series'] = [] # Set title title = '' for x_axis_num, vqs_group in self.x_axis_vqs_groups.items(): for vqs_num, x_y_terms in vqs_group.items(): for x_term, y_terms in x_y_terms.items(): title += ', '.join([dss[y_term]['field_alias'].title() for y_term in y_terms]) title += ' vs. ' title += dss[x_term]['field_alias'].title() title += ' & ' if not self.hcoptions['title']['text']: self.hcoptions['title']['text'] = title[:-3] # if xAxis and yAxis are supplied as a dict, embed it in a list # (needed for multiple axes) xAxis, yAxis = self.hcoptions['xAxis'], self.hcoptions['yAxis'] if isinstance(xAxis, dict): self.hcoptions['xAxis'] = [xAxis] if isinstance(yAxis, dict): self.hcoptions['yAxis'] = [yAxis] # set renderTo if not self.hcoptions['chart']['renderTo']: self.hcoptions['chart']['renderTo'] = 'container' term_x_axis = [(dss[d['_x_axis_term']]['field_alias'].title(), d.get('xAxis', 0)) for (k, d) in so.items()] term_y_axis = [(dss[k]['field_alias'].title(), d.get('xAxis', 0)) for (k, d) in so.items()] max_x_axis = max(t[1] for t in term_x_axis) max_y_axis = max(t[1] for t in term_y_axis) x_axis_len = len(self.hcoptions['xAxis']) y_axis_len = len(self.hcoptions['yAxis']) if max_x_axis >= x_axis_len: self.hcoptions['xAxis']\ .extend([HCOptions({})]*(max_x_axis+1-x_axis_len)) for i, x_axis in enumerate(self.hcoptions['xAxis']): if not x_axis['title']['text']: axis_title = set(t[0] for t in term_x_axis if t[1] == i) x_axis['title']['text'] = ' & '.join(axis_title) if max_x_axis == 1: if self.hcoptions['xAxis'][1]['opposite'] is not False: self.hcoptions['xAxis'][1]['opposite'] = True if max_y_axis >= y_axis_len: self.hcoptions['yAxis']\ .extend([HCOptions({})]*(max_y_axis+1-y_axis_len)) for i, y_axis in enumerate(self.hcoptions['yAxis']): if not y_axis['title']['text']: axis_title = set(t[0] for t in term_y_axis if t[1] == i) y_axis['title']['text'] = ' & '.join(axis_title) if max_y_axis == 1: if self.hcoptions['yAxis'][1]['opposite'] is not False: self.hcoptions['yAxis'][1]['opposite'] = True def generate_plot(self): # reset the series self.hcoptions['series'] = [] dss = self.datasource.series # find all x's from different datasources that need to be plotted on # same xAxis and also find their corresponding y's cht_typ_grp = lambda y_term: ('scatter' if self.series_options[y_term]['type'] in ['scatter', 'pie'] else 'line') for x_axis_num, vqs_groups in self.x_axis_vqs_groups.items(): y_hco_list = [] try: x_sortf, x_mapf, x_mts = self.x_sortf_mapf_mts[x_axis_num] except IndexError: x_sortf, x_mapf, x_mts = (None, None, False) ptype_x_y_terms = defaultdict(list) for vqs_group in vqs_groups.values(): x_term, y_terms_all = vqs_group.items()[0] y_terms_by_type = defaultdict(list) for y_term in y_terms_all: y_terms_by_type[cht_typ_grp(y_term)].append(y_term) for y_type, y_term_list in y_terms_by_type.items(): ptype_x_y_terms[y_type].append((x_term, y_term_list)) # ptype = plot type i.e. 'line', 'scatter', 'area', etc. for ptype, x_y_terms_tuples in ptype_x_y_terms.items(): y_fields_multi = [] y_aliases_multi = [] y_types_multi = [] y_hco_list_multi = [] y_values_multi = SortedDict() y_terms_multi = [] for x_term, y_terms in x_y_terms_tuples: # x related x_vqs = dss[x_term]['_data'] x_field = dss[x_term]['field'] # y related y_fields = [dss[y_term]['field'] for y_term in y_terms] y_aliases = [dss[y_term]['field_alias'] for y_term in y_terms] y_types = [self.series_options[y_term].get('type', 'line') for y_term in y_terms] y_hco_list = [HCOptions( copy.deepcopy(self.series_options[y_term])) for y_term in y_terms] for opts, alias, typ in zip(y_hco_list, y_aliases, y_types): opts.pop('_x_axis_term') opts['name'] = alias opts['type'] = typ opts['data'] = [] if ptype == 'scatter' or (ptype == 'line' and len(x_y_terms_tuples) == 1): if x_mts: if x_mapf: data = ((x_mapf(value_dict[x_field]), [value_dict[y_field] for y_field in y_fields]) for value_dict in x_vqs) sort_key = ((lambda(x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) else: sort_key = ((lambda(x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted( ((value_dict[x_field], [value_dict[y_field] for y_field in y_fields]) for value_dict in x_vqs), key=sort_key) if x_mapf: data = [(x_mapf(x), y) for (x, y) in data] if ptype == 'scatter': if self.series_options[y_term]['type'] == 'scatter': # scatter plot for x_value, y_value_tuple in data: for opts, y_value in izip(y_hco_list, y_value_tuple): opts['data'].append((x_value, y_value)) self.hcoptions['series'].extend(y_hco_list) else: # pie chart for x_value, y_value_tuple in data: for opts, y_value in izip(y_hco_list, y_value_tuple): opts['data'].append((str(x_value), y_value)) self.hcoptions['series'].extend(y_hco_list) if ptype == 'line' and len(x_y_terms_tuples) == 1: # all other chart types - line, area, etc. hco_x_axis = self.hcoptions['xAxis'] if len(hco_x_axis) - 1 < x_axis_num: hco_x_axis.extend([HCOptions({})] * (x_axis_num - (len(hco_x_axis) - 1))) hco_x_axis[x_axis_num]['categories'] = [] for x_value, y_value_tuple in data: hco_x_axis[x_axis_num]['categories']\ .append(x_value) for opts, y_value in izip(y_hco_list, y_value_tuple): opts['data'].append(y_value) self.hcoptions['series'].extend(y_hco_list) else: data = ((value_dict[x_field], [value_dict[y_field] for y_field in y_fields]) for value_dict in x_vqs) y_terms_multi.extend(y_terms) y_fields_multi.extend(y_fields) y_aliases_multi.extend(y_aliases) y_types_multi.extend(y_types) y_hco_list_multi.extend(y_hco_list) len_y_terms_multi = len(y_terms_multi) ext_len = len(y_terms_multi) - len(y_terms) for x_value, y_value_tuple in data: try: cur_y = y_values_multi[x_value] cur_y.extend(y_value_tuple) except KeyError: y_values_multi[x_value] = [None]*ext_len y_values_multi[x_value]\ .extend(y_value_tuple) for _y_vals in y_values_multi.values(): if len(_y_vals) != len_y_terms_multi: _y_vals.extend([None]*len(y_terms)) if y_terms_multi: hco_x_axis = self.hcoptions['xAxis'] if len(hco_x_axis) - 1 < x_axis_num: hco_x_axis\ .extend([HCOptions({})] * (x_axis_num - (len(hco_x_axis)-1))) hco_x_axis[x_axis_num]['categories'] = [] if x_mts: if x_mapf: data = ((x_mapf(x_value), y_vals) for (x_value, y_vals) in y_values_multi.iteritems()) sort_key = ((lambda(x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) else: data = y_values_multi.iteritems() sort_key = ((lambda(x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) if x_mapf: data = [(x_mapf(x), y) for (x, y) in data] for x_value, y_vals in data: hco_x_axis[x_axis_num]['categories']\ .append(x_value) for opts, y_value in izip(y_hco_list_multi, y_vals): opts['data'].append(y_value) self.hcoptions['series'].extend(y_hco_list_multi)
def _set_default_hcoptions(self, chart_options): """Set some default options, like xAxis title, yAxis title, chart title, etc. """ so = self.series_options dss = self.datasource.series self.hcoptions = HCOptions({}) if chart_options is not None: self.hcoptions.update(chart_options) self.hcoptions['series'] = [] # Set title title = '' for x_axis_num, vqs_group in self.x_axis_vqs_groups.items(): for vqs_num, x_y_terms in vqs_group.items(): for x_term, y_terms in x_y_terms.items(): title += ', '.join([ dss[y_term]['field_alias'].title() for y_term in y_terms ]) title += ' vs. ' title += dss[x_term]['field_alias'].title() title += ' & ' if not self.hcoptions['title']['text']: self.hcoptions['title']['text'] = title[:-3] # if xAxis and yAxis are supplied as a dict, embed it in a list # (needed for multiple axes) xAxis, yAxis = self.hcoptions['xAxis'], self.hcoptions['yAxis'] if isinstance(xAxis, dict): self.hcoptions['xAxis'] = [xAxis] if isinstance(yAxis, dict): self.hcoptions['yAxis'] = [yAxis] # set renderTo if not self.hcoptions['chart']['renderTo']: self.hcoptions['chart']['renderTo'] = 'container' term_x_axis = [(dss[d['_x_axis_term']]['field_alias'].title(), d.get('xAxis', 0)) for (k, d) in so.items()] term_y_axis = [(dss[k]['field_alias'].title(), d.get('xAxis', 0)) for (k, d) in so.items()] max_x_axis = max(t[1] for t in term_x_axis) max_y_axis = max(t[1] for t in term_y_axis) x_axis_len = len(self.hcoptions['xAxis']) y_axis_len = len(self.hcoptions['yAxis']) if max_x_axis >= x_axis_len: self.hcoptions['xAxis']\ .extend([HCOptions({})]*(max_x_axis+1-x_axis_len)) for i, x_axis in enumerate(self.hcoptions['xAxis']): if not x_axis['title']['text']: axis_title = set(t[0] for t in term_x_axis if t[1] == i) x_axis['title']['text'] = ' & '.join(axis_title) if max_x_axis == 1: if self.hcoptions['xAxis'][1]['opposite'] != False: self.hcoptions['xAxis'][1]['opposite'] = True if max_y_axis >= y_axis_len: self.hcoptions['yAxis']\ .extend([HCOptions({})]*(max_y_axis+1-y_axis_len)) for i, y_axis in enumerate(self.hcoptions['yAxis']): if not y_axis['title']['text']: axis_title = set(t[0] for t in term_y_axis if t[1] == i) y_axis['title']['text'] = ' & '.join(axis_title) if max_y_axis == 1: if self.hcoptions['yAxis'][1]['opposite'] != False: self.hcoptions['yAxis'][1]['opposite'] = True
def generate_plot(self): # reset the series self.hcoptions['series'] = [] dss = self.datasource.series # find all x's from different datasources that need to be plotted on # same xAxis and also find their corresponding y's cht_typ_grp = lambda y_term: ('scatter' if self.series_options[y_term][ 'type'] in ['scatter', 'pie'] else 'line') for x_axis_num, vqs_groups in self.x_axis_vqs_groups.items(): y_hco_list = [] try: x_sortf, x_mapf, x_mts = self.x_sortf_mapf_mts[x_axis_num] except IndexError: x_sortf, x_mapf, x_mts = (None, None, False) ptype_x_y_terms = defaultdict(list) for vqs_group in vqs_groups.values(): x_term, y_terms_all = vqs_group.items()[0] y_terms_by_type = defaultdict(list) for y_term in y_terms_all: y_terms_by_type[cht_typ_grp(y_term)].append(y_term) for y_type, y_term_list in y_terms_by_type.items(): ptype_x_y_terms[y_type].append((x_term, y_term_list)) # ptype = plot type i.e. 'line', 'scatter', 'area', etc. for ptype, x_y_terms_tuples in ptype_x_y_terms.items(): y_fields_multi = [] y_aliases_multi = [] y_types_multi = [] y_hco_list_multi = [] y_values_multi = SortedDict() y_terms_multi = [] for x_term, y_terms in x_y_terms_tuples: # x related x_vqs = dss[x_term]['_data'] x_field = dss[x_term]['field'] # y related y_fields = [dss[y_term]['field'] for y_term in y_terms] y_aliases = [ dss[y_term]['field_alias'] for y_term in y_terms ] y_types = [ self.series_options[y_term].get('type', 'line') for y_term in y_terms ] y_hco_list = [ HCOptions(copy.deepcopy(self.series_options[y_term])) for y_term in y_terms ] for opts, alias, typ in zip(y_hco_list, y_aliases, y_types): opts.pop('_x_axis_term') opts['name'] = alias opts['type'] = typ opts['data'] = [] if ptype == 'scatter' or (ptype == 'line' and len(x_y_terms_tuples) == 1): if x_mts: if x_mapf: data = ((x_mapf(value_dict[x_field]), [ value_dict[y_field] for y_field in y_fields ]) for value_dict in x_vqs) sort_key = ((lambda (x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) else: sort_key = ((lambda (x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(((value_dict[x_field], [ value_dict[y_field] for y_field in y_fields ]) for value_dict in x_vqs), key=sort_key) if x_mapf: data = [(x_mapf(x), y) for (x, y) in data] if ptype == 'scatter': #scatter plot and pie chart for x_value, y_value_tuple in data: for opts, y_value in izip( y_hco_list, y_value_tuple): opts['data'].append((x_value, y_value)) self.hcoptions['series'].extend(y_hco_list) if ptype == 'line' and len(x_y_terms_tuples) == 1: # all other chart types - line, area, etc. hco_x_axis = self.hcoptions['xAxis'] if len(hco_x_axis) - 1 < x_axis_num: hco_x_axis.extend([HCOptions({})] * (x_axis_num - (len(hco_x_axis) - 1))) hco_x_axis[x_axis_num]['categories'] = [] for x_value, y_value_tuple in data: hco_x_axis[x_axis_num]['categories']\ .append(x_value) for opts, y_value in izip( y_hco_list, y_value_tuple): opts['data'].append(y_value) self.hcoptions['series'].extend(y_hco_list) else: data = ((value_dict[x_field], [value_dict[y_field] for y_field in y_fields]) for value_dict in x_vqs) y_terms_multi.extend(y_terms) y_fields_multi.extend(y_fields) y_aliases_multi.extend(y_aliases) y_types_multi.extend(y_types) y_hco_list_multi.extend(y_hco_list) len_y_terms_multi = len(y_terms_multi) ext_len = len(y_terms_multi) - len(y_terms) for x_value, y_value_tuple in data: try: cur_y = y_values_multi[x_value] cur_y.extend(y_value_tuple) except KeyError: y_values_multi[x_value] = [None] * ext_len y_values_multi[x_value]\ .extend(y_value_tuple) for _y_vals in y_values_multi.values(): if len(_y_vals) != len_y_terms_multi: _y_vals.extend([None] * len(y_terms)) if y_terms_multi: hco_x_axis = self.hcoptions['xAxis'] if len(hco_x_axis) - 1 < x_axis_num: hco_x_axis\ .extend([HCOptions({})]* (x_axis_num - (len(hco_x_axis)-1))) hco_x_axis[x_axis_num]['categories'] = [] if x_mts: if x_mapf: data = ((x_mapf(x_value), y_vals) for (x_value, y_vals) in y_values_multi.iteritems()) sort_key = ((lambda (x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) else: data = y_values_multi.iteritems() sort_key = ((lambda (x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) if x_mapf: data = [(x_mapf(x), y) for (x, y) in data] for x_value, y_vals in data: hco_x_axis[x_axis_num]['categories']\ .append(x_value) for opts, y_value in izip(y_hco_list_multi, y_vals): opts['data'].append(y_value) self.hcoptions['series'].extend(y_hco_list_multi)
class Chart(object): def __init__(self, datasource, series_options, chart_options=None, x_sortf_mapf_mts=None): self.user_input = locals() if not isinstance(datasource, DataPool): raise APIInputError("%s must be an instance of DataPool." % datasource) self.datasource = datasource self.series_options = clean_cso(series_options, self.datasource) self.x_sortf_mapf_mts = clean_x_sortf_mapf_mts(x_sortf_mapf_mts) self.x_axis_vqs_groups = self._groupby_x_axis_and_vqs() self._set_default_hcoptions(chart_options) self.generate_plot() def _groupby_x_axis_and_vqs(self): """Returns a list of list of lists where each list has the term and option dict with the same xAxis and within each list with same xAxis, all items in same sub-list have items with same ValueQuerySet. Here is an example of what this function would return. :: [ [[(term-1-A-1, opts-1-A-1), (term-1-A-2, opts-1-A-2), ...], [(term-1-B-1, opts-1-B-1), (term-1-B-2, opts-1-B-2), ...], ...], [[term-2-A-1, opts-2-A-1), (term-2-A-2, opts-2-A-2), ...], [term-2-B-2, opts-2-B-2), (term-2-B-2, opts-2-B-2), ...], ...], ... ] In the above example, - term-1-*-* all have same xAxis. - term-*-A-* all are from same ValueQuerySet (table) """ dss = self.datasource.series x_axis_vqs_groups = defaultdict(dict) sort_fn = lambda (tk, td): td.get('xAxis', 0) so = sorted(self.series_options.items(), key=sort_fn) x_axis_groups = groupby(so, sort_fn) for (x_axis, itr1) in x_axis_groups: sort_fn = lambda (tk, td): dss[td['_x_axis_term']]['_data'] itr1 = sorted(itr1, key=sort_fn) for _vqs_num, (_data, itr2) in enumerate(groupby(itr1, sort_fn)): x_axis_vqs_groups[x_axis][_vqs_num] = _x_vqs = {} for tk, td in itr2: _x_vqs.setdefault(td['_x_axis_term'], []).append(tk) return x_axis_vqs_groups def _set_default_hcoptions(self, chart_options): """Set some default options, like xAxis title, yAxis title, chart title, etc. """ so = self.series_options dss = self.datasource.series self.hcoptions = HCOptions({}) if chart_options is not None: self.hcoptions.update(chart_options) self.hcoptions['series'] = [] # Set title title = '' for x_axis_num, vqs_group in self.x_axis_vqs_groups.items(): for vqs_num, x_y_terms in vqs_group.items(): for x_term, y_terms in x_y_terms.items(): title += ', '.join([ dss[y_term]['field_alias'].title() for y_term in y_terms ]) title += ' vs. ' title += dss[x_term]['field_alias'].title() title += ' & ' if not self.hcoptions['title']['text']: self.hcoptions['title']['text'] = title[:-3] # if xAxis and yAxis are supplied as a dict, embed it in a list # (needed for multiple axes) xAxis, yAxis = self.hcoptions['xAxis'], self.hcoptions['yAxis'] if isinstance(xAxis, dict): self.hcoptions['xAxis'] = [xAxis] if isinstance(yAxis, dict): self.hcoptions['yAxis'] = [yAxis] # set renderTo if not self.hcoptions['chart']['renderTo']: self.hcoptions['chart']['renderTo'] = 'container' term_x_axis = [(dss[d['_x_axis_term']]['field_alias'].title(), d.get('xAxis', 0)) for (k, d) in so.items()] term_y_axis = [(dss[k]['field_alias'].title(), d.get('xAxis', 0)) for (k, d) in so.items()] max_x_axis = max(t[1] for t in term_x_axis) max_y_axis = max(t[1] for t in term_y_axis) x_axis_len = len(self.hcoptions['xAxis']) y_axis_len = len(self.hcoptions['yAxis']) if max_x_axis >= x_axis_len: self.hcoptions['xAxis']\ .extend([HCOptions({})]*(max_x_axis+1-x_axis_len)) for i, x_axis in enumerate(self.hcoptions['xAxis']): if not x_axis['title']['text']: axis_title = set(t[0] for t in term_x_axis if t[1] == i) x_axis['title']['text'] = ' & '.join(axis_title) if max_x_axis == 1: if self.hcoptions['xAxis'][1]['opposite'] != False: self.hcoptions['xAxis'][1]['opposite'] = True if max_y_axis >= y_axis_len: self.hcoptions['yAxis']\ .extend([HCOptions({})]*(max_y_axis+1-y_axis_len)) for i, y_axis in enumerate(self.hcoptions['yAxis']): if not y_axis['title']['text']: axis_title = set(t[0] for t in term_y_axis if t[1] == i) y_axis['title']['text'] = ' & '.join(axis_title) if max_y_axis == 1: if self.hcoptions['yAxis'][1]['opposite'] != False: self.hcoptions['yAxis'][1]['opposite'] = True def generate_plot(self): # reset the series self.hcoptions['series'] = [] dss = self.datasource.series # find all x's from different datasources that need to be plotted on # same xAxis and also find their corresponding y's cht_typ_grp = lambda y_term: ('scatter' if self.series_options[y_term][ 'type'] in ['scatter', 'pie'] else 'line') for x_axis_num, vqs_groups in self.x_axis_vqs_groups.items(): y_hco_list = [] try: x_sortf, x_mapf, x_mts = self.x_sortf_mapf_mts[x_axis_num] except IndexError: x_sortf, x_mapf, x_mts = (None, None, False) ptype_x_y_terms = defaultdict(list) for vqs_group in vqs_groups.values(): x_term, y_terms_all = vqs_group.items()[0] y_terms_by_type = defaultdict(list) for y_term in y_terms_all: y_terms_by_type[cht_typ_grp(y_term)].append(y_term) for y_type, y_term_list in y_terms_by_type.items(): ptype_x_y_terms[y_type].append((x_term, y_term_list)) # ptype = plot type i.e. 'line', 'scatter', 'area', etc. for ptype, x_y_terms_tuples in ptype_x_y_terms.items(): y_fields_multi = [] y_aliases_multi = [] y_types_multi = [] y_hco_list_multi = [] y_values_multi = SortedDict() y_terms_multi = [] for x_term, y_terms in x_y_terms_tuples: # x related x_vqs = dss[x_term]['_data'] x_field = dss[x_term]['field'] # y related y_fields = [dss[y_term]['field'] for y_term in y_terms] y_aliases = [ dss[y_term]['field_alias'] for y_term in y_terms ] y_types = [ self.series_options[y_term].get('type', 'line') for y_term in y_terms ] y_hco_list = [ HCOptions(copy.deepcopy(self.series_options[y_term])) for y_term in y_terms ] for opts, alias, typ in zip(y_hco_list, y_aliases, y_types): opts.pop('_x_axis_term') opts['name'] = alias opts['type'] = typ opts['data'] = [] if ptype == 'scatter' or (ptype == 'line' and len(x_y_terms_tuples) == 1): if x_mts: if x_mapf: data = ((x_mapf(value_dict[x_field]), [ value_dict[y_field] for y_field in y_fields ]) for value_dict in x_vqs) sort_key = ((lambda (x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) else: sort_key = ((lambda (x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(((value_dict[x_field], [ value_dict[y_field] for y_field in y_fields ]) for value_dict in x_vqs), key=sort_key) if x_mapf: data = [(x_mapf(x), y) for (x, y) in data] if ptype == 'scatter': #scatter plot and pie chart for x_value, y_value_tuple in data: for opts, y_value in izip( y_hco_list, y_value_tuple): opts['data'].append((x_value, y_value)) self.hcoptions['series'].extend(y_hco_list) if ptype == 'line' and len(x_y_terms_tuples) == 1: # all other chart types - line, area, etc. hco_x_axis = self.hcoptions['xAxis'] if len(hco_x_axis) - 1 < x_axis_num: hco_x_axis.extend([HCOptions({})] * (x_axis_num - (len(hco_x_axis) - 1))) hco_x_axis[x_axis_num]['categories'] = [] for x_value, y_value_tuple in data: hco_x_axis[x_axis_num]['categories']\ .append(x_value) for opts, y_value in izip( y_hco_list, y_value_tuple): opts['data'].append(y_value) self.hcoptions['series'].extend(y_hco_list) else: data = ((value_dict[x_field], [value_dict[y_field] for y_field in y_fields]) for value_dict in x_vqs) y_terms_multi.extend(y_terms) y_fields_multi.extend(y_fields) y_aliases_multi.extend(y_aliases) y_types_multi.extend(y_types) y_hco_list_multi.extend(y_hco_list) len_y_terms_multi = len(y_terms_multi) ext_len = len(y_terms_multi) - len(y_terms) for x_value, y_value_tuple in data: try: cur_y = y_values_multi[x_value] cur_y.extend(y_value_tuple) except KeyError: y_values_multi[x_value] = [None] * ext_len y_values_multi[x_value]\ .extend(y_value_tuple) for _y_vals in y_values_multi.values(): if len(_y_vals) != len_y_terms_multi: _y_vals.extend([None] * len(y_terms)) if y_terms_multi: hco_x_axis = self.hcoptions['xAxis'] if len(hco_x_axis) - 1 < x_axis_num: hco_x_axis\ .extend([HCOptions({})]* (x_axis_num - (len(hco_x_axis)-1))) hco_x_axis[x_axis_num]['categories'] = [] if x_mts: if x_mapf: data = ((x_mapf(x_value), y_vals) for (x_value, y_vals) in y_values_multi.iteritems()) sort_key = ((lambda (x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) else: data = y_values_multi.iteritems() sort_key = ((lambda (x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) if x_mapf: data = [(x_mapf(x), y) for (x, y) in data] for x_value, y_vals in data: hco_x_axis[x_axis_num]['categories']\ .append(x_value) for opts, y_value in izip(y_hco_list_multi, y_vals): opts['data'].append(y_value) self.hcoptions['series'].extend(y_hco_list_multi)
def __init__(self, datasource, series_options, chart_options=None): """Creates the PivotChart object. **Arguments**: - **datasource** (**required**) - a ``PivotDataPool`` object that holds the terms and other information to plot the chart from. - **series_options** (**required**) - specifies the options to plot the terms on the chart. It is of the form :: [{'options': { #any items from HighChart series. For ex. 'type': 'column' }, 'terms': [ 'a_valid_term', 'other_valid_term': { #any options to override. For ex. 'type': 'area', ... }, ... ] }, ... #repeat dicts with 'options' & 'terms' ] Where - - **options** (**required**) - a ``dict``. Any of the parameters from the `Highcharts options object - series array <http://www.highcharts.com/ref/#series>`_ are valid as entries in the ``options`` dict except ``data`` (because data array is generated from your datasource by chartit). For example, ``type``, ``xAxis``, etc. are all valid entries here. .. note:: The items supplied in the options dict are not validated to make sure that Highcharts actually supports them. Any invalid options are just passed to Highcharts JS which silently ignores them. - **terms** (**required**) - a ``list``. Only terms that are present in the corresponding datasource are valid. .. note:: All the ``terms`` are plotted on the ``y-axis``. The **categories of the datasource are plotted on the x-axis. There is no option to override this.** Each of the ``terms`` must either be a ``str`` or a ``dict``. If entries are dicts, the keys need to be valid terms and the values need to be any options to override the default options. For example, :: [{'options': { 'type': 'column', 'yAxis': 0}, 'terms': [ 'temperature', {'rainfall': { 'type': 'line', 'yAxis': 1}}]}] plots a pivot column chart of temperature on yAxis: 0 and a line pivot chart of rainfall on yAxis: 1. This can alternatively be expressed as two separate entries: :: [{'options': { 'type': 'column', 'yAxis': 0}, 'terms': [ 'temperature']}, {'options': { 'type': 'line', 'yAxis': 1}, 'terms': [ 'rainfall']}] - **chart_options** (*optional*) - a ``dict``. Any of the options from the `Highcharts options object <http://www.highcharts.com/ref/>`_ are valid (except the options in the ``series`` array which are passed in the ``series_options`` argument. The following ``chart_options`` for example, set the chart title and the axes titles. :: {'chart': { 'title': { 'text': 'Weather Chart'}}, 'xAxis': { 'title': 'month'}, 'yAxis': { 'title': 'temperature'}} .. note:: The items supplied in the ``chart_options`` dict are not validated to make sure that Highcharts actually supports them. Any invalid options are just passed to Highcharts JS which silently ignores them. **Raises**: - ``APIInputError`` if any of the terms are not present in the corresponding datasource or if the ``series_options`` cannot be parsed. """ self.user_input = locals() if not isinstance(datasource, PivotDataPool): raise APIInputError("%s must be an instance of PivotDataPool." % datasource) self.datasource = datasource self.series_options = clean_pcso(series_options, self.datasource) if chart_options is None: chart_options = HCOptions({}) self.set_default_hcoptions() self.hcoptions.update(chart_options) # Now generate the plot self.generate_plot()
class Chart(object): def __init__(self, datasource, series_options, chart_options=None, x_sortf_mapf_mts=None): """Chart accept the datasource and some options to create the chart and creates it. **Arguments**: - **datasource** (**required**) - a ``DataPool`` object that holds the terms and other information to plot the chart from. - **series_options** (**required**) - specifies the options to plot the terms on the chart. It is of the form :: [{'options': { #any items from HighChart series. For ex., 'type': 'column' }, 'terms': { 'x_name': ['y_name', {'other_y_name': { #overriding options}}, ...], ... }, }, ... #repeat dicts with 'options' & 'terms' ] Where - - **options** (**required**) - a ``dict``. Any of the parameters from the `Highcharts options object - series array <http://www.highcharts.com/ref/#series>`_ are valid as entries in the ``options`` dict except ``data`` (because data array is generated from your datasource by chartit). For example, ``type``, ``xAxis``, etc. are all valid entries here. .. note:: The items supplied in the options dict are not validated to make sure that Highcharts actually supports them. Any invalid options are just passed to Highcharts JS which silently ignores them. - **terms** (**required**) - a ``dict``. keys are the x-axis terms and the values are lists of y-axis terms for that particular x-axis term. Both x-axis and y-axis terms must be present in the corresponding datasource, otherwise an APIInputError is raised. The entries in the y-axis terms list must either be a ``str`` or a ``dict``. If entries are dicts, the keys need to be valid y-term names and the values need to be any options to override the default options. For example, :: [{'options': { 'type': 'column', 'yAxis': 0}, 'terms': { 'city': [ 'temperature', {'rainfall': { 'type': 'line', 'yAxis': 1}}]}}] plots a column chart of city vs. temperature as a line chart on yAxis: 0 and city vs. rainfall as a line chart on yAxis: 1. This can alternatively be expressed as two separate entries: :: [{'options': { 'type': 'column', 'yAxis': 0}, 'terms': { 'city': [ 'temperature']}}, {'options': { 'type': 'line', 'yAxis': 1}, 'terms': { 'city': [ 'rainfall']}}] - **chart_options** (*optional*) - a ``dict``. Any of the options from the `Highcharts options object <http://www.highcharts.com/ref/>`_ are valid (except the options in the ``series`` array which are passed in the ``series_options`` argument. The following ``chart_options`` for example, set the chart title and the axes titles. :: {'chart': { 'title': { 'text': 'Weather Chart'}}, 'xAxis': { 'title': 'month'}, 'yAxis': { 'title': 'temperature'}} .. note:: The items supplied in the ``chart_options`` dict are not validated to make sure that Highcharts actually supports them. Any invalid options are just passed to Highcharts JS which silently ignores them. **Raises**: - ``APIInputError`` if any of the terms are not present in the corresponding datasource or if the ``series_options`` cannot be parsed. """ self.user_input = locals() if not isinstance(datasource, DataPool): raise APIInputError("%s must be an instance of DataPool." % datasource) self.datasource = datasource self.series_options = clean_cso(series_options, self.datasource) self.x_sortf_mapf_mts = clean_x_sortf_mapf_mts(x_sortf_mapf_mts) self.x_axis_vqs_groups = self._groupby_x_axis_and_vqs() self._set_default_hcoptions(chart_options) self.generate_plot() def _groupby_x_axis_and_vqs(self): """Returns a list of list of lists where each list has the term and option dict with the same xAxis and within each list with same xAxis, all items in same sub-list have items with same ValueQuerySet. Here is an example of what this function would return. :: [ [[(term-1-A-1, opts-1-A-1), (term-1-A-2, opts-1-A-2), ...], [(term-1-B-1, opts-1-B-1), (term-1-B-2, opts-1-B-2), ...], ...], [[term-2-A-1, opts-2-A-1), (term-2-A-2, opts-2-A-2), ...], [term-2-B-2, opts-2-B-2), (term-2-B-2, opts-2-B-2), ...], ...], ... ] In the above example, - term-1-*-* all have same xAxis. - term-*-A-* all are from same ValueQuerySet (table) """ dss = self.datasource.series x_axis_vqs_groups = defaultdict(dict) sort_fn = lambda (tk, td): td.get('xAxis', 0) so = sorted(self.series_options.items(), key=sort_fn) x_axis_groups = groupby(so, sort_fn) for (x_axis, itr1) in x_axis_groups: sort_fn = lambda (tk, td): dss[td['_x_axis_term']]['_data'] itr1 = sorted(itr1, key=sort_fn) for _vqs_num, (_data, itr2) in enumerate(groupby(itr1, sort_fn)): x_axis_vqs_groups[x_axis][_vqs_num] = _x_vqs = {} for tk, td in itr2: _x_vqs.setdefault(td['_x_axis_term'], []).append(tk) return x_axis_vqs_groups def _set_default_hcoptions(self, chart_options): """Set some default options, like xAxis title, yAxis title, chart title, etc. """ so = self.series_options dss = self.datasource.series self.hcoptions = HCOptions({}) if chart_options is not None: self.hcoptions.update(chart_options) self.hcoptions['series'] = [] # Set title title = '' for x_axis_num, vqs_group in self.x_axis_vqs_groups.items(): for vqs_num, x_y_terms in vqs_group.items(): for x_term, y_terms in x_y_terms.items(): title += ', '.join([ dss[y_term]['field_alias'].title() for y_term in y_terms ]) title += ' vs. ' title += dss[x_term]['field_alias'].title() title += ' & ' if not self.hcoptions['title']['text']: self.hcoptions['title']['text'] = title[:-3] # if xAxis and yAxis are supplied as a dict, embed it in a list # (needed for multiple axes) xAxis, yAxis = self.hcoptions['xAxis'], self.hcoptions['yAxis'] if isinstance(xAxis, dict): self.hcoptions['xAxis'] = [xAxis] if isinstance(yAxis, dict): self.hcoptions['yAxis'] = [yAxis] # set renderTo if not self.hcoptions['chart']['renderTo']: self.hcoptions['chart']['renderTo'] = 'container' term_x_axis = [(dss[d['_x_axis_term']]['field_alias'].title(), d.get('xAxis', 0)) for (k, d) in so.items()] term_y_axis = [(dss[k]['field_alias'].title(), d.get('xAxis', 0)) for (k, d) in so.items()] max_x_axis = max(t[1] for t in term_x_axis) max_y_axis = max(t[1] for t in term_y_axis) x_axis_len = len(self.hcoptions['xAxis']) y_axis_len = len(self.hcoptions['yAxis']) if max_x_axis >= x_axis_len: self.hcoptions['xAxis']\ .extend([HCOptions({})]*(max_x_axis+1-x_axis_len)) for i, x_axis in enumerate(self.hcoptions['xAxis']): if not x_axis['title']['text']: axis_title = set(t[0] for t in term_x_axis if t[1] == i) x_axis['title']['text'] = ' & '.join(axis_title) if max_x_axis == 1: if self.hcoptions['xAxis'][1]['opposite'] != False: self.hcoptions['xAxis'][1]['opposite'] = True if max_y_axis >= y_axis_len: self.hcoptions['yAxis']\ .extend([HCOptions({})]*(max_y_axis+1-y_axis_len)) for i, y_axis in enumerate(self.hcoptions['yAxis']): if not y_axis['title']['text']: axis_title = set(t[0] for t in term_y_axis if t[1] == i) y_axis['title']['text'] = ' & '.join(axis_title) if max_y_axis == 1: if self.hcoptions['yAxis'][1]['opposite'] != False: self.hcoptions['yAxis'][1]['opposite'] = True def generate_plot(self): # reset the series self.hcoptions['series'] = [] dss = self.datasource.series # find all x's from different datasources that need to be plotted on # same xAxis and also find their corresponding y's cht_typ_grp = lambda y_term: ('scatter' if self.series_options[y_term][ 'type'] in ['scatter', 'pie'] else 'line') for x_axis_num, vqs_groups in self.x_axis_vqs_groups.items(): y_hco_list = [] try: x_sortf, x_mapf, x_mts = self.x_sortf_mapf_mts[x_axis_num] except IndexError: x_sortf, x_mapf, x_mts = (None, None, False) ptype_x_y_terms = defaultdict(list) for vqs_group in vqs_groups.values(): x_term, y_terms_all = vqs_group.items()[0] y_terms_by_type = defaultdict(list) for y_term in y_terms_all: y_terms_by_type[cht_typ_grp(y_term)].append(y_term) for y_type, y_term_list in y_terms_by_type.items(): ptype_x_y_terms[y_type].append((x_term, y_term_list)) # ptype = plot type i.e. 'line', 'scatter', 'area', etc. for ptype, x_y_terms_tuples in ptype_x_y_terms.items(): y_fields_multi = [] y_aliases_multi = [] y_types_multi = [] y_hco_list_multi = [] y_values_multi = SortedDict() y_terms_multi = [] for x_term, y_terms in x_y_terms_tuples: # x related x_vqs = dss[x_term]['_data'] x_field = dss[x_term]['field'] # y related y_fields = [dss[y_term]['field'] for y_term in y_terms] y_aliases = [ dss[y_term]['field_alias'] for y_term in y_terms ] y_types = [ self.series_options[y_term].get('type', 'line') for y_term in y_terms ] y_hco_list = [ HCOptions(copy.deepcopy(self.series_options[y_term])) for y_term in y_terms ] for opts, alias, typ in zip(y_hco_list, y_aliases, y_types): opts.pop('_x_axis_term') opts['name'] = alias opts['type'] = typ opts['data'] = [] if ptype == 'scatter' or (ptype == 'line' and len(x_y_terms_tuples) == 1): if x_mts: if x_mapf: data = ((x_mapf(value_dict[x_field]), [ value_dict[y_field] for y_field in y_fields ]) for value_dict in x_vqs) sort_key = ((lambda (x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) else: sort_key = ((lambda (x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(((value_dict[x_field], [ value_dict[y_field] for y_field in y_fields ]) for value_dict in x_vqs), key=sort_key) if x_mapf: data = [(x_mapf(x), y) for (x, y) in data] if ptype == 'scatter': if self.series_options[y_term][ 'type'] == 'scatter': #scatter plot for x_value, y_value_tuple in data: for opts, y_value in izip( y_hco_list, y_value_tuple): opts['data'].append((x_value, y_value)) self.hcoptions['series'].extend(y_hco_list) else: # pie chart for x_value, y_value_tuple in data: for opts, y_value in izip( y_hco_list, y_value_tuple): opts['data'].append( (str(x_value), y_value)) self.hcoptions['series'].extend(y_hco_list) if ptype == 'line' and len(x_y_terms_tuples) == 1: # all other chart types - line, area, etc. hco_x_axis = self.hcoptions['xAxis'] if len(hco_x_axis) - 1 < x_axis_num: hco_x_axis.extend([HCOptions({})] * (x_axis_num - (len(hco_x_axis) - 1))) hco_x_axis[x_axis_num]['categories'] = [] for x_value, y_value_tuple in data: hco_x_axis[x_axis_num]['categories']\ .append(x_value) for opts, y_value in izip( y_hco_list, y_value_tuple): opts['data'].append(y_value) self.hcoptions['series'].extend(y_hco_list) else: data = ((value_dict[x_field], [value_dict[y_field] for y_field in y_fields]) for value_dict in x_vqs) y_terms_multi.extend(y_terms) y_fields_multi.extend(y_fields) y_aliases_multi.extend(y_aliases) y_types_multi.extend(y_types) y_hco_list_multi.extend(y_hco_list) len_y_terms_multi = len(y_terms_multi) ext_len = len(y_terms_multi) - len(y_terms) for x_value, y_value_tuple in data: try: cur_y = y_values_multi[x_value] cur_y.extend(y_value_tuple) except KeyError: y_values_multi[x_value] = [None] * ext_len y_values_multi[x_value]\ .extend(y_value_tuple) for _y_vals in y_values_multi.values(): if len(_y_vals) != len_y_terms_multi: _y_vals.extend([None] * len(y_terms)) if y_terms_multi: hco_x_axis = self.hcoptions['xAxis'] if len(hco_x_axis) - 1 < x_axis_num: hco_x_axis\ .extend([HCOptions({})]* (x_axis_num - (len(hco_x_axis)-1))) hco_x_axis[x_axis_num]['categories'] = [] if x_mts: if x_mapf: data = ((x_mapf(x_value), y_vals) for (x_value, y_vals) in y_values_multi.iteritems()) sort_key = ((lambda (x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) else: data = y_values_multi.iteritems() sort_key = ((lambda (x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) if x_mapf: data = [(x_mapf(x), y) for (x, y) in data] for x_value, y_vals in data: hco_x_axis[x_axis_num]['categories']\ .append(x_value) for opts, y_value in izip(y_hco_list_multi, y_vals): opts['data'].append(y_value) self.hcoptions['series'].extend(y_hco_list_multi)
class Chart(object): def __init__(self, datasource, series_options, chart_options=None, x_sortf_mapf_mts=None): self.user_input = locals() if not isinstance(datasource, DataPool): raise APIInputError("%s must be an instance of DataPool." %datasource) self.datasource = datasource self.series_options = clean_cso(series_options, self.datasource) self.x_sortf_mapf_mts = clean_x_sortf_mapf_mts(x_sortf_mapf_mts) self.x_axis_vqs_groups = self._groupby_x_axis_and_vqs() self._set_default_hcoptions(chart_options) self.generate_plot() def _groupby_x_axis_and_vqs(self): """Returns a list of list of lists where each list has the term and option dict with the same xAxis and within each list with same xAxis, all items in same sub-list have items with same ValueQuerySet. Here is an example of what this function would return. :: [ [[(term-1-A-1, opts-1-A-1), (term-1-A-2, opts-1-A-2), ...], [(term-1-B-1, opts-1-B-1), (term-1-B-2, opts-1-B-2), ...], ...], [[term-2-A-1, opts-2-A-1), (term-2-A-2, opts-2-A-2), ...], [term-2-B-2, opts-2-B-2), (term-2-B-2, opts-2-B-2), ...], ...], ... ] In the above example, - term-1-*-* all have same xAxis. - term-*-A-* all are from same ValueQuerySet (table) """ dss = self.datasource.series x_axis_vqs_groups = defaultdict(dict) sort_fn = lambda (tk, td): td.get('xAxis', 0) so = sorted(self.series_options.items(), key=sort_fn) x_axis_groups = groupby(so, sort_fn) for (x_axis, itr1) in x_axis_groups: sort_fn = lambda (tk, td): dss[td['_x_axis_term']]['_data'] itr1 = sorted(itr1, key=sort_fn) for _vqs_num, (_data, itr2) in enumerate(groupby(itr1, sort_fn)): x_axis_vqs_groups[x_axis][_vqs_num] = _x_vqs = {} for tk, td in itr2: _x_vqs.setdefault(td['_x_axis_term'], []).append(tk) return x_axis_vqs_groups def _set_default_hcoptions(self, chart_options): """Set some default options, like xAxis title, yAxis title, chart title, etc. """ so = self.series_options dss = self.datasource.series self.hcoptions = HCOptions({}) if chart_options is not None: self.hcoptions.update(chart_options) self.hcoptions['series'] = [] # Set title title = '' for x_axis_num, vqs_group in self.x_axis_vqs_groups.items(): for vqs_num, x_y_terms in vqs_group.items(): for x_term, y_terms in x_y_terms.items(): title += ', '.join([dss[y_term]['field_alias'].title() for y_term in y_terms]) title += ' vs. ' title += dss[x_term]['field_alias'].title() title += ' & ' if not self.hcoptions['title']['text']: self.hcoptions['title']['text'] = title[:-3] # if xAxis and yAxis are supplied as a dict, embed it in a list # (needed for multiple axes) xAxis, yAxis = self.hcoptions['xAxis'], self.hcoptions['yAxis'] if isinstance(xAxis, dict): self.hcoptions['xAxis'] = [xAxis] if isinstance(yAxis, dict): self.hcoptions['yAxis'] = [yAxis] # set renderTo if not self.hcoptions['chart']['renderTo']: self.hcoptions['chart']['renderTo'] = 'container' term_x_axis = [(dss[d['_x_axis_term']]['field_alias'].title(), d.get('xAxis', 0)) for (k, d) in so.items()] term_y_axis = [(dss[k]['field_alias'].title(), d.get('xAxis', 0)) for (k, d) in so.items()] max_x_axis = max(t[1] for t in term_x_axis) max_y_axis = max(t[1] for t in term_y_axis) x_axis_len = len(self.hcoptions['xAxis']) y_axis_len = len(self.hcoptions['yAxis']) if max_x_axis >= x_axis_len: self.hcoptions['xAxis']\ .extend([HCOptions({})]*(max_x_axis+1-x_axis_len)) for i, x_axis in enumerate(self.hcoptions['xAxis']): if not x_axis['title']['text']: axis_title = set(t[0] for t in term_x_axis if t[1] == i) x_axis['title']['text'] = ' & '.join(axis_title) if max_x_axis == 1: if self.hcoptions['xAxis'][1]['opposite'] != False: self.hcoptions['xAxis'][1]['opposite'] = True if max_y_axis >= y_axis_len: self.hcoptions['yAxis']\ .extend([HCOptions({})]*(max_y_axis+1-y_axis_len)) for i, y_axis in enumerate(self.hcoptions['yAxis']): if not y_axis['title']['text']: axis_title = set(t[0] for t in term_y_axis if t[1] == i) y_axis['title']['text'] = ' & '.join(axis_title) if max_y_axis == 1: if self.hcoptions['yAxis'][1]['opposite'] != False: self.hcoptions['yAxis'][1]['opposite'] = True def generate_plot(self): # reset the series self.hcoptions['series'] = [] dss = self.datasource.series # find all x's from different datasources that need to be plotted on # same xAxis and also find their corresponding y's cht_typ_grp = lambda y_term: ('scatter' if self.series_options[y_term]['type'] in ['scatter', 'pie'] else 'line') for x_axis_num, vqs_groups in self.x_axis_vqs_groups.items(): y_hco_list = [] try: x_sortf, x_mapf, x_mts = self.x_sortf_mapf_mts[x_axis_num] except IndexError: x_sortf, x_mapf, x_mts = (None, None, False) ptype_x_y_terms = defaultdict(list) for vqs_group in vqs_groups.values(): x_term, y_terms_all = vqs_group.items()[0] y_terms_by_type = defaultdict(list) for y_term in y_terms_all: y_terms_by_type[cht_typ_grp(y_term)].append(y_term) for y_type, y_term_list in y_terms_by_type.items(): ptype_x_y_terms[y_type].append((x_term, y_term_list)) # ptype = plot type i.e. 'line', 'scatter', 'area', etc. for ptype, x_y_terms_tuples in ptype_x_y_terms.items(): y_fields_multi = [] y_aliases_multi = [] y_types_multi = [] y_hco_list_multi = [] y_values_multi = SortedDict() y_terms_multi = [] for x_term, y_terms in x_y_terms_tuples: # x related x_vqs = dss[x_term]['_data'] x_field = dss[x_term]['field'] # y related y_fields = [dss[y_term]['field'] for y_term in y_terms] y_aliases = [dss[y_term]['field_alias'] for y_term in y_terms] y_types = [self.series_options[y_term].get('type','line') for y_term in y_terms] y_hco_list = [HCOptions( copy.deepcopy( self.series_options[y_term])) for y_term in y_terms] for opts, alias, typ in zip(y_hco_list,y_aliases,y_types): opts.pop('_x_axis_term') opts['name'] = alias opts['type'] = typ opts['data'] = [] if ptype == 'scatter' or (ptype == 'line' and len(x_y_terms_tuples) == 1): if x_mts: if x_mapf: data = ((x_mapf(value_dict[x_field]), [value_dict[y_field] for y_field in y_fields]) for value_dict in x_vqs) sort_key = ((lambda(x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) else: sort_key = ((lambda(x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted( ((value_dict[x_field], [value_dict[y_field] for y_field in y_fields]) for value_dict in x_vqs), key=sort_key) if x_mapf: data = [(x_mapf(x), y) for (x, y) in data] if ptype == 'scatter': #scatter plot and pie chart for x_value, y_value_tuple in data: for opts, y_value in izip(y_hco_list, y_value_tuple): opts['data'].append((x_value, y_value)) self.hcoptions['series'].extend(y_hco_list) if ptype == 'line' and len(x_y_terms_tuples) == 1: # all other chart types - line, area, etc. hco_x_axis = self.hcoptions['xAxis'] if len(hco_x_axis) - 1 < x_axis_num: hco_x_axis.extend([HCOptions({})]* (x_axis_num - (len(hco_x_axis) - 1))) hco_x_axis[x_axis_num]['categories'] = [] for x_value, y_value_tuple in data: hco_x_axis[x_axis_num]['categories']\ .append(x_value) for opts, y_value in izip(y_hco_list, y_value_tuple): opts['data'].append(y_value) self.hcoptions['series'].extend(y_hco_list) else: data = ((value_dict[x_field], [value_dict[y_field] for y_field in y_fields]) for value_dict in x_vqs) y_terms_multi.extend(y_terms) y_fields_multi.extend(y_fields) y_aliases_multi.extend(y_aliases) y_types_multi.extend(y_types) y_hco_list_multi.extend(y_hco_list) len_y_terms_multi = len(y_terms_multi) ext_len = len(y_terms_multi) - len(y_terms) for x_value, y_value_tuple in data: try: cur_y = y_values_multi[x_value] cur_y.extend(y_value_tuple) except KeyError: y_values_multi[x_value] = [None]*ext_len y_values_multi[x_value]\ .extend(y_value_tuple) for _y_vals in y_values_multi.values(): if len(_y_vals) != len_y_terms_multi: _y_vals.extend([None]*len(y_terms)) if y_terms_multi: hco_x_axis = self.hcoptions['xAxis'] if len(hco_x_axis) - 1 < x_axis_num: hco_x_axis\ .extend([HCOptions({})]* (x_axis_num - (len(hco_x_axis)-1))) hco_x_axis[x_axis_num]['categories'] = [] if x_mts: if x_mapf: data = ((x_mapf(x_value), y_vals) for (x_value, y_vals) in y_values_multi.iteritems()) sort_key = ((lambda(x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) else: data = y_values_multi.iteritems() sort_key = ((lambda(x, y): x_sortf(x)) if x_sortf is not None else None) data = sorted(data, key=sort_key) if x_mapf: data = [(x_mapf(x), y) for (x, y) in data] for x_value, y_vals in data: hco_x_axis[x_axis_num]['categories']\ .append(x_value) for opts, y_value in izip(y_hco_list_multi, y_vals): opts['data'].append(y_value) self.hcoptions['series'].extend(y_hco_list_multi)