Standard Dashboard Charts in ERPNext are great, but they can be restrictive. If you’ve ever tried to show two charts (e.g., Today vs. Yesterday) side-by-side inside a Document Form, you know that the UI usually forces a vertical stack and doesn’t always play nice with dynamic filters on Group By charts.
I found a way to “inject” these charts using a pure Client Script approach. No Virtual Doctypes or Server Scripts required.
The Challenge:
-
Layout: Making charts sit 50/50 horizontally.
-
Filtering: Passing dynamic dates (Today/Yesterday) to
Group Bycharts which don’t support standard “Timespan” filters in the API.
The Solution: We use frappe.call to hit the internal dashboard engine and then render the results into a custom HTML field using frappe.Chart.
1. Preparation
-
Create your Dashboard Chart as usual.
-
Set it to Is Public.
-
In the Filters table of the Chart, add your date field (e.g.,
transaction_date) with the condition=, but leave the value empty.
2. The Client Script
Create a Client Script for your Doctype. This script handles the CSS for the side-by-side layout and the API calls.
frappe.ui.form.on('Your Doctype Name', {
refresh: function(frm) {
// Step 1: Force side-by-side layout
var grid_style = {
'width': '50%', 'display': 'inline-block',
'padding': '10px', 'vertical-align': 'top'
};
['html_field_1', 'html_field_2'].forEach(function(field) {
if (frm.fields_dict[field]) frm.get_field(field).$wrapper.css(grid_style);
});
frm.trigger('render_custom_charts');
},
render_custom_charts: function(frm) {
var today = frappe.datetime.get_today();
var yesterday = frappe.datetime.add_days(today, -1);
var configs = [
{ field: 'html_field_1', id: 'chart-1', name: "Your Chart Name", date: today, title: "Today" },
{ field: 'html_field_2', id: 'chart-2', name: "Your Chart Name", date: yesterday, title: "Yesterday" }
];
configs.forEach(function(config) {
var wrapper = frm.get_field(config.field).$wrapper.empty();
var container = $('<div id="' + config.id + '" style="min-height: 300px;"></div>').appendTo(wrapper);
frappe.call({
method: "frappe.desk.doctype.dashboard_chart.dashboard_chart.get",
args: {
chart_name: config.name,
refresh: 1,
filters: JSON.stringify([["Sales Order", "transaction_date", "=", config.date]])
},
callback: function(r) {
if (r.message && r.message.labels) {
new frappe.Chart("#" + config.id, {
title: config.title,
data: { labels: r.message.labels, datasets: r.message.datasets },
type: 'pie',
height: 300
});
}
}
});
});
}
});
Why this works:
-
Direct Filter Injection: By passing
filtersas a JSON string in thefrappe.call, we override the dashboard’s default behavior. -
UI Flexibility: Using
$wrapper.css()allows us to redesign the form layout on the fly without touching the core CSS files.
