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

如何将Chart.js条形图上的点移至柱子底部(或替换为竖线)

解决方案:调整条形图数据点位置及替换为竖线

一、将绿色点移至柱子底部

当前你使用line类型数据集添加的点,默认会落在y轴标签的中间位置。要让点贴到柱子底部,需修改数据集的y值为对应柱子的索引,并调整偏移参数:

修改line数据集配置

替换原line数据集为以下代码:

{
  type: 'line',
  label: 'point',
  backgroundColor: "green",
  borderColor: "green",
  data: [{
    y: 0, // 对应"Sun 19th Feb 2023"的索引(从0开始)
    x: luxon.DateTime.fromFormat('09:45', "HH:mm").toMillis()
  }],
  pointOffset: -15, // 负数向下偏移,根据柱子高度微调数值
  pointRadius: 6,
  pointHoverRadius: 8
}
  • 索引值定位:用柱子的索引代替标签文本作为y值,能精准对应目标柱子
  • 偏移量调整:pointOffset控制点在y轴方向的位置,负数让点向柱子底部移动,直到贴合

二、将点替换为小竖线

如果要把点换成竖线,推荐用Chart.js的自定义插件直接绘制,灵活性更高:

添加自定义绘制插件

在options.plugins中新增自定义插件,移除原line数据集:

plugins: {
  legend: { display: false },
  dragData: { /* 原有配置保留 */ },
  // 新增绘制竖线的插件
  customVerticalLine: {
    draw: function(chart) {
      const ctx = chart.ctx;
      const yAxis = chart.scales.y;
      const xAxis = chart.scales.x;
      
      // 定位目标柱子的上下边界
      const barIndex = 0; // 对应"Sun 19th Feb 2023"
      const barBottom = yAxis.getPixelForValue(barIndex) - yAxis.tickHeight / 2;
      const barTop = yAxis.getPixelForValue(barIndex) + yAxis.tickHeight / 2;
      // 定位竖线的x轴位置
      const xPos = xAxis.getPixelForValue(luxon.DateTime.fromFormat('09:45', "HH:mm").toMillis());
      
      // 绘制绿色竖线
      ctx.save();
      ctx.strokeStyle = 'green';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(xPos, barBottom);
      ctx.lineTo(xPos, barTop);
      ctx.stroke();
      ctx.restore();
    }
  }
}

完整修改后的代码示例

const DATE = luxon.DateTime;

function arrayToDate(dateArray) {
  return [
    DATE.fromFormat(dateArray[0], "HH:mm").toMillis(),
    DATE.fromFormat(dateArray[1], "HH:mm").toMillis()
  ];
}

class Dataset {
  constructor(xAxislabels, label) {
    this.label = label;
    this.xAxisLabels = xAxislabels;
    this.data = Array(xAxislabels.length).fill([undefined, undefined]);
    this.fill = false;
    this.backgroundColor = ['rgba(153, 102, 255, 0.2)'];
    this.borderColor = ['rgb(153, 102, 255)'];
    this.borderSkipped = false;
    this.borderWidth = 1;
    this.pointHitRadius = 25;
    this.tension = 0.4;
    this.dragData = true;
    this.tooltip = {
      callbacks: {
        label: function (item) {
          if (!item.parsed._custom) {
            console.log("dynamic couldn't parse the item?");
            return;
          }
          const date = DATE.fromSeconds(item.parsed._custom.barStart / 1000).toLocaleString();
          const time_start = DATE.fromSeconds(item.parsed._custom.barStart / 1000).toLocaleString(DATE.TIME_24_SIMPLE);
          const time_stop = DATE.fromSeconds(item.parsed._custom.barEnd / 1000).toLocaleString(DATE.TIME_24_SIMPLE);
          return `${item.dataset.label} ${date} ${time_start} - ${time_stop}`
        }
      }
    };
  }

  setData(xAxisLabel, data) {
    const pos = this.xAxisLabels.indexOf(xAxisLabel);
    console.log("pos: ", pos);
    this.data[pos] = [
      luxon.DateTime.fromFormat(data[0], "HH:mm").toMillis(),
      luxon.DateTime.fromFormat(data[1], "HH:mm").toMillis()
    ];
  }
}

let myData = new Dataset([
  "Sun 19th Feb 2023",
  "Mon 20th Feb 2023",
  "Tue 21st Feb 2023",
  "Wed 22nd Feb 2023",
  "Thu 23rd Feb 2023",
  "Fri 24th Feb 2023",
  "Sat 25th Feb 2023"
], 'task_descript');

myData.setData('Tue 21st Feb 2023', ["10:30", "11:30"]);

