I have a floating chart that I am using as a scheduler and I am trying to make it so the y axis is sticky when scrolling through the chart. I found this site and a video that I was trying to follow it but I can't get it to work, http://www.java2s.com/example/javascript/chart.js/create-a-horizontal-scrolling-chartjs-line-chart-with-a-locked-y-axis.html and https://youtu.be/IV6Qy5EQZlA?si=FjXcGK6W-4PjN0LP. Is there an easier way to make a sticky y axis than what I have found online or what am I missing that would make this code work? I was hoping there was an option to enable y axis sticky. I can see I have my Chart Axis up through the dev tools, but I can't seem to get it populated with the labels.
HTML:
<div class="chartWrapper">
<div class="chartAreaWrapper">
<canvas id="myChart" height="300" width="3000%"></canvas>
</div>
<canvas id="myChartAxis" height="300" width="100px"></canvas>
</div>
CSS:
.chartWrapper {
position: relative;
}
.chartWrapper > canvas {
position: absolute;
left: 0;
top: 0;
pointer-events:none;
}
.chartAreaWrapper {
width: 600px;
overflow-x: scroll;
}
JavaScript:
var ctx = document.getElementById("myChart").getContext("2d");
const ctx2 = document.getElementById('myChartAxis');
const chartData = {
labels: ["Person 1", "Person 2"],
datasets: [{
data: [
[new Date('2024-11-18T08:00:00'), new Date('2024-11-18T10:30:00')],
[new Date('2024-11-18T07:00:00'), new Date('2024-11-18T09:45:00')]
],
backgroundColor: ["#8A2BE2", "#007F5C"]
},
{
data: [
[new Date('2024-11-18T11:00:00'), new Date('2024-11-18T15:00:00')],
[new Date('2024-11-18T10:15:00'), new Date('2024-11-18T13:00:00')]
],
backgroundColor: ["#8A2BE2", "#007F5C"]
},
{
data: [
[new Date('2024-11-18T14:00:00'), new Date('2024-11-18T15:00:00')],
[new Date('2024-11-18T15:15:00'), new Date('2024-11-18T17:00:00')]
],
backgroundColor: ["#8A2BE2", "#007F5C"]
},
{
data: [
[new Date('2024-11-18T15:15:00'), new Date('2024-11-18T17:00:00')],
null
],
backgroundColor: ["#8A2BE2", "#007F5C"]
},
],
}
var myChart = new Chart(ctx, {
type: 'bar',
data: chartData,
options: {
maintainAspectRatio: false,
responsive: false,
indexAxis: 'y',
plugins: {
legend: {
display: false
},
title: {
display: false
},
tooltip: {
enabled: false
}
},
scales: {
y: {
stacked: true,
grid: {
display: false
},
},
x: {
grid: {
display: false
},
type: 'time',
time: {
unit: 'minute',
},
ticks: {
stepSize: 15,
},
min: new Date('2024-11-18T06:00:00'),
max: new Date('2024-11-18T17:00:00'),
}
},
animation: {
onComplete: function () {
var sourceCanvas = this.chart.ctx.canvas;
var copyWidth = this.scale.xScalePaddingLeft - 5;
var copyHeight = this.scale.endPoint + 5;
var targetCtx = document.getElementById("myChartAxis").getContext("2d");
targetCtx.canvas.width = copyWidth;
targetCtx.drawImage(sourceCanvas, 0, 0, copyWidth, copyHeight, 0, 0, copyWidth, copyHeight);
}
}
}
});
There is no simple option combination that would give you a statically positioned
axis. The approach you included however will work, except for the onComplete
handler that seems to come from some older version of chart.js.
In a modern version of chart.js (the current version at the time of writing this is 4.4.6),
this
in onComplete
is the chart instance itself and a direct translation of the
included code could be:
animation: {
onComplete: function (){
const sourceCanvas = this.ctx.canvas;
const copyWidth = Math.ceil(this.scales.y.right);
const copyHeight = Math.ceil(this.scales.y.bottom);
const targetCtx = document.getElementById("myChartAxis").getContext("2d");
targetCtx.canvas.width = copyWidth;
targetCtx.fillStyle = 'white';
targetCtx.fillRect(0, 0, copyWidth, copyHeight);
targetCtx.drawImage(sourceCanvas, 0, 0, copyWidth, copyHeight, 0, 0, copyWidth, copyHeight);
}
}
This is however slightly naive, since there's no provision taken for the case when
there's a zoom level set in the browser, or the chart is viewed on a retina screen;
in those cases the devicePixelRatio
will not be 1
and the alignment of the two axes
will fail. Taking the device pixel ratio used by chart.js
, available as
chart.currentDevicePixelRatio
we'll get to:
animation: {
onComplete: function (){
const dpr = this.currentDevicePixelRatio;
const sourceCanvas = this.ctx.canvas;
const copyWidth = Math.ceil(this.scales.y.right);
const copyHeight = Math.ceil(this.scales.y.bottom);
const targetCtx = document.getElementById("myChartAxis").getContext("2d");
targetCtx.canvas.width = copyWidth;
targetCtx.canvas.height = copyHeight;
targetCtx.fillStyle = 'white';
targetCtx.fillRect(0, 0, copyWidth, copyHeight);
targetCtx.drawImage(sourceCanvas, 0, 0, copyWidth * dpr, copyHeight * dpr, 0, 0, copyWidth, copyHeight);
this.ctx.fillStyle = 'white';
this.ctx.fillRect(0, 0, copyWidth, copyHeight);
}
}
The final fillRect
on the original canvas is done to hide the original axis, after the
overlapping one is drawn, since on a tabled like the iPad one can drag the chart to the right
for some distance - elastic scroll or overscroll - it will come back to the original position
when released, but it will show the inconvenient double axis.
Here's that version in a jsFiddle.
For the case that there's no animation, and in order to avoid possible issues with rendering
while the animation is on, we could consider moving the code in a plugin's afterDraw
method.
That method will receive the chart instance as its first argument: afterDraw(chart)
, otherwise
the code is identical. here it is in a demo snippet:
const ctx = document.getElementById("myChart").getContext("2d");
const chartData = {
labels: ["Person 1", "Person 2"],
datasets: [{
data: [
[new Date('2024-11-18T08:00:00'), new Date('2024-11-18T10:30:00')],
[new Date('2024-11-18T07:00:00'), new Date('2024-11-18T09:45:00')]
],
backgroundColor: ["#8A2BE2", "#007F5C"]
},
{
data: [
[new Date('2024-11-18T11:00:00'), new Date('2024-11-18T15:00:00')],
[new Date('2024-11-18T10:15:00'), new Date('2024-11-18T13:00:00')]
],
backgroundColor: ["#8A2BE2", "#007F5C"]
},
{
data: [
[new Date('2024-11-18T14:00:00'), new Date('2024-11-18T15:00:00')],
[new Date('2024-11-18T15:15:00'), new Date('2024-11-18T17:00:00')]
],
backgroundColor: ["#8A2BE2", "#007F5C"]
},
{
data: [
[new Date('2024-11-18T15:15:00'), new Date('2024-11-18T17:00:00')],
null
],
backgroundColor: ["#8A2BE2", "#007F5C"]
},
],
}
Chart.defaults.borderColor = 'rgb(202, 202, 202)';
const myChart = new Chart(ctx, {
type: 'bar',
data: chartData,
options: {
maintainAspectRatio: false,
responsive: false,
indexAxis: 'y',
plugins: {
legend: {
display: false
},
title: {
display: false
},
tooltip: {
enabled: false
}
},
scales: {
y: {
stacked: true,
grid: {
display: false,
},
},
x: {
grid: {
display: false
},
type: 'time',
time: {
unit: 'minute',
},
ticks: {
stepSize: 15,
},
min: new Date('2024-11-18T06:00:00'),
max: new Date('2024-11-18T17:00:00'),
}
}
},
plugins: [{
afterDraw: function (chart) {
const dpr = chart.currentDevicePixelRatio;
const sourceCanvas = chart.ctx.canvas;
const copyWidth = Math.ceil(chart.scales.y.right);
const copyHeight = Math.ceil(chart.scales.y.bottom);
const targetCtx = document.getElementById("myChartAxis").getContext("2d");
targetCtx.canvas.width = copyWidth;
targetCtx.canvas.height = copyHeight;
targetCtx.fillStyle = 'white';
targetCtx.fillRect(0, 0, copyWidth, copyHeight);
targetCtx.drawImage(sourceCanvas, 0, 0, copyWidth * dpr, copyHeight * dpr, 0, 0, copyWidth, copyHeight);
chart.ctx.fillStyle = 'white';
chart.ctx.fillRect(0, 0, copyWidth, copyHeight);
document.querySelector('#dpr').innerText = `dpr = ${dpr}`;
}
}]
});
.chartWrapper {
position: relative;
}
.chartWrapper > canvas {
position: absolute;
left: 0;
top: 0;
pointer-events:none;
border1: 2px solid green
}
.chartAreaWrapper {
width: 600px;
overflow-x: scroll;
}
<div class="chartWrapper">
<div class="chartAreaWrapper">
<canvas id="myChart" height="220" width="3000%"></canvas>
</div>
<canvas id="myChartAxis" height="220" width="100px"></canvas>
</div>
<span id="dpr"></span>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
A slightly different approach, that avoids some of the complications of the previous method, (while
possibly creating others) is to use chart.js
itself to draw the secondary axis. We can construct
another chart for the second canvas with the same labels as the real one and with no data. We have to
be careful to match the sizing settings of both axes, and then hide the x axis and all its features for
the second canvas and hde the y axis for the first canvas, but not its grid lines. In order to have the
space allocated for axes but not make them visible, we'll use transparent color rgba(0,0,0,0)
:
const ctx = document.getElementById("myChart").getContext("2d");
const ctx2 = document.getElementById('myChartAxis');
const chartData = {
labels: ["Person 1", "Person 2"],
datasets: [{
data: [
[new Date('2024-11-18T08:00:00'), new Date('2024-11-18T10:30:00')],
[new Date('2024-11-18T07:00:00'), new Date('2024-11-18T09:45:00')]
],
backgroundColor: ["#8A2BE2", "#007F5C"]
},
{
data: [
[new Date('2024-11-18T11:00:00'), new Date('2024-11-18T15:00:00')],
[new Date('2024-11-18T10:15:00'), new Date('2024-11-18T13:00:00')]
],
backgroundColor: ["#8A2BE2", "#007F5C"]
},
{
data: [
[new Date('2024-11-18T14:00:00'), new Date('2024-11-18T15:00:00')],
[new Date('2024-11-18T15:15:00'), new Date('2024-11-18T17:00:00')]
],
backgroundColor: ["#8A2BE2", "#007F5C"]
},
{
data: [
[new Date('2024-11-18T15:15:00'), new Date('2024-11-18T17:00:00')],
null
],
backgroundColor: ["#8A2BE2", "#007F5C"]
},
],
}
new Chart(ctx, {
type: 'bar',
data: chartData,
options: {
maintainAspectRatio: false,
responsive: false,
indexAxis: 'y',
plugins: {
legend: {
display: false
},
title: {
display: false
},
tooltip: {
enabled: false
}
},
scales: {
y: {
stacked: true,
ticks:{
color: 'rgba(0,0,0,0)',
},
grid: {
display: false,
},
},
x: {
grid: {
display: false
},
type: 'time',
time: {
unit: 'minute',
},
ticks: {
stepSize: 15,
},
min: new Date('2024-11-18T06:00:00'),
max: new Date('2024-11-18T17:00:00'),
}
}
}
});
new Chart(ctx2, {
type: "bar",
data: {labels: chartData.labels, datasets: [{}]},
options: {
maintainAspectRatio: false,
responsive: false,
indexAxis: 'y',
scales:{
x: {
ticks: {
color: 'rgba(0,0,0,0)',
},
border: {
color: 'rgba(0,0,0,0)'
},
grid:{
color: 'rgba(0,0,0,0)'
}
},
y: {
grid:{
color: 'rgba(0,0,0,0)'
},
backgroundColor: 'white',
stacked: true
}
},
plugins:{
legend:{
display: false
}
}
}
});
.chartWrapper {
position: relative;
}
.chartWrapper > canvas {
position: absolute;
left: 0;
top: 0;
pointer-events:none;
border1: 2px solid green
}
.chartAreaWrapper {
width: 600px;
overflow-x: scroll;
}
<div class="chartWrapper">
<div class="chartAreaWrapper">
<canvas id="myChart" height="220" width="3000%"></canvas>
</div>
<canvas id="myChartAxis" height="220" width="300px"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>