123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537 |
- import logging
- from flask_babel import lazy_gettext
- from .jsontools import dict_to_json
- from .widgets import ChartWidget, DirectChartWidget
- from ..baseviews import BaseModelView, expose
- from ..models.group import DirectProcessData, GroupByProcessData
- from ..security.decorators import has_access
- from ..urltools import get_filter_args
- from ..widgets import SearchWidget
- log = logging.getLogger(__name__)
- class BaseChartView(BaseModelView):
- """
- This is the base class for all chart views.
- Use DirectByChartView or GroupByChartView, override their properties
- and their base classes
- (BaseView, BaseModelView, BaseChartView) to customise your charts
- """
- chart_template = "appbuilder/general/charts/chart.html"
- """ The chart template, override to implement your own """
- chart_widget = ChartWidget
- """ Chart widget override to implement your own """
- search_widget = SearchWidget
- """ Search widget override to implement your own """
- chart_title = "Chart"
- """ A title to be displayed on the chart """
- title = "Title"
- group_by_label = lazy_gettext("Group by")
- """ The label that is displayed for the chart selection """
- default_view = "chart"
- chart_type = "PieChart"
- """ The chart type PieChart, ColumnChart, LineChart """
- chart_3d = "true"
- """ Will display in 3D? """
- width = 400
- """ The width """
- height = "400px"
- group_bys = {}
- """ New for 0.6.4, on test, don't use yet """
- def __init__(self, **kwargs):
- self._init_titles()
- super(BaseChartView, self).__init__(**kwargs)
- def _init_titles(self):
- self.title = self.chart_title
- def _get_chart_widget(self, filters=None, widgets=None, **args):
- raise NotImplementedError
- def _get_view_widget(self, **kwargs):
- """
- :return:
- Returns a widget
- """
- return self._get_chart_widget(**kwargs).get("chart")
- class GroupByChartView(BaseChartView):
- definitions = []
- """
- These charts can display multiple series,
- based on columns or methods defined on models.
- You can display multiple charts on the same view.
- This data can be grouped and aggregated has you like.
- :label: (optional) String label to display on chart selection.
- :group: String with the column name or method from model.
- :formatter: (optional) function that formats the output of 'group' key
- :series: A list of tuples with the aggregation function and the column name
- to apply the aggregation
- ::
- [{
- 'label': 'String',
- 'group': '<COLNAME>'|'<FUNCNAME>'
- 'formatter: <FUNC>
- 'series': [(<AGGR FUNC>, <COLNAME>|'<FUNCNAME>'),...]
- }
- ]
- example::
- class CountryGroupByChartView(GroupByChartView):
- datamodel = SQLAInterface(CountryStats)
- chart_title = 'Statistics'
- definitions = [
- {
- 'label': 'Country Stat',
- 'group': 'country',
- 'series': [(aggregate_avg, 'unemployed_perc'),
- (aggregate_avg, 'population'),
- (aggregate_avg, 'college_perc')
- ]
- }
- ]
- """
- chart_type = "ColumnChart"
- chart_template = "appbuilder/general/charts/jsonchart.html"
- chart_widget = DirectChartWidget
- ProcessClass = GroupByProcessData
- def __init__(self, **kwargs):
- super(GroupByChartView, self).__init__(**kwargs)
- for definition in self.definitions:
- col = definition.get("group")
- # Setup labels
- try:
- self.label_columns[col] = (
- definition.get("label") or self.label_columns[col]
- )
- except Exception:
- self.label_columns[col] = self._prettify_column(col)
- if not definition.get("label"):
- definition["label"] = self.label_columns[col]
- # Setup Series
- for serie in definition["series"]:
- if isinstance(serie, tuple):
- if hasattr(serie[0], "_label"):
- key = serie[0].__name__ + serie[1]
- self.label_columns[key] = (
- serie[0]._label + " " + self._prettify_column(serie[1])
- )
- else:
- self.label_columns[serie] = self._prettify_column(serie)
- def get_group_by_class(self, definition):
- """
- intantiates the processing class (Direct or Grouped) and returns it.
- """
- group_by = definition["group"]
- series = definition["series"]
- if "formatter" in definition:
- formatter = {group_by: definition["formatter"]}
- else:
- formatter = {}
- return self.ProcessClass([group_by], series, formatter)
- def _get_chart_widget(
- self,
- filters=None,
- order_column="",
- order_direction="",
- widgets=None,
- direct=None,
- height=None,
- definition="",
- **args
- ):
- height = height or self.height
- widgets = widgets or dict()
- joined_filters = filters.get_joined_filters(self._base_filters)
- # check if order_column may be database ordered
- if not self.datamodel.get_order_columns_list([order_column]):
- order_column = ""
- order_direction = ""
- count, lst = self.datamodel.query(
- filters=joined_filters,
- order_column=order_column,
- order_direction=order_direction,
- )
- if not definition:
- definition = self.definitions[0]
- group = self.get_group_by_class(definition)
- value_columns = group.to_json(
- group.apply(lst, sort=order_column == ""), self.label_columns
- )
- widgets["chart"] = self.chart_widget(
- route_base=self.route_base,
- chart_title=self.chart_title,
- chart_type=self.chart_type,
- chart_3d=self.chart_3d,
- height=height,
- value_columns=value_columns,
- modelview_name=self.__class__.__name__,
- **args
- )
- return widgets
- @expose("/chart/<group_by>")
- @expose("/chart/")
- @has_access
- def chart(self, group_by=0):
- group_by = int(group_by)
- form = self.search_form.refresh()
- get_filter_args(self._filters)
- widgets = self._get_chart_widget(
- filters=self._filters,
- definition=self.definitions[group_by],
- order_column=self.definitions[group_by]["group"],
- order_direction="asc",
- )
- widgets = self._get_search_widget(form=form, widgets=widgets)
- self.update_redirect()
- return self.render_template(
- self.chart_template,
- route_base=self.route_base,
- title=self.chart_title,
- label_columns=self.label_columns,
- definitions=self.definitions,
- group_by_label=self.group_by_label,
- height=self.height,
- widgets=widgets,
- appbuilder=self.appbuilder,
- )
- class DirectByChartView(GroupByChartView):
- """
- Use this class to display charts with multiple series,
- based on columns or methods defined on models.
- You can display multiple charts on the same view.
- Default routing point is '/chart'
- Setup definitions property to configure the chart
- :label: (optional) String label to display on chart selection.
- :group: String with the column name or method from model.
- :formatter: (optional) function that formats the output of 'group' key
- :series: A list of tuples with the aggregation function and the column name
- to apply the aggregation
- The **definitions** property respects the following grammar::
- definitions = [
- {
- 'label': 'label for chart definition',
- 'group': '<COLNAME>'|'<MODEL FUNCNAME>',
- 'formatter': <FUNC FORMATTER FOR GROUP COL>,
- 'series': ['<COLNAME>'|'<MODEL FUNCNAME>',...]
- }, ...
- ]
- example::
- class CountryDirectChartView(DirectByChartView):
- datamodel = SQLAInterface(CountryStats)
- chart_title = 'Direct Data Example'
- definitions = [
- {
- 'label': 'Unemployment',
- 'group': 'stat_date',
- 'series': ['unemployed_perc',
- 'college_perc']
- }
- ]
- """
- ProcessClass = DirectProcessData
- # -------------------------------------------------------
- # DEPRECATED SECTION
- # -------------------------------------------------------
- class BaseSimpleGroupByChartView(BaseChartView): # pragma: no cover
- group_by_columns = []
- """ A list of columns to be possibly grouped by, this list must be filled """
- def __init__(self, **kwargs):
- if not self.group_by_columns:
- raise Exception(
- "Base Chart View property <group_by_columns> must not be empty"
- )
- else:
- super(BaseSimpleGroupByChartView, self).__init__(**kwargs)
- def _get_chart_widget(
- self,
- filters=None,
- order_column="",
- order_direction="",
- widgets=None,
- group_by=None,
- height=None,
- **args
- ):
- height = height or self.height
- widgets = widgets or dict()
- group_by = group_by or self.group_by_columns[0]
- joined_filters = filters.get_joined_filters(self._base_filters)
- value_columns = self.datamodel.query_simple_group(
- group_by, filters=joined_filters
- )
- widgets["chart"] = self.chart_widget(
- route_base=self.route_base,
- chart_title=self.chart_title,
- chart_type=self.chart_type,
- chart_3d=self.chart_3d,
- height=height,
- value_columns=value_columns,
- modelview_name=self.__class__.__name__,
- **args
- )
- return widgets
- class BaseSimpleDirectChartView(BaseChartView): # pragma: no cover
- direct_columns = []
- """
- Make chart using the column on the dict
- chart_columns = {'chart label 1':('X column','Y1 Column','Y2 Column, ...),
- 'chart label 2': ('X Column','Y1 Column',...),...}
- """
- def __init__(self, **kwargs):
- if not self.direct_columns:
- raise Exception(
- "Base Chart View property <direct_columns> must not be empty"
- )
- else:
- super(BaseSimpleDirectChartView, self).__init__(**kwargs)
- def get_group_by_columns(self):
- """
- returns the keys from direct_columns
- Used in template, so that user can choose from options
- """
- return list(self.direct_columns.keys())
- def _get_chart_widget(
- self,
- filters=None,
- order_column="",
- order_direction="",
- widgets=None,
- direct=None,
- height=None,
- **args
- ):
- height = height or self.height
- widgets = widgets or dict()
- joined_filters = filters.get_joined_filters(self._base_filters)
- count, lst = self.datamodel.query(
- filters=joined_filters,
- order_column=order_column,
- order_direction=order_direction,
- )
- value_columns = self.datamodel.get_values(lst, list(direct))
- value_columns = dict_to_json(
- direct[0], direct[1:], self.label_columns, value_columns
- )
- widgets["chart"] = self.chart_widget(
- route_base=self.route_base,
- chart_title=self.chart_title,
- chart_type=self.chart_type,
- chart_3d=self.chart_3d,
- height=height,
- value_columns=value_columns,
- modelview_name=self.__class__.__name__,
- **args
- )
- return widgets
- class ChartView(BaseSimpleGroupByChartView): # pragma: no cover
- """
- **DEPRECATED**
- Provides a simple (and hopefully nice) way to draw charts on your application.
- This will show Google Charts based on group by of your tables.
- """
- @expose("/chart/<group_by>")
- @expose("/chart/")
- @has_access
- def chart(self, group_by=""):
- form = self.search_form.refresh()
- get_filter_args(self._filters)
- group_by = group_by or self.group_by_columns[0]
- widgets = self._get_chart_widget(filters=self._filters, group_by=group_by)
- widgets = self._get_search_widget(form=form, widgets=widgets)
- return self.render_template(
- self.chart_template,
- route_base=self.route_base,
- title=self.chart_title,
- label_columns=self.label_columns,
- group_by_columns=self.group_by_columns,
- group_by_label=self.group_by_label,
- height=self.height,
- widgets=widgets,
- appbuilder=self.appbuilder,
- )
- class TimeChartView(BaseSimpleGroupByChartView): # pragma: no cover
- """
- **DEPRECATED**
- Provides a simple way to draw some time charts on your application.
- This will show Google Charts based on count and group
- by month and year for your tables.
- """
- chart_template = "appbuilder/general/charts/chart_time.html"
- chart_type = "ColumnChart"
- def _get_chart_widget(
- self,
- filters=None,
- order_column="",
- order_direction="",
- widgets=None,
- group_by=None,
- period=None,
- height=None,
- **args
- ):
- height = height or self.height
- widgets = widgets or dict()
- group_by = group_by or self.group_by_columns[0]
- joined_filters = filters.get_joined_filters(self._base_filters)
- if period == "month" or not period:
- value_columns = self.datamodel.query_month_group(
- group_by, filters=joined_filters
- )
- elif period == "year":
- value_columns = self.datamodel.query_year_group(
- group_by, filters=joined_filters
- )
- widgets["chart"] = self.chart_widget(
- route_base=self.route_base,
- chart_title=self.chart_title,
- chart_type=self.chart_type,
- chart_3d=self.chart_3d,
- height=height,
- value_columns=value_columns,
- modelview_name=self.__class__.__name__,
- **args
- )
- return widgets
- @expose("/chart/<group_by>/<period>")
- @expose("/chart/")
- @has_access
- def chart(self, group_by="", period=""):
- form = self.search_form.refresh()
- get_filter_args(self._filters)
- group_by = group_by or self.group_by_columns[0]
- widgets = self._get_chart_widget(
- filters=self._filters, group_by=group_by, period=period, height=self.height
- )
- widgets = self._get_search_widget(form=form, widgets=widgets)
- return self.render_template(
- self.chart_template,
- route_base=self.route_base,
- title=self.chart_title,
- label_columns=self.label_columns,
- group_by_columns=self.group_by_columns,
- group_by_label=self.group_by_label,
- widgets=widgets,
- appbuilder=self.appbuilder,
- )
- class DirectChartView(BaseSimpleDirectChartView): # pragma: no cover
- """
- **DEPRECATED**
- This class is responsible for displaying a Google chart with
- direct model values. Chart widget uses json.
- No group by is processed, example::
- class StatsChartView(DirectChartView):
- datamodel = SQLAInterface(Stats)
- chart_title = lazy_gettext('Statistics')
- direct_columns = {'Some Stats': ('X_col_1', 'stat_col_1', 'stat_col_2'),
- 'Other Stats': ('X_col2', 'stat_col_3')}
- """
- chart_type = "ColumnChart"
- chart_widget = DirectChartWidget
- @expose("/chart/<group_by>")
- @expose("/chart/")
- @has_access
- def chart(self, group_by=""):
- form = self.search_form.refresh()
- get_filter_args(self._filters)
- direct_key = group_by or list(self.direct_columns.keys())[0]
- direct = self.direct_columns.get(direct_key)
- if self.base_order:
- order_column, order_direction = self.base_order
- else:
- order_column, order_direction = "", ""
- widgets = self._get_chart_widget(
- filters=self._filters,
- order_column=order_column,
- order_direction=order_direction,
- direct=direct,
- )
- widgets = self._get_search_widget(form=form, widgets=widgets)
- return self.render_template(
- self.chart_template,
- route_base=self.route_base,
- title=self.chart_title,
- label_columns=self.label_columns,
- group_by_columns=self.get_group_by_columns(),
- group_by_label=self.group_by_label,
- height=self.height,
- widgets=widgets,
- appbuilder=self.appbuilder,
- )
|