Draw a line on Google Charts ScatterChart - javascript

I have a ScatterChart where I need to draw a diagonal line (of the function y=x) that serves as a visual aid. Is it possible ? if so, how can I do it?

you can use a ComboChart to display both scatter and line series'
and a DataView to provide the function for y=x
see following working snippet...
google.charts.load('current', {
callback: drawChart,
packages:['corechart']
});
function drawChart() {
var data = new google.visualization.DataTable();
data.addColumn('number', 'x');
data.addColumn('number', 'y0');
data.addRows([
[1, 37.8],
[2, 30.9],
[3, 25.4],
[4, 11.7],
[5, 11.9],
[6, 8.8],
[7, 7.6],
[8, 12.3],
[9, 16.9],
[10, 12.8],
[11, 5.3],
[12, 6.6],
[13, 4.8],
[14, 4.2]
]);
var view = new google.visualization.DataView(data);
view.setColumns([0, 1, {
label: 'y1=x',
type: 'number',
calc: function (dt, row) {
return dt.getValue(row, 0)
}
}]);
var options = {
seriesType: 'scatter',
series: {
1: {
type: 'line'
}
}
};
var chart = new google.visualization.ComboChart(document.getElementById('chart_div'));
chart.draw(view, options);
}
<script src="https://www.gstatic.com/charts/loader.js"></script>
<div id="chart_div"></div>

Another solution is that you can use Scatter chart with multi-series, and set lineWidth = 1 and pointSize = 0 for Line charts!
See the following example:
function main() {
const xMin = -2 * Math.PI;
const xMax = 2 * Math.PI;
const f1 = x => Math.cos(x) + Math.random();
const f2 = x => Math.cos(x);
const x = linspace(xMin, xMax, 20);
const y1 = x.map(f1);
const y2 = x.map(f2);
scatterLineChart(document.getElementById('scatter-line-chart'), x, [y1, y2], 'Scatter: Cos(x) + Noise, Line: Cos(x)', 'x', ['Cos(x) + Noise', 'Cos(x)']);
}
function linspace(a, b, n = 100) {
const data = new Array(n);
const d = (b - a) / (n - 1);
data[0] = a;
data[n - 1] = b;
n = n - 1;
for (let i = 1; i < n; i++) {
data[i] = data[i - 1] + d;
}
return data;
}
function createSeriesDataTable(x, ys, xLabel, yLabels) {
// data
const data = new google.visualization.DataTable();
// - columns
data.addColumn('number', xLabel);
yLabels.forEach(yLabel => {
data.addColumn('number', yLabel);
});
// - rows
const n = x.length;
const m = ys.length;
const rows = Array(n);
for (let i = 0; i < n; i++) {
rows[i] = Array(1 + m);
rows[i][0] = x[i];
for (let j = 0; j < m; j++) {
rows[i][j + 1] = ys[j][i];
}
}
data.addRows(rows);
// return
return data;
}
function scatterLineChart(parentElement, x, ys, title, xLabel, yLabels) {
google.charts.load('current', {
'packages': ['corechart']
});
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
const data = createSeriesDataTable(x, ys, xLabel, yLabels);
const options = {
title: title,
series: {
1: {
lineWidth: 3,
pointSize: 0,
curveType: 'function'
},
},
legend: 'none'
};
const chart = new google.visualization.ScatterChart(parentElement);
chart.draw(data, options);
}
}
main();
<script src="https://www.gstatic.com/charts/loader.js"></script>
<div id="scatter-line-chart"></div>

Related

implementing conway game of life

