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.
- Client Action: Acts as the container in the Odoo backend.
- Owl Component: The JavaScript logic that controls the lifecycle.
- 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:
- onWillStart: To load the D3 library from a CDN before the component initializes.
- onMounted: To draw the graph after the HTML is rendered.
- useRef: To access the we defined in the XML.
- 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.