Merging topojson using topomerge messes up winding order - geojson

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),
};
}),
};

Related

Map implementation with duplicate keys in Dart

I want to have a map with duplicate keys. Is there such a map in Dart or a utility library that has this functionality?
I'm using the following get a count of items:
myList.forEach(
(element) {
if (!myMap.containsKey(element)) {
myMap[element] = 1;
} else {
myMap[element] += 1;
}
},
);
then convert keys/values to lists: Need to switch key/values...
final keys = myMap.keys.toList();
final itemSpit = keys.map((e) => e.toString().split('§º')).toList();
final values = myMap.values.toList();
put it in a map
final map = Map();
for (var i = 0; i < values.length; i++) {
map[values[i]] = itemSpit[i];
}
Obviously the keys are overridden in the for loop.
then
iterate over map (Put keys/values in flutter widgets)
final cells = map.entries
.map((e) => ........
THe first method increases the value count if there's a duplicate value. So I have this. ... {breadwhitelarge: 3, cornyellowsmall:5 ..etc..}
I then have to split the strings and have output like this
5 bread white large
3 corn yellow small
Instead of defining a map which allows duplicated keys you can instead create a Map<K,List<V>> like this example:
void main() {
final map = <String, List<int>>{};
addValueToMap(map, 'Test 1', 1);
addValueToMap(map, 'Test 1', 2);
addValueToMap(map, 'Test 2', 3);
addValueToMap(map, 'Test 1', 4);
addValueToMap(map, 'Test 2', 5);
addValueToMap(map, 'Test 3', 6);
print(map); // {Test 1: [1, 2, 4], Test 2: [3, 5], Test 3: [6]}
}
void addValueToMap<K, V>(Map<K, List<V>> map, K key, V value) =>
map.update(key, (list) => list..add(value), ifAbsent: () => [value]);
You can then ask for a given key and get a list of all values connected to this key.
package:quiver provides a MultiMap class with List-based and Set-based implementations.
Could you just create the Flutter widgets directly from the first map?
var widgets = [for (var e in myMap.entries) MyWidget(
count: e.value,
strings: [... e.key.split("§º")])];
Building the intermediate map seems to be what is causing the trouble.
this is an example map has duplicate keys
withDuplicateKey() {
List<dynamic> demoList = [
{1},
{2},
{3},
{1}
];
var toRemove = {};
demoList.forEach((e) {
toRemove.putIfAbsent(e, () => e);
});
print(toRemove.keys.toList());
}
output is ( printed list of key )
[{1}, {2}, {3}, {1}]

Google Calendar Orderby when using two linq queries

I am using google charts to display a stacked column chart. I am using entity framework and linq queries to gather my data from the db.
The problems I am having is:
that it will not order the chart. I have ordered the chart but the x-axis remains un-ordered. Can this be done through the linq query or could I do it in the script?
Currently it only displays x-axis values for data that I have. Example is on the x-axis I have month number but it only displays marks for data I have eg. 1,4,5,6. Is there a way to include from 1-12 although there is no data for that particular month number?
Code:
#region Total Hours Per Month sick
var querythpshols = (from r in db.HolidayRequestForms
where (r.StartDate) >= dateAndTime
group r by r.MonthOfHoliday into g
select new { Value = g.Key, Count = g.Sum(h => h.HoursTaken)});
var resultthpshols = querythpshols.ToList();
var datachartthpshols = new object[resultthpshols.Count];
int G = 0;
foreach (var i in resultthpshols)
{
datachartthpshols[G] = new object[] { i.Value.ToString(), i.Count };
G++;
}
string datathpshols = JsonConvert.SerializeObject(datachartthpshols, Formatting.None);
ViewBag.datajthpshols = new HtmlString(datathpshols);
#endregion
#region Total Hours Per Month
var querythpshols1 = (from r in db.HolidayRequestForms
where (r.StartDate) <= dateAndTime
group r by r.MonthOfHoliday into g
select new { Value = g.Key, Count1 = g.Sum(r => r.HoursTaken) })
;
var resultthpshols1 = querythpshols1.ToList();
var datachartthpshols1 = new object[resultthpshols1.Count];
int P = 0;
foreach (var i in resultthpshols1)
{
datachartthpshols1[P] = new object[] { i.Value.ToString(), i.Count1 };
P++;
}
string datathpshols1 = JsonConvert.SerializeObject(datachartthpshols1, Formatting.None);
ViewBag.datajthpshols1 = new HtmlString(datathpshols1);
#endregion
Script:
#*TOTAL HOURS PER MONTH CHART*#
<scipt>
<script>
var datathpshols = '#ViewBag.datajthpshols';
var datassthpshols = JSON.parse(datathpshols);
var datathpshols1 = '#ViewBag.datajthpshols1';
var datassthpshols1 = JSON.parse(datathpshols1);
</script>
<script type="text/javascript">
// Load the Visualization API and the corechart package.
google.charts.load('current', { 'packages': ['corechart'] });
// Set a callback to run when the Google Visualization API is loaded.
google.charts.setOnLoadCallback(drawChartA);
// Callback that creates and populates a data table,
// instantiates the pie chart, passes in the data and
// draws it.
function drawChartA() {
// Create the data table.
var data1 = new google.visualization.DataTable();
data1.addColumn('string', 'Value');
data1.addColumn('number', 'Holiday Hours Booked');
data1.addRows(datassthpshols);
var data2 = new google.visualization.DataTable();
data2.addColumn('string', 'Value');
data2.addColumn('number', 'Holiday Hours Taken');
data2.addRows(datassthpshols1);
var joinedData = google.visualization.data.join(data1, data2, 'full', [[0, 0]], [1], [1]);
// Set chart options
var options = {
'title': 'Holiday Hours Taken Per Month',
'width': 600,
'height': 350,
'hAxis': { title: 'Month Number' },
'vAxis': { title: 'Holiday Hours Taken' },
'is3D': true,
'isStacked': true,
'legend': 'right'
};
// Instantiate and draw our chart, passing in some options.
var chart = new google.visualization.ColumnChart(document.getElementById('chartTHPShols_div'));
chart.draw(joinedData, options);
}
</script>
1) Use data table method --> sort -- to order the x-axis.
joinedData.sort([{column: 0}]);
2) strings produce a discrete axis, and will only display the data available. numbers produce a continuous axis, and provide much more flexibility for the axis ticks. probably the most simplest solution would be to use a data view to convert the x-axis to numbers. (use the data view to draw the chart)
var joinedData = google.visualization.data.join(data1, data2, 'full', [[0, 0]], [1], [1]);
var dataView = new google.visualization.DataView(joinedData);
dataView.setColumns([{
calc: function (dt, row) {
return parseFloat(dt.getValue(row, 0));
},
label: joinedData.getColumnLabel(0),
type: 'number'
}, 1, 2]);
chart.draw(dataView, options);