function seed() {
return Array.prototype.slice.call(arguments);
}
function same([x, y], [j, k]) {
return x === j && y === k;
}
// The game state to search for `cell` is passed as the `this` value of the function.
function contains(cell) {
return this.some(c => same(c, cell));
}
const sum = ([x, y], [j, k]) => [x + j, y + k];
const getNeighborsOf = ([x, y]) => {
const neighborDeltas = [
[-1, 1],
[0, 1],
[1, 1],
[-1, 0], /* */ , [1, 0],
[-1, -1],
[0, -1],
[1, -1]
];
return neighborDeltas.map((d) => sum(d, [x, y]));
}
const livingNeighbors = getLivingNeighbors(cell, state);
return (
livingNeighbors.length === 3 ||
contains.call(state, cell) && livingNeighbors.length === 2);
}
const corners = (state = []) => {
const xs = state.map(([x, _]) => x);
const ys = state.map(([_, y]) => y);
return {
topRight: [Math.max(...xs), Math.max(...ys)],
bottomLeft: [Math.min(...xs), Math.min(...ys)]
};
};
const calculateNext = (state) => {
const { bottomLeft, topRight } = corners(state);
let result = [];
for (let y = topRight[1] + 1; y >= bottomLeft[1] - 1; y--) {
for (let x = bottomLeft[0] - 1; x <= topRight[0] + 1; x++) {
result = result.concat(isAlive([x, y], state) ? [
[x, y]
] : []);
}
}
return result;
};
const printCell = (cell, state) => {
return contains.call(state, cell) ? 'x' : '_';
};
const printCells = (state) => {
const { bottomLeft, topRight } = corners(state);
for (let y = topRight[1]; y >= bottomLeft[1]; y--) {
let row = [];
for (let x = bottomLeft[0]; x <= topRight[0]; x++) {
row.push(printCell([x, y], state));
}
console.log(row.join(''));
}
};
exports.seed = seed;
exports.same = same;
exports.contains = contains;
exports.sum = sum;
exports.getNeighborsOf = getNeighborsOf;
exports.getLivingNeighbors = getLivingNeighbors;
exports.isAlive = isAlive;
exports.corners = corners;
exports.calculateNext = calculateNext;
exports.printCell = printCell;
exports.printCells = printCells;
I am getting an error.
In this project you will build a simple two dimensional simulation of living cells, known as Conway’s Game of Life. In the process you will learn all about JavaScript functions, including what you can do with them and their many features.

Strongly Connected Component Algorithm

