Related
with my current configs, I can't get tooltips to appear. I am using the latest version of Chart.js, with the latest luxon, and the luxon-chartjs plugin via cdn.
i am using a logarithmic Y axis and cartesian timeline x-axis
i can only get a tooltip to appear when I include a blank callback for label. However, that tooltip that appears only has the unformatted date of the datapoint and no other info. when i ask the label callback to return something, nothing is displayed... when i include a blank callback for title or return anything in the title callback, this works and is displayed. i have hacked together a tooltip but in previous versions I've had no issue with the chart automagically creating these for me.
here is my current code and the working fiddle
let maxArray, correctArray, incorrectArray, objectArray
let yourImage = new Image()
yourImage.src = `https://res.cloudinary.com/behaviorreportcard/image/upload/w_10,h_18,c_scale/v1623785517/Untitled_10_qbwmx4.png`;
let ctx = document.getElementById('myChart').getContext('2d');
Chart.defaults.font.size = '10px';
Chart.defaults.plugins.tooltip.enabled = true
const Interval = luxon.Interval,
DateTime = luxon.DateTime,
Duration = luxon.Duration;
let minTime = "2017-01-19",
maxTime = "2017-04-01",
start = DateTime.fromISO(minTime),
end = DateTime.fromISO(maxTime),
duration = Duration.fromObject({
days: 1
});
function getStart() {
let newStart
if (start.weekday != 7) {
return start.minus({
days: start.weekday
}).toFormat("yyyy-MM-dd")
} else {
return start.toFormat("yyyy-MM-dd")
}
}
let emptyArray = Interval.fromDateTimes(start, end).splitBy(duration).map(t => ({
date: t.s.toFormat("yyyy-MM-dd"),
correct: null,
incorrect: null
}));
let incomingData = [{
date: '2017-01-01',
correct: 10,
incorrect: 20
},
{
date: '2017-01-02',
correct: 10,
incorrect: 0.09
},
{
date: '2017-01-03',
correct: 0.9,
incorrect: null
}, {
date: '2017-01-04',
correct: 3,
incorrect: 0.09
}, {
date: '2017-01-27',
correct: 5,
incorrect: 0.09
}, {
date: '2017-01-28',
correct: 10,
incorrect: null
}, {
date: '2017-02-19',
correct: null,
incorrect: 50
}, {
date: '2017-02-20',
correct: 75,
incorrect: 50
}, {
date: '2017-02-28',
correct: 60,
incorrect: 0.085
}, {
date: '2017-03-01',
correct: 75,
incorrect: null
}, {
date: '2017-03-02',
correct: 10,
incorrect: null
}
]
let returns = []
emptyArray.forEach(x => {
let objToReplace = incomingData.find(o => o.date === x.date)
let objIndex = incomingData.indexOf(objToReplace);
if (objIndex != -1) {
returns.push(incomingData[objIndex])
} else {
returns.push(x)
}
})
let dates = []
let corrects = []
let incorrects = []
let nulls = []
let nullVal = 0.025
returns.forEach(x => {
dates.push(x.date)
if (x.correct != null && x.incorrect != null) {
nulls.push(null)
} else if (x.correct == null && x.incorrect != null) {
nulls.push(nullVal)
}
if (x.incorrect == null) {
incorrects.push(null)
} else {
incorrects.push(x.incorrect)
}
if (x.correct == null) {
corrects.push(null)
} else {
corrects.push(x.correct)
}
})
function returnList(key) {
return returns.map(x => {
return x[key]
})
}
function returnNulls() {
let list = []
returns.forEach(x => {
if ((x.correct != null && x.incorrect == null) || (x.correct == null && x.incorrect != null)) {
list.push(nullVal)
} else {
list.push(null)
}
})
return list
}
console.log(returnNulls())
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: returnList('date'),
datasets: [{
label: 'none',
data: returnNulls(),
spanGaps: false,
pointStyle: yourImage,
},
{
label: 'Corrects',
data: returnList('correct'),
borderWidth: 1,
pointRadius: 5,
spanGaps: false,
fill: false,
borderColor: 'green',
pointStyle: 'circle'
},
{
label: 'Incorrects',
data: returnList('incorrect'),
fill: false,
spanGaps: false,
borderColor: 'red',
pointStyle: 'triangle',
pointRadius: 5,
borderWidth: 1
}
]
},
options: {
events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'],
interaction: {
mode: 'nearest'
},
plugins: {
tooltip: {
enabled: true,
callbacks: {
label: function(item) {},
title: function(item) {
let date = item[0].label.split(',').slice(0, 2).join(',')
let dataSet = item[0].dataset.label
if (dataSet == 'none') {
return
}
return date + '\n' + dataSet
}
}
},
legend: {
display: true,
position: 'bottom',
labels: {
filter: (x, y) => {
if (!x.text.includes('none')) {
return x
}
},
usePointStyle: 'true',
fontColor: 'black',
fontSize: 12
}
},
title: {
display: true,
text: 'Your daily average results for chapter __',
fontColor: 'black',
fontSize: 18,
}
},
scales: {
x: {
ticks: {
font: {
size: 12
}
},
min: getStart(),
max: '2017-03-31',
type: 'time',
time: {
unit: 'day',
stepSize: 7
}
},
y: {
type: 'logarithmic',
max: 100,
min: 0.01,
ticks: {
autoSkip: false,
callback: function(value,
index,
values) {
switch (value) {
case 1:
return '1 per minute';
case 0.1:
return '.1/min';
case 0.01:
return '.01/min';
case 0.001:
return '.001/min';
case 10:
return '10 per min';
case 100:
return '100 per min';
case 1000:
return '1000 per min';
}
},
}
},
},
},
}
);
the HTML
<div id='chart' class='chart' width='400px' height='100%'>
<canvas id="myChart" class='chart' ></canvas>
</div>
and the scripts I'm calling out to
https://cdn.jsdelivr.net/npm/chart.js#3.3.2/dist/chart.min.js
https://cdnjs.cloudflare.com/ajax/libs/luxon/1.27.0/luxon.min.js
https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon#0.2.1
I am using Highcharts to create a timeline chart which shows the flow of different "states" over time. The current implementation is at http://jsfiddle.net/hq1kdpmo/8/ and it looks like .
The current code is as follows:
Highcharts.chart('container', {
"chart": {
"type": "xrange"
},
"title": {
"text": "State Periods"
},
"xAxis": {
"type": "datetime"
},
"yAxis": [{
"title": {
"text": "Factions"
},
"categories": ["A", "B", "C", "D"],
"reversed": true
}],
"plotOptions": {
"xrange": {
"borderRadius": 0,
"borderWidth": 0,
"grouping": false,
"dataLabels": {
"align": "center",
"enabled": true,
"format": "{point.name}"
},
"colorByPoint": false
}
},
"tooltip": {
"headerFormat": "<span style=\"font-size: 0.85em\">{point.x} - {point.x2}</span><br/>",
"pointFormat": "<span style=\"color:{series.color}\">●</span> {series.name}: <b>{point.yCategory}</b><br/>"
},
"series": [{
"name": "State A",
"pointWidth": 20,
"data": [{
"x": 1540430613000,
"x2": 1540633768100,
"y": 0
}, {
"x": 1540191009000,
"x2": 1540633768100,
"y": 1
}, {
"x": 1540191009000,
"x2": 1540530613000,
"y": 2
}, {
"x": 1540530613000,
"x2": 1540633768100,
"y": 3
}]
}, {
"name": "State B",
"pointWidth": 20,
"data": [{
"x": 1540191009000,
"x2": 1540430613000,
"y": 0
}, {
"x": 1540530613000,
"x2": 1540633768100,
"y": 2
}, {
"x": 1540191009000,
"x2": 1540330613000,
"y": 3
}]
}, {
"name": "State C",
"pointWidth": 20,
"data": [{
"x": 1540330613000,
"x2": 1540530613000,
"y": 3
}]
}],
"exporting": {
"enabled": true,
"sourceWidth": 1200
}
});
Now what I am looking forward to is create something of this sort (pardon my paint skills).
Here the categories A to D are the only axis on y. But I would like to group a variable number of parallel ranges. The use case is that there can be multiple states at any point of time and the number of states at any point of time is variable. How do I go on about doing this?
To create such a chart you will have to add 12 yAxis (0-11) and set proper ticks and labels so that only A-D categories will be plotted. Additionally, adjust plotOptions.pointPadding and plotOptions.groupPadding properties to set points width automatically (series.pointWidth should be undefined then).
yAxis options:
yAxis: [{
title: {
text: "Factions"
},
categories: ["A", "B", "C", "D"],
tickPositions: [-1, 2, 5, 8, 11],
lineWidth: 0,
labels: {
y: -20,
formatter: function() {
var chart = this.chart,
axis = this.axis,
label;
if (!chart.yaxisLabelIndex) {
chart.yaxisLabelIndex = 0;
}
if (this.value !== -1) {
label = axis.categories[chart.yaxisLabelIndex];
chart.yaxisLabelIndex++;
if (chart.yaxisLabelIndex === 4) {
chart.yaxisLabelIndex = 0;
}
return label;
}
},
},
reversed: true
}]
Demo:
https://jsfiddle.net/wchmiel/s9qefg7t/1/
I have managed to solve this thanks to support from Highcharts themselves. The idea is to set the tick position on the load event and use the labels.formatter for formatting each individual label.
events: {
load() {
let labelGroup = document.querySelectorAll('.highcharts-yaxis-labels');
// nodeValue is distance from top
let ticks = document.querySelectorAll('.highcharts-yaxis-grid');
let tickPositions = Array.from(ticks[0].childNodes).map(
function(node){
return +node.attributes.d.nodeValue.split(" ")[2];
}
);
let labelPositions = [];
for(let i =1 ;i<tickPositions.length;i++){
labelPositions.push((tickPositions[i] + tickPositions[i-1])/2);
}
labelGroup[0].childNodes[0].attributes.y.nodeValue = labelPositions[0] + parseFloat(labelGroup[0].childNodes[0].style["font-size"], 10) / 2;
labelGroup[0].childNodes[1].attributes.y.nodeValue = labelPositions[1] + parseFloat(labelGroup[0].childNodes[1].style["font-size"], 10) / 2;
labelGroup[0].childNodes[2].attributes.y.nodeValue = labelPositions[2] + parseFloat(labelGroup[0].childNodes[2].style["font-size"], 10) / 2;
labelGroup[0].childNodes[3].attributes.y.nodeValue = labelPositions[3] + parseFloat(labelGroup[0].childNodes[3].style["font-size"], 10) / 2;
labelGroup[0].childNodes[4].attributes.y.nodeValue = labelPositions[4] + parseFloat(labelGroup[0].childNodes[4].style["font-size"], 10) / 2;
}
}
And the labels are formatted as:
labels: {
formatter: function() {
var chart = this.chart,
axis = this.axis,
label;
if (!chart.yaxisLabelIndex) {
chart.yaxisLabelIndex = 0;
}
if (this.value !== -1) {
label = axis.categories[chart.yaxisLabelIndex];
chart.yaxisLabelIndex++;
if (chart.yaxisLabelIndex === groups.length) {
chart.yaxisLabelIndex = 0;
}
return label;
}
},
}
Fiddle at https://jsfiddle.net/yvnp4su0/42/
As I suggested in the comment above it is a better idea to use Highcharts renderer and add custom labels than manipulate Dom elements as you did in the previous answer, because it is a much cleaner solution.
Disable default labels:
yAxis: [{
title: {
text: "Factions",
margin: 35
},
categories: ["A", "B", "C", "D", "E"],
tickPositions: tickPositions,
lineWidth: 0,
labels: {
enabled: false
},
reversed: true
}]
Add custom labels in proper positions using renderer:
chart: {
type: 'xrange',
height: 500,
marginLeft: 60,
events: {
load: function() {
this.customLabels = [];
},
render: function() {
var chart = this,
yAxis = chart.yAxis[0],
categories = yAxis.categories,
xOffset = 15,
yOffset = 20,
xPos = yAxis.left - xOffset,
tickPositions = yAxis.tickPositions,
text,
label,
yPos,
tick1Y,
tick2Y,
i;
for (i = 0; i < tickPositions.length - 1; i++) {
if (chart.customLabels[i]) {
chart.customLabels[i].destroy();
}
tick1Y = yAxis.toPixels(tickPositions[i]);
tick2Y = yAxis.toPixels(tickPositions[i + 1]);
yPos = (tick1Y + tick2Y) / 2 + yOffset;
text = categories[i];
label = chart.renderer.text(text, xPos, yPos)
.css({
color: '#ccc',
fontSize: '14px'
})
.add();
chart.customLabels[i] = label;
}
}
}
}
Demo: https://jsfiddle.net/wchmiel/vkz7o1hw/
Basically, am working with json objects however my skills are not that much because I am still a newbie in javascript and json or object literals. Am trying to achieve where I would like to push or insert a custom json object at the last element of the other xml/json file. Is there any on way how to do this? I've been trying to do it for quite some time now but could not make it work. Any idea how to make it work? because honestly, I don't have any left :-)
I use getJSON to request a JSON from my website. It works great, but I need to somehow insert another custom object literals at the end of the json is it possible?
By the way here is my code.
$(function() {
$.getJSON('https://some_link_from_a_server_that_produces_xml_file_or_json',
function(data) {
//var dataLength = data.length;
fillData();
function fillData() {
var jsonData = [{
"LastModification": "04:27:48",
"Symbol": "EURUSD",
"Bid": '1.20568',
"Ask": "1.21238",
"High": '1.21789',
"Low": '1.19253',
"Direction": "-1",
"InserTime": "\/Date(1358760600163)\/",
"volume": "0"
}];
for (var i = 0; i < jsonData.length; i++) {
data.push([
parseFloat(jsonData[i].Bid),
parseFloat(jsonData[i].High),
parseFloat(jsonData[i].Low),
parseFloat(jsonData[i].Ask),
parseInt(jsonData[i].InserTime.substr(6)),
parseInt(jsonData[i].volume)
]);
}
CreateChart();
} // end of function fillData()
function CreateChart() {
var chart = new Highcharts.stockChart('container2',
{
title: {
text: 'EUR/USD',
floating: true,
align: 'left',
x: 0,
y: 55
},
subtitle: {
text: 'highest: 1.23223 / lowest: 1.21774',
floating: true,
align: 'left',
x: 0,
y: 70
},
xAxis: {
gridLineWidth: 1
},
yAxis: {
gridLineWidth: 1
},
rangeSelector: {
buttons: [
{
type: 'hour',
count: 1,
text: '1h'
}, {
type: 'day',
count: 1,
text: '1D'
}, {
type: 'all',
count: 1,
text: 'All'
}
],
selected: 1,
inputEnabled: true
},
series: [
{
name: 'EURUSD',
type: 'candlestick',
data: data,
tooltip: {
valueDecimals: 5
}
}
]
}); // end of highcharts.stockchart
}
});
});
I think you are making a array of JSON which looks like [{...}, {...}]
However, in your code you are making an array of array which looks like [[...], [...]].
Try :
let data = [];
const func = function(data) {
fillData();
function fillData() {
var jsonData = [{
"LastModification": "04:27:48",
"Symbol": "EURUSD",
"Bid": '1.20568',
"Ask": "1.21238",
"High": '1.21789',
"Low": '1.19253',
"Direction": "-1",
"InserTime": "\/Date(1358760600163)\/",
"volume": "0"
}];
for (var i = 0; i < jsonData.length; i++) {
data.push({
Bid :parseFloat(jsonData[i].Bid),
High : parseFloat(jsonData[i].High),
Low : parseFloat(jsonData[i].Low),
Ask : parseFloat(jsonData[i].Ask),
InserTime : parseInt(jsonData[i].InserTime.substr(6)),
Volume :parseInt(jsonData[i].volume)
});
}
console.log(data);
} // end of function fillData()
}
func(data);
When creating a combo chart with 2 different sets of data(national and regional).
I get a different result.
The regional dataset works but the national dataset makes a new line for every bar.
I can't personally find where I went wrong and it might be something obvious im missing.
This is part of the data I am using (full data in jsfiddle)
SupplyRaw = {
"regional": [
{
"category": 1,
"min": 75,
"max": 150,
"avarage": 113,
"standardDeviation": 32.036437588054845
}
],
"national": [
{
"category": 3,
"min": 20,
"max": 464,
"avarage": 104,
"standardDeviation": 55.76672091291433
}
]
}
and this is how I convert the data for the combo chart
var header = [['Category', 'Min',
'Min Deviation',
'Max Divation', 'Max',
{ type: 'string', role: 'style' },
'Avarage']];
var dataRegional = google.visualization.arrayToDataTable(
header.concat(SupplyRaw.regional.map(function (x) {
return [x.category, x.min,
x.avarage - x.standardDeviation,
x.avarage + x.standardDeviation,
x.max, x.category == selected ?
'color: #244c8e' : 'color: #4285f4', x.avarage];
})), false);
var dataNational = google.visualization.arrayToDataTable(
header.concat(SupplyRaw.national.map(function (x) {
return [x.category, x.min,
x.avarage - x.standardDeviation,
x.avarage + x.standardDeviation,
x.max, x.category == selected ?
'color: #244c8e' : 'color: #4285f4', x.avarage];
})), false);
var options = {
legend: 'none',
hAxis: {
title: 'Metrage', gridlines: { count: 7 },
ticks: new Array(7).fill().map(function (val, i) {
return { v: i, f: getMetrageCategoryString(i) };
})
},
seriesType: "candlesticks",
series: { 1: { type: "line" } },
animation: {
duration: 1000,
easing: 'out'
},
};
var chart =
new google.visualization.ComboChart(document.getElementById("chart_div"));
chart.draw(dataRegional, options);
I've put my code in this jsFiddle : https://jsfiddle.net/znrsrhzc/1/
I hope someone can spot the issue here
since the data is not loaded in order of the x-axis,
need to sort the data table before drawing.
dataNational.sort([{column: 0}]);
see following working snippet...
SupplyRaw = {
"regional": [
{
"category": 1,
"min": 75,
"max": 150,
"avarage": 113,
"standardDeviation": 32.036437588054845
},
{
"category": 2,
"min": 89,
"max": 162,
"avarage": 117,
"standardDeviation": 26.979004182264877
},
{
"category": 3,
"min": 31,
"max": 50,
"avarage": 42,
"standardDeviation": 10.214368964029715
}
],
"national": [
{
"category": 3,
"min": 20,
"max": 464,
"avarage": 104,
"standardDeviation": 55.76672091291433
},
{
"category": 6,
"min": 20,
"max": 115,
"avarage": 65,
"standardDeviation": 28.04067083325969
},
{
"category": 1,
"min": 23,
"max": 500,
"avarage": 192,
"standardDeviation": 89.87525674143646
},
{
"category": 7,
"min": 25,
"max": 100,
"avarage": 49,
"standardDeviation": 23.556315501368204
},
{
"category": 4,
"min": 20,
"max": 300,
"avarage": 88,
"standardDeviation": 48.83288977327806
},
{
"category": 5,
"min": 20,
"max": 210,
"avarage": 72,
"standardDeviation": 38.35082894261975
},
{
"category": 2,
"min": 20,
"max": 500,
"avarage": 137,
"standardDeviation": 72.39801425371081
}
]
}
function getMetrageCategoryString(id) {
if (id === 1)
return '< 250';
else if (id === 2)
return '250 - 500';
else if (id === 3)
return '500 - 1k';
else if (id === 4)
return '1k - 2.5k';
else if (id === 5)
return '2.5k - 5k';
else if (id === 6)
return '5k - 10k';
else if (id === 7)
return '10k >';
else
return "";
}
var selected = 2;
google.charts.load('current', {
callback: function () {
var header = [['Category', 'Min',
'Min Deviation',
'Max Divation', 'Max',
{ type: 'string', role: 'style' },
'Avarage']];
var dataRegional = google.visualization.arrayToDataTable(
header.concat(SupplyRaw.regional.map(function (x) {
return [x.category, x.min,
x.avarage - x.standardDeviation,
x.avarage + x.standardDeviation,
x.max, x.category == selected ?
'color: #244c8e' : 'color: #4285f4', x.avarage];
})), false);
var dataNational = google.visualization.arrayToDataTable(
header.concat(SupplyRaw.national.map(function (x) {
return [x.category, x.min,
x.avarage - x.standardDeviation,
x.avarage + x.standardDeviation,
x.max, x.category == selected ?
'color: #244c8e' : 'color: #4285f4', x.avarage];
})), false);
var options = {
legend: 'none',
hAxis: {
title: 'Metrage', gridlines: { count: 7 },
ticks: new Array(7).fill().map(function (val, i) {
return { v: i, f: getMetrageCategoryString(i) };
})
},
seriesType: "candlesticks",
series: { 1: { type: "line" } },
animation: {
duration: 1000,
easing: 'out'
},
};
var chart =
new google.visualization.ComboChart(document.getElementById("chart_div"));
chart.draw(dataRegional, options);
$("#Regional").click(function () {
dataRegional.sort([{column: 0}]);
chart.draw(dataRegional, options);
});
$("#National").click(function () {
dataNational.sort([{column: 0}]);
chart.draw(dataNational, options);
});
},
packages: ['corechart', 'table']
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://www.gstatic.com/charts/loader.js"></script>
<div id="chart_div"></div>
<button id="Regional" type="button">Regional</button>
<button id="National" type="button">National</button>
I'm using the 2.0 (Alpha) version of Chart.js, and...
The tooltips of my bar chart display the string "rgb(0,0,0)" instead of the label value with the code above. You can see that this.data.labels contains numeric strings. If I change them to regular ints, they're being displayed fine after a strange effect where you see a lot of decimals changing so fast...
var ScoresFrequencyChartDrawer = function () {
this.data = {
labels: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
datasets: [{
backgroundColor: "rgba(150, 150, 150, 0.9)",
data: [1, 1, 2, 3, 7, 12, 15, 17, 23, 15, 10]
}]
};
this.options = {
events: ["mousemove"],
scales: {
xAxes: [{
display: true,
gridLines: {
drawOnChartArea: false
},
labels: {
fontSize: 15,
fontStyle: "bolder"
}
}],
yAxes: [{
display: false,
gridLines: {
drawOnChartArea: false
}
}]
}
};
this.chart = {};
}
ScoresFrequencyChartDrawer.prototype.draw = function() {
var canvas = document.getElementById("scores-frequency-chart");
var ctx = canvas.getContext("2d");
this.chart = new Chart(ctx, {
type: 'bar',
data: this.data,
options: this.options
});
}
The issue at github -> https://github.com/nnnick/Chart.js/issues/1261
I have fixed it with the code above...
// Adding this to the this.data.labels initialization
labels: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(eachToString),
// The eachToString function
function eachToString(el) {
return el.toString()+"%";
}
But as the bug it is, the issue at Github remains open.