const options = {
  type: "bar",
  data: {
    labels: [
      "Sun 19th Feb 2023",
      "Mon 20th Feb 2023",
      "Tue 21st Feb 2023",
      "Wed 22nd Feb 2023",
      "Thu 23rd Feb 2023",
      "Fri 24th Feb 2023",
      "Sat 25th Feb 2023"
    ],
    datasets: [
      myData, {
        label: "",
        data: [
          arrayToDate(["08:25", "08:39"]),
          arrayToDate(["09:45", "09:55"]),
          [undefined, undefined],
          [undefined, undefined],
          [undefined, undefined],
          [undefined, undefined],
          [undefined, undefined]
        ],
        backgroundColor: ['rgba(153, 50, 255, 0.2)'],
        borderColor: ['rgb(153, 102, 255)'],
        borderWidth: 1,
        pointHitRadius: 25,
        tension: 0.4,
        dragData: true,
        tooltip: {
          callbacks: {
            label: function (item) {
              const date = DATE.fromSeconds(item.parsed._custom.barStart / 1000).toLocaleString();
              const time_start = DATE.fromSeconds(item.parsed._custom.barStart / 1000).toLocaleString(DATE.TIME_24_SIMPLE);
              const time_stop = DATE.fromSeconds(item.parsed._custom.barEnd / 1000).toLocaleString(DATE.TIME_24_SIMPLE);
              return `${item.dataset.label} ${date} ${time_start} - ${time_stop}`;
            }
          }
        }
      }, {
        label: "",
        data: [
          arrayToDate(["08:39", "08:59"]),
          arrayToDate(["08:39", "09:00"])
        ],
        backgroundColor: "blue",
        borderWidth: 1,
        pointHitRadius: 25,
        tension: 0.4,
        dragData: true,
        tooltip: {
          callbacks: {
            label: function (item) {
              if (!item.parsed._custom) {
                console.log("Tis couldn't parse the item?");
                return;
              }
              const date = DATE.fromSeconds(item.parsed._custom.barStart / 1000).toLocaleString();
              const time_start = DATE.fromSeconds(item.parsed._custom.barStart / 1000).toLocaleString(DATE.TIME_24_SIMPLE);
              const time_stop = DATE.fromSeconds(item.parsed._custom.barEnd / 1000).toLocaleString(DATE.TIME_24_SIMPLE);
              return `${item.dataset.label} ${date} ${time_start} - ${time_stop}`;
            }
          }
        }
      }, {
        label: "",
        data: [
          arrayToDate(["10:39", "15:59"]),
          arrayToDate(["11:00", "11:30"])
        ],
        borderSkipped: false,
        backgroundColor: ['rgba(30, 130, 76, 0.2)'],
        borderColor: ['red'],
        borderWidth: 1,
        pointHitRadius: 25,
        tension: 0.4,
        dragData: true,
        tooltip: {
          callbacks: {
            label: function (item) {
              if (!item.parsed._custom) {
                console.log("couldn't parse the item?");
                return;
              }
              const date = DATE.fromSeconds(item.parsed._custom.barStart / 1000).toLocaleString();
              const time_start = DATE.fromSeconds(item.parsed._custom.barStart / 1000).toLocaleString(DATE.TIME_24_SIMPLE);
              const time_stop = DATE.fromSeconds(item.parsed._custom.barEnd / 1000).toLocaleString(DATE.TIME_24_SIMPLE);
              return `${item.dataset.label} ${date} ${time_start} - ${time_stop}`;
            }
          }
        }
      }
    ]
  },
  options: {
    indexAxis: "y",
    onHover: function (e) {
      const point = e.chart.getElementsAtEventForMode(e, 'nearest', { intersect: true }, false)
      if (point.length)
        e.native.target.style.cursor = 'grab'
      else
        e.native.target.style.cursor = 'default'
    },
    scales: {
      y: {
        stacked: true
      },
      x: {
        parsing: false,
        stacked: false,
        position: "top",
        type: "time",
        time: {
          unit: "hour"
        },
        min: "07:00",
        max: "19:00"
      }
    },
    plugins: {
      legend: {
        display: false
      },
      dragData: {
        round: 1,
        showTooltip: true,
        onDragStart: function (e, datasetIndex, index, curValue) {
          console.log(datasetIndex, index, curValue);
        },
        onDrag: function (e, datasetIndex, index, value) {
          e.target.style.cursor = "grabbing";
          console.log(value);
        },
        onDragEnd: function (e, datasetIndex, index, value) {
          e.target.style.cursor = "default";
        }
      },
      customVerticalLine: {
        draw: function(chart) {
          const ctx = chart.ctx;
          const yAxis = chart.scales.y;
          const xAxis = chart.scales.x;
          const barIndex = 0;
          const barBottom = yAxis.getPixelForValue(barIndex) - yAxis.tickHeight / 2;
          const barTop = yAxis.getPixelForValue(barIndex) + yAxis.tickHeight / 2;
          const xPos = xAxis.getPixelForValue(luxon.DateTime.fromFormat('09:45', "HH:mm").toMillis());
          
          ctx.save();
          ctx.strokeStyle = 'green';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(xPos, barBottom);
          ctx.lineTo(xPos, barTop);
          ctx.stroke();
          ctx.restore();
        }
      }
    }
  }
}

let ctx = document.getElementById("chartJSContainer").getContext("2d");
const myChart = new Chart(ctx, options);

关键说明

  1. 索引定位:在indexAxis: "y"模式下,每个y轴标签对应一个从0开始的索引,用索引能精准定位柱子位置
  2. 插件绘制优势:自定义插件直接操作Canvas上下文,可完全控制竖线的颜色、粗细和位置,比line数据集更灵活
  3. 偏移微调:如果选择保留点的样式,pointOffset的数值需要根据实际柱子高度调整,确保点完全贴合底部

内容的提问来源于stack exchange,提问作者Macka

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 20:13:08