I have hard time to solve Strongly connected component algorithm.
I have done these implementations blow so far but I got an unexpected result.
DFS and add nodes to stack (named var leader in my code) in order to keep depth of each vertices
Reverse graph edges' direction (getReverseGraph())
Second DFS and create Strongly connected component
I have an issue on step 3 and can not detect component properly.
Expected:
[ [ '1', '7', '5' ], ['2', '4'], [ 6, '3' ] ]
Result:
[ [ '1', '7', '5', '2', '4'], [ 6, '3' ] ].
Would anyone give me some advice? Thanks!
function Graph() {
this.list = {};
}
Graph.prototype.insert = function(newVertex, neighborVertex) {
if (!this.list[newVertex]) {
if (neighborVertex) {
this.list[newVertex] = [neighborVertex];
} else {
this.list[newVertex] = [];
}
} else {
// If neighborVertex is not initialized
if (!this.list[neighborVertex]) {
this.list[neighborVertex] = [];
}
// Add neighborVertex to
this.list[newVertex].push(neighborVertex);
}
};
Graph.prototype.addEdge = function(vertexFrom, vertexTo) {
if (this.list[vertexFrom] || this.list[vertexTo]) {
throw new Error('Vertex does not exsists')
}
this.list[vertexFrom].push(vertexTo);
};
/*
* DFS
*
* #param graph {object}: Takes different graph as optional
* #param vertex {string|integer}
* #param cb {function}
*/
Graph.prototype.dfs = function(graph, vertex, cb) {
// track which node visited
var visited = {};
// Take graph as option
var list = graph ? graph : this.list;
// get initial nodes
var currentNodes = list[vertex];
// Invoke given function for inital node
cb(vertex);
// Mark vertex as visited
visited[vertex] = true;
// If there is no node to traverse return
if (currentNodes.length === 0) {
return;
}
var stack = [...currentNodes];
for (var node of currentNodes) {
visited[node] = true;
}
while (stack.length > 0) {
// Get a node from stack
var nextNode = stack.pop();
// Invoke given function
cb(nextNode);
// Mark the vertex as visited
visited[nextNode] = true;
// Iterate adjacent nodes
if (list[nextNode]) {
// console.log('stack', stack)
for (var neighbor of list[nextNode]) {
// If the vertex is not visited, push each nodes to stack
if (!visited[neighbor]) {
stack.push(neighbor);
visited[neighbor] = true;
}
}
}
}
}
function getReverseGraph(graph) {
var vertices = Object.keys(graph);
var reverseEdgeGraph = {};
for (let vertex of vertices) {
for (let neighbor of graph[vertex]) {
if (reverseEdgeGraph[neighbor]) {
reverseEdgeGraph[neighbor].push(vertex);
} else {
reverseEdgeGraph[neighbor] = [vertex];
}
}
}
return reverseEdgeGraph;
}
Graph.prototype.getStrongComponent = function(vertex) {
if (vertex && !this.list[vertex]) {
throw new("No vertex exsits")
}
vertex = vertex ? vertex.toString() : Object.keys(this.list)[0].toString();
/*
Create Copy of current Adjacency list
*/
var reverseEdgeGraph = getReverseGraph(this.list);
/*
Create Leader
*/
var leader = []; // stack to keep the depth
var keys = Object.keys(this.list);
while (keys.length > 0) {
var indexOfVertex = keys.indexOf(vertex);
keys.splice(indexOfVertex, 1);
this.dfs(null, vertex, function(vertex) {
// If leader does not have the vertex
if (leader.indexOf(vertex) < 0) {
// Get the key (vertex)
var indexOfVertex = keys.indexOf(vertex);
// Delete vertex
keys.splice(indexOfVertex, 1);
// Add vertex to leader
leader.unshift(vertex);
}
});
// Move to next key (remaining node)
vertex = keys[0];
}
/**
*
* Create SCC
*
**/
var allSCC = [];
var visited = {};
while (leader.length > 0) {
var SCC = [];
var target = leader.pop();
if (visited[target]) {
break;
}
this.dfs(reverseEdgeGraph, target, function(vertex) {
// Create component
if (!visited[vertex]) {
visited[vertex] = true;
SCC.push(vertex);
}
});
if (SCC.length > 0) {
allSCC.push(SCC);
}
}
return allSCC
}
function test() {
var graph = new Graph();
var result = [
[2, 4],
[4, 2],
[7, 5],
[5, 1],
[1, 7],
[1, 5],
[5, 7],
[7, 1],
[6, 3],
[3, 6],
[2, 7],
[1, 6]
]
result.forEach(function(line) {
// console.log(line)
graph.insert(line[0], line[1]);
});
var result = graph.getStrongComponent().map(function(components) {
return components.length
}).sort(function(a, b) {
return b - a
});
console.log('result => ', graph.getStrongComponent(1));
}
function dfs() {
var graph = new Graph();
graph.insert('a', 'b');
graph.insert('a', 'g');
graph.insert('b', 'c');
graph.insert('c', 'd');
graph.insert('d', 'e');
graph.insert('e', 'f');
graph.insert('f', 'd');
graph.insert('f', 'g');
graph.insert('g', 'e');
graph.insert('g', 'a');
graph.insert('g', 'b');
graph.insert('g', 'c');
graph.insert('g', 'd');
graph.insert('g', 'h');
graph.dfs(null, 'a', function(v) {
console.log(v);
})
}
// dfs();
test(); // should be [ [ '1', '7', '5' ], ['2', '4'], [ 6, '3' ] ]
You are running the dfs for each node, highly inefficient as you start each dfs with all nodes as unvisited. I added a fourth parameter to your dfs function to keep a single visited object for all the calls. That also avoids repeating nodes in your callback, giving the order you want. Also slightly modified it to be a bit shorter.
With this I refactored both the first dfs traversal and the second one on the reversed graph. Callback is much more cleaner an easier to understand this way, took me a long time to understand what were you doing in your first dfs callback.
Code works now:
function Graph() {
this.list = {};
}
Graph.prototype.insert = function(newVertex, neighborVertex) {
if (!this.list[newVertex]) {
if (neighborVertex) {
this.list[newVertex] = [neighborVertex];
} else {
this.list[newVertex] = [];
}
} else {
// If neighborVertex is not initialized
if (!this.list[neighborVertex]) {
this.list[neighborVertex] = [];
}
// Add neighborVertex to
this.list[newVertex].push(neighborVertex);
}
};
Graph.prototype.addEdge = function(vertexFrom, vertexTo) {
if (this.list[vertexFrom] || this.list[vertexTo]) {
throw new Error('Vertex does not exsists')
}
this.list[vertexFrom].push(vertexTo);
};
/*
* DFS
*
* #param graph {object}: Takes different graph as optional
* #param vertex {string|integer}
* #param cb {function}
*/
Graph.prototype.dfs = function(graph, vertex, cb, visited) {
// track which node visited
var visited = visited || {};
// Take graph as option
var list = graph ? graph : this.list;
if (visited[vertex]) return;
// Mark vertex as visited
visited[vertex] = true;
var stack = [vertex];
while (stack.length > 0) {
// Get a node from stack
var nextNode = stack.pop();
// Invoke given function
cb(nextNode);
// Iterate adjacent nodes
if (list[nextNode]) {
// console.log('stack', stack)
for (var neighbor of list[nextNode]) {
// If the vertex is not visited, push each nodes to stack
if (!visited[neighbor]) {
stack.push(neighbor);
visited[neighbor] = true;
}
}
}
}
}
function getReverseGraph(graph) {
var vertices = Object.keys(graph);
var reverseEdgeGraph = {};
for (let vertex of vertices) {
for (let neighbor of graph[vertex]) {
if (reverseEdgeGraph[neighbor]) {
reverseEdgeGraph[neighbor].push(vertex);
} else {
reverseEdgeGraph[neighbor] = [vertex];
}
}
}
return reverseEdgeGraph;
}
Graph.prototype.getStrongComponent = function(vertex) {
if (vertex && !this.list[vertex]) {
throw new("No vertex exsits")
}
vertex = vertex ? vertex.toString() : Object.keys(this.list)[0].toString();
/*
Create Copy of current Adjacency list
*/
var reverseEdgeGraph = getReverseGraph(this.list);
var stack = [];
var visited = {}
for (var vertex in this.list) {
this.dfs(null, vertex, function(v) {
stack.push(v);
}, visited)
}
/**
*
* Create SCC
*
**/
var allSCC = [];
visited = {};
stack.reverse().forEach((vertex) => {
var SCC = []
this.dfs(reverseEdgeGraph, vertex, function(v) {
SCC.push(v);
}, visited)
if (SCC.length) allSCC.push(SCC)
})
return allSCC
}
function test() {
var graph = new Graph();
var result = [
[2, 4],
[4, 2],
[7, 5],
[5, 1],
[1, 7],
[1, 5],
[5, 7],
[7, 1],
[6, 3],
[3, 6],
[2, 7],
[1, 6]
]
result.forEach(function(line) {
// console.log(line)
graph.insert(line[0], line[1]);
});
console.log('result => ', graph.getStrongComponent(1));
}
// dfs();
test(); // should be [ [ '1', '7', '5' ], ['2', '4'], [ 6, '3' ] ]

