You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 08:45:28