Enable Dark Mode!
how-to-create-graphs-using-d3-js.jpg
By: Dilshad A

How to Create Graphs Using D3.js

Technical Odoo 18

Odoo 18 comes equipped with a fantastic reporting engine. The standard Graph and Pivot views (powered by Chart.js) cover 90% of business needs. But what about that remaining 10%?

Sometimes, a client asks for a Sankey diagram to track logistics flow, a Bubble chart for risk analysis, or a Force-Directed graph for relationship mapping. For these scenarios, standard views fall short.

In this post, we will walk through how to integrate D3.js—the industry standard for custom data visualization—into Odoo 18 using the Owl Framework.

The Architecture

Unlike standard views, we cannot simply add a <graph> tag in XML. Instead, we use a Client Action.

  1. Client Action: Acts as the container in the Odoo backend.
  2. Owl Component: The JavaScript logic that controls the lifecycle.
  3. D3.js: The library that renders the SVG elements.

We will build a "Top Sales Dashboard" that fetches live data from the sale.order model and renders an interactive bar chart.

Step 1: The Manifest

First, we need to define our module and tell Odoo where to find our JavaScript and XML files. In Odoo 18, we add these to the web.assets_backend bundle.

File: __manifest__.py

{
    'name': 'Odoo 18 D3 Dashboard',
    'version': '1.0',
    'category': 'Reporting',
    'summary': 'Advanced Data Visualization using D3.js',
    'depends': ['base', 'web', 'sale'],
    'data': [
        'views/d3_action.xml',
    ],
    'assets': {
        'web.assets_backend': [
            'odoo_d3_demo/static/src/xml/d3_dashboard.xml',
            'odoo_d3_demo/static/src/js/d3_dashboard.js',
        ],
    },
    'installable': True,
}

Step 2: The Client Action

We need a menu item that, when clicked, triggers our custom JavaScript instead of loading a standard tree or form view. We do this using the ir.actions.client model.

File: views/d3_action.xml

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="action_d3_sales_dashboard" model="ir.actions.client">
        <field name="name">Sales D3 Dashboard</field>
        <field name="tag">odoo_d3_demo.sales_dashboard</field> 
        <field name="target">current</field>
    </record>
    <menuitem id="menu_d3_dashboard"
              name="D3 Sales Dashboard"
              action="action_d3_sales_dashboard"
              parent="sale.sale_menu_root"
              sequence="100"/>
</odoo>

Step 3: The Owl Template

We need an HTML container where D3 will "draw" the graph.

Crucial Concept: In Owl, we use t-ref (Template Reference) instead of IDs. This allows us to safely grab this specific HTML element in our JavaScript without traversing the entire DOM.

File: static/src/xml/d3_dashboard.xml

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
    <t t-name="odoo_d3_demo.SalesDashboard">
        <div class="o_d3_dashboard p-3 h-100 overflow-auto">
            <div class="d-flex justify-content-between align-items-center mb-4">
                <h2 class="text-primary">Top Sales Analysis</h2>
                <button class="btn btn-secondary" t-on-click="loadData">
                    <i class="fa fa-refresh"/> Refresh Data
                </button>
            </div>
            
            <div class="chart-container bg-white shadow-sm p-4 rounded" 
                 t-ref="d3Container" 
                 style="height: 500px; width: 100%;">
            </div>
        </div>
    </t>
</templates>

Step 4: The JavaScript Controller

This is where the magic happens. We will use specific Owl hooks:

  1. onWillStart: To load the D3 library from a CDN before the component initializes.
  2. onMounted: To draw the graph after the HTML is rendered.
  3. useRef: To access the we defined in the XML.
  4. useService("orm"): To fetch data from the Odoo database.

File: static/src/js/d3_dashboard.js