Maximum values series in Highcharts

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/

Angular - building JSON dynamically for use with chartist

I have an ionic project and am using the following library:
http://gionkunz.github.io/chartist-js/index.html
Actually drawing the chart is achieved with the following:
var chart = new Chartist.Line('.ct-chart', {
series: [
{
name: 'series-1',
data: [
{x: new Date(143134652600), y: 53},
{x: new Date(143234652600), y: 40},
{x: new Date(143340052600), y: 45},
{x: new Date(143366652600), y: 40},
{x: new Date(143410652600), y: 20},
{x: new Date(143508652600), y: 32},
{x: new Date(143569652600), y: 18},
{x: new Date(143579652600), y: 11}
]
},
{
name: 'series-2',
data: [
{x: new Date(143134652600), y: 53},
{x: new Date(143234652600), y: 35},
{x: new Date(143334652600), y: 30},
{x: new Date(143384652600), y: 30},
{x: new Date(143568652600), y: 10}
]
}
]
}, {
axisX: {
type: Chartist.FixedScaleAxis,
divisor: 5,
labelInterpolationFnc: function(value) {
return moment(value).format('MMM D');
}
}
});
With a DIV:
<div class="ct-chart ct-perfect-fourth"></div>
Instead of having a hardcoded array for the series as shown above, I would like to build this dynamically through a function call.
Any example of how I might do that?
Thanks!
You could generate the data with a little randomness and some fixed variables using a generate function. It's probably also nicer to parametize the creation of your chart for easier re-creation later. Chartist also has a update() function that lets you hand it new data and options, so is especially useful for this.
JSFIDDLE
Javascript
var button = document.getElementById('button');
var options = {
axisX: {
type: Chartist.FixedScaleAxis,
divisor: 5,
labelInterpolationFnc: function(value) {
return moment(value).format('MMM D');
}
}
};
var chart; // initialise the chart variable
function createChart(){
chart = new Chartist.Line('.ct-chart', changeData(), options);
}
function updateChart(){
chart.update(changeData());
}
function changeData(){
var series = [];
// set up series ranges
var numberOfSeries = 2;
var numberOfItems = 8;
var startDate = 143134652600;
var endDate = 143579652600;
var minY = 11;
var maxY = 53;
// creates the 'step' each x-axis item should take
var dateDiff = Math.floor((endDate - startDate) / numberOfItems);
// alternatively set the step to a whole day etc. (makes endDate unnecessary)
// var dateDiff = 24 * 60 * 60 * 1000;
for(var x = 0; x < numberOfSeries; x++){
var seriesData = [];
for(var y = 0; y < numberOfItems; y++){
seriesData.push({
x: new Date(startDate + (dateDiff*y)),
y: getRandomInt(minY, maxY)
})
}
series.push({
name: 'series-'+ (x+1),
data: seriesData
});
}
// return the data to display in the chart
return {series:series};
}
/**
* Returns a random integer between min (inclusive) and max (inclusive)
* Using Math.round() will give you a non-uniform distribution!
*/
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
button.addEventListener('click', updateChart);
createChart(); // generate chart initially
HTML
<button id="button">
Change Data
</button>
<div class="ct-chart ct-perfect-fourth"></div>
This is a rough example; you could replace the input series1, series2 with an array of arrays and make the lower for-loops two wrapped loops to handle multiple series. This would also entail adding the objects to the series array in the outer loop.
For now, try something like this:
function generateJSON(series1, series2) {
var chartInternal = {
series: [
{
name: 'series-1',
data: []
},
{
name: 'series-2',
data: []
}
]
}, {
axisX: {
type: Chartist.FixedScaleAxis,
divisor: 5,
labelInterpolationFnc: function(value) {
return moment(value).format('MMM D');
}
}
};
for (var i = 0, len = series1.length; i < len; i++) {
chartInternal.series[0].data.push({x: new Date(series1[i].date), y: series1[i].y});
}
for (var i = 0, len = series2.length; i < len; i++) {
chartInternal.series[1].data.push({x: new Date(series2[i].date), y: series2[i].y});
}
return chartInternal;
}
Usage:
var series1 = [
{ date: 1234567, y:52 },
... more
];
var series2 = [
{ date: 7654321, y:52 },
... more
];
var chart = new Chartist.Line('.ct-chart', generateJSON(series1, series2))

