I am creating a chart.js which has both positive and negative values
but how to make all values be on the top half
(ignore the if it's positive or negative when drawing but keep the label)
var tax_dash = new Chart(ctx_tax_dash, {
type: "bar",
data: {
labels: lable_set,
datasets: [{
label: "Tax in",
data: total_tax_in_t_data__year,
backgroundColor: '#0fd96d',
// borderColor: sales_t_data,
borderWidth: 1,
},
{
label: "Tax out",
data: total_tax_out_t_data__year,
backgroundColor: '#0f81d9',
// borderColor: sales_t_data,
borderWidth: 1,
},
{
label: "Net VAT",
data: total_tax_in_out_t_data__year,
backgroundColor: '#d96a0f',
// borderColor: sales_t_data,
borderWidth: 1,
},
],
},
options: {
legend: {
display: true,
}
},
});
EDIT
what I am trying to do
possible solution: is (multi-axis) dual y axis.multi-axis example
~ issue: how to flip the axis so that the -100 be to the top and 0 be on the bottom
~ issue: how to split the data set base on the (sign)
OR
possible solution 2 : make all variable positive
#Gkiokan> solution: use the popup modifier to the showing values with negative
~ ++ issue: how the function will know if the value is negative
~ issue: the user needs to know that this value is negative in the label
Solution 2
I did it this morning user what Math.abs from #Lawrence comment and "popup modifier" from #Gkiokan comment as well as this jsfiddle
Thank you very much for the help. Chatting with smarter people rubs off on you :)
total_tax_in_t_data_portal_month_year = [Math.abs(12),Math.abs(-234),Math.abs(234)];
total_tax_in_t_data_portal_month_year_sign = [12,-234,234];
var tax_dash_portal = new Chart(ctx_tax_dash_portal, {
type: "bar",
data: {
labels: lable_set,
datasets: [
{
label: "VAT In",
data: total_tax_in_t_data_portal_month_year,
sign: total_tax_in_t_data_portal_month_year_sign,
backgroundColor: "#0fd96d",
// borderColor: sales_t_data,
borderWidth: 1,
},
{
label: "VAT Out",
data: total_tax_out_t_data_portal_month_year,
sign: total_tax_out_t_data_portal_month_year_sign,
backgroundColor: "#0f81d9",
// borderColor: sales_t_data,
borderWidth: 1,
},
{
label: "Net VAT",
data: total_tax_t_data_portal_month_year,
sign: total_tax_t_data_portal_month_year_sign,
backgroundColor: "#d96a0f",
// borderColor: sales_t_data,
borderWidth: 1,
},
],
},
options: {
legend: {
display: true,
},
tooltips: {
enabled: true,
callbacks: {
label: function (tooltipItem, data) {
var label = data.labels[tooltipItem.index];
var val = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
var sign = data.datasets[tooltipItem.datasetIndex].sign[tooltipItem.index];
if(sign < 0){
return label + ": -" + val;
}else{
return label + ': ' + val;
}
}
}
}
},
});
total_tax_in_t_data_portal_month_year is an example as the values come from a function
Math.abs is used to remove the negative sign
then I added sign to the datasets for essay access
tooltips callbacks is called on every variable so I added the if statement there
to add - if sign < 0 and do nothing if not
In my opinion you can have a data Set to save the orginal Data and the modified
Data and then use the values as you need. You can not trust the value characters. My solution will work kind cross over as you have control over both values.
I've made a jsfiddle for you which demonstrates the orginal Data vs modified Data usage. Please click first on Modify Data which will then map the data, so you can see the work in progress. In your case you would modify the data before calling the charts.
Actually you will need just a couple of methods as followed:
updateItemValues to modify the negative values and put it to the other object
tooltipCallback callback for the tooltip to use the mapped orginal value
let data = {
modified: false,
orginalData : {
'tax_in' : [10, 20, -30, -40, -100, -50],
'tax_out' : [-10, 10, 20, 10, -40, -70],
'net_vat' : [-50, -9, -40, -20, -10, -90],
},
modifiedData : {
// this modified data will be calculated before putting it in the charts
// for demo purpose we will just copy the values for now.
'tax_in' : [10, 20, -30, -40, -100, -50],
'tax_out' : [-10, 10, 20, 10, -40, -70],
'net_vat' : [-50, -9, -40, -20, -10, -90],
},
updateModifiedData(){
// loop though the orginal Data
Object.keys(this.orginalData).forEach( (item, indexx) => {
console.log('modifying item chart data for: ', item)
this.updateItemValues(item)
})
this.modified = true
document.getElementById('status').innerHTML = 'modified'
},
updateItemValues(dataKey){
let temp = []
this.orginalData[dataKey].forEach( (value, index) => {
console.log('- validating ', dataKey, 'index: ', index, '; value: ', value)
// if we have a negative value, just multiply by -1 so get it positive
if(value <= 0){
value = value * -1
}
// add to the temporary variable
temp.push(value)
})
// put the modified data to some place to have it saved
this.modifiedData[dataKey] = temp
console.log('-- final data modded ', temp)
},
tooltipCallback(tooltipItem, chartData) {
// find reference values
let index = tooltipItem.index
let dataIndex = tooltipItem.datasetIndex
// find the name of dataset
let key = chartData.datasets[dataIndex].name
// validate or whatever with the orginal value
let orginalValueOfItem = data.orginalData[key][index]
let modifiedValueOfItem = data.modifiedData[key][index]
// Modify your final tooltip here
return 'Orginal Value: ' + orginalValueOfItem + ' ; Modified Value: ' + modifiedValueOfItem
}
}
How can you use this solution?
Pretty simple.
Copy that data Object in your code.
Fill the data.orginalData value with your orginal charts data based on key
example data.orginalData.tax_in = [...]
In your datasets add name property with the corresponding key
Extend the Charts options with the tooltipCallback
Call data.updateModifiedData() to get the modified data
Checkout the jsFiddle for reference if you need to.
Have fun.
Related
I am trying to figure out how to render a section of each column in a simple, single series, column chart with multiple colors. Using series.zones:
series: [
{
name: "Mod",
colorByPoint: true,
data: seriesData,
zones: [
{ value: 101, color: '#1D681B' },
{ value: 121, color: '#ECC518' },
{ color: '#D50D0D' }
]
}
]
I can get each column to be a different color based on the zone that the y value is within.
In my example above the zone are:
0 through 100 should be green
101 to 120 should be yellow
121 and above should be red
The above works to an extent, but looks like the following:
However, what my boss wants is something like this:
Can this be achieved using highcharts?
Instead of zones you can use stacked columns. Below is a simple example of how you can automatically calculate series structure:
const steps = [100, 20];
const data = [42, 100, 96, 120, 110, 90, 140];
const series = [{
color: '#1D681B',
data: []
}, {
linkedTo: ':previous',
color: '#ECC518',
data: []
}, {
linkedTo: ':previous',
color: '#D50D0D',
data: []
}];
data.forEach((dataEl, i) => {
let rest = dataEl;
let counter = 0;
let value;
while (rest > 0) {
value = steps[counter] < rest ? steps[counter] : rest;
series[counter].data.push({
x: i,
y: value
});
rest -= value;
counter++;
}
});
Highcharts.chart('container', {
chart: {
type: 'column'
},
yAxis: {
reversedStacks: false
},
plotOptions: {
series: {
stacking: 'normal'
}
},
series
});
Live demo: http://jsfiddle.net/BlackLabel/3h6o0ncg/
Docs: https://www.highcharts.com/docs/advanced-chart-features/stacking-charts
Lets say I have a Donut chart with 5 items in data like this
const data = {
labels: ['E-commerce', 'Enterprise', 'Green', 'Grey', 'Purple'],
datasets: [
{
label: '# of Votes',
data: [12, 19, 3, 5, 3],
backgroundColor: ['#C07CC3', '#9C3848', '#9DDBAD', '#ADA8B6', '#606EDA'],
borderWidth: 1,
},
],
}
I don't want it to show all the legends as I don't have space or whatever reason
How can I hide the Green and purple in this example?
I mean only from legends not from chart
I see two easy ways, how you could approach this problem:
(I personally I would use the second option below, it is more configurable, but it needs just abit of coding)
You can simply delete the labels you want to delete from the labels- array, and they won't show up in the legend.
But keep in mind you would have to change the order of the data and backgroundColor arrays, to match this change.
Here a short demo:
const data = {
labels: ['E-commerce', 'Enterprise', 'Grey'], // <-- just remove the unwanted labels
datasets: [{
data: [12, 19, 5, 3, 3], // <-- reorder
backgroundColor: ['#C07CC3', '#9C3848', '#ADA8B6', '#9DDBAD', '#606EDA'], // <-- reorder
borderWidth: 1,
}],
};
const config = {
type: 'doughnut',
data: data,
options: {
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
usePointStyle: true,
},
}
},
}
};
new Chart(
document.getElementById('chart'),
config
);
<script src="//cdn.jsdelivr.net/npm/chart.js"></script>
<div class="chart" style="height:184px; width:350px;">
<canvas id="chart" ></canvas>
</div>
Better and cleaner (but some coding is needed), you can filter out label-items, you don't want to display, with the function array filter. (details can be found in the documentation)
UPDATED Alternative Version Demo:
here only the Top 3 labels (limiting the amount of labels with the variable maxLabelsToShow) will be shown (sort order is descending, but changing this is would be easy)
function getLabelsOnlyTopX(num, data, labels){
let selectedLabels = []
//we don't want to alter the order
let helperData = [...data];
//sort in descending order
helperData.sort( (a,b) => b-a);
//get top X Values
helperData = helperData.slice(0, num);
//get index for the data
let indexes = data.map( (value, index) => ({value,index}) ).filter(item => helperData.some(n1 => n1 == item.value))
//slecet only labels with the correct index
selectedLabels = labels.filter((value, index) => indexes.some( n => n.index == index))
// just be sure that a maximum of num labels are sent
return selectedLabels.slice(0, num);
}
let maxLabelsToShow = 3;
let serverData = [12, 19, 3, 5, 3]
let labels = ['E-commerce', 'Enterprise', 'Green', 'Grey', 'Purple'];
// Calling the newly created function
let showOnly = getLabelsOnlyTopX(maxLabelsToShow, serverData, labels);
const data = {
labels: labels,
datasets: [{
data: serverData,
backgroundColor: ['#C07CC3', '#9C3848',
'#9DDBAD', '#ADA8B6', '#606EDA'],
borderWidth: 1,
}],
};
const config = {
type: 'doughnut',
data: data,
options: {
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
usePointStyle: true,
/* FILTER function */
filter: function(item, chart) {
return showOnly.indexOf( item.text) > -1;
}
},
}
},
}
};
new Chart(
document.getElementById('chart'),
config
);
<script src="//cdn.jsdelivr.net/npm/chart.js"></script>
<div class="chart" style="height:184px; width:350px;">
<canvas id="chart" ></canvas>
</div>
When i draw a chart with 4 lines, each with its own data ofc, i programmatically create the options for the LineChart that has 4 Yaxis, first one on the left and the rest on the right side. Now, after the chart is drawn and i de-select some datasources from the list (less lines to draw), the now-obsolete yAxis ticks stay there, even when the chart correctly draws only the selected lines, and the options are updated as well correctly. I cant think of a way to remove them!
I have googled for 2 days and cant find a solution. I am using react in functional style and it makes things more complicated because every advice seems to be in the classic style.
I am using react-chartjs-2 wrapper as well, if this helps.
I am also quite new to react, and asking in Stackoverflow, so please cut me some slack :)
I assume the chart is being re-rendered or something because the amount of lines etc do change.
In the images, the "createYaxis" that is shown in the console.log is the generated yAxes- part of the options object (which is functional otherwise). The problem yAxises are on the right side in red and yellow. Images show before and after situation.
Image of the options-object generated by the code below the img:
var yAxisItems = [];
function createYaxises (num){
var arr = [];
for (var i=0;i<num.length;i++){
if (i===0){
arr.push({
display: true,
id: i,
type: 'linear',
position: 'left',
gridLines: {
display:false,
//color: 'blue'
},
ticks: {
fontColor: lineColourArray[i],
fontSize: 14,
}
})
}
else {
arr.push({
display: true,
id: i,
type: 'linear',
position: 'right',
gridLines: {
display:false,
//color: 'blue'
},
ticks: {
display:true,
fontColor: lineColourArray[i],
fontSize: 14,
}
})
}}
yAxisItems = arr;
console.log("createyaxis arr: " , arr);
console.log("createyaxis: " , yAxisItems); //JSON.stringify(yAxisItems));
}
//get data for selected sensors and set it to chart data
const handleGetSelectedSensorData = function () {
var d = getSelectedSensorData();
console.log("d: ", d);
var dSets = [];
if (d[0]){
d.map((dItem,index)=> {
var newDsetData =[];
if (dItem.data){
dItem.data.map((innerDataItem)=> {
var dSet = {};
dSet.x = innerDataItem.timestamp;
dSet.y = innerDataItem.v;
newDsetData.push(dSet);
})
var newset = {
data: newDsetData,
label: dItem.sensorTag,
borderColor: lineColourArray[index],
fill: false,
pointRadius: 1.5,
backgroundColor:lineColourArray[index],
borderWidth: 2,
showLine: true,
pointHoverRadius: 5,
lineTension: 1,
};
dSets.push(newset);
}})
var dDataTemp = {};
var optionsTemp = new Object();
dDataTemp.datasets =dSets;
//create yaxises only once
createYaxises(dDataTemp.datasets);
//more than one set (TODO)
//console.log("dDataTemp.datasets : ", dDataTemp.datasets)
if (dDataTemp.datasets.length >1){
console.log("dset > 1");
for(var i=0;i< dDataTemp.datasets.length;i++) {
dDataTemp.datasets[i].yAxisID = i;
console.log("setting options");
optionsTemp ={
tooltips: {
enabled: true,
intersect:false,
mode:'x',
callbacks: {
title: function(tooltipItem, data) {
var toSplit = tooltipItem[0].label.split(",");
return (toSplit[0]);
},
label: function (tooltipItem) {
var split = tooltipItem.xLabel.split(',');
//return ( Number(tooltipItem.yLabel).toFixed(3));
return (split[2] + " : " + Number(tooltipItem.yLabel).toFixed(3));
}
},
},
hover: {
mode: 'nearest',
intersect: true,
},
title:{
display:true,
text:'Valittu sensoridata',
fontSize:20
},
legend:{
display:true,
position:'right'
},
scales: {
xAxes: [{
display: true,
type: 'time',
ticks: {
}
}],
yAxes:
yAxisItems
}
}
}
setOptions(optionsTemp);
console.log("options: " , optionsTemp);
setdData(dDataTemp);
}}
else {
console.log("error in handleGetSelectedSensorData()");
}
}
And the Line is just added like this:
<Line data={dData} options = {options} />
Instead of setting display: true set display: 'auto', this will make the axis dissapear as long as there is no dataset visable that is linked to that scale, as soon as a dataset becomes visable that is linked to that scale it will show the scale again.
Doc: https://www.chartjs.org/docs/master/axes/cartesian/#common-options-to-all-axes
SOLVED
Solution: Changed the spelling of axis to axes
I have just begun working with Chart.js as an alternative to the Python Plot.ly library.
So far I have been able to get the chart formatted the way I would like but there are still a couple of nuances I cant get right.
The first of which is that I cannot get any solution for adding a $ before Y-Axis values.
Here is my chart followed by the chart's code:
<script>
var ctx = document.getElementById("myChart").getContext('2d');
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
var O1 = [1000,2000,3000,4000,5000,6000,7000,8000,9000,10000,11000,12000];
var O2 = [3000,6000,5000,3000,8000,6000,4000,7000,8000,0000,3000,1000];
var mychart = new Chart(ctx, {
type: 'line',
options: {
title:{
text: 'Test Chart',
display: true,
fontStyle: 'bold',
fontSize: 16,
},
legend:{
position: 'right',
},
scales:{
yAxis:[{
scaleLabel:{
display: true,
labelString: 'Tester',
},
ticks:{
callback: function(value, index, values) {
if(parseInt(value) >= 1000){
return '$' + value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
} else {
return '$' + value;
}
}
}
}]
},
tooltips:{
callbacks: {
label: function(tooltipItem, data){
var dataLabel = data.labels[tooltipItem.index];
var value = ': $ ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index].toLocaleString();
if (Chart.helpers.isArray(dataLabel)) {
dataLabel = dataLabel.slice();
dataLabel[0] += value;
} else {
dataLabel += value;
}
return dataLabel1;
}
}
}
},
data: {
labels: months,
datasets: [
{
label: 'Officer1',
data: O1,
fill: false,
backgroundColor: 'rgba(255, 99, 132)',
borderColor: 'rgba(255, 99, 132)',
},
{
label: 'Officer2',
data: O2,
fill: false,
backgroundColor: 'rgba(0,0,255)',
borderColor: 'rgba(0,0,255)',
}
]
}
});
</script>
As you may be able to see I would simply like to add a $ before my Y-Axis data values. The last solution I tried also broke the data up by 1000s.
Additional:
The next few issues I've come across are very minor, which is why I chose to include them here.
First I would like a way to center my title over the chart itself, rather than centered over the whole chart+legend item.
Second is I would like to add some space to the left of my legend, as to separate it more from the chart.
Lastly, my Y-Axis title will not display. I would like it to display here while im testing in case I need it in the future.
This issue was solved by changing the spelling of axis to axes...subtle
Change the spelling of axis to axes
Is there a way to add color zones to the High Charts navigator ? If it doesn't fall between a zone it should have a default color. Something like, from this value to this value it has this color, otherwise it's blue.
I basically have some reporting periods that have a start and end date and a specific color. I'd like that for each reporting period to appear with their respective color on the High Charts navigator. I have it somewhat working, using zones but for zones with no reporting period it takes the color of the next one.
This is what it currently looks like.
And here's the code:
const zones = reportingPeriods.map((rp, i) => {
return {
value: new Date(rp.endDate),
color: rp.color
}
})
const seriesData = _.map(graphData, (metricData, key) => {
return {
name: key,
data: metricData.sort(function(a, b) {
return a[0] - b[0];
}),
zoneAxis: 'x',
zones: zones,
tooltip: {
valueDecimals: 0
},
visible: false
}
})
const graphOptions = {
rangeSelector: {
selected: 1
},
navigator: {
maskFill: 'rgba(0, 0, 0, 0.25)'
},
backgroundColor: {
linearGradient: [0, 0, 500, 500],
stops: [
[0, 'rgb(255, 255, 255)'],
[1, 'rgb(200, 200, 255)']
]
},
title: {
text: group.groupName
},
series: seriesData,
credits: false,
legend: {
enabled: true
}
}
Is there a way where you could define a start and end point for a zone ? and also restrict the coloring strictly to the navigator ?
Ideally it would be nice if the zones could be defined like this:
from: rp.startDate,
to: rp.endDate,
color: rp.color
Any push in the right direction would be greatly appreciated.
I've answered the questions below. Here is the TLDR fiddle example.
Is there a way where you could define a start and end point for a zone ?
The format you've suggested is invalid, but the workaround is to fill in the blanks with zones using the "default color" you want. That means changing your map to a more elaborate function. For example:
periods = [
{ start: 2, end: 5, color: 'green' },
{ start: 5, end: 7, color: 'pink' },
{ start: 10, end: 15, color: 'red' },
{ start: 19, end: 21, color: 'yellow' }
];
defaultColor = 'black';
zones = [];
for(var i = 0; i < periods.length; i++) {
// This zone is a "in-between" zone to fill the blanks.
zones.push({
value: periods[i].start,
color: defaultColor
});
// This zone is an actual period
zones.push({
value: periods[i].end,
color: periods[i].color
});
}
// This zone is cover the values from your last period to infinity
zones.push({
color: defaultColor
});
Here I imagine the periods array to match your reportingPeriods and the zones array to be the end product which you apply to your series.
and also restrict the coloring strictly to the navigator ?
To only apply the zones to the navigator you simply use the navigator.series object. For example:
navigator: {
series: {
type: 'line',
zones: zones
}
}
And not apply them to the actual series.