I'm struggling with the problem of showing additional serie that will contains maximum values for every x. Like this:
I was able to parse all series to aggregate points and also found cross points to include them. Currently I have a problem with cross point (A) that are below one of the series. (Solution A in fiddle)I was thinking about calculating line and check if the point A belongs to that line. This will fix scenario but will cause problem for point B bc it doesn't belong to calculated line but should be included in serie.
Could any one point me to the right direction?
var chart = Highcharts.chart('container', {
chart: {
type: 'area',
zoomType: 'x',
backgroundColor:null,
},
xAxis: {
type: 'datetime',
dateTimeLabelFormats: {
month: '%b %e',
year: '%b'
},
},
plotOptions: {
series: {
fillOpacity: 0.1,
marker: {
enabled: false
}
}
}
});
chart.addSeries({"id":0,"connectNulls":true,"data":[[1524469020000,30],[1524469080000,30],[1524469140000,30],[1524469200000,30],[1524469260000,30],[1524469320000,30],[1524469380000,30],[1524469440000,30],[1524469500000,30],[1524469560000,58],[1524469620000,4],[1524469680000,4],[1524469740000,4],[1524469800000,4],[1524469860000,4],[1524469920000,4],[1524469980000,4],[1524470040000,4],[1524470100000,4],[1524470160000,4],[1524470220000,4],[1524470280000,4],[1524470340000,4],[1524470400000,4],[1524470460000,4],[1524470520000,22],[1524470580000,22],[1524470640000,22],[1524470700000,22]],"name":"Serie A","color":"#30e430","yAxis":0});
chart.addSeries({"id":1,"connectNulls":true,"data":[[1524469020000,35],[1524469080000,35],[1524469140000,35],[1524469200000,35],[1524469260000,35],[1524469320000,35],[1524469380000,35],[1524469440000,35],[1524469500000,35],[1524469560000,25],[1524469620000,25],[1524469680000,25],[1524469740000,25],[1524469800000,25],[1524469860000,25],[1524469920000,25],[1524469980000,59],[1524470040000,59],[1524470100000,59],[1524470160000,59],[1524470220000,59],[1524470280000,59],[1524470340000,59],[1524470400000,59],[1524470460000,59],[1524470520000,59],[1524470580000,59],[1524470640000,59],[1524470700000,59]],"name":"Serie B","color":"#0cb5ed","yAxis":0});
chart.addSeries({"id":2,"connectNulls":true,"data":[[1524469020000,18],[1524469080000,18],[1524469140000,18],[1524469200000,18],[1524469260000,18],[1524469320000,18],[1524469380000,18],[1524469440000,18],[1524469500000,18],[1524469560000,18],[1524469620000,18],[1524469680000,18],[1524469740000,18],[1524469800000,18],[1524469860000,18],[1524469920000,18],[1524469980000,18],[1524470040000,18],[1524470100000,18],[1524470160000,18],[1524470220000,18],[1524470280000,18],[1524470340000,18],[1524470400000,18],[1524470460000,18],[1524470520000,18],[1524470580000,18],[1524470640000,18],[1524470700000,18]],"name":"Serie C","color":"#e8ad23","yAxis":0});
$('#button').click(function () {
var results = getChartPointValues();
var data = prepareSummarySeries(results);
//clean previously added virutal series
for(var i in chart.series){
var serie = chart.series[i];
if(serie.userOptions.is_virtual == true)
serie.remove();
}
chart.addSeries({
id: 'virtual_max_b',
is_virtual: true,
'connectNulls': true,
'data' : data.max_b,
'name' : 'Solution A',
'color' : '#000',
'yAxis': 0,
});
chart.addSeries({
id: 'virtual_max_a',
is_virtual: true,
'connectNulls': true,
'data' : data.max_a,
'name' : 'Base Solution',
'color' : '#ff0000',
'yAxis': 0,
});
});
/*
* Calculate max values for every point
*/
var prepareSummarySeries = function(data){
var tmp_keys = Object.keys(data); ///sort
tmp_keys = tmp_keys.sort();
var counter = tmp_keys.length;
var results = {
'max_a': [],
'max_b': [],
};
for(var k = 0; k < counter; k++){
var key = tmp_keys[k];
var x_pos = parseFloat(key);
if(x_pos % 1 !== 0)
{
var prev_point = results.max_b.slice(-1)[0];
var current_point = [x_pos, data[key][0]];
var next_point = [ parseFloat(tmp_keys[k+1]), Math.max.apply(null, data[tmp_keys[k+1]] )];
if( checkIfPointBelongsToChart(prev_point, current_point, next_point) ){
results.max_b.push([ x_pos, current_point[1] ]);
}
} else {
results.max_b.push([ x_pos, Math.max.apply(null, data[key]) ]);
}
results.max_a.push([ x_pos, Math.max.apply(null, data[key]) ]);
}
return results;
};
var get_line_intersection = function(p0,p1,p2,p3){
var p0_x = p0.x;
var p0_y = p0.y;
var p1_x = p1.x;
var p1_y = p1.y;
var p2_x = p2.x;
var p2_y = p2.y;
var p3_x = p3.x;
var p3_y = p3.y;
var s1_x, s1_y, s2_x, s2_y;
s1_x = p1_x - p0_x; s1_y = p1_y - p0_y;
s2_x = p3_x - p2_x; s2_y = p3_y - p2_y;
var s = (-s1_y * (p0_x - p2_x) + s1_x * (p0_y - p2_y)) / (-s2_x * s1_y + s1_x * s2_y);
var t = ( s2_x * (p0_y - p2_y) - s2_y * (p0_x - p2_x)) / (-s2_x * s1_y + s1_x * s2_y);
if (s >= 0 && s <= 1 && t >= 0 && t <= 1)
{
return [p0_x + (t * s1_x),p0_y + (t * s1_y)];
}
return false;
};
var checkIfPointBelongsToChart = function(prev_point, current_point, next_point)
{
var slope = (next_point[1] - prev_point[1]) / (next_point[0] - prev_point[0]);
var b = prev_point[1] - slope * prev_point[0];
var tmp_y = slope * current_point[0] + b;
return (tmp_y == current_point[1])? true : false;
};
// create array with every x point with every possible y value
// im not taking only max here bc later we could need min instead of max
var getChartPointValues = function(){
var results = {}
var self = this;
var points = [];
var checked_series = [];
for(var k =0; k < chart.series.length; k++)
{
var entry = chart.series[k];
if(entry.userOptions.is_virtual == true || entry.visible == false)
continue;
var s1_points = entry.data;
var c1 = s1_points.length;
//add all points from serie
for(var i = 0; i < c1; i++)
{
if(s1_points[i] == undefined || !s1_points[i].isInside)
continue;
if(points[s1_points[i].x] == undefined){
points[s1_points[i].x] = [];
}
points[s1_points[i].x].push(s1_points[i].y);
}
//check all points in all charts for crossing points
for(var s in chart.series){
var serie = chart.series[s];
if(serie.userOptions.is_virtual == true || serie.visible == false)
continue;
//skip serie if combination of series was already checked
var current_check = entry.userOptions.id + '_' + serie.userOptions.id;
if(checked_series.indexOf(current_check) != -1 || serie.userOptions.is_virtual == true)
continue;
checked_series.push(current_check);
checked_series.push(serie.userOptions.id + '_' + entry.userOptions.id);
if(serie.index != entry.index){
var s2_points = serie.data;
var c2 = s2_points.length;
for(var i = 0; i < c1; i++)
{
if(s1_points[i] == undefined || !s1_points[i].isInside)
continue;
for(var j = 0; j < c2; j++)
{
if(s2_points[j] == undefined || !s2_points[j].isInside)
continue;
var cross = [];
if(s1_points[i-1] != undefined && s2_points[j-1] != undefined){
if(cross = get_line_intersection(s1_points[i-1], s1_points[i], s2_points[j-1], s2_points[j])){
if(points[cross[0]] == undefined){
points[cross[0]] = [];
}
points[cross[0]].push(cross[1])
}
}
}
}
}
};
};
return points;
}
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<button id="button">Add Maximum series</button>
<div id="container" style="height: 400px"></div>
jsFiddle
I have tried to solve this using a slightly different method. The gist of it is:
Find the series that has the max point
Find the series that has the next max point
Check if there is an intersect between the lines of these two series
If there is an intersect, add that as a separate point
Add the next max point
There is one caveat with doing it like this, a sparse series has the ability to ruin the graph, like this: https://jsfiddle.net/ewolden/3koe86sx/15/
Solving that issue would be tedious, requiring you to find all x points, and for lines without all points present, fill in their values.
That being said, as long as the series have roughly the same number of points, this should work well.
var get_line_intersection = function(p0, p1, p2, p3) {
var p0_x = p0.x;
var p0_y = p0.y;
var p1_x = p1.x;
var p1_y = p1.y;
var p2_x = p2.x;
var p2_y = p2.y;
var p3_x = p3.x;
var p3_y = p3.y;
var s1_x, s1_y, s2_x, s2_y;
s1_x = p1_x - p0_x;
s1_y = p1_y - p0_y;
s2_x = p3_x - p2_x;
s2_y = p3_y - p2_y;
var s = (-s1_y * (p0_x - p2_x) + s1_x * (p0_y - p2_y)) / (-s2_x * s1_y + s1_x * s2_y);
var t = (s2_x * (p0_y - p2_y) - s2_y * (p0_x - p2_x)) / (-s2_x * s1_y + s1_x * s2_y);
if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
return [p0_x + (t * s1_x), p0_y + (t * s1_y)];
}
return false;
};
//Gets the next point
function getNextPoint(series, current_x, current_y) {
nextPoint = {
next_x: 0,
next_y: 0,
x: 0,
y: -1,
prev_x: 0,
prev_y: 0
}
for (var i = 0; i < series.length; i++) {
for (var j = 1; j < series[i].length; j++) {
if (series[i][j].x > current_x) { //Goes one step past current timestamp
if (series[i][j].y > nextPoint.y || nextPoint.y == -1) { //Checks that this is the max number, could be changed to find min as well
if (j < series[i].length - 1) { //Checks if this is the last point (to avoid going past last index)
nextPoint = {
next_x: series[i][j + 1].x,
next_y: series[i][j + 1].y,
x: series[i][j].x,
y: series[i][j].y,
prev_x: series[i][j - 1].x,
prev_y: series[i][j - 1].y,
}
} else {
nextPoint = {
x: series[i][j].x,
y: series[i][j].y,
prev_x: series[i][j - 1].x,
prev_y: series[i][j - 1].y,
}
}
}
break;
}
}
}
return nextPoint
}
function getAllSeries(chart) {
var allSeries = []
for (var i = 0; i < chart.series.length; i++) {
allSeries.push(chart.series[i].data)
}
return allSeries
}
var chart = Highcharts.chart('container', {
chart: {
type: 'area',
zoomType: 'x',
backgroundColor: null,
},
xAxis: {
type: 'datetime',
dateTimeLabelFormats: {
month: '%b %e',
year: '%b'
},
},
plotOptions: {
series: {
fillOpacity: 0.1,
marker: {
enabled: false
}
}
}
});
chart.addSeries({
"id": 0,
"connectNulls": true,
"data": [
[1524469020000, 30],
[1524469080000, 30],
[1524469140000, 30],
[1524469200000, 30],
[1524469260000, 30],
[1524469320000, 30],
[1524469380000, 30],
[1524469440000, 30],
[1524469500000, 30],
[1524469560000, 58],
[1524469620000, 4],
[1524469680000, 4],
[1524469740000, 4],
[1524469800000, 4],
[1524469860000, 4],
[1524469920000, 4],
[1524469980000, 4],
[1524470040000, 4],
[1524470100000, 4],
[1524470160000, 4],
[1524470220000, 4],
[1524470280000, 4],
[1524470340000, 4],
[1524470400000, 4],
[1524470460000, 4],
[1524470520000, 22],
[1524470580000, 22],
[1524470640000, 22],
[1524470700000, 22]
],
"name": "Serie A",
"color": "#30e430",
"yAxis": 0
});
chart.addSeries({
"id": 1,
"connectNulls": true,
"data": [
[1524469020000, 35],
[1524469080000, 35],
[1524469140000, 35],
[1524469200000, 35],
[1524469260000, 35],
[1524469320000, 35],
[1524469380000, 35],
[1524469440000, 35],
[1524469500000, 35],
[1524469560000, 25],
[1524469620000, 25],
[1524469680000, 25],
[1524469740000, 25],
[1524469800000, 25],
[1524469860000, 25],
[1524469920000, 25],
[1524469980000, 59],
[1524470040000, 59],
[1524470100000, 59],
[1524470160000, 59],
[1524470220000, 59],
[1524470280000, 59],
[1524470340000, 59],
[1524470400000, 59],
[1524470460000, 59],
[1524470520000, 59],
[1524470580000, 59],
[1524470640000, 59],
[1524470700000, 59]
],
"name": "Serie B",
"color": "#0cb5ed",
"yAxis": 0
});
chart.addSeries({
"id": 2,
"connectNulls": true,
"data": [
[1524469020000, 18],
[1524469080000, 18],
[1524469140000, 18],
[1524469200000, 18],
[1524469260000, 18],
[1524469320000, 18],
[1524469380000, 18],
[1524469440000, 18],
[1524469500000, 18],
[1524469560000, 18],
[1524469620000, 18],
[1524469680000, 18],
[1524469740000, 18],
[1524469800000, 18],
[1524469860000, 18],
[1524469920000, 18],
[1524469980000, 18],
[1524470040000, 18],
[1524470100000, 18],
[1524470130000, 80],
[1524470160000, 18],
[1524470220000, 18],
[1524470280000, 18],
[1524470340000, 18],
[1524470400000, 18],
[1524470460000, 18],
[1524470520000, 18],
[1524470580000, 18],
[1524470640000, 18],
[1524470700000, 18]
],
"name": "Serie C",
"color": "#e8ad23",
"yAxis": 0
});
$('#button').click(function() {
series = getAllSeries(chart)
var currentPoint = {
next_x: 0,
next_y: 0,
x: -1,
y: 0,
prev_x: 0,
prev_y: 0
}
var max_x = 0;
//finds the first point
for (var i = 0; i < series.length; i++) {
if (currentPoint.y < series[i][0].y || currentPoint.x == -1) { //makes sure this is the largest point
currentPoint = {
prev_x: series[i][0].x,
prev_y: series[i][0].y,
x: series[i][0].x,
y: series[i][0].y,
next_x: series[i][1].x,
next_y: series[i][1].y
}
}
if (max_x < series[i][series[i].length - 1].x) {
max_x = series[i][series[i].length - 1].x;
}
}
result = []
result.push({ //since the first point comes from the above code, we need to add it explicitly
x: currentPoint.x,
y: currentPoint.y
})
while (currentPoint.x != max_x) { //loop through all points
nextPoint = getNextPoint(series, currentPoint.x, currentPoint.y);
let intersect = get_line_intersection({
x: nextPoint.prev_x,
y: nextPoint.prev_y
}, {
x: nextPoint.x,
y: nextPoint.y
}, {
x: currentPoint.x,
y: currentPoint.y
}, {
x: currentPoint.next_x,
y: currentPoint.next_y
})
if (intersect != false) { //if there is an intersect point, make sure to add it
result.push({
x: intersect[0],
y: intersect[1]
})
}
result.push({
x: nextPoint.x,
y: nextPoint.y
});
currentPoint = nextPoint
}
chart.addSeries({
name: 'Max Points',
lineColor: 'red',
//dashStyle: 'LongDash',
data: result
})
})
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<button id="button">Add Maximum series</button>
<div id="container" style="height: 400px"></div>
jsfiddle example: https://jsfiddle.net/ewolden/3koe86sx/14/
How do you determine the bucket to which a number belongs to? For ex, lets say we have buckets 0 - 20, 21 - 50, 51 - 80, 81 - 100 or the equivalent grades as 'Poor', 'Average', 'Good', 'Great'. Is there an efficient way using jquery/lodash/d3/underscore to find out that '45' belongs to the '21 - 50' bucket or is 'Average'?
Edit: Is this the best way to do it? In terms of speed, minimal code.
Here's what I have with a lot of help;
// Set up your data
var range = [[0, 20], [21, 50], [51, 80], [81, 100]]
var number = 45
range.find(function(val) { return val[1] >= number })
// Returns [21, 50]
range.findIndex(function(val) { return val[1] >= number })
// Returns 1
This should work...
var range = [[0, 20], [21, 50], [51, 80], [81, 100]]
var number = 45
var bucket = range.filter(function(a) {
if (number >= a[0] && number <= a[1]) return a
})
console.log(bucket[0])
You can use an array of objects with properties set to from "Poor" to "Great" corresponding to the range of numbers set as value of the property, Array.prototype.filter()
var range = [{
Poor: [0, 20]
}, {
Average: [21, 50]
}, {
Good: [51, 80]
}, {
Great: [81, 100]
}];
var number = 45;
var res = range.filter(function(el) {
var key = el[Object.keys(el)];
return number > key[0] && number < key[1]
});
console.log(Object.keys(res[0])[0])
The D3 way using scales:
var scale = d3.scaleQuantize()
.domain([0,100])
.range(["very bad","bad","average","good","very good"]);
console.log(scale(34));
console.log(scale(55));
console.log(scale(91));
<script src="https://d3js.org/d3.v4.min.js"></script>
The nice thing of D3 is that the scale automatically divides the domain based on the number of values of the range. For instance, the snippet above has 5 values ("very bad","bad","average","good","very good"), and so 34 is "bad". In the snippet below, using only 3 values, 34 is "average":
var scale = d3.scaleQuantize()
.domain([0,100])
.range(["bad","average","good"]);
console.log(scale(34));
console.log(scale(55));
console.log(scale(91));
<script src="https://d3js.org/d3.v4.min.js"></script>
var ranges = [
{
name: 'Poor',
range: [0, 20]
},
{
name: 'Average',
range: [21, 50]
},
{
name: 'Good',
range: [51, 80]
},
{
name: 'Great',
range: [81, 100]
},
]
var number = 45;
var range = _.find(ranges, function (r) {
return _.inRange(number, r.range[0], r.range[1] + 1);
}).name;
I have some data, which is the time in seconds (from people completing a workout). I want to graph these times in HighCharts, but I feel like I'm going about this all wrong. Right now I'm having to fill in the spaces between values with 0's. Also, note that some people finished in the exact same times
//Time in seconds, already ordered
var rawData = [183, 189, 195, 237, 256, 298, 306, 314, 328, 330, 330, 330, 338, 364, 370, 411, 411, 458, 474, 513, 513, 566, 572, 574, 600];
//Get the smallest & largest values.
//This is simple since it's already ordered
var min = rawData[0];
var max = rawData[rawData.length - 1];
//Now fill in the missing places in the data.
//Count how many items have the exact same value
var graphData = [];
for (var i = min; i <= max; i++) {
graphData.push(_.filter(rawData,function(m){
return m === i;
}).length);
}
I know this can't be the best way to do what I want, and I know it has some bugs with it. Can anyone suggest a better way of doing this?
Demo: http://codepen.io/chrismbarr/pen/epGGYX?editors=001
You don't need to do that. You only need to create a 2D array with the x and y values, eg.
var graphData = [];
for (var i in rawData) {
graphData.push([rawData[i], _.filter(rawData,function(m){
return m === rawData[i];
}).length]);
}
which creates an array like
[
[183, 1],
[189, 1],
[195, 1],
[237, 1],
[256, 1],
[298, 1],
// so on
[330, 3],
[330, 3],
[330, 3],
// ...
]
Update
another option is
var graphData = [];
for (var i = min; i <= max; i++) {
var ocurrences = _.filter(rawData, function(m){
return m === i;
}).length;
if (ocurrences > 0) {
graphData.push([i,ocurrences]);
}
}
which creates
[
[183, 1],
[189, 1],
[195, 1],
[237, 1],
[256, 1],
[298, 1],
// so on
[330, 3],
// ...
]
Both options work, you don't have to worry about the repeated items in the first resulting array since Highcharts draws the bars exactly in the same position, so you won't notice the difference.
Demo http://codepen.io/anon/pen/RWLjed?editors=001