How to start lines on columns in HighCharts

I have a combnined chart with groups of columns and lines. Each column corresponds to a line. I want each line to start on their respective column. Here is an example:
http://jsfiddle.net/aozdjab2/1/
Now, The number of columns is variable, so I want to calculate the pointPlacement for the lines dynamically.
What is the best way to do this? Is there a way to tell the lines to start at the columns?
If not, how do I calculate the point placements? The number of lines and columns will vary, as well as the number of groups on the x-axis.
Thanks.
Because SO requires code:
{
type: 'line',
name: 'Jane-line',
data: [3, 2, 1, 3, 4],
pointPlacement: -0.2
},
{
type: 'line',
name: 'john-line',
data: [2, 3, 5, 7, 6],
pointPlacement: 0
},
{
type: 'line',
name: 'joe-line',
data: [4, 3, 3, 9, 0],
pointPlacement: 0.2
}
You can add new xAxis that will be related to you line series. This series will have more categories, so ticks will be thicker than in your normal axis.
You can check, if line series has related visible column series in your chart and if yes, you can add this series to your chart.
Here you can find custom function I wrote for this case:
var positioningLineSeries = function(chart, event) {
if (redraw) {
var columnSeries = [],
each = Highcharts.each,
numberOfColumnSeries = 0,
categoriesFor2Axis = [],
i;
each(chart.series, function(s) {
if (s.type === 'column' && s.visible) {
columnSeries.push(s);
}
});
for (i = 0; i < chart.series.length; i++) {
if (chart.series[i].type === 'line') {
chart.series[i].remove(false);
i--;
}
}
numberOfColumnSeries = columnSeries.length;
redraw = false;
each(chart.xAxis[0].categories, function(c) {
for (i = 0; i < numberOfColumnSeries + 2; i++) {
categoriesFor2Axis.push(c);
}
});
chart.xAxis[1].update({
categories: categoriesFor2Axis,
min: 0,
max: categoriesFor2Axis.length - 1
}, false);
var newData, newLineSeries = $.extend(true, [], lineSeries);
each(newLineSeries, function(s) {
newData = [];
each(columnSeries, function(cS, i) {
if (cS.options.ID === s.columnID) {
each(s.data, function(p, j) {
newData.push([p[0] + i + 1 + j * (numberOfColumnSeries + 1), p[1]]);
});
s.data = newData;
chart.addSeries(s, false);
}
});
});
chart.redraw();
} else {
redraw = true;
}
};
And here you can find an example how it can work: http://jsfiddle.net/aozdjab2/6/

Categories

Resources