I have a problem with a data string returning 'undefined' in my tooltip, while that same piece of data does affect the colors of my svg.
Objective: I want to display a map, chloropleth style, that displays a tooltip for each country with
a) country names, loaded from a .json object (works fine)
b) some values corresponding to each country, loaded from a tsv (works partially so far)
File structure :
The main .js file calls 1 topojson file (with country paths and names) as well as a .tsv file (with specific area data for each country)
Resources used:
1- MBostock's chloropleth (see note below)
2- d3.tip by Justin Palmer
Play with it
Here is a Plunker for people to play with it (due to the heaviness of the world.json I use, it may take a while to load).
---------------
Relevant code bit :
queue()
.defer(d3.json, "world.json")
.defer(d3.tsv, "winemap.tsv", function setMaptoTotal(d) { rate.set(d.name, +d.total); })
.await(ready);
var tip = d3.tip()
.attr('class', 'd3-tip')
.html(function mylabel(d) { return d.properties.name + "<table><tr><td><span style='color: #fcf772'> Total area cultivated: " + d.total +" ha</span></td></tr><tr><td> <span style='color:#bf2a2a'> Global rank: "+ d.rank + " </span></td></tr></table>";})
.direction('sw')
.offset([0, 2]);
var svg = d3.selectAll('body')
.append('svg')
.attr('width',width)
.attr('height',height)
.call(zoom)
.call(tip);
// ACTION STARTS HERE //
function ready(error, world) {
svg.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(world, world.objects.countries).features)
.enter().append("path")
.attr("class", function (d) { return quantize(rate.get(d.properties.name)); })
.attr("d", path)
.call(tip)
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
;
Options tried:
1- Moving .call(tip) after or inside the ready function which triggers data load via queue(). >>> This still returns undefined in my tooltip.
2- Use queue.defer(mylabel(d)). >>> Nothing appears on screen.
3- Taking into account the asynchronous nature of d3.tsv, I went through this question. If I understand well, you have to refer to the function you want to call both inside and after d3.tsv (with or without setting a global variable). I thus planned to refer to the mylabel(d) function both inside and after d3.tsv. However I noticed that function (d) { return quantize(rate.get(d.properties.name)); }) is not written in my d3.tsv yet it works fine.
Note: sorry I could not post more links at this stage, e.g. to MBostick's chloropleth or d3.tip, but it requires more reputation :)
In case anyone faces the same issue, I finally found the answer.
There were 2 problems:
1- Data should be first associated with an array.
First you need to pair, inside the ready function, the id with each data entry
function ready (error, world, data) {
var Total= {};
var Name= {};
var Rank= {};
data.forEach(function(d) {
Total[d.id] = +d.total;
Name[d.id] = d.name;
Rank[d.id] = +d.rank;
});
2- This method only works with referring explicitly to "id"
The command .data(topojson.feature(world, world.objects.countries).features) will search for the id property of each json object that you want to apply a transformation with. The bridge between your .tsv and .json MUST be id, both in your .json and .tsv files as well as the code.
The reason you can't replace id with just any name while working with topoJSON is that it holds a specific function in the topojson code itself (see below).
{var r={type:"Feature",id:t.id,properties:t.properties||{},geometry:u(n,t)};return null==t.id&&delete r.id,r}
I solved this by adding into my .tsv file a column with the id attributes of all countries objects inside my .json file. I named this column idso the pairing described in 1- can function.
Careful, you want to represent all countries that are in your .json on your .tsv, otherwise "undefined" will appear again when you hover the countries that would not be mentioned on your .tsv.
Final, working code -
function ready (error, world, data) {
var pairDataWithId= {};
var pairNameWithId= {};
var pairRankWithId= {};
// “d” refers to that data parameter, which in this case is our .csv that (while not necessary) is also named “data.”
data.forEach(function(d) {
pairDataWithId[d.id] = +d.total;
pairNameWithId[d.id] = d.name;
pairRankWithId[d.id] = +d.rank;
});
// Below my tooltip vars
var tipin = function(d) { d3.select(this).transition().duration(300).style('opacity', 1);
div.transition().duration(300).style('opacity', 1);
div.text(pairNameWithId[d.id] + " Total Area: " + pairDataWithId[d.id]+ " Rank: " + pairRankWithId[d.id])
.style('left', (d3.event.pageX) + "px")
.style('top', (d3.event.pageY -30) + "px");
};
var tipout = function() {
d3.select(this)
.transition().duration(300).style('opacity', 0.8);
div.transition().duration(300)
.style('opacity', 0);
};
// Resume
function colorme(d) { return color (pairDataWithId[d.id]);
}
svg.append('g')
.attr('class', 'region')
.selectAll('path')
.data(topojson.feature(world,world.objects.countries).features)
.enter()
.append('path')
.attr('d', path)
.style('fill', colorme)
.style('opacity', 0.8)
.on ('mouseover', tipin)
.on('mouseout', tipout);
};
Note: I stopped using d3.tip for a simpler version with full code above.
Related
the function .map applies a function to every individual image in an ImageCollection. And the function .iterate applies a function to one image and the output of the calculation done to the precedent image on an ImageCollection.
The first only works with one image each time, and the second implies modifying each image and utilize it to any calculation with the next one.
I need a function that works like .iterate, but does not modify the precedent image. I just need to do:
image (time -1) / image (time 0).
I can not find a way to do it,
thanks for your help
i have tried,
var first = ee.List([
ee.Image(1).set('system:time_start', time0).select([0], ['pc1'])
]);
var changeDET = function(image, list) {
var previous = ee.Image(ee.List(list).get(-1));
var change = previous.divide(image.select('pc1'))
.set('system:time_start', image.get('system:time_start'));
return ee.List(list).add(change);
};
var cumulative = ee.ImageCollection(ee.List(imageCollection.iterate(changeDET, first)))
.sort('system:time_start', false)
What you can do is to convert your imageCollection into a ee.List object, then map over that list with an index variable to access the previous image. Example code is below:
var length = yourImageCollection.size();
var list = yourImageCollection.toList(length);
var calculated_list = list.map(function(img) {
var index = list.indexOf(img)
img = ee.Image(img);
var previousIndex = ee.Algorithms.If(index.eq(0), index, index.subtract(1));
var previousImage = ee.Image(list.get(previousIndex)):
var change = ee.Image(previousImage.divide(img)
.copyProperties(img, ["system:time_start"]));
return change;
})
I'm not sure what you want to do with the first image, so when map function reach the first image, previousIndex will equal to index. In other words, the first image will be divided by itself (as there is no image before it).
Hope this helps.
I have a range of data in a Google Sheet and I want to store that data into an array using the app script. At the moment I can bring in the data easily enough and put it into an array with this code:
var sheetData = sheet.getSheetByName('Fruit').getRange('A1:C2').getValues()
However, this puts each row into an array. For example, [[Apple,Red,Round],[Banana,Yellow,Long]].
How can I arrange the array by columns so it would look: [[Apple,Banana],[Red,Yellow],[Round,Long]].
Thanks.
It looks like you have to transpose the array. You can create a function
function transpose(data) {
return (data[0] || []).map (function (col , colIndex) {
return data.map (function (row) {
return row[colIndex];
});
});
}
and then pass the values obtained by .getValues() to that function..
var sheetData = transpose(sheet.getSheetByName('Fruit').getRange('A1:C2').getValues())
and check the log. See if that works for you?
Use the Google Sheets API, which allows you to specify the primary dimension of the response. To do so, first you must enable the API and the advanced service
To acquire values most efficiently, use the spreadsheets.values endpoints, either get or batchGet as appropriate. You are able to supply optional arguments to both calls, and one of which controls the orientation of the response:
const wb = SpreadsheetApp.getActive();
const valService = Sheets.Spreadsheets.Values;
const asColumn2D = { majorDimension: SpreadsheetApp.Dimension.COLUMNS };
const asRow2D = { majorDimension: SpreadsheetApp.Dimension.ROWS }; // this is the default
var sheet = wb.getSheetByName("some name");
var rgPrefix = "'" + sheet.getName() + "'!";
// spreadsheetId, range string, {optional arguments}
var single = valService.get(wb.getId(), rgPrefix + "A1:C30");
var singleAsCols = valService.get(wb.getId(), rgPrefix + "A1:C30", asColumn2D);
// spreadsheetId, {other arguments}
var batchAsCols = valService.batchGet(wb.getId(), {
ranges: [
rgPrefix + "A1:C30",
rgPrefix + "J8",
...
],
majorDimension: SpreadsheetApp.Dimension.COLUMNS
});
console.log({rowResp: single, colResp: singleAsCols, batchResponse: batchAsCols});
The reply will either be a ValueRange (using get) or an object wrapping several ValueRanges (if using batchGet). You can access the data (if any was present) at the ValueRange's values property. Note that trailing blanks are omitted.
You can find more information in the Sheets API documentation, and other relevant Stack Overflow questions such as this one.
How would one go about adding programmatically triggered touch/mouse events in Matter.js? I have a few collision events set up for the engine, but can not trigger a mouseup event that stops the current dragging action. I've tried various combinations of targeting the canvas element, the mouse/mouseConstraint, and the non-static body.
If you, like me, came here trying to figure out how to be able to click on a Matter.js body object, let me give you one way. My goal in my project was to assign some attributes to my rectangle objects and call a function when they were clicked on.
The first thing to do was to distinguish between dragging and clicking, so I wrote(using Jquery):
$("body").on("mousedown", function(e){
mouseX1 = e.pageX;
mouseY1 = e.pageY;
});
$("body").on("mouseup", function(e){
mouseX2 = e.pageX;
mouseY2 = e.pageY;
if((mouseX1 == mouseX2) && (mouseY1 == mouseY2)){
//alert("click!\n" + mouseX2 + " " + mouseY2 +"\n");
var bodiesUnder = Matter.Query.point(books, { x: mouseX2, y: mouseY2 });
//alert("click!\n" + mouseX2 + " " + mouseY2 +"\n");
if (bodiesUnder.length > 0) {
var bodyToClick = bodiesUnder[0];
alert(bodyToClick.title2);
}
}
});
This was accomplished when listening for "mouseup" and asking if ((mouseX1 == mouseX2) && (mouseY1 == mouseY2)).
Second- the juicy part- create a var array to hold the objects, or 'bodies', we are going to dig up under the mouse. Thankfully there's this function:
var bodiesUnder = Matter.Query.point(books, { x: mouseX2, y: mouseY2 });
For the first element in here I entered "books". For you this needs to be the name of an array you've put all your objects, or 'bodies' into. If you don't have them in an array, it's not hard to throw them all in, like so:
var books = [book1, book2, book3];
Once that was all done, I was able to alert(book1.title2) to see what the title of that book (body) is. My bodies were coded as follows:
var book2 = Bodies.rectangle(390, 200, 66, 70, {
render : {
sprite : {
texture: "img/tradingIcon.jpg"
}
},
restitution : 0.3,
title1 : 'Vanessa and Terry',
title2 : 'Trading'
});
Hope that helps! This one had me hung up for a whole day.
It turns out I had incorrectly configured the Matter.Mouse module, and was re-assigning the mouse input that had already been set in MouseConstraint. The following works in regards to my original question:
Matter.mouseConstraint.mouse.mouseup(event);
While working with JqxWidges I met a problem with exporting nested grids which use one JSON as a source file. The common solution doesn't work. Actually it exports only parent grid colums.
$("#excelExport").click(function () {
$("#jqxGrid").jqxGrid('exportdata', 'csv', chartName + ' ' + date);
});
One of the existing solutions (http://www.jqwidgets.com/community/reply/reply-to-export-data-from-a-nested-grid-13/) propose to push nested rows into data array while calling initrowdetails function.
Yes it works! But only for nested grids and in case when this grid was selected.
So, from this step I am moving to next aproach:
To collect all necessary data into array using initial JSON (prevent you from gathering only separate selected data);
To initialise parent grid columns with all existing data and mark nested columns as hidden. Then when export don't forget to add true parameter to export both non/hidden columns;
Use standard export with custom array parameter;
That's it!
Data collecting:
var toExport = data.allClientsCountChart;
var exp = new Array();
for(var i in toExport){
var client = {};
var countr = toExport[i].countries;
client[labels.clientType]=toExport[i].clientType;
client[labels.clientTypeCount]=toExport[i].clientTypeCount;
exp.push(client);
for(var j in countr) {
var country = {}
var detailes = countr[j].clientDetails;
country[labels.countryType]=countr[j].countryType;
country[labels.clientsNumber]=countr[j].clientsNumber;
exp.push(country);
for(var d in detailes) {
var det = {}
det[labels.scriptName]=detailes[d].scriptName;
det[labels.clientsCount]=detailes[d].clientsCount;
exp.push(det);
}
}
}
Export:
$("#excelExport").click(function () {
$("#jqxGrid").jqxGrid('exportdata', 'csv', chartName + ' ' + date, true, exp, true);
}
And don't forget to set the fifth pafameter into true to export hidden columns.
No doubds, it looks hardcoded. But it works for me.
So, if you have a good solution - please leave a comment!!!
Following the example given by mbostock for creating a force graph with a csv:
How to convert to D3's JSON format?
http://bl.ocks.org/2949937
I'm creating a force graph with D3 but am unsure how / where to call upon a value from the CSV line to set node size, color, or link length.
I tried a few things e.g.:
links.forEach(function(link) {
link.source = nodeByName(link.user1);
link.target = nodeByName(link.user2);
link.size = nodeByName(link.somevaluefromcsv)
link.distance = nodeByName(link.somevaluefromcsv);
});
This is just wrong. From what I can tell, it just generates empty nodes and the values aren't callable elsewhere.
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", function(d) {return d[3];}) //this is not returing any value as far as I can tell.
.call(force.drag);
or further down in the tick function:
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) {return d[7];});
There are probably a few things causing problems:
1. I don't seem to have a good conceptual model of the links function or the nodesbyName array or function.
Typical lines from the CSV (as it is now), in the order of:
time, user1, user2, similarity score, total points, points against, points for, length
are:
1223.8167,john6,john5,0.153846153846,1,0,1,5
1223.9166,john6,john5,0.185185185185,8,0,8,6
1223.9667,bobby4,bobby3,0.402777777778,224,320,-96,15
1224.1167,bobby4,bobby3,0.402777777778,226,310,-84,15
1224.2,bobby4,bobby3,0.402777777778,240,283,-43,15
1224.2,john6,john5,0.185185185185,2,0,2,5
1224.2,john6,john5,0.153846153846,2,0,2,5
1224.2667,bobby4,bobby3,0.397058823529,0,24,-24,13
1224.2833,john6,john5,0.153846153846,1,0,1,5
1224.45,bobby4,bobby3,0.397058823529,0,21,-21,13
1224.55,bobby4,bobby3,0.442857142857,0,18,-18,14
function nodeByName(name) - create new node or return existing node with name specified as argument.
if you need to pass node properties from links, you can do it in forEach
links.forEach(function(link) {
link.source = nodeByName(link.user1);
link.target = nodeByName(link.user2);
link.source.radius = link.radius
link.size = link.somevaluefromcsv;
link.distance = link.somevaluefromcsv;
});
links - array of links between nodes.
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", function(d) {return d.radius;}) // radius property of node
.call(force.drag);