Limit the number of records to be used in a chart.
This commit is contained in:
parent
ae65448519
commit
b998e564f4
347
dashboard.py
347
dashboard.py
|
@ -89,6 +89,7 @@ class Widget(ModelSQL, ModelView):
|
|||
('pie', 'Pie'),
|
||||
('scatter', 'Scatter'),
|
||||
('scatter-map', 'Scatter Map'),
|
||||
('sunburst', 'Sunburst'),
|
||||
('table', 'Table'),
|
||||
('value', 'Value'),
|
||||
], 'Type')
|
||||
|
@ -103,6 +104,8 @@ class Widget(ModelSQL, ModelView):
|
|||
'Parameters')
|
||||
chart = fields.Function(fields.Text('Chart'), 'on_change_with_chart')
|
||||
timeout = fields.Integer('Timeout (s)', required=True)
|
||||
limit = fields.Integer('Limit', required=True,
|
||||
help='Limit the number of rows')
|
||||
show_title = fields.Boolean('Show Title')
|
||||
show_legend = fields.Boolean('Show Legend')
|
||||
static = fields.Boolean('Static')
|
||||
|
@ -132,6 +135,7 @@ class Widget(ModelSQL, ModelView):
|
|||
], 'Box Points', states={
|
||||
'invisible': Eval('type') != 'box',
|
||||
}, depends=['type'])
|
||||
total_branch_values = fields.Boolean('Total Branch Values')
|
||||
|
||||
@staticmethod
|
||||
def default_timeout():
|
||||
|
@ -139,6 +143,10 @@ class Widget(ModelSQL, ModelView):
|
|||
config = Config(1)
|
||||
return config.default_timeout or 30
|
||||
|
||||
@staticmethod
|
||||
def default_limit():
|
||||
return 1000
|
||||
|
||||
@staticmethod
|
||||
def default_show_title():
|
||||
return True
|
||||
|
@ -151,9 +159,9 @@ class Widget(ModelSQL, ModelView):
|
|||
def default_image_format():
|
||||
return 'svg'
|
||||
|
||||
@fields.depends('type', 'where', 'parameters', 'timeout', 'show_title',
|
||||
'show_toolbox', 'show_legend', 'static', 'name', 'image_format',
|
||||
'zoom', 'box_points', methods=['get_values'])
|
||||
@fields.depends('type', 'where', 'parameters', 'timeout', 'limit',
|
||||
'show_title', 'show_toolbox', 'show_legend', 'static', 'name',
|
||||
'image_format', 'zoom', 'box_points', methods=['get_values'])
|
||||
def on_change_with_chart(self, name=None):
|
||||
data = []
|
||||
layout = {
|
||||
|
@ -175,161 +183,173 @@ class Widget(ModelSQL, ModelView):
|
|||
if self.show_toolbox != 'on-hover':
|
||||
config['displayModeBar'] = self.show_toolbox == 'always'
|
||||
|
||||
values = self.get_values()
|
||||
|
||||
try:
|
||||
chart = {
|
||||
'type': self.type,
|
||||
}
|
||||
data.append(chart)
|
||||
if self.type == 'area':
|
||||
chart.update(values)
|
||||
chart['type'] = 'scatter'
|
||||
chart['fill'] = 'tonexty'
|
||||
elif self.type == 'bar':
|
||||
chart.update(values)
|
||||
elif self.type == 'box':
|
||||
x = values.get('x', [])
|
||||
if x:
|
||||
chart['x'] = x
|
||||
y = values.get('y', [])
|
||||
if y:
|
||||
chart['y'] = y
|
||||
chart['type'] = 'box'
|
||||
chart['boxpoints'] = self.box_points or False
|
||||
elif self.type == 'bubble':
|
||||
chart['type'] = 'scatter'
|
||||
chart.update({
|
||||
'x': values.get('x', []),
|
||||
'y': values.get('y', []),
|
||||
})
|
||||
chart['mode'] = 'markers'
|
||||
chart['marker'] = {
|
||||
'size': values.get('sizes', []),
|
||||
}
|
||||
elif self.type == 'country-map':
|
||||
chart['type'] = 'scattergeo'
|
||||
chart['mode'] = 'markers'
|
||||
chart['text'] = values.get('labels', [])
|
||||
chart['locations'] = values.get('locations', [])
|
||||
sizes = values.get('sizes', [])
|
||||
if sizes:
|
||||
chart['marker'] = {
|
||||
'size': values.get('sizes', []),
|
||||
}
|
||||
colors = values.get('colors', [])
|
||||
if colors:
|
||||
chart['marker'] = {
|
||||
'color': values.get('colors', []),
|
||||
}
|
||||
elif self.type == 'doughnut':
|
||||
chart.update(values)
|
||||
chart['type'] = 'pie'
|
||||
chart['hole'] = 0.4
|
||||
elif self.type == 'funnel':
|
||||
chart['type'] = 'funnelarea'
|
||||
chart['values'] = values.get('values', [])
|
||||
chart['text'] = values.get('labels', [])
|
||||
layout.update({
|
||||
'funnelmode': 'stack',
|
||||
})
|
||||
elif self.type == 'gauge':
|
||||
value = values.get('value', [])
|
||||
chart['value'] = value and value[0] or '-'
|
||||
chart['mode'] = 'gauge'
|
||||
if 'delta' in values:
|
||||
chart['mode'] += '+delta'
|
||||
delta = values['delta']
|
||||
chart['delta'] = {
|
||||
'reference': delta and delta[0] or '-',
|
||||
}
|
||||
min = values.get('min', [])
|
||||
min = min and min[0] or 0
|
||||
max = values.get('max', [])
|
||||
max = max and max[0] or 100
|
||||
chart['type'] = 'indicator'
|
||||
chart['gauge'] = {
|
||||
'axis': {
|
||||
'visible': False,
|
||||
'range': [min, max],
|
||||
},
|
||||
}
|
||||
print(chart)
|
||||
elif self.type == 'line':
|
||||
chart['type'] = 'scatter'
|
||||
chart.update(values)
|
||||
elif self.type == 'pie':
|
||||
chart.update(values)
|
||||
elif self.type == 'scatter':
|
||||
chart['type'] = 'scatter'
|
||||
chart.update(values)
|
||||
chart['mode'] = 'markers'
|
||||
elif self.type == 'scatter-map':
|
||||
chart['text'] = values.get('labels', [])
|
||||
chart['lat'] = values.get('latitude', [])
|
||||
chart['lon'] = values.get('longitude', [])
|
||||
chart['type'] = 'scattermapbox'
|
||||
if chart['lat']:
|
||||
# Latitude median:
|
||||
center_latitude = sum(chart['lat']) / len(chart['lat'])
|
||||
# Longitude median:
|
||||
center_longitude = sum(chart['lon']) / len(chart['lon'])
|
||||
else:
|
||||
center_latitude = 0
|
||||
center_longitude = 0
|
||||
sizes = values.get('sizes', [])
|
||||
if sizes:
|
||||
chart['marker'] = {
|
||||
'size': sizes,
|
||||
}
|
||||
colors = values.get('colors', [])
|
||||
if colors:
|
||||
chart['marker'] = {
|
||||
'color': colors,
|
||||
}
|
||||
layout['dragmode'] = 'zoom'
|
||||
layout['mapbox'] = {
|
||||
'style': 'open-street-map',
|
||||
'center': {
|
||||
'lat': center_latitude,
|
||||
'lon': center_longitude,
|
||||
},
|
||||
'zoom': self.zoom or 1,
|
||||
}
|
||||
elif self.type == 'table':
|
||||
chart['type'] = 'table'
|
||||
header = []
|
||||
columns = []
|
||||
for key, vals in values.items():
|
||||
header.append(key)
|
||||
columns.append(vals)
|
||||
chart['header'] = {
|
||||
'values': header,
|
||||
}
|
||||
chart['cells'] = {
|
||||
'values': columns,
|
||||
}
|
||||
elif self.type == 'value':
|
||||
value = values.get('value', [])
|
||||
chart['value'] = value and value[0] or '-'
|
||||
chart['mode'] = 'number'
|
||||
if 'delta' in values:
|
||||
chart['mode'] += '+delta'
|
||||
delta = values['delta']
|
||||
chart['delta'] = {
|
||||
'reference': delta and delta[0] or '-',
|
||||
}
|
||||
chart['type'] = 'indicator'
|
||||
chart['gauge'] = {
|
||||
'axis': {
|
||||
'visible': False,
|
||||
},
|
||||
}
|
||||
values = self.get_values()
|
||||
except Exception as e:
|
||||
data = [{
|
||||
'type': 'error',
|
||||
'message': str(e),
|
||||
}]
|
||||
return json.dumps({
|
||||
'data': data,
|
||||
'layout': layout,
|
||||
'config': config,
|
||||
})
|
||||
|
||||
chart = {
|
||||
'type': self.type,
|
||||
}
|
||||
data.append(chart)
|
||||
if self.type == 'area':
|
||||
chart.update(values)
|
||||
chart['type'] = 'scatter'
|
||||
chart['fill'] = 'tonexty'
|
||||
elif self.type == 'bar':
|
||||
chart.update(values)
|
||||
elif self.type == 'box':
|
||||
x = values.get('x', [])
|
||||
if x:
|
||||
chart['x'] = x
|
||||
y = values.get('y', [])
|
||||
if y:
|
||||
chart['y'] = y
|
||||
chart['type'] = 'box'
|
||||
chart['boxpoints'] = self.box_points or False
|
||||
elif self.type == 'bubble':
|
||||
chart['type'] = 'scatter'
|
||||
chart.update({
|
||||
'x': values.get('x', []),
|
||||
'y': values.get('y', []),
|
||||
})
|
||||
chart['mode'] = 'markers'
|
||||
chart['marker'] = {
|
||||
'size': values.get('sizes', []),
|
||||
}
|
||||
elif self.type == 'country-map':
|
||||
chart['type'] = 'scattergeo'
|
||||
chart['mode'] = 'markers'
|
||||
chart['text'] = values.get('labels', [])
|
||||
chart['locations'] = values.get('locations', [])
|
||||
sizes = values.get('sizes', [])
|
||||
if sizes:
|
||||
chart['marker'] = {
|
||||
'size': values.get('sizes', []),
|
||||
}
|
||||
colors = values.get('colors', [])
|
||||
if colors:
|
||||
chart['marker'] = {
|
||||
'color': values.get('colors', []),
|
||||
}
|
||||
elif self.type == 'doughnut':
|
||||
chart.update(values)
|
||||
chart['type'] = 'pie'
|
||||
chart['hole'] = 0.4
|
||||
elif self.type == 'funnel':
|
||||
chart['type'] = 'funnelarea'
|
||||
chart['values'] = values.get('values', [])
|
||||
chart['text'] = values.get('labels', [])
|
||||
layout.update({
|
||||
'funnelmode': 'stack',
|
||||
})
|
||||
elif self.type == 'gauge':
|
||||
value = values.get('value', [])
|
||||
chart['value'] = value and value[0] or '-'
|
||||
chart['mode'] = 'gauge'
|
||||
if 'delta' in values:
|
||||
chart['mode'] += '+delta'
|
||||
delta = values['delta']
|
||||
chart['delta'] = {
|
||||
'reference': delta and delta[0] or '-',
|
||||
}
|
||||
min = values.get('min', [])
|
||||
min = min and min[0] or 0
|
||||
max = values.get('max', [])
|
||||
max = max and max[0] or 100
|
||||
chart['type'] = 'indicator'
|
||||
chart['gauge'] = {
|
||||
'axis': {
|
||||
'visible': False,
|
||||
'range': [min, max],
|
||||
},
|
||||
}
|
||||
print(chart)
|
||||
elif self.type == 'line':
|
||||
chart['type'] = 'scatter'
|
||||
chart.update(values)
|
||||
elif self.type == 'pie':
|
||||
chart.update(values)
|
||||
elif self.type == 'scatter':
|
||||
chart['type'] = 'scatter'
|
||||
chart.update(values)
|
||||
chart['mode'] = 'markers'
|
||||
elif self.type == 'scatter-map':
|
||||
chart['text'] = values.get('labels', [])
|
||||
chart['lat'] = values.get('latitude', [])
|
||||
chart['lon'] = values.get('longitude', [])
|
||||
chart['type'] = 'scattermapbox'
|
||||
if chart['lat']:
|
||||
# Latitude median:
|
||||
center_latitude = sum(chart['lat']) / len(chart['lat'])
|
||||
# Longitude median:
|
||||
center_longitude = sum(chart['lon']) / len(chart['lon'])
|
||||
else:
|
||||
center_latitude = 0
|
||||
center_longitude = 0
|
||||
sizes = values.get('sizes', [])
|
||||
if sizes:
|
||||
chart['marker'] = {
|
||||
'size': sizes,
|
||||
}
|
||||
colors = values.get('colors', [])
|
||||
if colors:
|
||||
chart['marker'] = {
|
||||
'color': colors,
|
||||
}
|
||||
layout['dragmode'] = 'zoom'
|
||||
layout['mapbox'] = {
|
||||
'style': 'open-street-map',
|
||||
'center': {
|
||||
'lat': center_latitude,
|
||||
'lon': center_longitude,
|
||||
},
|
||||
'zoom': self.zoom or 1,
|
||||
}
|
||||
elif self.type == 'sunburst':
|
||||
chart['type'] = 'sunburst'
|
||||
chart['labels'] = values.get('labels', [])
|
||||
chart['parents'] = values.get('parents', [])
|
||||
chart['values'] = values.get('values', [])
|
||||
chart['branchvalues'] = ('total' if self.total_branch_values
|
||||
else 'relative')
|
||||
elif self.type == 'table':
|
||||
chart['type'] = 'table'
|
||||
header = []
|
||||
columns = []
|
||||
for key, vals in values.items():
|
||||
header.append(key)
|
||||
columns.append(vals)
|
||||
chart['header'] = {
|
||||
'values': header,
|
||||
}
|
||||
chart['cells'] = {
|
||||
'values': columns,
|
||||
}
|
||||
elif self.type == 'value':
|
||||
value = values.get('value', [])
|
||||
chart['value'] = value and value[0] or '-'
|
||||
chart['mode'] = 'number'
|
||||
if 'delta' in values:
|
||||
chart['mode'] += '+delta'
|
||||
delta = values['delta']
|
||||
chart['delta'] = {
|
||||
'reference': delta and delta[0] or '-',
|
||||
}
|
||||
chart['type'] = 'indicator'
|
||||
chart['gauge'] = {
|
||||
'axis': {
|
||||
'visible': False,
|
||||
},
|
||||
}
|
||||
return json.dumps({
|
||||
'data': data,
|
||||
'layout': layout,
|
||||
|
@ -374,6 +394,10 @@ class Widget(ModelSQL, ModelView):
|
|||
records = self.table.execute_query(fields, self.where, groupby,
|
||||
self.timeout)
|
||||
|
||||
if len(records) > self.limit:
|
||||
raise UserError(gettext('babi.msg_chart_limit',
|
||||
widget=self.rec_name, count=len(records), limit=self.limit))
|
||||
|
||||
res = {}
|
||||
types = [x.type for x in self.parameters]
|
||||
# Transpose records and fields
|
||||
|
@ -581,6 +605,24 @@ class Widget(ModelSQL, ModelView):
|
|||
'aggregate': 'required',
|
||||
},
|
||||
}
|
||||
elif self.type == 'sunburst':
|
||||
return {
|
||||
'labels': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'forbidden',
|
||||
},
|
||||
'parents': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'forbidden',
|
||||
},
|
||||
'values': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
}
|
||||
elif self.type == 'table':
|
||||
return {
|
||||
'labels': {
|
||||
|
@ -623,6 +665,7 @@ class WidgetParameter(ModelSQL, ModelView):
|
|||
('longitude', 'Longitude'),
|
||||
('minimum', 'Minimum'),
|
||||
('maximum', 'Maximum'),
|
||||
('parents', 'Parents'),
|
||||
('sizes', 'Sizes'),
|
||||
('value', 'Value'),
|
||||
('values', 'Values'),
|
||||
|
|
|
@ -96,5 +96,8 @@ Exception: %(error)s</field>
|
|||
<record model="ir.message" id="msg_invalid_parameter_type">
|
||||
<field name="text">Parameter "%(parameter)s" is not valid in widget "%(widget)s". Only the following types can be used: "%(types)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_chart_limit">
|
||||
<field name="text">The number of records "%(count)s" exceeds the limit "%(limit)s" for chart "%(widget)s". You must either narrow the query or increase the limit.</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
|
|
11
table.py
11
table.py
|
@ -183,15 +183,17 @@ class Table(DeactivableMixin, ModelSQL, ModelView):
|
|||
limit=None):
|
||||
if timeout is None:
|
||||
timeout = 10
|
||||
if not backend.TableHandler.table_exist(self.table_name):
|
||||
if (self.type != 'query'
|
||||
and not backend.TableHandler.table_exist(self.table_name)):
|
||||
return []
|
||||
with Transaction().new_transaction() as transaction:
|
||||
cursor = transaction.connection.cursor()
|
||||
cursor.execute('SET statement_timeout TO %s;' % timeout * 1000)
|
||||
cursor.execute('SET statement_timeout TO %s;' % int(timeout * 1000))
|
||||
query = self.get_query(fields, where=where, groupby=groupby,
|
||||
limit=limit)
|
||||
cursor.execute(query)
|
||||
records = cursor.fetchall()
|
||||
cursor.execute('SET statement_timeout TO 0;')
|
||||
return records
|
||||
|
||||
def timeout_exception(self):
|
||||
|
@ -251,7 +253,10 @@ class Table(DeactivableMixin, ModelSQL, ModelView):
|
|||
def _compute_query(self):
|
||||
with Transaction().new_transaction() as transaction:
|
||||
cursor = Transaction().connection.cursor()
|
||||
cursor.execute('%s LIMIT 1' % self._stripped_query)
|
||||
# We must use a subquery because the _stripped_query may contain a
|
||||
# LIMIT clause
|
||||
cursor.execute('SELECT * FROM (%s) AS subquery LIMIT 1' %
|
||||
self._stripped_query)
|
||||
|
||||
field_names = [x[0] for x in cursor.description]
|
||||
self.update_fields(field_names)
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
<field name="table"/>
|
||||
<label name="where"/>
|
||||
<field name="where" colspan="5"/>
|
||||
<label name="timeout"/>
|
||||
<field name="timeout"/>
|
||||
<newline/>
|
||||
<notebook colspan="4">
|
||||
<page name="parameters">
|
||||
|
@ -29,6 +27,13 @@
|
|||
<field name="zoom"/>
|
||||
<label name="box_points"/>
|
||||
<field name="box_points"/>
|
||||
<label name="total_branch_values"/>
|
||||
<field name="total_branch_values"/>
|
||||
<newline/>
|
||||
<label name="timeout"/>
|
||||
<field name="timeout"/>
|
||||
<label name="limit"/>
|
||||
<field name="limit"/>
|
||||
</page>
|
||||
</notebook>
|
||||
<field name="chart" widget="chart" colspan="2"/>
|
||||
|
|
Loading…
Reference in New Issue