/** @odoo-module **/
import { registry } from "@web/core/registry";
import { loadJS } from "@web/core/assets"; // Helper to load external libs
import { useService } from "@web/core/utils/hooks";
import { Component, onWillStart, onMounted, useRef } from "@odoo/owl";
export class SalesD3Dashboard extends Component {
    setup() {
        this.orm = useService("orm");
        this.containerRef = useRef("d3Container"); // Links to t-ref="d3Container"
        // 1. Load D3.js Library
        onWillStart(async () => {
            // You can also bundle d3.min.js locally, but this is easier for demos
            await loadJS("https://d3js.org/d3.v7.min.js");
        });
        // 2. Fetch data and render when ready
        onMounted(() => {
            this.loadData();
        });
    }
    async loadData() {
        // Fetch top 10 Sales Orders
        const domain = [['state', 'in', ['sale', 'done']]];
        const fields = ['name', 'amount_total', 'date_order', 'partner_id'];
        
        const data = await this.orm.searchRead("sale.order", domain, fields, {
            limit: 10,
            order: 'amount_total desc',
        });
        this.renderChart(data);
    }
    renderChart(data) {
        const container = this.containerRef.el;
        
        // Prevent duplicate charts on refresh
        d3.select(container).selectAll("*").remove();
        if (!data.length) {
            container.innerHTML = "<p>No data found</p>";
            return;
        }
        // D3 Dimensions
        const margin = {top: 20, right: 30, bottom: 40, left: 90};
        const width = container.clientWidth - margin.left - margin.right;
        const height = container.clientHeight - margin.top - margin.bottom;
        // Append SVG
        const svg = d3.select(container)
            .append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .append("g")
            .attr("transform", `translate(${margin.left},${margin.top})`);
        // X Scale
        const x = d3.scaleLinear()
            .domain([0, d3.max(data, d => d.amount_total)])
            .range([0, width]);
        // Y Scale
        const y = d3.scaleBand()
            .range([0, height])
            .domain(data.map(d => d.name))
            .padding(0.1);
        // Add Axes
        svg.append("g")
            .attr("transform", `translate(0, ${height})`)
            .call(d3.axisBottom(x))
            .selectAll("text")
            .style("text-anchor", "end");
        svg.append("g")
            .call(d3.axisLeft(y));
        // Add Bars
        svg.selectAll("myRect")
            .data(data)
            .join("rect")
            .attr("x", x(0))
            .attr("y", d => y(d.name))
            .attr("width", d => x(d.amount_total))
            .attr("height", y.bandwidth())
            .attr("fill", "#71639e") // Odoo Purple
            .on("mouseover", function() { d3.select(this).attr("fill", "#00A09D"); }) // Teal on hover
            .on("mouseout", function() { d3.select(this).attr("fill", "#71639e"); });
    }
}
// Link the template
SalesD3Dashboard.template = "odoo_d3_demo.SalesDashboard";
// Register the component to the action tag
registry.category("actions").add("odoo_d3_demo.sales_dashboard", SalesD3Dashboard);

Why This Approach?

1. The useRef Hook

In older Odoo versions (Widgets), we used this.$el to find elements. In Owl, we avoid touching the DOM directly whenever possible. useRef provides a stable reference to the element, ensuring D3 draws in exactly the right place, even if the component re-renders.

2. onWillStart for External Libraries

We use onWillStart to ensure d3 is loaded before the component even tries to mount. This prevents "d3 is undefined" errors.

3. Data Reactivity

By keeping the data fetching in loadData() and calling it from onMounted and the "Refresh" button, we ensure the graph represents the real-time state of the database without reloading the page.

Conclusion

Integrating D3.js with Odoo 18 opens up a world of possibilities. You are no longer restricted to standard charts; you can build interactive maps, intricate process flows, or custom dashboards tailored specifically to your client's business logic.

To read more about How to Create 3D Charts with JavaScript Libraries in 2024, refer to our blog, How to Create 3D Charts with JavaScript Libraries in 2024.


If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



whatsapp_icon
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message