I would like to present a bar chart using Apex Charts.
The graphs shows some items, the more items the longer you can do some stuff. The items are presented in the Y axis. The X axis should represent months in a year, therefore I want it to be fixed between 0 and 12.
With the following options, it skips some numbers in the x axis:
var options = {
series: [{ data: [3,4,6,7,8.5] }],
chart: { type: 'bar', height: 250 },
plotOptions: {
bar: { borderRadius: 4, horizontal: true }
},
dataLabels: { enabled: false },
xaxis: {
categories: [2,3,5,6,7],
min: 0,
max: 12,
},
yaxis: {
labels: { formatter: function (val) { return val + ' items'; } },
},
title: {
text: 'Chart title', floating: true, align: 'center',
style: { color: '#444' }
}
};
If I omit the min/max in the xaxis, it works fine but the 12 months are not fixed as required.
In the example the 9 is skipped, but the actual bars are scalled correctly
I am loading the library using de direct script include:
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
Here you can find the official example/docs: https://apexcharts.com/javascript-chart-demos/bar-charts/basic/
Have I overlooked something? Or is it a bug?
Setting tickAmount to 12 seems to fix the problem, but I does not behave correctly with other numbers. (if I use 13 then it inserts two 6 tiks instead of inserting decimals)
Related
I am using Chartjs 4.0.1 and chartjs-plugin-zoom 2.0.0 and my chart look like this:
I have set the drag option to be enabled so the user can draw a rectangle to zoom in. Also I have set the zoom mode to 'x'. So the user can only zoom in on the x axis but not on the y axis.
Now I want to limit how far the user can zoom in, to a timespan of one month. I have managed to do that when using the mousewheel to zoom in. But I dont know how to achive the same when using the drag option. I have it configured like this:
drag:{
enabled: true,
backgroundColor:'rgba(180,180,180,0.4)',
threshold: 25,
}
The threshold seems to be my best option to a limit. However that is in pixels and it only says how wide the drawn rectangle has to be for a zoom to occur.
I am already using the onZoomStart callback to check how far the chart is zoomed in and based on that decide if the user can zoom in even more. But apparently that callback is only executed when zooming by mousewheel but not when dragging. So I think I would need to be able to set the threshold of the drag object dynamically. Does anyone know how to do that?
Also I was wondering, is it possible to change the border color of the rectangle when dragging to show the user if it is big enough for a scroll to occur?
The standard solution seems to be to set a limits:{x:{minRange:...}} option. It took me a while to realise where that option should be inserted.
Below is a code snippet with some data resembling yours and a minRange set to 90 days (so I can skip adjusting the tick interval).
Also, there's a hack that changes the color of the drag rectangle to red if the interval is less than the 90 days. It can easily be adapted to completely reject the zoom for less than the desired interval, instead of the current standard behavior which is to adjust (extend) the interval until it is equal to minRange.
The same in this fiddle.
const nPoints = 400,
t0 = Date.parse("2018-06-02T00:00:00Z"),
dt = 2.5*365/nPoints*24*3600*1000;
const data = Array.from(
{length: nPoints},
(_, i)=>({
"timestamp":(t0+dt*i),
value: 80*Math.sin(i*Math.PI/nPoints)+2*Math.random()
})
);
let mouseMoveHandler = null;
chart = new Chart(document.getElementById("myChart"), {
type: 'line',
data: {
datasets: [{
label: "Count",
//pointStyle: false,
pointRadius: 2,
showLine: true,
fill: true,
tension: 0,
borderColor: '#aa6577',
//pointRadius: 4,
//pointBorderWidth: 1,
//pointBackgroundColor: '#7265ce',
data: data
}]
},
options: {
parsing: {
xAxisKey: 'timestamp',
yAxisKey: 'value'
},
spanGaps: false,
responsive: false,
scales: {
x: {
bounds: 'ticks',
type: 'time',
time: {
unit: 'month',
},
title: {
display: false,
text: 'time'
},
ticks: {
display: true,
color: '#cecece'
}
},
y: {
type: 'linear',
display: true,
min: -10,
max: 140,
ticks: {
autoSkip: true,
color: '#cecece'
},
grid:{
color: ctx => ctx.tick.value === 0 ? '#000' : '#ddd',
lineWidth: ctx => ctx.tick.value === 0 ? 3 : 1,
},
title: {
display: false,
text: 'Count',
align: 'end'
},
}
},
plugins:{
legend:{
display: false
},
zoom: {
zoom: {
drag: {
enabled: true,
backgroundColor:'rgba(180,180,180,0.4)',
},
mode: 'x',
onZoomStart({chart, event}){
const x0 = chart.scales.x.getValueForPixel(event.clientX);
if(event.type==="mousedown"){
mouseMoveHandler = function(e){
if(
Math.abs(chart.scales.x.getValueForPixel(e.clientX) - x0) <
chart.options.plugins.zoom.limits.x.minRange
){
chart.options.plugins.zoom.zoom.drag.backgroundColor = 'rgba(255,180,180,0.4)';
}
else{
chart.options.plugins.zoom.zoom.drag.backgroundColor = 'rgba(180,180,180,0.4)';
}
};
chart.canvas.addEventListener("mousemove", mouseMoveHandler);
chart.canvas.addEventListener("mouseup", function(){
if(mouseMoveHandler){
chart.canvas.removeEventListener("mousemove", mouseMoveHandler);
mouseMoveHandler = null;
}
}, {once: true});
}
},
onZoomComplete({chart}){
if(mouseMoveHandler){
chart.canvas.removeEventListener("mousemove", mouseMoveHandler);
mouseMoveHandler = null;
}
document.querySelector('#zoom').innerText = chart.getZoomLevel().toFixed(1)+'x';
document.querySelector('#xSpan').innerText =
Math.round((chart.scales.x.max-chart.scales.x.min)/24/3600/1000)+'days';
}
},
limits:{
x: {
minRange: 90 * 24* 3600 * 1000
}
}
}
}
}
});
document.querySelector('#resetZoom').addEventListener('click', function(){chart.resetZoom();});
document.querySelector('#xSpan').innerText = Math.round((chart.scales.x.max-chart.scales.x.min)/24/3600/1000)+'days';
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.0.1/chart.umd.min.js"
integrity="sha512-HyprZz2W40JOnIBIXDYHCFlkSscDdYaNe2FYl34g1DOmE9J+zEPoT4HHHZ2b3+milFBtiKVWb4sorDVVp+iuqA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/2.0.0/chartjs-plugin-zoom.min.js"
integrity="sha512-B6F98QATBNaDHSE7uANGo5h0mU6fhKCUD+SPAY7KZDxE8QgZw9rewDtNiu3mbbutYDWOKT3SPYD8qDBpG2QnEg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js">
</script>
<canvas id="myChart" style="height:500px; width: 90vw"></canvas>
<button id="resetZoom">Reset zoom</button> <br>
zoom: <span id="zoom">1x</span><br>
X axis span: <span id="xSpan"></span>
I previously had a scatter graph in Chart.js 2 with the zoom plugin, in which I just had to add data and change the xAxis ticks' min/max to see a nice animated horizontal scroll from my old xAxis range to my new xAxis range.
For some reason I now use Chart.js 3.24 and the zoom plugin 1.1.1. Now the same graph with nearly the same options became quite ugly when animated:
newly added points e.g. at position (X,Y) have an animation going from (X,0) to (X,Y), instead of appearing directly at (X,Y).
when a point from a dataset is at the same position than a line from another dataset, they move at a different speed.
if many points and line are added, sometimes the lines are only shown after the "scrolling animation" ended.
sometimes a point appear at its correct final position before the "scrolling animation" even started.
The only solution I found was to disable animation when updating my graph, then use window.requestAnimationFrame to manually pan the graph myself with the zoom plugin.
Are you aware of a better/simpler recommended way to achieve this ?
Thanks in advance,
My old Chart.js options:
let zoom_options = {
pan: {
enabled: true,
mode: 'x',
rangeMin: { x: 0, y: null},
rangeMax: { x: null, y: null}
},
zoom: {
enabled: true,
drag: false,
mode: 'x',
rangeMin: { x: 0, y: null },
rangeMax: { x: null, y: null },
speed: 0.1
}
};
var ctx = graph.getContext('2d');
this._chartjs = new Chart(ctx, {
type: 'scatter',
data: {
datasets: []
},
options: {
legend: {
//display: false
},
scales: {
xAxes: [{
type: 'linear',
position: 'bottom',
}]
},
plugins: {
zoom: zoom_options // https://github.com/chartjs/chartjs-plugin-zoom
}
}
});
My old line options :
{
label: name,
showLine: false,
fill:false,
spanGaps: false, // I have some NaN values in my datasets.
backgroundColor: color.points_borders,
borderColor: color.line,
borderWidth:5,
pointRadius:5,
pointBorderWidth:2,
pointBorderColor: color.points_borders,
pointBackgroundColor: color.points,
data: []
}
My old update function:
this._chartjs.options.scales.xAxes[0].ticks.min = min;
this._chartjs.options.scales.xAxes[0].ticks.max = length;
this._chartjs.update();
I'm building a scatter chart using Chart.JS(latest version), one of the behaviours I'm trying to implement is clicking on a single point and highlighting it by changing the background color of the selected point.
I've used the getElementsAtEvent method from the Chart.JS API in order to get the active element and change it's background. For a brief moment I can see it changing the color but it returns to its original color and all the other points now have the color I wanted to apply to the selected one... I tried various approaches to this, using the updated and render methods but with no desired result...
Here's the code inside the function that'll run onClick
function (evt, activeElements, chart) {
const selectedPoint = chart.getElementsAtEventForMode(evt, 'nearest', { intersect: true }, true);
selectedPoint[0].element.options.backgroundColor = '#fa6400';
chart.update();
}
Here's a fiddle
https://jsfiddle.net/dc3x70yg/1/
Thanks in advance
You can define the option pointBackgroundColor on the dataset. When the user clicks on a point, you recreate pointBackgroundColor, but now use an array that contains the desired color for each point.
Please take a look at your amended code below and see how it works.
new Chart('myChart', {
type: 'scatter',
data: {
datasets: [{
label: '# of Votes',
data: [{ x: -10, y: 0 }, { x: 0, y: 10 }, { x: 10, y: 5 }, { x: 0.5, y: 5.5 }],
pointBackgroundColor: '#ddd',
pointRadius: 5,
}]
},
options: {
onClick: (event, elements, chart) => {
const dataset = chart.data.datasets[0];
dataset.pointBackgroundColor = dataset.data.map((v, i) => i == elements[0]?.index ? '#fa6400': '#ddd');
chart.update();
},
scales: {
y: {
beginAtZero: true
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></script>
<canvas id="myChart" width="400" height="200"></canvas>
I'm using chart.js 3.6.1 with VueJS. These are options for particular data I'm having trouble:
hover: {
mode: 'x',
intersect: false
},
scales: {
y: {
// not relevant
},
x: {
type: 'time',
min: minRangeDate.value,
max: maxRangeDate.value,
time: {
displayFormats: {
day: 'dd.MM.',
hour: 'HH:mm',
minute: 'HH:mm',
second: 'HH:mm:ss'
}
},
grid: {
display: true
},
ticks: {
color: 'white'
}
}
plugins: {
zoom: {
pan: {
enabled: true,
mode: 'x',
modifierKey: 'ctrl'
},
zoom: {
mode: 'x',
wheel: {
enabled: true,
speed: 0.2
},
pinch: {
enabled: true
}
},
limits: {
x: {
min: minRangeDate.value,
max: maxRangeDate.value,
minRange: 20000 // minimum zoom to seconds
}
}
}
}
}
When the graph is zoomed everything looks fine like in the picture below:
but when the graph is zoomed out hover is picking multiple points which is not desired behavior, pic below:
And as far as zoom is, the more points hover is picking.
Any idea how to overcome this. I've tried using 'point' and 'nearest' but then I get only one data in the tooltip as it is the default behavior.
Also, using 'index' is not possible because two datasets are not always the same length and it is not logical and desired to compare values by index.
Ok, here is the solution I came up with, followed by this ChartJS issue on GitHub. I've made a filter for tooltip and it looks like this now:
plugins: {
tooltip: {
mode: 'x',
displayColors: false,
intersect: false,
filter: function (tooltipItem, currentIndex, tooltipItems) {
// On zoomed out graph, hovering mouse picks a lot of data for x-axis and displays them all in tooltip.
// This filter returns only first item from each dataSet from hover picked items passed
// to tooltip, so it can be correctly displayed in tooltip for different line charts(datasets)
return tooltipItems[currentIndex].datasetIndex !== tooltipItems[currentIndex - 1]?.datasetIndex
},
.
.
.
}
Hover still selects multiple data, but the tooltip shows only one for each dataset.
I have this chart:
...which is displaying exactly how I want it to with one exception... The data in the bars is for between the two times in the x axis... so all the labels need shifting to lie on the grid lines, not between them as default for a bar chart. So the red and blue bar is data between 8:00 and 9:00. I hope I've explained that clearly enough.
I'm trawling through the Chart.js docs and it just doesn't seem like this is possible! I know I could change my labels to be, for example, 8pm - 9pm, but that seems a much more visually clunky way of doing it. Is there a way anyone know of achieving this? Ideally there would be another '12am' on the last vertical grid line too.
You can draw the tick lables at the desired position directly on to the canvas using the Plugin Core API. It offers number of hooks that may be used for performing custom code. In below code snippet, I use the afterDraw hook to draw my own labels on the xAxis.
const hours = ['00', '01', '02', '03', '04', '05', '06'];
const values = [0, 0, 0, 0, 10, 6, 0];
const chart = new Chart(document.getElementById('myChart'), {
type: 'bar',
plugins: [{
afterDraw: chart => {
var xAxis = chart.scales['x-axis-0'];
var tickDistance = xAxis.width / (xAxis.ticks.length - 1);
xAxis.ticks.forEach((value, index) => {
if (index > 0) {
var x = -tickDistance + tickDistance * 0.66 + tickDistance * index;
var y = chart.height - 10;
chart.ctx.save();
chart.ctx.fillText(value == '0am' ? '12am' : value, x, y);
chart.ctx.restore();
}
});
}
}],
data: {
labels: hours,
datasets: [{
label: 'Dataset 1',
data: values,
categoryPercentage: 0.99,
barPercentage: 0.99,
backgroundColor: 'blue'
}]
},
options: {
responsive: true,
legend: {
display: false
},
scales: {
xAxes: [{
type: 'time',
time: {
parser: 'HH',
unit: 'hour',
displayFormats: {
hour: 'Ha'
},
tooltipFormat: 'Ha'
},
gridLines: {
offsetGridLines: true
},
ticks: {
min: moment(hours[0], 'HH').subtract(1, 'hours'),
fontColor: 'white'
}
}]
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script>
<canvas id="myChart" height="90"></canvas>