ChartJS v2: Scale value at click coordinates (time scale) - javascript

I have a time-base line chart and I'm attempting to obtain the values for each scale at the click coordinates.
My onClick function specified in ChartJS options:
onClick: function(event, elementsAtEvent)
{
console.log(event, elementsAtEvent, this);
var valueX = null, valueY = null;
for (var scaleName in this.scales) {
var scale = this.scales[scaleName];
console.log(scale.id, scale.isHorizontal());
if (scale.isHorizontal()) {
valueX = scale.getValueForPixel(event.offsetX);
} else {
valueY = scale.getValueForPixel(event.offsetY);
}
}
console.log(event.offsetX, valueX, null, event.offsetY, valueY);
},
JSFiddle: https://jsfiddle.net/AllenJB/dmebo9g5/1/
The code above appears to work well for the Y axis, but not the X (time scale) - it always returns the last value regardless of where in the chart you click.
What might I be doing wrong?

This code actually works fine. The only problem was that I was looking at the _i value of the moment object to check it's value (this is the value used as the initial input when creating the object, not necessarily the current value).
Changing the console.log line to the following yields the expected / correct result:
console.log(event.offsetX, valueX.format('YYYY-MM-DD HH:mm:ss'), null, event.offsetY, valueY);

If you want to get the nearest x axis value you could do it this way:
onClick: function (event) {
const activeElements = this.getElementsAtXAxis(event);
const xval = this.scales['x-axis-0']._timestamps.data[activeElements[0]._index];
console.log(xval)
}

Related

Picking quartile value on each point

