Does anyone know if it is possible to group TopoJSON geometries with a given same property in multi-geometries?
For examples, given a TopoJSON with the elevation isolines of an area, this should make possible to group geometries with the same elevation, keeping properties.
I found this in the TopoJSON documentation:
https://github.com/mbostock/topojson/blob/master/bin/topojson-group
I tested it using the ID for the different elevations, but the output doesn't preserves the properties (even using the -p parameter).
Use topjson.js to convert topojson to geojson features. Then you can group the features based on elevation. You can use geojson-groupby to group based on any attributes then use mutligeojson to combine the geometries to MultiGeometry.
var features = [f1, f2, ..,fn]
var grouped = GeoJSONGroupBy(features, 'properties.elevation');
// grouped is
// {
// '100': [f1, f3, ..],
// '200': [f5, f8, f6, ..],
// ....
// }
var merged = {}; // final output merged with geometry and other attributes
Object.keys(grouped).reduce(function(merged,groupKey) {
var group = grouped[groupKey];
var reduced = {
geomCollection: []
attributes: {
elevation: group[0].attributes.elevation,
length: 0 // any other that you want to include
}
};
group.reduce(function(reduced, cur) {
reduced.geomCollection.push(cur.geometry);
reduced.attributes.length += length(cur.geometry); //length functon
},reduced);
return {
type: 'Feature',
geometry: multigeojson.implode(reduced.geomCollection),
attributes: reduced.attributes
}
},merged);
Related
I'm trying to create a custom world map where countries are merged into regions instead of having individual countries. Unfortunately for some reason something seems to get messed up with the winding order along the process.
As base data I'm using the natural earth 10m_admin_0_countries shape files available here. As criteria for merging countries I have a lookup map that looks like this:
const countryGroups = {
"EUR": ["ALA", "AUT", "BEL"...],
"AFR": ["AGO", "BDI", "BEN"...],
...
}
To merge the shapes I'm using topojson-client. Since I want to have a higher level of control than the CLI commands offer, I wrote a script. It goes through the lookup map and picks out all the topojson features that belong to a group and merges them into one shape and places the resulting merged features into a geojson frame:
const topojsonClient = require("topojson-client");
const topojsonServer = require("topojson-server");
const worldTopo = topojsonServer.topology({
countries: JSON.parse(fs.readFileSync("./world.geojson", "utf-8")),
});
const geoJson = {
type: "FeatureCollection",
features: Object.entries(countryGroups).map(([region, ids]) => {
const relevantCountries = worldTopo.objects.countries.geometries.filter(
(country, i) =>
ids.indexOf(country.properties.ISO_A3) >= 0
);
return {
type: "Feature",
properties: { region, countries: ids },
geometry: topojsonClient.merge(worldTopo, relevantCountries),
};
}),
};
So far everything works well (allegedly). When I try to visualise the map using github gist (or any other visualisation tool like vega lite) the shapes seem to be all messed up. I'm suspecting that I'm doing something wrong during the merging of the features but I can't figure out what it is.
When I try to do the same using the CLI it seems to work fine. But since I need more control over the merging, using just the CLI is not really an option.
The last feature, called "World", should contain all remaining countries, but instead, it contains all countries, period. You can see this in the following showcase.
var w = 900,
h = 300;
var projection = d3.geoMercator().translate([w / 2, h / 2]).scale(100);
var path = d3.geoPath().projection(projection);
var color = d3.scaleOrdinal(d3.schemeCategory10);
var svg = d3.select('svg')
.attr('width', w)
.attr('height', h);
var url = "https://gist.githubusercontent.com/Flave/832ebba5726aeca3518b1356d9d726cb/raw/5957dca433cbf50fe4dea0c3fa94bb4f91c754b7/world-regions-wrong.topojson";
d3.json(url)
.then(data => {
var geojson = topojson.feature(data, data.objects.regions);
geojson.features.forEach(f => {
console.log(f.properties.region, f.properties.countries);
});
svg.selectAll('path')
// Reverse because it's the last feature that is the problem
.data(geojson.features.reverse())
.enter()
.append('path')
.attr('d', path)
.attr('fill', d => color(d.properties.region))
.attr('stroke', d => color(d.properties.region))
.on('mouseenter', function() {
d3.select(this).style('fill-opacity', 1);
})
.on('mouseleave', function() {
d3.select(this).style('fill-opacity', null);
});
});
path {
fill-opacity: 0.3;
stroke-width: 2px;
stroke-opacity: 0.4;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.js"></script>
<script src="https://d3js.org/topojson.v3.js"></script>
<svg></svg>
To fix this, I'd make sure to always remove all assigned countries from the list. From your data, I can't see where "World" is defined, and if it contains all countries on earth, or if it's a wildcard assignment.
In any case, you should be able to fix it by removing all matches from worldTopo:
const topojsonClient = require("topojson-client");
const topojsonServer = require("topojson-server");
const worldTopo = topojsonServer.topology({
countries: JSON.parse(fs.readFileSync("./world.geojson", "utf-8")),
});
const geoJson = {
type: "FeatureCollection",
features: Object.entries(countryGroups).map(([region, ids]) => {
const relevantCountries = worldTopo.objects.countries.geometries.filter(
(country, i) =>
ids.indexOf(country.properties.ISO_A3) >= 0
);
relevantCountries.forEach(c => {
const index = worldTopo.indexOf(c);
if (index === -1) throw Error(`Expected to find country ${c.properties.ISO_A3} in worldTopo`);
worldTopo.splice(index, 1);
});
return {
type: "Feature",
properties: { region, countries: ids },
geometry: topojsonClient.merge(worldTopo, relevantCountries),
};
}),
};
I have an image collection of Landsat 5 and a feature collection of thousands polygons and points (in the example, I only have three). I would like to compute the NDVI (and NDWI) mean for each polygon over time like if I was using: ui.Chart.image.series;
If I am testing only one polygon with this code:
var p1= ee.Geometry.Point([-78.55995626672507,35.05443673532838])
var pol = ee.Geometry.Polygon([[[-78.57239414626946,35.01247143741747],
[-78.57186843330254,35.012559309453266],
[-78.57199717933526,35.01277020195395],
[-78.57253362113823,35.01272626606113],
[-78.57239414626946,35.01247143741747]
]]);
var ens = [
ee.Feature(pol, {name: 'Thiessen'})
];
var col = ee.FeatureCollection(ens)
print(col)
// NDVI: B4 and B3
var addNDVI = function(image) {
var ndvi = image.normalizedDifference(['B4', 'B3']).rename('NDVI');
return image.addBands(ndvi);
};
// Apply the cloud mask and NDVI function to Landsat 5 imagery and print the chart
var l5 = ee.ImageCollection("LANDSAT/LT05/C01/T1_TOA")
.filter(ee.Filter.calendarRange(1985,2007,'year'))
.filter(ee.Filter.calendarRange(1,1,'month'))
.filterBounds(p1)
.map(addNDVI)
print(ui.Chart.image.series(l5.select('NDVI'), col, ee.Reducer.mean(), 30));
With this code I obtained this figure. I would like to obtain the same type of figure with several polygon (one figure contains all the time series of the polygons).
I tried this code:
var p1= ee.Geometry.Point([-78.55995626672507,35.05443673532838])
var p2= ee.Geometry.Point([-78.5725093420931,35.05908805245044])
var pol = ee.Geometry.Polygon([[[-78.57239414626946,35.01247143741747],
[-78.57186843330254,35.012559309453266],
[-78.57199717933526,35.01277020195395],
[-78.57253362113823,35.01272626606113],
[-78.57239414626946,35.01247143741747]
]]);
var ens = [
ee.Feature(p2, {name: 'Thiessen'}),
ee.Feature(pol, {name: 'Thiessen'})
];
var col = ee.FeatureCollection(ens)
print(col)
// NDVI: B4 and B3
var addNDVI = function(image) {
var ndvi = image.normalizedDifference(['B4', 'B3']).rename('NDVI');
return image.addBands(ndvi);
};
// Apply the cloud mask and NDVI function to Landsat 5 imagery and print the chart
var l5 = ee.ImageCollection("LANDSAT/LT05/C01/T1_TOA")
.filter(ee.Filter.calendarRange(1985,2007,'year'))
.filter(ee.Filter.calendarRange(1,1,'month'))
.filterBounds(p1)
.map(addNDVI)
//Create a graph of the time-series.
var graph = ui.Chart.image.seriesByRegion({
imageCollection: l5,
regions: col,
reducer: ee.Reducer.mean(),
scale: 30,
})
print(graph)
This code provides this figure
The last code presents the graph as I would like. However, it does not compute what I want. For the polygon pol, I should have the same graphs with the two codes which is not the case. How could I have the same computation made with the code 1 but presented as the code 2?
You need to add the 'band' argument to the second call, like this:
var graph = ui.Chart.image.seriesByRegion({
imageCollection: l5,
regions: col,
band: 'NDVI',
reducer: ee.Reducer.mean(),
scale: 30,
})
I am trying to write script that will get a cell that has Data validation from a control spreadsheet and add it to a location (same) in another spreadsheet.
I am using:
var myCell = SpreadsheetApp.getActive().getRange('D4');
var rule = myCell.getDataValidation();
but apparently the spreadsheet is not active during the script process so the rule is null.
If you can check this product forum, it was answered there.
Also it was cited in one of the SO post that this feature was currently not supported. But they gave a workaround to do it.
Copy the sheet from the original post, then follow the steps below:
Select a range of cells across which you want to copy a data
validation rule, relatively
From the Validation+ custom menu, select the appropriate option (all
references relative, columns absolute, or rows absolute)
The validation of the upper-left cell will be copied to the rest of
the range
Also, if you would like to create from scratch, I've post the same script that was specified in the SO post:
function onOpen()
{
SpreadsheetApp.getActiveSpreadsheet().addMenu
(
"Validation+",
[
{name: "Copy validation (all relative references)", functionName: "copyValidation"},
{name: "Copy validation (relative rows, absolute columns)", functionName: "copyValidationColumnsAbsolute"},
{name: "Copy validation (absolute rows, relative columns)", functionName: "copyValidationRowsAbsolute"}
]
);
}
function copyValidation(rowsAbsolute, columnsAbsolute)
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var r = ss.getActiveRange();
var dv = r.getDataValidations();
var dvt = dv[0][0].getCriteriaType();
if (dvt != SpreadsheetApp.DataValidationCriteria.VALUE_IN_RANGE) return;
var dvv = dv[0][0].getCriteriaValues();
Logger.log(dvv);
for (var i = 0; i < dv.length; i++)
{
for (var j = i ? 0 : 1; j < dv[0].length; j++)
{
dv[i][j] = dv[0][0].copy().withCriteria(dvt, [dvv[0].offset(rowsAbsolute ? 0 : i, columnsAbsolute ? 0 : j), dvv[1]]).build();
}
}
r.setDataValidations(dv);
}
function copyValidationRowsAbsolute()
{
copyValidation(true, false);
}
function copyValidationColumnsAbsolute()
{
copyValidation(false, true);
}
Is there a more complete tutorial or guide to creating charts with dc.js than what is offered in their documentation? I'm trying to create a simple line chart with 2 stacked levels. I'm making use of the following csv:
I want the WasteDate to be on the x-axis and the WasteData to be on the y-axis. Further I want one layer to be of the WasteFunction Minimisation and the other to be of the WasteFunction Disposal. This should give me something like the following (very roughly):
Now, as I understand it, I need to create a dimension for the x-axis using crossfilter and then a filtered dimension for my 2 stacks.
The dimension for the x-axis will be the dates:
// dimension by month
var Date_dim = ndx.dimension(function (d) {
return d.WasteDate;
});
// Get min/max date for x-axis
var minDate = Date_dim.bottom(1)[0].WasteDate;
var maxDate = Date_dim.top(1)[0].WasteDate;
Then I need to create a dimension for the y-axis, then filter it for each of my stacks?
// WasteType dimension
var WasteFunction_dim = ndx.dimension(function (d) {
return d.WasteFunction;
});
// Minimisation Filter
var WasteFunction_Disposal = WasteFunction_dim.filter("Disposal");
// Disposal Filter
var WasteFunction_Minimisation = WasteFunction_dim.filter("Minimisation");
Then I should be able to use these to setup the chart:
moveChart
.renderArea(true)
.width(900)
.height(200)
.dimension(Date_dim)
.group(WasteFunction_Minimisation, 'Minimisation')
.stack(WasteFunction_Disposal, 'Disposal')
.x(d3.time.scale().domain([minDate, maxDate]));
Now, I can't get passed this error on the RenderAll() function:
The full code:
< script type = "text/javascript" >
$(document).ready(function() {
var moveChart = dc.lineChart('#monthly-move-chart');
d3.csv('minimisation-vs-disposal.csv', function(data) {
/* format the csv file a bit */
var dateFormat = d3.time.format('%d/%M/%Y');
var numberFormat = d3.format('.2f');
data.forEach(function(d) {
d.dd = dateFormat.parse(d.WasteDate);
d.WasteData = +d.WasteData // coerce to number
});
// Cross Filter instance
var ndx = crossfilter(data);
var all = ndx.groupAll();
// dimension by month
var Date_dim = ndx.dimension(function(d) {
return d.WasteDate;
});
// Get min/max date for x-axis
var minDate = Date_dim.bottom(1)[0].WasteDate;
var maxDate = Date_dim.top(1)[0].WasteDate;
// Waste Data dimension
var WasteData_dim = ndx.dimension(function(d) {
return d.WasteData;
});
// WasteType dimension
var WasteFunction_dim = ndx.dimension(function(d) {
return d.WasteFunction;
});
// Minimisation Filter
var WasteFunction_Disposal = WasteFunction_dim.filter("Disposal");
// Disposal Filter
var WasteFunction_Minimisation = WasteFunction_dim.filter("Minimisation");
moveChart
.renderArea(true)
.width(900)
.height(200)
.transitionDuration(1000)
.dimension(Date_dim)
.group(WasteFunction_Minimisation, 'Minimisation')
.stack(WasteFunction_Disposal, 'Disposal')
.x(d3.time.scale().domain([minDate, maxDate]));
dc.renderAll();
});
});
< /script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="monthly-move-chart">
<strong>Waste minimisation chart</strong>
</div>
It's true, dc.js does not have much documentation. Someone could write a book but it hasn't happened. People mostly rely on examples to get started, and the annotated stock example is a good first read.
The biggest problem in your code is that those are not crossfilter groups. You really need to learn the crossfilter concepts to use dc.js effectively. Crossfilter has very strong documentation, but it's also very dense and you'll have to read it a few times.
Feel free to join us on the dc.js user group if you want to talk it through to get a better understanding. It does take a while to get the idea but it's worth it!
I have to parse a document containing groups of variable-value-pairs which is serialized to a string e.g. like this:
4^26^VAR1^6^VALUE1^VAR2^4^VAL2^^1^14^VAR1^6^VALUE1^^
Here are the different elements:
Group IDs:
4^26^VAR1^6^VALUE1^VAR2^4^VAL2^^1^14^VAR1^6^VALUE1^^
Length of string representation of each group:
4^26^VAR1^6^VALUE1^VAR2^4^VAL2^^1^14^VAR1^6^VALUE1^^
One of the groups:
4^26^VAR1^6^VALUE1^VAR2^4^VAL2^^1^14 ^VAR1^6^VALUE1^^
Variables:
4^26^VAR1^6^VALUE1^VAR2^4^VAL2^^1^14^VAR1^6^VALUE1^^
Length of string representation of the values:
4^26^VAR1^6^VALUE1^VAR2^4^VAL2^^1^14^VAR1^6^VALUE1^^
The values themselves:
4^26^VAR1^6^VALUE1^VAR2^4^VAL2^^1^14^VAR1^6^VALUE1^^
Variables consist only of alphanumeric characters.
No assumption is made about the values, i.e. they may contain any character, including ^.
Is there a name for this kind of grammar? Is there a parsing library that can handle this mess?
So far I am using my own parser, but due to the fact that I need to detect and handle corrupt serializations the code looks rather messy, thus my question for a parser library that could lift the burden.
The simplest way to approach it is to note that there are two nested levels that work the same way. The pattern is extremely simple:
id^length^content^
At the outer level, this produces a set of groups. Within each group, the content follows exactly the same pattern, only here the id is the variable name, and the content is the variable value.
So you only need to write that logic once and you can use it to parse both levels. Just write a function that breaks a string up into a list of id/content pairs. Call it once to get the groups, and then loop through them calling it again for each content to get the variables in that group.
Breaking it down into these steps, first we need a way to get "tokens" from the string. This function returns an object with three methods, to find out if we're at "end of file", and to grab the next delimited or counted substring:
var tokens = function(str) {
var pos = 0;
return {
eof: function() {
return pos == str.length;
},
delimited: function(d) {
var end = str.indexOf(d, pos);
if (end == -1) {
throw new Error('Expected delimiter');
}
var result = str.substr(pos, end - pos);
pos = end + d.length;
return result;
},
counted: function(c) {
var result = str.substr(pos, c);
pos += c;
return result;
}
};
};
Now we can conveniently write the reusable parse function:
var parse = function(str) {
var parts = {};
var t = tokens(str);
while (!t.eof()) {
var id = t.delimited('^');
var len = t.delimited('^');
var content = t.counted(parseInt(len, 10));
var end = t.counted(1);
if (end !== '^') {
throw new Error('Expected ^ after counted string, instead found: ' + end);
}
parts[id] = content;
}
return parts;
};
It builds an object where the keys are the IDs (or variable names). I'm asuming as they have names that the order isn't significant.
Then we can use that at both levels to create the function to do the whole job:
var parseGroups = function(str) {
var groups = parse(str);
Object.keys(groups).forEach(function(id) {
groups[id] = parse(groups[id]);
});
return groups;
}
For your example, it produces this object:
{
'1': {
VAR1: 'VALUE1'
},
'4': {
VAR1: 'VALUE1',
VAR2: 'VAL2'
}
}
I don't think it's a trivial task to create a grammar for this. But on the other hand, a simple straight forward approach is not that hard. You know the corresponding string length for every critical string. So you just chop your string according to those lengths apart..
where do you see problems?