Dart Chartjs Bubble Example

I'm struggling to get the chart.js dart port to properly render a bubble chart when serving the web app with the --release flag. Using dart2js seems to be breaking the chart, as it works perfectly with --no-release.
The chart renders in properly, and the axes look fine. It's just the data that isn't rendering in. No errors are thrown in the console.
I've tried two approaches to adding the data. One with a simple map, like so:
List<Map> myData = [];
myData.add({'x': 0, 'y': 0, 'r': 5}); //for example..
ChartDataSets dataset = new ChartDataSets(
label: 'outer',
data: myData
);
var chartData = new LinearChartData(datasets: <ChartDataSets>[
dataset,
]);
ChartConfiguration config = new ChartConfiguration(
type: 'bubble', data: dataset, options: new ChartOptions(
responsive: true,
legend: new ChartLegendOptions(display: false),
scales: scales,
));
I've also tried creating an class to hold the data, and handing objects to the input list:
class BubbleObject{
int x;
int y;
double r;
BubbleObject(this.x, this.y, this.r);
}
so myData becomes List<BubbleObject> myData = []
Any idea why the --release flag would break this approach, and how this issue can be fixed?
Thanks in advance!

Highcharts bar chart with varied bar widths?

I want to make stacked bar chart where each portion has a width that encodes one value (say "Change" in the data below) and a height that encodes another value ("Share")
In some ways this is like a histogram with different bin sizes. There are a few "histogram" questions but none seem to address this. Plot Histograms in Highcharts
So given data like this:
Category Share Price Change
Apples 14.35 0.1314192423
Horseradish 46.168 0.1761474117
Figs 2.871 0.018874249
Tomatoes 13.954 0.0106121298
Mangoes 7.264 0.1217297011
Raisins 5.738 0.0206787136
Eggplant 6.31 0.0110160732
Other produce 3.344 0.0945377722
I can make a stacked bar that captures the "share" column in widths:
And another that captures the "change" column in heights:
And I can use an image editor to combine those into this histogram-like beast:
Which really captures that horseradish is a huge deal. So my question is, can I do that within Highcharts?
You can realise that by using snippet.
(function (H) {
var seriesTypes = H.seriesTypes,
each = H.each,
extendClass = H.extendClass,
defaultPlotOptions = H.getOptions().plotOptions,
merge = H.merge;
defaultPlotOptions.marimekko = merge(defaultPlotOptions.column, {
pointPadding: 0,
groupPadding: 0
});
seriesTypes.marimekko = extendClass(seriesTypes.column, {
type: 'marimekko',
pointArrayMap: ['y', 'z'],
parallelArrays: ['x', 'y', 'z'],
processData: function () {
var series = this;
this.totalZ = 0;
this.relZ = [];
seriesTypes.column.prototype.processData.call(this);
each(this.zData, function (z, i) {
series.relZ[i] = series.totalZ;
series.totalZ += z;
});
},
translate: function () {
var series = this,
totalZ = series.totalZ,
xAxis = series.xAxis;
seriesTypes.column.prototype.translate.call(this);
// Distort the points to reflect z dimension
each(this.points, function (point, i) {
var shapeArgs = point.shapeArgs,
relZ = series.relZ[i];
shapeArgs.x *= (relZ / totalZ) / (shapeArgs.x / xAxis.len);
shapeArgs.width *= (point.z / totalZ) / (series.pointRange / series.xAxis.max);
});
}
});
}(Highcharts));
Example: http://jsfiddle.net/highcharts/75oucp3b/

Group TopoJSON geometries by a given property

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);

Resources