Chart Builder (#5019)

* Chart Builder

- X Axis selectable
- Multiple Y Axes
- Change Chart Type

* minor

* codacy
This commit is contained in:
Faris Ansari 2018-02-19 11:08:17 +05:30 committed by GitHub
parent 41598d22f0
commit ef3a3fce9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 140 additions and 44 deletions

View file

@ -51,7 +51,7 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat
raise
def send_updated_docs(doc):
from load import get_docinfo
from .load import get_docinfo
get_docinfo(doc)
d = doc.as_dict()

View file

@ -52,7 +52,16 @@
<li><a class="help-link" data-doctype="{{ doctype }}">{{ __("Help") }}</a></li>
{% } %}
</ul>
<ul class="list-unstyled sidebar-menu charts-menu hide">
<li class="h6">{%= __("Charts") %}</li>
<li class="list-link">
<a class="toggle-charts">{%= __("Toggle Charts") %}</a>
</li>
<li class="list-link">
<a class="configure-charts hidden">{%= __("Configure Charts") %}</a>
</li>
</ul>
<ul class="list-unstyled close-sidebar-button visible-xs visible-sm">
<li class="divider"></li>
<li class="close-sidebar">Close</li>
</ul>
</ul>

View file

@ -65,7 +65,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
this.get_fields_in_list_view(),
[this.meta.title_field, this.meta.image_field],
(this.settings.add_fields || []),
this.meta.track_seen ? '_seen' : null
this.meta.track_seen ? '_seen' : null,
'enabled',
'disabled'
);
fields.forEach(f => this._add_field(f));

View file