I'm plotting sentiment value of tweet over last 10 years.
The csv file has the three columns like below.
I plotted each value by date successfully.
However, when I tried to generate an area graph,
I encountered a problem which is,
each date has multiple values.
That's because each data point is from one single tweets that
one x point ended up with having multiple y values.
So I tried to pick quartile value of each date or pick largest or least y value.
For clarity, please see the example below.
January 8 has multiple y values (textblob)
I want to draw area graph by picking the largest value or 2nd quartile value of each point.
How do I only pick the point?
I would like to feed the point in the following code as a
x/y coordinate for line or area greaph.
function* vlinedrawing(data){
for(let i;i<data.length;i++){
if( i%500==0) yield svg.node();
let px = margin+xscale(data[i].date)
let py = height-margin-yscale(data[i].vader)
paths.append('path')
.attr('x',px)
.attr('y',py)
}
yield svg.node()
}
The entire code is in the following link.
https://jsfiddle.net/soonk/uh5djax4/2/
Thank you in advance.
( The reason why it is a generator is that I'm going to visualize the graph in animated way)
For getting the 2nd quartile you can use d3.quantile like this:
d3.quantile(dataArray, 0.5);
Of course, since the 2nd quartile is the median, you can also just use:
d3.median(dataArray);
But d3.quantile is a bit more versatile, you can just change the p value for any quartile you want.
Using your data, without parsing the dates (so we can use a Set for unique values`), here is a possible solution:
const aggregatedData = [...new Set(data.map(function(d) {
return d.date
}))].map(function(d) {
return {
date: parser(d),
textblob: d3.quantile(data.filter(function(e) {
return e.date === d
}).map(function(e) {
return e.textblob
}), 0.5)
}
});
This is just a quick answer for showing you the way: that's not a optimised code, because there are several loops within loops. You can try to optimise it.
Here is the demo:
var parser = d3.timeParse("%m/%d/%y");
d3.csv('https://raw.githubusercontent.com/jotnajoa/Javascript/master/tweetdata.csv', row).then(function(data) {
const aggregatedData = [...new Set(data.map(function(d) {
return d.date
}))].map(function(d) {
return {
date: parser(d),
textblob: d3.quantile(data.filter(function(e) {
return e.date === d
}).map(function(e) {
return e.textblob
}), 0.5)
}
});
console.log(aggregatedData)
});
function row(d) {
d.vader = +d.vader;
d.textblob = +d.textblob;
return d;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Is there a way to change the axis labels?

i'm updating some code in my job and got in a problem, on amcharts3, the labels of the x axis are not what the data is binded to. Like the data of the chart is binded to a date, but shows another value (the value is in the dataset) in the labels, and didn't find a way to do that on amcharts4.
I tried creating another x axis but then the data doesn't look right.
It should be a x axis showing a value that is in the dataset but just showing, the axis should be binded to another value in the dataset. if that makes sense.
Here's an amCharts v3 example that uses labelFunction to, as you say, bind data to what gets written out on the axis (courtesy xorspark):
https://codepen.io/team/amcharts/pen/74df55e029228d9a1867b65f351ca48f
For v4, you would use an Adapter on the AxisLabel. A tricky part with DateAxis Axis Labels is that their dataItem doesn't tie to the original chart or series' DataItem to give you access to the rest of the data (e.g. via dataItem.dataContext["field name here"]). So as the adapter tries to provide text to its DateAxis AxisLabel, if that label has a Date, you'll have to compare said AxisLabel to each of your data objects to see if its Date and your data's date (if it's a string, convert it to Date) match. If it does, then pull from that data's field that you want to "bind" it to.
Relevant code (presuming your data field for dates is "date"):
function dataToDate(dateStr) {
return chart.dateFormatter.parse(dateStr);
}
function sameDates(date1, date2) {
return date1.getTime() === date2.getTime();
}
function matchAxisDateToData(axisLabel) {
var length = chart.data.length;
for (var i = 0; i < length; ++i) {
// there are more axis labels than we see, and some of them will return undefined for their date
if (axisLabel.dataItem.dates.date && sameDates(axisLabel.dataItem.dates.date, dataToDate(chart.data[i].date))) {
return chart.data[i].timeOfDay;
}
}
return false;
}
dateAxis.renderer.labels.template.adapter.add("text", function(text, axisLabel) {
var dataText;
if (axisLabel.dataItem.dates.date) {
dataText = matchAxisDateToData(axisLabel);
}
// if there's a match, return that, otherwise return the default "text"
return dataText || text;
});
Here's a demo of the above:
https://codepen.io/team/amcharts/pen/eb455855b460998f4e4282a0216a9443

Highcharts stacked bar chart - how to get the stacks values

I have created an highcharts stacked bar chart, but when the data is skewed, the bars are not visible or the numbers overlap, as shown in below image.
I have seen many posts, but there is no out of the box solution for this, so i am making my custom solution.
I am setting a default height of 150 if the y value is less than 150.
This solution works, but the total of the bars now is shown to be 300 instead of the actual original value. How can i change the total stacklabel value on my own? I am unable to find a way to do that.
Here is the code to change the height to default values. I am storing the actual value in realValue variable in the point object.
chartOptions = {
type: CHARTING.CHART_OPTIONS.TYPE.COLUMN,
// On chart load, apply custom logic
events : {
load: function () {
var chart = this,
minColHeightVal = 150;
chart.series.forEach(function (s) {
s.points.forEach(function (p) {
if (p.y < minColHeightVal) {
p.update({
y: minColHeightVal,
realValue: p.y
}, false);
}
});
});
// How to iterate over the bars here and sum the actual value? i.e. point.realValue and set the stacklabel?
chart.redraw();
}
}
}
Did you try to use minPointLength option? It may be a simpler solution in your case: https://api.highcharts.com/highcharts/series.column.minPointLength
However, using your code to get the wanted result, use stackLabels.formatter function:
formatter: function() {
var series = this.axis.series,
x = this.x,
sum = 0;
series.forEach(function(s) {
if (s.points && s.points[x]) {
sum += s.points[x].realValue ? s.points[x].realValue : s.points[x].y
}
});
return sum;
}
Live demo: https://jsfiddle.net/BlackLabel/uocdykbL/
API Reference: https://api.highcharts.com/highcharts/yAxis.stackLabels.formatter

AmCharts - set max value axis using dataprovider

I'm trying to set max value axis using dataprovider, since I'm dynamically loading data in my bar chart I need to see the "progress" on one of the bars compared to the one that is supposed to be the total.
How do I achieve this?
I' tried with:
"valueAxes": [
{
"id": "ValueAxis-1",
"stackType": "regular",
"maximum": myDataProviderAttribute
}
But no luck.
Any suggestion will be much apreciated.
I've submited a ticket to AmCharts support and got this feedback:
You can set max before the chart is initialized based on the data you have, inside addInitHandler. Here is an example for a simple column chart:
AmCharts.addInitHandler(function(chart) {
// find data maximum:
var min = chart.dataProvider[0].visits;
for (var i in chart.dataProvider) {
if (chart.dataProvider[i].visits > max) {
max = chart.dataProvider[i].visits;
}
}
// set axes max based on value above:
chart.valueAxes[0].maximum = max + 100;
chart.valueAxes[0].strictMinMax = true;
});
You may need to use strictMinMax as above to enforce the value:
https://docs.amcharts.com/3/javascriptcharts/ValueAxis#strictMinMax
Example of setting minimum inside addInitHandler:
https://codepen.io/team/amcharts/pen/b4be8cb4e3c073909860720e0909a876?editors=1010
If you refresh the data, or use live data, then before you animate or validate the chart to show the updated data, you should recalculate the max and, if you want the axis to change then use chart.valueAxes[0].maximum = max; where max is something you calculate based on data input.
Here is an example:
function loop() {
var data = generateChartData();
chart.valueAxes[0].maximum = max;
// refresh data:
chart.animateData(data, {
duration: 1000,
complete: function () {
setTimeout(loop, 2000);
}
});
}
The lines above were used in this example:
https://codepen.io/team/amcharts/pen/d0d5d03cfdcc2cc256e28ec52ad8b95c/?editors=1010

Crossfilter - Double Dimensions (second value linked to daily max)

Quite an oddly specific question here but something I've been having a lot of trouble with over the past day or so. Broadly, I'm trying to calculate the maximum of an array using crossfilter and then use this value to find a maximum.
For example, I have a series of Timestamps with an associated X Value and a Y Value. I want to aggregate the Timestamps by day and find the maximum X Value and then report the Y Value associated with this Timestamp. In essence this is a double dimension as I understand it.
I'm able to do the first stage simply to find the maximum values. But am having a lot of difficulty getting through to the second value.
Working code for the first, (using Crossfilter and Reductio). Assuming that each row has the following four values.
[(Timestamp, Date, XValue, YValue),
(2015-05-15 16:00:00, 2015-05-15, 30, 15),
(2015-05-15 16:45:00, 2015-05-15, 25, 33)
... (many thousand of rows)]
First Dimension
ndx = crossfilter(data);
dailyDimension = ndx.dimension(function(d) { return d.date; });
Get the max of the X Value using reductio
maxXValue = reductio().max(function(d) { return d.XValue;});
XValues = maxXValue(dailyDimension.group())
XValues now contains all of the maximum X Values on a Daily Basis.
I would now like to use these X Values to identify the corresponding Y Values on a date basis.
Using the same data above the appropriate value returned would be:
[(date, YValue),
('2015-05-15', 15)]
// Note, that it is 15 as it is the max X Value we find, not the max Y Value.
In Python/Pandas I would set the index of a DataFrame to X and then do an index match to find the Y Values
(Note, it can safely be assumed that the X Values are unique in this case but in reality we should really identify the Timestamp linked to this period and then match on that as they are strictly guaranteed to be unique, not loosely).
I believe this can be accomplished by modifying the reductio maximum code which I don't fully understand properly Source Code is from here
var reductio_max = {
add: function (prior, path) {
return function (p, v) {
if(prior) prior(p, v);
path(p).max = path(p).valueList[path(p).valueList.length - 1];
return p;
};
},
remove: function (prior, path) {
return function (p, v) {
if(prior) prior(p, v);
// Check for undefined.
if(path(p).valueList.length === 0) {
path(p).max = undefined;
return p;
}
path(p).max = path(p).valueList[path(p).valueList.length - 1];
return p;
};
},
initial: function (prior, path) {
return function (p) {
p = prior(p);
path(p).max = undefined;
return p;
};
}
};
Perhaps this can be modified so that there is a second valueList of Y Values which maps 1:1 with the X Values associated in the max function. In that case it would be the same index look up of both in the functions and could be assigned simply.
My apologies that I don't have any more working code.
An alternative approach would be to use some form of Filtering Function to remove entries which don't satisfy the X Criteria and then group by day (there should only be one value in this setting so a simple reduceSum for example will still return the correct value).
// Pseudo non working code
dailyDimension.filter(function(p) {return p.XValue === XValues;})
dailyDimension.group().reduceSum(function(d) {return d.YValue;})
Eventual results will be plotted in dc.js
Not sure if this will work, but maybe give it a try:
maxXValue = reductio()
.valueList(function(d) {
return ("0000000000" + d.XValue).slice(-10) + ',' + d.YValue;
})
.aliasProp({
max: function(g) {
return +(g.valueList[g.valueList.length - 1].split(',')[0]);
},
yValue: function(g) {
return +(g.valueList[g.valueList.length - 1].split(',')[1]);
}
});
XValues = maxXValue(dailyDimension.group())
This is kind of a less efficient and less safe re-implementation of the maximum calculation using the aliasProp option, which let's you do pretty much whatever you want to to a group on every record addition and removal.
My untested assumption here is that the undocumented valueList function that is used internally in max/min/median will properly order. Might be easier/better to write a Crossfilter maximum aggregation and then modify it to also add the y-value to the group.
If you want to work through this with Reductio, I'm happy to do that with you here, but it will be easier if we have a working example on something like JSFiddle.

Categories