How to Create Custom Graphs Using D3.js in Odoo 18: Complete Guide
By Braincuber Team
Published on December 14, 2025
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, a Bubble chart for risk analysis, or a Force-Directed graph for relationship mapping.
In this tutorial, we'll walk through how to integrate D3.js—the industry standard for custom data visualization—into Odoo 18 using the OWL Framework.
What You'll Build:
- A "Top Sales Dashboard" with live data from sale.order
- Interactive bar chart using D3.js
- Refresh functionality for real-time updates
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
Step 1: The Manifest
First, define your module and tell Odoo where to find your JavaScript and XML files:
{
'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
Create a menu item that triggers your custom JavaScript instead of loading a standard view:
<?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
Create an HTML container where D3 will "draw" the graph. In OWL, we use t-ref instead of IDs:
<?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 use specific OWL hooks:
- onWillStart: Load the D3 library from CDN before component initializes
- onMounted: Draw the graph after HTML is rendered
- useRef: Access the container element defined in XML
- useService("orm"): Fetch data from the Odoo database
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { loadJS } from "@web/core/assets";
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");
onWillStart(async () => {
await loadJS("https://d3js.org/d3.v7.min.js");
});
onMounted(() => {
this.loadData();
});
}
async loadData() {
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;
d3.select(container).selectAll("*").remove();
if (!data.length) {
container.innerHTML = "<p>No data found</p>";
return;
}
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;
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})`);
const x = d3.scaleLinear()
.domain([0, d3.max(data, d => d.amount_total)])
.range([0, width]);
const y = d3.scaleBand()
.range([0, height])
.domain(data.map(d => d.name))
.padding(0.1);
svg.append("g")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(x));
svg.append("g").call(d3.axisLeft(y));
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")
.on("mouseover", function() { d3.select(this).attr("fill", "#00A09D"); })
.on("mouseout", function() { d3.select(this).attr("fill", "#71639e"); });
}
}
SalesD3Dashboard.template = "odoo_d3_demo.SalesDashboard";
registry.category("actions").add("odoo_d3_demo.sales_dashboard", SalesD3Dashboard);
Why This Approach Works
1. The useRef Hook
In OWL, we avoid touching the DOM directly. useRef provides a stable reference to the element, ensuring D3 draws in exactly the right place.
2. onWillStart for External Libraries
We use onWillStart to ensure D3 is loaded before the component mounts. This prevents "d3 is undefined" errors.
3. Data Reactivity
By keeping data fetching in loadData() and calling it from onMounted and the Refresh button, the graph represents real-time database state.
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.
Need Custom Data Visualization in Odoo?
Our experts can help you build interactive dashboards, custom charts, and advanced data visualizations using D3.js and other libraries.
