如何将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);
关键说明
- 索引定位:在
indexAxis: "y"模式下,每个y轴标签对应一个从0开始的索引,用索引能精准定位柱子位置 - 插件绘制优势:自定义插件直接操作Canvas上下文,可完全控制竖线的颜色、粗细和位置,比line数据集更灵活
- 偏移微调:如果选择保留点的样式,
pointOffset的数值需要根据实际柱子高度调整,确保点完全贴合底部
内容的提问来源于stack exchange,提问作者Macka
相关产品推荐
相关产品推荐

