I can't figure out why I'm not able to remove this random padding injected inside my line chart in Chart.js (file attached).
Here is a part of my chart configuration :
myChart.current = new Chart(canvasRef.current, {
type: "line",
data,
options: {
tension: 0.6,
scales: {
x: {
display: false,
},
y: {
display: false,
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
external: (context) => {
const tooltipModel = context.tooltip;
if (tooltipModel.opacity === 0) {
setShowTooltip(false);
return;
}
setShowTooltip(true);
const tooltipEl = tooltipRef.current;
const currentTooltipWidth = tooltipEl.clientWidth;
const currentTooltipHeight = tooltipEl.clientHeight;
const currentTooltipCenterAlign = currentTooltipWidth / 2;
let left =
tooltipModel.dataPoints[0].element.x -
currentTooltipCenterAlign;
if (left < 0) {
left = Math.abs(left);
}
let top =
tooltipModel.dataPoints[0].element.y -
currentTooltipHeight -
offsetY;
if (top < 0) {
top = tooltipModel.dataPoints[0].element.y + offsetY;
}
const date = tooltipModel.dataPoints[0].label;
const value = tooltipModel.dataPoints[0].raw;
setPositionAndData(top, left, date, value);
},
},
},
},
});
Thank you very much for your help :) !
[1]: https://i.stack.imgur.com/lNi5p.png
Related
Actual behaviour Now a series of all days of the week is being built on the X-axis. like now: 2022-5-30 2022-5-31 2022-6-1 2022-6-2 2022-6-3 2022-6-4 2022-6-5 2022-6-6
Live demo with steps to reproduce
https://jsfiddle.net/5z1b9xnp/
let globalData = []; let finRez = [];
let chart;
let duration = 500; // Determines how long the animation between new points should be take
let startIterator = 1; // Determines how many points will be rendered on chart's init
let currentIterator = startIterator;
let maxIterator = 1;
let guiButton = document.getElementById('start');
let guiButtonState = 'Start';
let intervalId;
// Fetch data:
fetch('https://raw.githubusercontent.com/veleg/trade/main/trade.json')
.then(response => response.json())
.then(data => {
parseData(data);
createChart();
initEvents();
});
function initEvents() {
guiButton.addEventListener('click', function() {
if (guiButtonState === 'Stop') {
// User clicked "Stop" -> stop animation and allow to resume
intervalId = clearInterval(intervalId);
guiButton.innerText = guiButtonState = 'Resume';
} else {
// If animation has finished, recreate chart
if (guiButtonState === 'Restart') {
createChart();
}
guiButton.innerText = guiButtonState = 'Stop';
// Start animation:
redrawChart(currentIterator += 1);
intervalId = setInterval(function() {
// If we reached last available point, stop animation:
if (currentIterator === maxIterator) {
intervalId = clearInterval(intervalId);
currentIterator = startIterator;
guiButton.innerText = guiButtonState = 'Restart';
} else {
redrawChart(currentIterator += 1);
}
}, duration);
}
});
}
function redrawChart(index) {
// Set new subtitle on every redraw
chart.setTitle(null, {
text: ' Заработано: ' + globalData[0].data[index][1]
}, false);
const newValues = globalData.map(series => series.data[index][1]);
const maxIndex = newValues.indexOf(Math.max.apply(null, newValues));
// To each series, add a point:
chart.series.forEach(
(series, seriesIndex) => {
const enabled = maxIndex === seriesIndex && ((index < 5) || (index % 5 === 0));
series.addPoint(
{
x: globalData[seriesIndex].data[index][0],
y: globalData[seriesIndex].data[index][1]
<!-- dataLabels: { -->
<!-- enabled -->
<!-- }, -->
<!-- marker: { -->
<!-- enabled -->
<!-- } -->
},
false,
false,
false
);
}
);
// Now, once everything is updated, redraw chart:
chart.redraw({
duration
});
}
function parseData(data) {
Highcharts.objectEach(
data,
// Prepare Highcharts data-format:
// series: [{
// data: [ [x, y], [x, y], ..., [x, y]]
// }]
(countryData, country) => {
if (country !== 'Diamond Princess' && country !== 'Holy See') {
globalData.push({
name: country,
data: countryData.map(p => [Date.parse(p.date), p.recovered / getCountryPopulation(country)])
<!-- data: countryData.map(p => [p.date, p.recovered / getCountryPopulation(country)]), -->
<!-- datas: countryData.date, -->
<!-- datass: countryData.map(p => [p.date, p.date]) -->
});
}
}
);
// Sort and limit dataset:
globalData = globalData
.sort((countryA, countryB) => {
let countryALen,
countryBLen;
if (!countryA || !countryA.data || countryA.data.length === 0) {
return 1;
}
if (!countryB || !countryB.data || countryB.data.length === 0) {
return -1;
}
return countryB.data[countryB.data.length - 1][1] - countryA.data[countryA.data.length - 1][1];
})
.splice(0, 8);
maxIterator = Math.max.apply(null, globalData.map(series => series.data.length - 1));
}
function createChart() {
function format(y) {
return y.toFixed(2);
}
chart = Highcharts.chart('container', {
chart: {
type: 'spline',
marginLeft: 100
},
legend: {
layout: 'proximate',
align: 'right'
},
title: {
floating: true,
align: 'left',
x: 93,
y: 20,
text: 'Confirmed cases per country per 1000 inhabitants'
},
subtitle: {
floating: true,
align: 'left',
y: 60,
x: 90,
text: ' Заработано: ' + globalData[0].data[0][1],
style: {
fontSize: '20px'
}
},
tooltip: {
split: true,
pointFormatter: function() {
<!-- var value = format(this.y); -->
<!-- return `<span style="color:${this.color}">●</span> ${this.series.name}: <b>${value}</b><br/>`; -->
}
},
yAxis: {
title: {
text: ''
},
maxPadding: 0.2,
softMax: 1
},
xAxis: {
gridLineWidth: 2,
min: globalData[0].data[0][0],
minRange: 7 * 24 * 3600 * 1000,
type: 'datetime'
<!-- categories: [1] -->
},
plotOptions: {
series: {
animation: {
duration
},
<!-- marker: { -->
<!-- symbol: 'circle' -->
<!-- }, -->
dataLabels: {
formatter: function() {
return format(this.y);
}
}
}
},
series: globalData.map(series => {
return {
name: series.name,
data: series.data.slice(0, startIterator).map(point => {
return { x: point[0], y: point[1] }
})
}
})
});
}
function getCountryPopulation(country) {
return {
"Заработано": 1,
"Финансовый результат": 1
}[country];
}
Product version Highcharts JS v10.3.2 (2022-11-28)
It is necessary to skip days on the X axis that are not in the JSON file. need: 2022-5-30 2022-5-31 2022-6-1 2022-6-2 2022-6-3 2022-6-6
I don't need to filter JSON. The JSON file just indicates what I need, but the script itself substitutes dates that are not in JSON.
Be careful, dates are skipped in JSON, and the library inserts missing dates. I need only those dates that are specified in JSON, it can be weekends or two days a week.
I'm trying to show weight_id retrieved from mysql data in a chart.js tooltip (shown as (weight_ids[index]) in the image). And later, I intend to show a modal instead of a tooltip to let users update or delete that data. I presume I cannot achieve that without linking the linechart's point data with id stored in mysql. How can I incorporate this id data?
I would appreciate any help very much.
enter image description here
My code is as follows:
<canvas id="myChart"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js#2.9.4/dist/Chart.min.js"></script>
{{-- グラフを描画--}}
<script>
//ラベル
const labels = #json($date_labels);
// id
const weight_ids = #json($weight_ids);
//体重ログ
const weight_logs = #json($weight_logs);
const aryMax = function(a, b) {
return Math.max(a, b);
};
const aryMin = function(a, b) {
return Math.min(a, b);
};
let min_label = Math.floor((weight_logs).reduce(aryMin) - 0.5);
let max_label = Math.ceil((weight_logs).reduce(aryMax) + 0.5);
console.log(weight_ids);
console.log(weight_logs);
console.log(min_label, max_label);
//グラフを描画
var ctx = document.getElementById("myChart");
var myChart = new Chart(ctx, {
type: 'line',
data : {
labels: labels, // x軸ラベル
datasets: [
{
label: `Weight (weight_ids[index])`,
data: weight_logs,
tension: 0,
borderColor: "rgba(37,78,255,1)",
backgroundColor: "rgba(0,0,0,0)",
pointRadius: 3
}
]
},
options: {
title: {
display: false,
text: ''
},
legend: {
display: false,
},
scales: {
yAxes: [
{
ticks: {
min: min_label, // ラベル最小値
max: max_label, // ラベル最大値
},
scaleLabel: {
display: true,
fontSize: 16,
labelString: '体重 (kg)'
}
}
],
},
hover: {
mode: 'point'
},
onClick: function clickHandler(evt) {
var firstPoint = myChart.getElementAtEvent(evt)[0];
if (firstPoint) {
var label = myChart.data.labels[firstPoint._index];
var value = myChart.data.datasets[firstPoint._datasetIndex].data[firstPoint._index];
console.log(label);
console.log(value);
if (value) {
$('#weidhtModal').modal('show');
}
}
}
}
});
</script>
Thank you!
I found a way to retrieve weight_id using the following function.
onClick: function clickHandler(evt, activeElements) {
if (activeElements.length) {
var element = this.getElementAtEvent(evt);
var index = element[0]._index;
var _datasetIndex = element[0]._datasetIndex;
var weightId = weight_ids[index];
var weightLog = weight_logs[index];
console.log(index);
console.log(weightId);
console.log(this.data.labels[index]);
console.log(weightLog);
}
}
I am using chart js to display barchart as :
$(function(){
let aid;
let $radio_input;
let none_obs;
let display = true;
let $js_dom_array = ["76.44", "120.00"];
let $js_am_label_arr = ["None $0", "Terrace - Large $2000"];
if($js_am_label_arr.length > 20){
display = false;
}
let $js_backgroundColor = ["rgba(26,179,148,0.5)", "rgba(26,179,148,0.5)"];
// let $div = document.getElementById("barChart");
// $div.height="140";
let ctx2 = document.getElementById("barChart").getContext("2d");
let chart = new Chart(ctx2, {
type: 'bar',
data: {
labels: $js_am_label_arr,
datasets: [{
label: 'DOM',
data: $js_dom_array,
backgroundColor: $js_backgroundColor,
barPercentage: 0.4,
maxBarThickness: 50,
// maxBarLength: 5,
}]
},
options: {
legend: {
display: false
},
responsive: true,
maintainAspectRatio: false,
legendCallback: function(chart) {
var text = [];
for (var i=0; i<chart.data.datasets.length; i++) {
text.push(chart.data.labels[i]);
}
return text.join("");
},
tooltips: {
mode: 'index',
callbacks: {
// Use the footer callback to display the sum of the items showing in the tooltip
title: function(tooltipItem, data) {
let title_str = data['labels'][tooltipItem[0]['index']];
// let lastIndex = title_str.lastIndexOf(" ");
// return title_str.substring(0, lastIndex);
return title_str;
},
label: function(tooltipItem, data) {
return 'Avg. DOM: '+data['datasets'][0]['data'][tooltipItem['index']];
},
},
},
scales: {
xAxes: [{
stacked: false,
beginAtZero: true,
// scaleLabel: {
// labelString: 'Month'
// },
ticks: {
display: display,
min: 0,
autoSkip: false,
maxRotation: 60,
callback: function(label, index, labels) {
// let lastIndex = label.lastIndexOf(" ");
// let avg_amount = label.split(" ").pop();
// let am_name = label.substring(0, lastIndex);
// let truncated_am_name = am_name.length > 30 ? (am_name.substring(0, 30)+'...') : am_name;
// return truncated_am_name+' '+avg_amount;
return label;
}
}
}]
},
layout: {
margin: {
top: 5
}
},
},
plugins:[{
afterDatasetsDraw: function(chart,options) {
// var chartInstance = chart,
let ctx = chart.ctx;
ctx.font = Chart.defaults.global.defaultFontStyle;
ctx.fillStyle = Chart.defaults.global.textColor;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
chart.data.datasets.forEach(function (dataset, i) {
var meta = chart.controller.getDatasetMeta(i);
meta.data.forEach(function (bar, index) {
ctx.fillText(Math.round(dataset.data[index]), bar._model.x, bar._model.y); //bar._model.y - 5
});
})
}
}]
});
document.getElementById('barChart').innerHTML = chart.generateLegend();
})
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js#2.9.3"></script>
<div style="height:400px;">
<canvas id="barChart" xheight="140px"></canvas>
</div>
If you need jsfiddle
Here, the value of second barchart is 120 and it is being cut off. I could pull text little down as: ctx.fillText(Math.round(dataset.data[index]), bar._model.x, bar._model.y + 5); But I don't wat the value to be overlapped with the bar. I have also tried with following options
layout: {
margin: {
top: 5
}
},
and
axisY:{
viewportMaximum: 130
},
But, none of these seems to work. Is there any way to increase the height or viewport of the chart js? It would be helpful if you could provide the js fiddle as well.
Instead of layout.margin.top, you need to define layout.padding.top as follows:
layout: {
padding: {
top: 20
}
},
I'm trying to add a new point (to series data) on spline chart after clicking in the place where I'm clicked on the line. But point click event doesn't return xAxis, yAxis (only in pixels). I decide to calculate the difference between point pixels position and click, but point adds not on the click place. What I'm doing wrong? How to handle this?
My JS
var setDragStatus = function (status) {
document.getElementById('dragstatus').innerHTML = status;
};
Highcharts.chart('container', {
title: {
text: 'Spline Drag&Drop'
},
plotOptions: {
series: {
turboThreshold: 4,
minPointLength: 5,
dragDrop: {
draggableY: true,
dragMaxY: 1,
},
point: {
events: {
click: function (e) {
let pointPlotX = e.point.plotX
let pointPlotY = e.point.plotY
let pointX = e.point.x
let pointY = e.point.y
let clickX = e.chartX
let clickY = e.chartY
let pointDiffX = clickX / pointPlotX
let pointDiffY = clickY / pointPlotY
let newPointX = pointX * pointDiffX
let newPointY = pointDiffY * pointY
this.series.addPoint([newPointX, newPointY])
}
}
},
}
},
xAxis: {
reversed: false,
showFirstLabel: false,
showLastLabel: true
},
series: [
{
name: 'spline top',
data: [0, 0.3, 0.6, 1],
type: 'spline'
}
]
}
);
Result - https://jsfiddle.net/antiaf/1hfuyjbr/
To calculate x and y values you can use toValue Axis method:
plotOptions: {
series: {
...,
point: {
events: {
click: function(e) {
let series = this.series,
yAxis = series.yAxis,
xAxis = series.xAxis,
newPointX = xAxis.toValue(e.chartX),
newPointY = yAxis.toValue(e.chartY);
this.series.addPoint([newPointX, newPointY])
}
}
}
}
}
Live demo: https://jsfiddle.net/BlackLabel/hg81o4ej/
API Reference: https://api.highcharts.com/class-reference/Highcharts.Axis#toValue
When I dragged the datapoint out of chart's ticks limit like from max x and y axis value, the canvas increase the limits too fast. How can I control this scaling speed? So that it increase with specific number defined in the config of chart.
Here is the js fiddle link.
https://jsfiddle.net/rz7pw6j0/67/
JS
(function () {
let chartInstance;
let chartElement;
function createChart(chartElement) {
const chartConfig = {
type: 'scatter',
data: {
datasets: [
{
data: [
{
x: 0,
y:0
}
],
fill: true,
showLine: true,
lineTension: 0
}
]
},
options: {
legend: {
display: false,
},
layout: {
padding: {
left: 50,
right: 50,
top: 0,
bottom: 0
}
},
title: {
display: false,
text: 'Chart.js Interactive Points',
},
scales: {
xAxes: [
{
type: 'linear',
display: true,
padding: 20,
paddingLeft: 10,
paddingRight: 10,
paddingTop: 10,
paddingBottom: 10,
scaleLabel: {
display: true,
labelString: 'Time',
},
ticks: {
suggestedMin: -5,
suggestedMax: 5,
stepValue: 1,
}
}
],
yAxes: [
{
type: 'linear',
display: true,
scaleLabel: {
display: true,
labelString: 'Weight'
},
paddingLeft: 10,
paddingRight: 10,
paddingTop: 10,
paddingBottom: 10,
ticks: {
suggestedMin: 0,
suggestedMax: 3,
stepValue: 1
},
}
]
},
responsive: true,
maintainAspectRatio: true,
tooltips: {
intersect: true,
}
}
};
chartInstance = new Chart(chartElement.getContext('2d'), chartConfig);
let element = null;
let scaleY = null;
let scaleX = null;
let datasetIndex = null;
let index = null;
let valueY = null;
let valueX = null;
function onGetElement() {
const event = d3.event.sourceEvent;
element = chartInstance.getElementAtEvent(event)[0];
if (!element) {
chartClickHandler(event);
return;
}
if (event.shiftKey) {
const tempDatasetIndex = element['_datasetIndex'];
const tempIndex = element['_index'];
chartInstance.data.datasets[tempDatasetIndex].data = chartInstance
.data.datasets[tempDatasetIndex].data.filter((v, i) => i !== tempIndex);
chartInstance.update();
return;
}
scaleY = element['_yScale'].id;
scaleX = element['_xScale'].id;
}
function onDragStart() {
const event = d3.event.sourceEvent;
datasetIndex = element['_datasetIndex'];
index = element['_index'];
valueY = chartInstance.scales[scaleY].getValueForPixel(event.offsetY);
valueX = chartInstance.scales[scaleX].getValueForPixel(event.offsetX);
chartInstance.data.datasets[datasetIndex].data[index] = {
x: valueX,
y: valueY
};
chartInstance.update(0);
}
function onDragEnd() {
if (
chartInstance.data.datasets[datasetIndex] &&
chartInstance.data.datasets[datasetIndex].data) {
chartInstance.data.datasets[datasetIndex].data.sort((a, b) => a.x - b.x > 0 ? 1 : -1);
chartInstance.update(0);
}
element = null;
scaleY = null;
scaleX = null;
datasetIndex = null;
index = null;
valueY = null;
valueX = null;
}
d3.select(chartInstance.chart.canvas).call(
d3.drag().container(chartInstance.chart.canvas)
.on('start', onGetElement)
.on('drag', onDragStart)
.on('end', onDragEnd)
);
}
function chartClickHandler (event) {
let scaleRef;
let valueX = 0;
let valueY = 0;
Object.keys(chartInstance.scales).forEach((scaleKey) => {
scaleRef = chartInstance.scales[scaleKey];
if (scaleRef.isHorizontal() && scaleKey === 'x-axis-1') {
valueX = scaleRef.getValueForPixel(event.offsetX);
} else if (scaleKey === 'y-axis-1') {
valueY = scaleRef.getValueForPixel(event.offsetY);
}
});
const newPoint = {
x: valueX,
y: valueY
};
const dataSeries = chartInstance.data.datasets[0].data;
for (let i = 0; i < dataSeries.length; i++) {
if (dataSeries[i].x === newPoint.x) {
dataSeries.y = newPoint.y;
chartInstance.update();
return;
}
}
let inserted = false;
for (let j = dataSeries.length - 1; j >= 0; j--) {
if (dataSeries[j].x > newPoint.x) {
dataSeries[j + 1] = dataSeries[j];
} else {
dataSeries[j + 1] = newPoint;
inserted = true;
break;
}
}
if (!inserted) {
dataSeries.push(newPoint);
}
chartInstance.update();
}
chartElement = document.getElementById("chart");
createChart(chartElement);
})();
HTML
<body>
<div>Chart Drag and Click Test</div>
<div class="wrapper">
<canvas id="chart"></canvas>
</div>
</body>
CSS
.wrapper {
display: "block";
}
I want to control the scaling speed.
If a dragStart event occurs beyond the scale limits, the increment should be a fixed value to avoid the issue you mentioned. Also, ticks.min and ticks.max should be set for the same purpose. Below is a sample jsfiddle and code (you can control speed by step).
https://jsfiddle.net/oLrk3fb2/
function onDragStart() {
const event = d3.event.sourceEvent;
const scales = chartInstance.scales;
const scaleInstanceY = scales[scaleY];
const scaleInstanceX = scales[scaleX];
const scalesOpts = chartInstance.options.scales;
const ticksOptsX = scalesOpts.xAxes[0].ticks;
const ticksOptsY = scalesOpts.yAxes[0].ticks;
const step = 1;
datasetIndex = element['_datasetIndex'];
index = element['_index'];
valueY = scaleInstanceY.getValueForPixel(event.offsetY);
valueX = scaleInstanceX.getValueForPixel(event.offsetX);
if (valueY < scaleInstanceY.min) {
ticksOptsY.min = valueY = scaleInstanceY.min - step;
}
if (valueY > scaleInstanceY.max) {
ticksOptsY.max = valueY = scaleInstanceY.max + step;
}
if (valueX < scaleInstanceX.min) {
ticksOptsX.min = valueX = scaleInstanceX.min - step;
}
if (valueX > scaleInstanceX.max) {
ticksOptsX.max = valueX = scaleInstanceX.max + step;
}
chartInstance.data.datasets[datasetIndex].data[index] = {
x: valueX,
y: valueY
}
chartInstance.update(0);
}