@ -32,6 +32,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
setup_view() {
this.setup_columns();
this.bind_charts_button();
}
setup_result_area() {
@ -223,87 +224,138 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
});
}
bind_charts_button() {
this.list_sidebar.sidebar.find('.charts-menu').removeClass('hide');
this.list_sidebar.sidebar.on('click', '.toggle-charts', (e) => {
e.preventDefault();
this.toggle_charts();
});
this.list_sidebar.sidebar.on('click', '.configure-charts', (e) => {
e.preventDefault();
this.get_chart_data().then(() => this.refresh_charts());
});
this.refresh_charts_sidebar_button();
}
refresh_charts_sidebar_button() {
// show configure charts button if charts is shown
const $configure_btn = this.list_sidebar.sidebar.find('.configure-charts');
const charts_visible = this.chart && !this.$charts_wrapper.hasClass('hidden');
if (charts_visible) {
$configure_btn.removeClass('hidden');
} else {
$configure_btn.addClass('hidden');
}
}
toggle_charts() {
if (!this.chart) {
this.setup_charts();
return;
}
this.$charts_wrapper.toggleClass('hidden');
this.chart.refresh();
if (!this.$charts_wrapper.hasClass('hidden')) {
this.chart.refresh();
}
this.refresh_charts_sidebar_button();
}
setup_charts() {
this.get_chart_data()
.then(args => {
this.chart_args = args;
let data = {
labels: this.data.map(d => d.name),
datasets: [ args.dataset ]
};
const df = frappe.meta.get_docfield('Task', args.field);
const get_doc = (value) => {
return this.data.find(d => {
return d[args.field] === value;
});
labels: args.labels,
datasets: args.datasets
};
this.last_chart_type = args.chart_type;
const get_df = (field) => frappe.meta.get_docfield(this.doctype, field);
const get_doc = (value, field) => this.data.find(d => d[field] === value);
this.chart = new Chart({
parent: this.$charts_wrapper[0],
title: __("{0} Chart", [this.doctype]),
data: data,
type: 'bar', // or 'line', 'scatter', 'pie', 'percentage'
type: args.chart_type, // 'bar', 'line', 'scatter', 'pie', 'percentage'
height: 150,
colors: ['violet', 'blue'],
colors: ['violet', 'light-blue', 'orange', 'red'],
format_tooltip_x: d => (d + '').toUpperCase(),
format_tooltip_y: value => frappe.format(value, df, { always_show_decimals: true, inline: true }, get_doc(value))
format_tooltip_x: value => value.doc.name,
format_tooltip_y:
value => frappe.format(value, get_df(value.field), { always_show_decimals: true, inline: true }, get_doc(value.doc))
});
this.refresh_charts_sidebar_button();
});
}
refresh_charts() {
if (!this.chart) return;
const new_dataset = {
label: this.chart_args.field,
values: this.data.map(d => d[this.chart_args.field])
};
const labels = this.data.map(d => d.name);
this.chart.update_values([new_dataset], labels);
const { x_field, y_fields, chart_type } = this.chart_args;
const args = this.get_chart_args(x_field, y_fields, chart_type);
this.chart.update_values(args.datasets, args.labels);
this.chart.refresh();
if (args.chart_type !== this.last_chart_type) {
this.chart.get_different_chart(args.chart_type);
}
}
get_chart_data() {
return new Promise(resolve => {
// const x_fields = [];
const cur_list_fields = this.fields.map(f => f[0]);
const x_fields = this.meta.fields.filter(df =>
!df.hidden && cur_list_fields.includes(df.fieldname)
).map(df => df.fieldname);
const y_fields = this.meta.fields.filter(df =>
!df.hidden && frappe.model.is_numeric_field(df)
&& cur_list_fields.includes(df.fieldname)
).map(df => df.fieldname);
const defaults = this.chart_args || {};
const dialog = new frappe.ui.Dialog({
title: __('Configure Chart'),
fields: [
{
label: __('Y Axis Field'),
label: __('X Axis Field'),
fieldtype: 'Autocomplete',
fieldname: 'y_axis',
fieldname: 'x_axis',
options: x_fields,
default: defaults.x_field
},
{
label: __('Y Axis Fields'),
fieldtype: 'MultiSelect',
fieldname: 'y_axes',
options: y_fields,
description: __('Showing only Numeric fields from Report')
description: __('Showing only Numeric fields from Report'),
default: defaults.y_fields
},
{
label: __('Chart Type'),
fieldtype: 'Select',
options: ['Bar', 'Line', 'Scatter', 'Pie', 'Percentage'],
fieldname: 'chart_type',
default: toTitle(defaults.chart_type || 'Bar')
}
],
primary_action: ({ y_axis }) => {
if (y_fields.includes(y_axis)) {
const data = this.data.map(d => d[y_axis]);
const args = {
field: y_axis,
dataset: {
label: y_axis,
values: data
}
}
resolve(args);
dialog.hide();
}
primary_action: ({ x_axis, y_axes, chart_type }) => {
y_axes = y_axes.split(',').map(d => d.trim()).filter(Boolean);
if (!(
y_axes.every(d => y_fields.includes(d))
&& x_fields.includes(x_axis)
)) return;
const args = this.get_chart_args(x_axis, y_axes, chart_type);
this.chart_args = args;
resolve(args);
dialog.hide();
}
});
@ -311,6 +363,41 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
});
}
get_chart_args(x_axis, y_axes, chart_type) {
const labels = this.data.map(d => {
// HACK: labels need strings,
// so we return objects that
// look like strings and also
// monkey patch the doc
// javascript is awesome
return {
doc: d,
toString() {
return d[x_axis];
},
slice: String.prototype.slice
};
});
return {
chart_type: chart_type.toLowerCase(),
x_field: x_axis,
y_fields: y_axes,
labels: labels,
datasets: y_axes.map(y_axis => ({
title: frappe.meta.get_docfield(this.doctype, y_axis).label,
values: this.data.map(d => ({
doc: d,
field: y_axis,
toString() {
return d[y_axis];
},
slice: String.prototype.slice
}))
}))
};
}
get_editing_object(colIndex, rowIndex, value, parent) {
const control = this.render_editing_input(colIndex, value, parent);
if (!control) return false;
@ -412,10 +499,8 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
if (this.report_name && this.report_doc.json.fields) {
this.fields = this.report_doc.json.fields;
return;
}
// get from user_settings
else if (this.view_user_settings.fields) {
} else if (this.view_user_settings.fields) {
// get from user_settings
this.fields = this.view_user_settings.fields;
return;
}