Fix ECharts "Cannot get DOM width or height" in hidden or zero‑sized containers
When a chart is initialized while its container has no measurable size (for example, in side a hidden tab with display:none or a div without explicit width/height), ECharts logs "Can’t get dom width or height!" and the chart remains blank. This commonly happens when rendering multiple charts across tabs where only the first (visible) chart renders and the others stay empty.
Below are practical ways to avoid this by ensuring the container has a size before initialization, deferring initialization until the chart is visible, or resizing after it becomes visible.
Quick fix: size the container and listen for resize
- Give the chart container explicit width and height before callling echarts.init
- Update the container and chart on window resize
Vanilla JS version:
function ensureContainerSize(el) {
el.style.width = window.innerWidth + 'px';
el.style.height = Math.floor(window.innerHeight * 0.8) + 'px';
}
function initChartWithSizing(el) {
ensureContainerSize(el);
const chart = echarts.init(el);
window.addEventListener('resize', () => {
ensureContainerSize(el);
chart.resize();
});
return chart;
}
jQuery-style resize hook (if you already use jQuery):
function sizeContainer($el) {
$el.css({
width: window.innerWidth + 'px',
height: (window.innerHeight * 0.8) + 'px'
});
}
function initChartWithSizingJQ($el) {
sizeContainer($el);
const chart = echarts.init($el[0]);
$(window).on('resize', () => {
sizeContainer($el);
chart.resize();
});
return chart;
}
Full example: build and render a chart safely
The following example transforms input data, sizes the container, initializes the chart, and handles resize. Variable names and control flow are refactored for clarity.
// Example data shape assumption: data[0] is an object keyed by 'YYYY-MM'
// with fields like xfsr, xfbk, shouldPay, pay.
function renderMainChart(dataArray, containerId) {
const host = document.getElementById(containerId);
// Ensure the container is measurable before init
function setHostSize() {
host.style.width = window.innerWidth + 'px';
host.style.height = Math.floor(window.innerHeight * 0.8) + 'px';
}
setHostSize();
// Prepare series data
const src = (dataArray && dataArray[0]) ? dataArray[0] : {};
const labels = Object.keys(src).sort((a, b) => {
const [ay, am] = a.split('-').map(Number);
const [by, bm] = b.split('-').map(Number);
return ay === by ? am - bm : ay - by;
});
const seriesBuckets = {
perfTotal: [], // Total performance = xfbk + xfsr
receivables: [], // shouldPay
registrationIncome: [],// xfsr
totalCost: [], // pay
grossProfit: [], // (xfbk + xfsr) - pay
growthCurve: [] // using total performance for line
};
for (let i = 0; i < labels.length; i++) {
const k = labels[i];
const item = src[k];
if (item) {
const feeBack = Math.round(item.xfbk || 0);
const feeIncome = Math.round(item.xfsr || 0);
const payable = Math.round(item.pay || 0);
const shouldPay = Math.round(item.shouldPay || 0);
const total = feeBack + feeIncome;
seriesBuckets.registrationIncome.push(feeIncome);
seriesBuckets.perfTotal.push(total);
seriesBuckets.receivables.push(shouldPay);
seriesBuckets.totalCost.push(payable);
seriesBuckets.grossProfit.push(total - payable);
seriesBuckets.growthCurve.push(total);
} else {
seriesBuckets.registrationIncome.push(0);
seriesBuckets.perfTotal.push(0);
seriesBuckets.receivables.push(0);
seriesBuckets.totalCost.push(0);
seriesBuckets.grossProfit.push(0);
seriesBuckets.growthCurve.push(0);
}
}
// Initialize chart
const chart = echarts.init(host);
// Auto-resize
window.addEventListener('resize', () => {
setHostSize();
chart.resize();
});
const option = {
tooltip: { show: true, trigger: 'axis' },
toolbox: {
feature: {
dataView: { show: true, readOnly: true, title: 'Data view' },
magicType: { show: true, type: ['line', 'bar'] },
saveAsImage: { show: true }
}
},
title: { text: '' },
legend: {
data: [
'Total performance',
'Total receivables',
'Registration income',
'Total cost',
'Gross profit',
'Performance growth ratio'
]
},
xAxis: [
{
type: 'category',
name: 'Month',
axisLabel: { show: true },
data: labels
}
],
yAxis: {
type: 'value',
name: 'Amount of money',
min: 0,
axisLabel: { formatter: '${value}' }
},
series: [
{ name: 'Total performance', type: 'bar', data: seriesBuckets.perfTotal },
{ name: 'Total receivables', type: 'bar', data: seriesBuckets.receivables },
{ name: 'Registration income', type: 'bar', data: seriesBuckets.registrationIncome },
{ name: 'Total cost', type: 'bar', data: seriesBuckets.totalCost },
{ name: 'Gross profit', type: 'bar', data: seriesBuckets.grossProfit },
{ name: 'Performance growth ratio', type: 'line', data: seriesBuckets.perfTotal }
],
color: ['#f68484', '#75b9e6', '#87b87f', '#ae91e1', '#f6ac61', '#c4ccd3']
};
chart.setOption(option);
return chart;
}
Tabbed layouts: initialize when visible or resize on show
Charts inside hiddden tabs typically have zero width/height. Either defer initialization until the tab is shown or call chart.resize() when the tab becomes visible.
Bootstrap example (shown.bs.tab):
const charts = new Map(); // tabId -> echarts instance
$('a[data-bs-toggle="tab"]').on('shown.bs.tab', function (e) {
const targetSelector = $(e.target).attr('href'); // e.g. '#tab-2'
const panel = document.querySelector(targetSelector);
const el = panel.querySelector('.chart-host');
if (!charts.has(targetSelector)) {
// initialize only once when the panel is visible
const chart = initChartWithSizing(el);
chart.setOption(/* your option here */);
charts.set(targetSelector, chart);
} else {
charts.get(targetSelector).resize();
}
});
Generic approach with IntersectionObserver (no framework dependancy):
function lazyInitEChart(chartEl, option) {
let instance = null;
const obs = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
if (!instance) {
instance = initChartWithSizing(chartEl);
instance.setOption(option);
} else {
instance.resize();
}
}
});
}, { threshold: 0.1 });
obs.observe(chartEl);
}
Alternative: supply width/height to echarts.init (ECharts 5+)
If you know the intended size even while hidden, you can pass it to echarts.init so it doesn’t rely on DOM measurements:
// ECharts 5+ accepts width/height in the third argument
const chart = echarts.init(containerEl, null, { width: 800, height: 480 });
chart.setOption(option);
// Later, when layout changes, call resize()
chart.resize();
CSS sizing that works reliably
- Ensure the container and its ancestors have determinable sizes. Percentage heights require the parent chain to have explicit heights.
- If you want a chart to fill a region:
.chart-wrapper {
position: relative;
width: 100%;
height: 60vh; /* or a fixed px height */
}
.chart-host {
width: 100%;
height: 100%;
}
<div class="chart-wrapper">
<div id="main" class="chart-host"></div>
</div>
Common pitfalls that cause the warning
- The chart is initialized inside an element with display:none
- The container lacks explicit size (e.g., height: auto with no content)
- Parent containers collapse to 0 height due to CSS
- The chart is mounted before the DOM is ready or before layout finishes; use requestAnimationFrame or a tab shown event to defer
Using these patterns, you can ensure ECharts always measures a non-zero container and avoid "Can’t get dom width or height!" while rendering in multi-tab pages.