Quick Answer
Interactive charts in Odoo visualize business data for executives and decision-makers. The problem: Wrong visualizations = executives can't see trends, reports take 10 minutes loading 1M rows in Python, no interactivity (can't click bar to drill into orders), static images don't update real-time = $40k-$100k in lost insights. Three-level solution: (1) Graph Views (simple): Built-in XML views for pivot tables, bar charts, line charts, pie charts. No Python code. Use <graph type="bar"> with field aggregations (sum, count, avg). Result: 4 charts in 5 minutes. (2) Chart.js (interactive): OWL component with Chart.js library. Use loadJS to load library, orm.readGroup to fetch aggregated data, render interactive charts with drill-down. Setup: dashboard.js (component), dashboard.xml (template), dashboard_views.xml (menu action). Result: Professional dashboards with hover tooltips, click events, real-time updates. (3) D3.js (advanced): Maximum control for custom visualizations (heatmaps showing inventory by warehouse, network diagrams, custom layouts). Import D3, use d3.select() for SVG rendering. Data fetching: Always use orm.readGroup for aggregations (not search + loop = slow). Real examples: Daily sales trend (line chart, order_date interval="day"), top 10 products (bar chart, limit=10, order DESC), sales by region (pie chart, group by country_id). Impact: Right = executives use dashboards daily, data-driven decisions instant. Wrong = reports sit unused, gut feel decisions.
The Visualization Challenge
Your D2C brand needs to see:
π Daily sales trend (line chart showing revenue growth)
π Top 10 products (bar chart by units sold)
π₯§ Sales by region (pie chart showing geographic split)
π Customer lifetime value distribution (histogram)
πΊοΈ Inventory heatmap (which products have low stock)
Odoo's default graph views handle basic bar/line/pie charts. But those custom heatmaps, interactive dashboards, real-time KPIs? You need custom code.
Get Visualizations Wrong
- β Executives can't see business trends
- β Reports take 10 minutes to load (aggregating 1M rows in Python)
- β No interactivity (click a bar to drill into orders)
- β Static images that don't update in real-time
Get Visualizations Right
- β Executives see critical metrics instantly
- β Interactive dashboards with drill-down capability
- β Real-time updates without page refresh
- β Beautiful, professional charts that impress customers
We've implemented 150+ Odoo systems. The ones with great visualizations? Executives use dashboards daily, data-driven decisions happen instantly. The ones with bad visualizations? Reports sit unused, decisions are made on gut feel. That's $40,000-$100,000 in lost insights.
The Visualization Spectrum
| Level | Technology | Complexity | Use Case |
|---|---|---|---|
| Level 1 | Graph Views (XML) | Simple (5 min) | Bar, line, pie charts |
| Level 2 | Chart.js + OWL | Medium (2 hours) | Interactive dashboards |
| Level 3 | D3.js | Advanced (1 day) | Heatmaps, custom layouts |
Level 1: Graph Views (No Code)
What they are: XML views that aggregate and display data automatically.
Real D2C Example: Sales by Region
<!-- views/sale_order_views.xml -->
<record id="view_sale_pivot" model="ir.ui.view">
<field name="name">Sales Analysis - Pivot</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<!-- PIVOT VIEW -->
<pivot string="Sales Analysis">
<field name="partner_id.country_id" type="row"/>
<field name="state" type="col"/>
<field name="amount_total" type="measure" aggregate="sum"/>
<field name="id" type="measure" aggregate="count" string="Orders"/>
</pivot>
</field>
</record>
<record id="view_sale_graph" model="ir.ui.view">
<field name="name">Sales Analysis - Graph</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<!-- BAR CHART -->
<graph string="Sales by Region" type="bar" stacked="False">
<field name="partner_id.country_id" type="row"/>
<field name="amount_total" type="measure" aggregate="sum"/>
</graph>
</field>
</record>
<record id="view_sale_line_chart" model="ir.ui.view">
<field name="name">Daily Sales Trend</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<!-- LINE CHART -->
<graph string="Revenue Trend" type="line">
<field name="order_date" type="row" interval="day"/>
<field name="amount_total" type="measure" aggregate="sum"/>
</graph>
</field>
</record>
<record id="view_sale_pie_chart" model="ir.ui.view">
<field name="name">Top Products</field>
<field name="model">sale.order.line</field>
<field name="arch" type="xml">
<!-- PIE CHART -->
<graph string="Sales by Product" type="pie">
<field name="product_id" type="row"/>
<field name="price_total" type="measure" aggregate="sum"/>
</graph>
</field>
</record>
Result: 4 different charts, zero Python code, 5 minutes to create.
Level 2: Chart.js Dashboards (Interactive)
What it is: JavaScript component using Chart.js library for interactive charts with drill-down capability.
Module Structure
custom_module/
βββ static/
β βββ src/
β βββ js/
β β βββ dashboard.js
β β βββ chart_renderer.js
β βββ xml/
β βββ dashboard.xml
βββ views/
βββ dashboard_views.xml
Step 1: Create OWL Component
/** @odoo-module */
import { registry } from "@web/core/registry"
import { loadJS } from "@web/core/assets"
import { onMounted, useState } from "@odoo/owl"
const { Component } = owl
export class SalesDashboard extends Component {
setup() {
this.state = useState({
topProducts: [],
salesByRegion: [],
monthlySales: [],
loading: true,
})
onMounted(async () => {
// Load Chart.js library
await loadJS("https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js")
// Fetch data and render charts
await this.loadData()
this.renderCharts()
})
}
async loadData() {
// Fetch top products
const topProducts = await this.orm.readGroup(
"sale.order.line",
[["state", "=", "sale"]],
["product_id", "price_total:sum"],
["product_id"],
{ limit: 10, order: "price_total:sum DESC" }
)
this.state.topProducts = topProducts
// Fetch sales by region
const salesByRegion = await this.orm.readGroup(
"sale.order",
[["state", "=", "done"]],
["partner_id.country_id", "amount_total:sum"],
["partner_id.country_id"]
)
this.state.salesByRegion = salesByRegion
// Fetch monthly sales
const monthlySales = await this.orm.readGroup(
"sale.order",
[["state", "=", "done"]],
["order_date", "amount_total:sum"],
["order_date:month"]
)
this.state.monthlySales = monthlySales
this.state.loading = false
}
renderCharts() {
this.renderTopProductsChart()
this.renderSalesByRegionChart()
this.renderMonthlySalesChart()
}
renderTopProductsChart() {
const ctx = document.getElementById("topProductsChart").getContext("2d")
const labels = this.state.topProducts.map(p => p.product_id[1])
const data = this.state.topProducts.map(p => p.price_total)
new Chart(ctx, {
type: "bar",
data: {
labels: labels,
datasets: [{
label: "Revenue",
data: data,
backgroundColor: "#E10600",
}]
},
options: {
responsive: true,
onClick: (event, elements) => {
if (elements.length > 0) {
const index = elements[0].index
const productId = this.state.topProducts[index].product_id[0]
this.drillDownProduct(productId)
}
}
}
})
}
renderSalesByRegionChart() {
const ctx = document.getElementById("salesByRegionChart").getContext("2d")
const labels = this.state.salesByRegion.map(r => r["partner_id.country_id"][1])
const data = this.state.salesByRegion.map(r => r.amount_total)
new Chart(ctx, {
type: "pie",
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: ["#E10600", "#111827", "#3B82F6", "#10B981", "#F59E0B"],
}]
},
options: { responsive: true }
})
}
renderMonthlySalesChart() {
const ctx = document.getElementById("monthlySalesChart").getContext("2d")
const labels = this.state.monthlySales.map(m => m.order_date)
const data = this.state.monthlySales.map(m => m.amount_total)
new Chart(ctx, {
type: "line",
data: {
labels: labels,
datasets: [{
label: "Monthly Revenue",
data: data,
borderColor: "#E10600",
fill: false,
}]
},
options: { responsive: true }
})
}
drillDownProduct(productId) {
// Navigate to product sales details
this.action.doAction({
type: "ir.actions.act_window",
res_model: "sale.order.line",
views: [[false, "list"]],
domain: [["product_id", "=", productId]],
})
}
}
SalesDashboard.template = "custom_module.SalesDashboard"
registry.category("actions").add("custom_module.sales_dashboard", SalesDashboard)
Step 2: Create XML Template
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="custom_module.SalesDashboard" owl="1">
<div class="dashboard-container">
<h1>Sales Dashboard</h1>
<t t-if="state.loading">
<div class="spinner">Loading...</div>
</t>
<t t-else="">
<div class="row">
<!-- Top Products Chart -->
<div class="col-lg-6">
<div class="card">
<div class="card-body">
<canvas id="topProductsChart"></canvas>
</div>
</div>
</div>
<!-- Sales by Region Chart -->
<div class="col-lg-6">
<div class="card">
<div class="card-body">
<canvas id="salesByRegionChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Monthly Sales Chart (Full Width) -->
<div class="row mt-4">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<canvas id="monthlySalesChart"></canvas>
</div>
</div>
</div>
</div>
</t>
</div>
</t>
</templates>
Step 3: Register Dashboard Action
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_sales_dashboard" model="ir.actions.client">
<field name="name">Sales Dashboard</field>
<field name="tag">custom_module.sales_dashboard</field>
</record>
<menuitem id="menu_sales_dashboard"
name="Dashboard"
action="action_sales_dashboard"
parent="sale.sale_menu_root"
sequence="1"/>
</odoo>
Result: Interactive dashboard with 3 charts, real data from database, drill-down capability.
Level 3: D3.js Custom Visualizations
For truly custom visualizations (heatmaps, network diagrams, custom layouts):
// Use D3.js for maximum control
// Similar setup but with D3 library
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7"
export class InventoryHeatmap extends Component {
async renderHeatmap() {
const data = await this.fetchInventoryData()
const svg = d3.select("#heatmap")
.append("svg")
.attr("width", 1000)
.attr("height", 600)
// Render heatmap using D3
// Products on X-axis, Warehouses on Y-axis
// Color intensity shows stock level
// Create scales
const xScale = d3.scaleBand()
.domain(data.products)
.range([0, 800])
const yScale = d3.scaleBand()
.domain(data.warehouses)
.range([0, 500])
const colorScale = d3.scaleSequential(d3.interpolateReds)
.domain([0, d3.max(data.values)])
// Render cells
svg.selectAll("rect")
.data(data.values)
.enter()
.append("rect")
.attr("x", d => xScale(d.product))
.attr("y", d => yScale(d.warehouse))
.attr("width", xScale.bandwidth())
.attr("height", yScale.bandwidth())
.attr("fill", d => colorScale(d.stock_level))
.on("click", d => this.drillDownInventory(d))
}
}
Action Items: Build Visualizations
Start with Graph Views
β Create bar/line/pie charts using built-in graph views
β Test with your data
β Share with team
Move to Chart.js (If Needed)
β Create OWL component
β Load Chart.js library with loadJS
β Fetch data using orm.readGroup
β Render interactive charts
Advanced (For Specialized Needs)
β Use D3.js for custom visualizations
β Build real-time dashboards with WebSocket
β Create drill-down capability
Frequently Asked Questions
What is the easiest way to create charts in Odoo?
Use built-in Graph Views with XML (no Python code required). Syntax: <graph string="Chart Title" type="bar|line|pie"> with field definitions. Field types: type="row" (X-axis grouping), type="col" (series grouping), type="measure" (Y-axis values with aggregate="sum|count|avg"). Example: Bar chart showing sales by region = <field name="partner_id.country_id" type="row"/> + <field name="amount_total" type="measure" aggregate="sum"/>. Chart types: bar (vertical bars), line (trend over time with interval="day|week|month"), pie (percentage distribution), pivot (interactive table with drill-down). Time: Create 4 different charts in 5 minutes. Best for: Standard business reports (sales trends, product analysis, regional distribution). Limitation: Can't customize colors, interactivity, or create complex layouts like heatmaps.
How do I create interactive dashboards with Chart.js in Odoo?
Build OWL component with Chart.js library integration. Steps: (1) Create dashboard.js with OWL Component class, (2) Use loadJS to load Chart.js CDN, (3) Fetch data with orm.readGroup (aggregated queries), (4) Render charts with new Chart(ctx, config), (5) Create XML template with canvas elements, (6) Register action in views/dashboard_views.xml. Key methods: loadJS("https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js"), orm.readGroup(model, domain, fields, groupby, options), new Chart(ctx, {type, data, options}). Interactivity: Add onClick handler in chart options to drill down (open filtered list view). Real-time: Use useState + setInterval to refresh data every 30 seconds. Structure: static/src/js/dashboard.js, static/src/xml/dashboard.xml, views/dashboard_views.xml. Result: Professional interactive dashboard in 2 hours.
When should I use D3.js instead of Chart.js for Odoo visualizations?
Use D3.js for custom visualizations Chart.js doesn't support. Chart.js best for: Standard charts (bar, line, pie, doughnut, radar). Easy setup, responsive out of box, good for 80% of business dashboards. D3.js best for: Custom layouts (heatmaps showing inventory by warehouse/product, network diagrams showing customer relationships, hierarchical tree maps, geographic maps with custom regions, force-directed graphs, custom axis scales). Complexity: D3.js requires more code (manual SVG rendering, scale creation, axis generation) vs Chart.js (declarative config). Learning curve: Chart.js = 2 hours to productive, D3.js = 1 day to understand concepts. Example D3 use case: Inventory heatmap with products on X-axis, warehouses on Y-axis, color intensity showing stock levels. Decision rule: Start with Chart.js, move to D3.js only when you need custom layouts Chart.js can't do.
How do I efficiently fetch data for Odoo charts?
Always use orm.readGroup for aggregated data, never search + loop. Wrong (slow): orders = orm.search("sale.order", []), then loop to sum amounts = loads 1M records into memory = 10 minute timeout. Right (fast): data = orm.readGroup("sale.order", domain, ["amount_total:sum"], ["order_date:month"]) = database aggregates, returns only grouped results = 100ms response. Syntax: orm.readGroup(model, domain, fields_with_aggregates, groupby, options). Aggregates: field:sum, field:count, field:avg, field:max, field:min. Groupby: Use field_name for regular fields, field_name:day|week|month for date grouping. Options: {limit: 10, order: "field:sum DESC"} for top N. Example: Top 10 products = readGroup("sale.order.line", [["state","=","sale"]], ["product_id","price_total:sum"], ["product_id"], {limit:10, order:"price_total:sum DESC"}). Performance: 1000x faster than Python loops.
Free Dashboard Strategy Workshop
Stop showing executives spreadsheets. We'll identify your critical KPIs, design optimal visualizations, build custom dashboards, add interactivity and drill-down, and set up real-time updates. Most D2C brands don't have dashboards at all. Building them delivers $40,000-$100,000 in better decision-making.
