Dynamic Side-by-Side Dashboard Charts inside any Doctype (v15)

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:

  1. Layout: Making charts sit 50/50 horizontally.

  2. Filtering: Passing dynamic dates (Today/Yesterday) to Group By charts 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 filters as a JSON string in the frappe.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.

4 Likes

Thanks for sharing your findings!

1 Like

tks for sharing

1 Like