Chart.js水平柱状图无法居中:首尾柱子贴近边框、对齐异常求助
Chart.js水平柱状图无法居中:首尾柱子贴近边框、对齐异常求助
嘿,我最近刚上手Chart.js做水平条形图,结果遇到了个头疼的对齐问题:首尾的柱子紧紧贴着图表边框,其他柱子也没法在对应的Y轴分类里居中。我翻遍了文档里的水平条形图配置选项,试了各种设置都没搞定,实在摸不着头脑,有没有大佬能帮忙看看问题出在哪?
问题表现
- 加载图表后,默认状态下首尾条形就紧贴着边框,完全没有间距
- 所有条形都没法在各自的Worker分类里居中显示
我的实现代码
console.log('script.js loaded'); // Fetch test execution data from the JSON file fetch('test-execution-data.json') .then((response) => { if (!response.ok) { throw new Error('Failed to fetch test execution data'); } return response.json(); }) .then((testExecutionData) => { // Debug: Log the fetched data console.log('Fetched test execution data:', testExecutionData); // Group tests by worker const workers = {}; testExecutionData.forEach((test) => { const parallelIndex = test.parallelIndex.toString(); if (!workers[parallelIndex]) { workers[parallelIndex] = []; } workers[parallelIndex].push({ label: test.title, startTime: new Date(test.startTime), endTime: new Date(test.endTime), status: test.status[0], // Get the first status attempt: test.attempt, totalAttempts: test.totalAttempts }); }); // Calculate relative timestamps (in minutes from the earliest test) const allTimes = testExecutionData.flatMap((test) => [ new Date(test.startTime).getTime(), new Date(test.endTime).getTime() ]); const minTimestamp = Math.min(...allTimes); const maxTimestamp = Math.max(...allTimes); const timeRangeInMinutes = (maxTimestamp - minTimestamp) / (1000 * 60); // Create datasets const datasets = Object.keys(workers).map((worker) => ({ label: `Worker ${worker}`, data: workers[worker].map((test) => ({ x: [ (test.startTime.getTime() - minTimestamp) / (1000 * 60), (test.endTime.getTime() - minTimestamp) / (1000 * 60) ], y: worker, label: test.label, start: test.startTime.toLocaleTimeString(), end: test.endTime.toLocaleTimeString(), status: test.status, attempt: test.attempt, totalAttempts: test.totalAttempts })), backgroundColor: (context) => { const status = context.raw.status; const attempt = context.raw.attempt; const totalAttempts = context.raw.totalAttempts; if (status === 'passed') { return 'rgba(75, 192, 192, 0.5)'; } else if (attempt < totalAttempts) { return 'rgba(255, 159, 64, 0.5)'; // Orange for retried failures } else { return 'rgba(255, 99, 132, 0.5)'; // Red for final failure } }, borderColor: (context) => { const status = context.raw.status; const attempt = context.raw.attempt; const totalAttempts = context.raw.totalAttempts; if (status === 'passed') { return 'rgba(75, 192, 192, 1)'; } else if (attempt < totalAttempts) { return 'rgba(255, 159, 64, 1)'; // Orange for retried failures } else { return 'rgba(255, 99, 132, 1)'; // Red for final failure } }, borderWidth: 1, borderSkipped: false, // This ensures borders are drawn on all sides borderRadius: 2, // Optional: slightly rounded corners barThickness: 5 })); // Render chart const ctx = document.getElementById('timelineChart').getContext('2d'); new Chart(ctx, { type: 'bar', data: { datasets }, options: { responsive: true, maintainAspectRatio: false, indexAxis: 'y', scales: { x: { type: 'linear', position: 'bottom', min: 0, max: Math.ceil(timeRangeInMinutes), title: { display: true, text: 'Time (minutes from start)' }, ticks: { stepSize: Math.max(1, Math.ceil(timeRangeInMinutes / 15)), // At most 15 ticks callback: (value) => { const date = new Date(value * 60 * 1000 + minTimestamp); return date.toLocaleTimeString(); } } }, y: { // type: 'category', // Use category scale for discrete worker indices // labels: Object.keys(workers), // Use worker indices as labels title: { display: true, text: 'Worker' } // offset: true // Add some padding to center the bars } }, plugins: { tooltip: { callbacks: { title: (context) => context[0].raw.label, label: (context) => { const { start, end, status } = context.raw; return [`Status: ${status}`, `Start: ${start}`, `End: ${end}`]; } } }, legend: { display: false } } } }); }) .catch((error) => { // Debug: Log any errors console.error('Error loading test execution data:', error); });
示例输入数据
[ { "title": "@workflow @tagAdded @AUTOMATIONS_WORKFLOW_SMOKE_TRIGGER DND enabled for all<> All Filter - Verify that when DND enabled trigger should fire", "parallelIndex": 3, "startTime": "2025-02-04T10:10:21.503Z", "endTime": "2025-02-04T10:10:33.748Z", "status": ["passed"], "retries": 0, "attempt": 1, "totalAttempts": 1, "isRetry": false }, { "title": "@workflow @tagAdded @whatsapp DND enabled specific for Whatsapp - Verify that when DND enabled for Whatsapp trigger should fire", "parallelIndex": 5, "startTime": "2025-02-04T10:10:21.546Z", "endTime": "2025-02-04T10:10:27.457Z", "status": ["passed"], "retries": 0, "attempt": 1, "totalAttempts": 1, "isRetry": false } ]
图表效果说明
首尾条形紧贴图表上下边框(因是水平图,Y轴为垂直方向),中间的条形也没有在对应的Worker分类区域内居中,整体布局显得很局促。
解决方案(针对该问题的修复)
兄弟,我看了你的代码,问题核心出在Y轴的配置上!你把关键的分类轴配置给注释掉了,还没开启偏移参数,导致Chart.js没法正确计算柱子的间距和居中。按下面的步骤修改,应该就能解决:
1. 恢复并完善Y轴的分类配置
把你注释掉的Y轴type: 'category'和labels打开,再加上offset: true和内边距:
y: { type: 'category', // 必须明确指定Y轴为分类轴,因为Worker是离散的分类 labels: Object.keys(workers), // 把Worker的ID作为Y轴的分类标签 title: { display: true, text: 'Worker' }, offset: true, // 核心!开启这个参数,首尾柱子就不会贴边框了 padding: { top: 10, bottom: 10 } // 可选:增加上下内边距,让布局更宽松 }
2. 调整条形的居中参数
在options里新增elements.bar配置,控制柱子在分类区域内的占比,确保居中:
// 在options里和scales、plugins同级的位置添加 elements: { bar: { categoryPercentage: 0.8, // 柱子在分类宽度中的占比(0-1,值越小,柱子两侧留白越多) barPercentage: 0.9, // 柱子自身的宽度占categoryPercentage的比例 borderSkipped: false, borderRadius: 2 } },
3. 可选:调整柱子厚度
你当前设置的barThickness: 5太细了,建议改成10-15,这样居中的效果会更明显。
为什么这些修改能解决问题?
type: 'category'告诉Chart.js Y轴是离散的分类,不是连续的数值,这样库会正确计算每个分类的间距offset: true是解决首尾柱子贴边的核心,它会在第一个和最后一个分类的外侧自动添加间距categoryPercentage和barPercentage配合,让柱子在每个分类的中间区域显示,不会偏上或偏下
把这些修改加到代码里再运行,应该就能看到柱子完美居中,首尾也不会贴边框了!
备注:内容来源于stack exchange,提问作者Shanjay Nithin
相关产品推荐
相关产品推荐

