Related
I would like to know how to make a new DataTable from an existing DataTable?
Use case:
I have a proxyTable which is hidden and connected to a CategoryFilter. This is used used to construct other tables on the page which are all linked to one CategoryFilter.
Goal:
Include a Grand Total row in each new table which reflects summation of the filtered selection.
Initial solution:
I have tried extracting an array from sourceData, creating a new data table called dataResults, adding grand total row to dataResults, and drawing the final table. It works but seems like a lot of effort.
var sourceData = proxyTable.getDataTable();
var rowCount = sourceData.getNumberOfRows();
var colCount = sourceData.getNumberOfColumns();
var tempRow = [];
var tempArray = [];
var pushValue;
//push header row
for (var k = 0; k < colCount; k++) {
pushValue = sourceData.getColumnLabel(k);
tempRow.push(pushValue);
}
tempArray.push(tempRow);
tempRow = []; //reset
//push data rows
for (var i = 0; i < rowCount; i++) {
for (var j = 0; j < colCount; j++) {
pushValue = sourceData.getValue(i, j);
tempRow.push(pushValue);
}
tempArray.push(tempRow);
tempRow = []; //reset
}
//Create new Google DataTable from Array
var dataResults = new google.visualization.arrayToDataTable(tempArray);
My question:
How can I make a data table which contains all records from sourceData without going through the above steps I've tried?
You guidance is appreciated greatly!
Working Example:
UPDATE:
Added var dataResults = sourceData.clone(); per answer from #WhiteHat and I get an error sourceData.clone is not a function
Did I get the syntax wrong? Perhaps it's the ChartWrapper I'm using?
UPDATE 2:
Added var dataResults = sourceData.toDataTable().clone(); per answer #2 from #WhiteHat and it works.
google.charts.load('current', {
'packages': ['corechart', 'table', 'gauge', 'controls', 'charteditor']
});
$(document).ready(function() {
renderChart_onPageLoad();
});
function renderChart_onPageLoad() {
google.charts.setOnLoadCallback(function() {
drawDashboard();
});
}
function drawDashboard() {
var data = google.visualization.arrayToDataTable([
['Name', 'RoolNumber', 'Gender', 'Age', 'Donuts eaten'],
['Michael', 1, 'Male', 12, 5],
['Elisa', 2, 'Female', 20, 7],
['Robert', 3, 'Male', 7, 3],
['John', 4, 'Male', 54, 2],
['Jessica', 5, 'Female', 22, 6],
['Aaron', 6, 'Male', 3, 1],
['Margareth', 7, 'Female', 42, 8],
['Miranda', 8, 'Female', 33, 6]
]);
var dashboard = new google.visualization.Dashboard(document.getElementById('dashboard'));
var categoryPicker = new google.visualization.ControlWrapper({
controlType: 'CategoryFilter',
containerId: 'categoryPicker',
options: {
filterColumnLabel: 'Gender',
ui: {
labelStacking: 'vertical',
allowTyping: false,
allowMultiple: false
}
}
});
var proxyTable = new google.visualization.ChartWrapper({
chartType: 'Table',
containerId: 'proxyTable',
options: {
width: '500px'
}
});
var table = new google.visualization.ChartWrapper({
chartType: 'Table',
containerId: 'table',
options: {
width: '500px'
}
});
dashboard.bind([categoryPicker], [proxyTable]);
dashboard.draw(data);
google.visualization.events.addListener(dashboard, 'ready', function() {
redrawChart();
});
function redrawChart() {
var sourceData = proxyTable.getDataTable();
//WhiteHat suggestion2 - WORKS
var dataResults = sourceData.toDataTable().clone();
//WhiteHat suggestion1 - Didn't work
//var dataResults = sourceData.clone();
//INITIAL SOLUTION - works
//var rowCount = sourceData.getNumberOfRows();
//var colCount = sourceData.getNumberOfColumns();
//var tempRow = [];
//var tempArray = [];
//var pushValue;
//for (var k = 0; k < colCount; k++) {
//pushValue = sourceData.getColumnLabel(k);
//tempRow.push(pushValue);
//}
//tempArray.push(tempRow);
//tempRow = []; //reset
//push data rows
//for (var i = 0; i < rowCount; i++) {
//for (var j = 0; j < colCount; j++) {
//pushValue = sourceData.getValue(i, j);
//tempRow.push(pushValue);
//}
//tempArray.push(tempRow);
//tempRow = []; //reset
//}
//var dataResults = new google.visualization.arrayToDataTable(tempArray);
var group = google.visualization.data.group(sourceData, [{
// we need a key column to group on, but since we want all rows grouped into 1,
// then it needs a constant value
column: 0,
type: 'number',
modifier: function() {
return 1;
}
}], [{
column: 1,
id: 'SumRool',
label: 'SumRool',
type: 'number',
aggregation: google.visualization.data.sum
}, {
column: 3,
id: 'SumAge',
label: 'SumAge',
type: 'number',
aggregation: google.visualization.data.sum
}, {
// get the average age
column: 4,
id: 'SumEaten',
label: 'SumEaten',
type: 'number',
aggregation: google.visualization.data.sum
}]);
dataResults.insertRows(0, [
['Grand Total', group.getValue(0, 1), null, group.getValue(0, 2), group.getValue(0, 3)],
]);
//Set dataTable
table.setDataTable(dataResults);
table.draw();
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="https://www.gstatic.com/charts/loader.js"></script>
<div id="dashboard">
<div id="categoryPicker"></div><br /> Proxy Table<br />
<div id="proxyTable"></div><br /> Table
<br />
<div id="table"></div><br /><br />
</div>
data table method --> clone()
var newDataTable = oldDataTable.clone();
from the docs...
clone() - Returns a clone of the data table. The result is a deep copy of the data table except for the cell properties, row properties, table properties and column properties, which are shallow copies; this means that non-primitive properties are copied by reference, but primitive properties are copied by value.
I wanted to write a simple function that allows to roll random item from list, i did it with this code:
this.resources = [false, 'nitrogen', 'silicon', 'cobalt', 'magnesium'];
this.assign_resource = function() {
var index = tools.rnd(0, this.resources.length - 1);
return this.resources[index];
};
But it doesn't play well, so i wanted to change it to different system that allows a list of items (including empty one) and it picks one at random but each one has different chance (for example this one has 10%, this one has 20%). Maybe someone could help me with this kind of function.
Edited -----
for example this could be new list:
this.resources = [
{ type: 'empty', chance: 30 },
{ type: 'nitrogen', chance: 10 },
{ type: 'silicon', chance: 20 },
{ type: 'cobalt', chance: 30 },
{ type: 'magnesium', chance: 10 }
];
How to use it now to make it happen properly?
Edited 2 -----
I am trying to figure out well done programming solution using math rather then simply duplicating items in array, answers presented in this topic are just work arounds to a problem.
I'd solve it by having an array of objects with a chance to be the result, totalling 1.0, then picking a random number between 0 and 1, and then iterating over the resources and check if adding it to a cumulative total includes your random number.
var resources = [
{ resource: false, chance: 0.2 },
{ resource: 'nitrogen', chance: 0.1 },
{ resource: 'silicon', chance: 0.2 },
{ resource: 'cobalt', chance: 0.45 },
{ resource: 'mangesium', chance: 0.05 }
];
function get_result(resouceList) {
//get our random from 0 to 1
var rnd = Math.random();
//initialise our cumulative percentage
var cumulativeChance = 0;
//iterate over our resources
for (var i = 0; i < resouceList.length; i++) {
//include current resource
cumulativeChance += resouceList[i].chance;
if (rnd < cumulativeChance)
return resouceList[i].resource;
}
return false;
}
//test
console.log(get_result(resources));
console.log(get_result(resources));
console.log(get_result(resources));
console.log(get_result(resources));
console.log(get_result(resources));
I would set it up so that only the actual resources are in your array and "empty" happens if the random roll falls outside of those.
this.resources = [
{ type: 'nitrogen', chance: 10 },
{ type: 'silicon', chance: 20 },
{ type: 'cobalt', chance: 30 },
{ type: 'magnesium', chance: 10 }
];
this.assign_resource = function() {
var rnd = Math.random();
var acc = 0;
for (var i=0, r; r = this.resources[i]; i++) {
acc += r.chance / 100;
if (rnd < acc) return r.type;
}
// rnd wasn't less than acc, so no resource was found
return 'empty';
}
You can do something like this.
Creating an array with the same value multiple times gives it a higher chance of being selected.
var resources = [{
type: 'empty',
chance: 30
},
{
type: 'nitrogen',
chance: 10
},
{
type: 'silicon',
chance: 20
},
{
type: 'cobalt',
chance: 30
},
{
type: 'magnesium',
chance: 10
}
];
function GetRandom(list) {
var array = [];
for (var i = 0; i < list.length; i++) {
var item = list[i];
var chance = item.chance / 10;
for (var j = 0; j < chance; j++) {
array.push(item.type);
}
}
var idx = Math.floor(Math.random() * array.length);
return array[idx];
}
console.log(GetRandom(resources))
.as-console-wrapper { max-height: 100% !important; top: 0; }
This is how I'd implement the solution.
Step 1: accumulate all the possible chances
Step 2: pick a random value in proportion to total chance
Step 3: loop through the resources to see in which part it random value falls under.
var resources = [
{ type: 'empty', chance: 30 },
{ type: 'nitrogen', chance: 10 },
{ type: 'silicon', chance: 20 },
{ type: 'cobalt', chance: 30 },
{ type: 'magnesium', chance: 10 }
];
function solution(resources) {
let chanceTotal = resources.reduce((acc, val) => { acc += val.chance ; return acc;}, 0);
let randomVal = parseInt(Math.random() * chanceTotal);
let chanceAcc = 0;
let ans;
resources.forEach(r => {
chanceAcc += r.chance;
if (chanceAcc > randomVal && !ans) {
ans = r;
}
});
return ans;
}
console.log(solution(resources));
Here is another implementation.
var res = [
["empty", 3],
["nitrogen", 1],
["silicon", 2],
["cobalt", 3],
["magnesium", 1]
];
var flat = [];
var item;
for(var i = 0, l = res.length; i < l; i++) {
item = Array(res[i][1]+1).join(i+",");
item = item.substr(0, item.length-1);
flat.push(item);
}
flat = flat.join(",").split(",");
function get_random_item() {
var ridx = Math.floor(Math.random() * (flat.length));
return res[flat[ridx]][0];
}
var pick;
for(var p = 0; p < 50; p++) {
pick = get_random_item();
console.log(p, pick);
}
I'm extracting some data from an SQL source, which I can get into a javascript script as a simple array (shown grouped by dates) which consists of week no, task number and hours spent:
mydata = [
// weekno, taskno, hours
["2014-14",160,37.5],
["2014-15",160,30],
["2014-15",243,7.5],
["2014-16",160,37.5],
["2014-17",0,7.5],
["2014-17",3,7.5],
["2014-17",321,22.5],
["2014-18",0,7.5],
["2014-18",321,30],
["2014-19",3,7.5],
["2014-19",295,30]
];
I'm going to be charting it using HighCharts, and I need to get it into two property arrays like this:
properties = {
categories: [ "2014-14","2014-15","2014-16","2014-17","2014-18","2014-19"],
series: [
// Task Week
// No 14 15 16 17 18 19
//
{ name: '0', data: [ 0, 0, 0, 7.5, 7.5, 0 ] },
{ name: '3', data: [ 0, 0, 0, 7.5, 0, 7.5 ] },
{ name: '160', data: [ 37.5, 30, 37.5, 0, 0, 0 ] },
{ name: '243', data: [ 0, 7.5, 0, 0, 0, 0 ] },
{ name: '295', data: [ 0, 0, 0, 0, 0, 30 ] },
{ name: '321', data: [ 0, 0, 0, 22.5, 30, 0 ] }
]
}
Aside from looping, am I missing some succinct, idiomatic method for doing this?
In case it's of use to anyone, here's a cobbled together solution:
function onlyUnique(value, index, self) {
return self.indexOf(value) === index;
}
var categories = [];
var subcategories = [];
var temp = {};
for (var i = 0; i < myChartData.length; i++) {
key = myChartData[i][0];
taskno = myChartData[i][1];
hours = myChartData[i][2];
if (taskno in temp == false) temp[taskno] = {};
if (key in temp[taskno] == false) temp[taskno][key] = 0;
temp[taskno][key] += hours;
categories.push(myChartData[i][0]);
subcategories.push(myChartData[i][1])
}
var uniqueCategories = categories.filter(onlyUnique).sort();
var uniqueSubcategories = subcategories.filter(onlyUnique).sort(function(a, b) {
return a - b
});
var series = [];
for (var i = 0; i < uniqueSubcategories.length; i++) {
subcatKey = uniqueSubcategories[i];
series[i] = { name: 'Task ' + subcatKey, data: [] };
for (var j = 0; j < uniqueCategories.length; j++) {
catKey = uniqueCategories[j];
series[i]['data'].push(temp[subcatKey][catKey] ? temp[subcatKey][catKey] : 0);
}
}
where series and uniqueCategories are the required data.
I'm building irregular time graphs with HighCharts that at the moment look like so:
And I'm wondering if it's possible to create an 'average' line for the three (or possibly more in future) lines.
It would start following the blue line, then go closer to the green line mid-January, etc.
At the moment the code I'm working with looks like:
$('#chart').highcharts({
chart: { type: 'spline' },
title: { text: '' },
xAxis: { type: 'datetime' },
yAxis: {
title: { text: '' }
}
series: [{
name: 'Line 1',
data: [
[Date.UTC(2014,0,16), 173.33],
[Date.UTC(2014,0,23), 163.33],
[Date.UTC(2014,0,30), 137.67],
[Date.UTC(2014,1,6), 176.33],
[Date.UTC(2014,1,13), 178.67],
[Date.UTC(2014,1,27), 167.33],
],
color: 'purple'
},
{
name: 'Line 2',
data: [
[Date.UTC(2014,0,11), 156.33],
[Date.UTC(2014,1,15), 167.67],
],
color: 'green'
},
{
name: 'Line 3',
data: [
[Date.UTC(2014,0,1), 135],
[Date.UTC(2014,0,5), 146.33],
[Date.UTC(2014,0,27), 146.75],
],
color: 'blue'
}]
});
What you are describing is called a trend or regression line. Highcharts doesn't have a built in ability to add these lines, but the math isn't too difficult (and besides, it's more fun to do it yourself). I've coded up the simplest example I can using least squared linear regression.
/////////////////////
//utility functions//
////////////////////
// linear regression
// given array of x values and array of y values
// returns rV object with slope/intercept
lineFit = function(xs, ys, rV){
rV.slope = 0.0;
rV.intercept = 0.0;
rV.rSquared = 1.0; // assume perfection
if (xs.length < 2)
{
return false;
}
if (xs.Count != ys.Count)
{
return false;
}
var N = xs.length;
var sumX = sumFunc(xs,null);
var sumY = sumFunc(ys,null);
var funcSq = function(i){return (i*i);}
var funcSq2 = function(i,j){return (i*j);}
var sumXx = sumFunc(xs, funcSq);
var sumYy = sumFunc(ys, funcSq);
var sumXy = sumFunc(zip(xs,ys),funcSq2);
rV.slope = ((N * sumXy) - (sumX * sumY)) / (N * sumXx - (sumX*sumX));
rV.intercept = (sumY - rV.slope * sumX) / N;
rV.rSquared = Math.abs((rV.slope * (sumXy - (sumX * sumY) / N)) / (sumYy - ((sumY * sumY) / N)));
return true;
}
// sums arrays with optional function transformation
sumFunc = function(arr, func){
var total = 0;
$.each(arr, function(i,k){
if ($.isArray(k)){
if (func == null){
k = k[0] + k[1];
}else{
k = func(k[0],k[1]);
}
} else {
if (func != null){
k = func(k);
}
}
total += k;
});
return total;
}
// python style zip function
// to pair to array together
zip = function(arr1,arr2) {
var rV = [];
for(var i=0; i<arr1.length; i++){
rV.push([arr1[i],arr2[i]]);
}
return rV;
}
The lineFit function will return the rV object (by reference) with attributes of slope and intercept. After that you can add a line to Highcharts with good old fashioned y = slope * x + intercept and minX is the starting value for the regression line and maxX is the ending value.
{
name: 'Regression Line',
data: [[minX, reg.slope * minX + reg.intercept],
[maxX, reg.slope * maxX + reg.intercept]],
color: 'red',
marker:{enabled:false},
lineWidth: 5
}
Working fiddle here.
Based on ideas provided by the answer from Mark, I wrote some code to generate a custom fourth line, using the data from all three lines, and calculating the required value for each point.
My new code is as follows:
line1 = [
[Date.UTC(2014,0,16), 173.33],
[Date.UTC(2014,0,23), 163.33],
[Date.UTC(2014,0,30), 137.67],
[Date.UTC(2014,1,6), 176.33],
[Date.UTC(2014,1,13), 178.67],
[Date.UTC(2014,1,27), 167.33],
];
line2 = [
[Date.UTC(2014,0,11), 156.33],
[Date.UTC(2014,1,15), 167.67],
];
line3 = [
[Date.UTC(2014,0,1), 135],
[Date.UTC(2014,0,5), 146.33],
[Date.UTC(2014,0,27), 146.75],
[Date.UTC(2014,2,2), 168.75]
];
function average(array, index) {
sum = array[0][1];
for(var i = 1; i <= index; i++) {
sum += array[i][1];
}
value = sum / (index + 1);
return parseFloat(value.toFixed(2));
}
// Make a fourth line with all of the data points for the other
// three lines, sorted by date
all_lines = line1.concat(line2).concat(line3);
all_lines.sort(function(a, b) { return a[0] - b[0]});
// Calculate the value for each data point in the fourth line -
// the average of all the values before it
average_line = [];
for(var i = 0; i < all_lines.length; i++) {
average_line.push([all_lines[i][0], average(all_lines, i)])
}
$('#chart').highcharts({
chart: { type: 'spline' },
title: {
text: '',
},
xAxis: {
type: 'datetime'
},
yAxis: {
title: {
text: ''
}
},
legend: {
layout: 'vertical',
align: 'right',
verticalAlign: 'middle',
borderWidth: 0
},
series: [{
name: 'Line 1',
data: line1,
color: 'purple'
},
{
name: 'Line 2',
data: line2,
color: 'green'
},
{
name: 'Line 3',
data: line3,
color: 'blue'
},
{
name: 'Average',
data: average_line,
color: 'red'
}]
});
The graph as it looks now (with one extra data point on the blue line) is:
I have a stacked bar chart I am trying to make each item in the chart that is clicked open the sharepoint view based on what was clicked.
I am building the data here
seriesData.push({ name: xStatus[i], data: dataArray, url: countArray[i].url });
my chart code looks like this:
loadStackedBarChartWithLink = function (xCategories, seriesData, divId, chartTitle, yAxisTitle) {
//Build Stacked Bar Chart
$(divId).highcharts({
chart: {
type: 'bar'
},
credits: {
enabled: false
},
title: {
text: chartTitle
},
xAxis: {
categories: xCategories,
allowDecimals: false
},
yAxis: {
min: 0,
allowDecimals: false,
title: {
text: yAxisTitle
}
},
legend: {
backgroundColor: '#FFFFFF',
reversed: true
},
plotOptions: {
series: {
cursor: 'pointer',
stacking: 'normal',
point: {
events: {
click: function () {
location.href = this.options.url;
}
}
}
}
},
series: seriesData
});
}
The chart has the correct data, pointer is correct but I get 'undefined' for the url.
Any help is greatly appreciated.
IWSChartBuilder.PersonEngagementsByStatusChart = function () {
var load = function () {
$.when(
IWSChartBuilder.RESTQuery.execute(GET THE DATA HERE)
).done(
function (engagements1) {
var data = [];
var countArray = [];
var urls = [];
var results = engagements1.d.results;
for (var i = 0; i < results.length; i++) {
if (results[i].Consultant.Title == undefined) {
continue;
}
var found = false;
for (var j = 0; j < data.length; j++) {
if (data[j].personName == results[i].Consultant.Title && data[j].statusName == results[i].Engagement_x0020_Status) {
data[j].statusCount = data[j].statusCount + 1;
found = true;
}
}
if (!found) {
data.push(new IWSChartBuilder.StatusByPerson(results[i].Consultant.Title, results[i].Engagement_x0020_Status, 1));
}
}
//Put data into format for stacked bar chart
var seriesData = [];
var xCategories = [];
var xStatus = [];
var i, j, cat, stat;
//Get Categories (Person Name)
for (i = 0; i < data.length; i++) {
cat = data[i].personName;
if (xCategories.indexOf(cat) === -1) {
xCategories[xCategories.length] = cat;
}
}
//Get Status values
for (i = 0; i < data.length; i++) {
stat = data[i].statusName;
if (xStatus.indexOf(stat) === -1) {
xStatus[xStatus.length] = stat;
}
}
//Create initial series data with 0 values
for (i = 0; i < xStatus.length; i++) {
var dataArray = [];
for (j = 0; j < xCategories.length; j++) {
dataArray.push(0);
}
seriesData.push({ name: xStatus[i], data: dataArray });
}
//Cycle through data to assign counts to the proper location in the series data
for (i = 0; i < data.length; i++) {
var personIndex = xCategories.indexOf(data[i].personName);
for (j = 0; j < seriesData.length; j++) {
if(seriesData[j].name == data[i].statusName){
seriesData[j].data[personIndex] = data[i].statusCount;
break;
}
}
}
//Build Chart
IWSChartBuilder.Utilities.loadStackedBarChartWithLink(xCategories, seriesData, "#engagementsByStatusChart", "Engagements by Status", "Total Projects");
}
).fail(
function (engagements1) {
$("#engagementsByStatusChart").html("<strong>An error has occurred.</strong>");
}
);
};
return {
load: load
}
}();
The way I am trying now
plotOptions: {
series: {
cursor: 'pointer',
stacking: 'normal',
point: {
events: {
click: function () {
getURL(this, xCategories, seriesData);
}
}
}
}
},
function getURL(chartInfo, xCategories, seriesData) {
var baseUrl = "correctURL" + xCategories[chartInfo._i] + "-FilterField2%3DEngagement%255Fx0020% 255FStatus-FilterValue2%3D" + chartInfo.name;
location.href = baseUrl;
}
You are hooking into the point click event handler. In this handler, 'this' refers to the point, not the series.
Try this:
plotOptions: {
series: {
cursor: 'pointer',
stacking: 'normal',
events: {
click: function () {
location.href = this.options.url;
}
}
}
},
http://jsfiddle.net/2DG84/
If you need URL's which depend on point values as well as the series, you can access both. e.g.
plotOptions: {
series: {
cursor: 'pointer',
stacking: 'normal',
point: {
events: {
click: function () {
alert(this.series.options.url + "&y=" + this.y);
}
}
}
}
},
In this example, there is a base URL on the series, by I am adding to the UTL with the point.y value.
http://jsfiddle.net/Fygg2/