Skip to content

Commit

Permalink
Merge pull request #333 from NREL/echarts
Browse files Browse the repository at this point in the history
Replace charting library in admin
  • Loading branch information
GUI authored Feb 5, 2017
2 parents 36743e3 + a945881 commit 10140f6
Show file tree
Hide file tree
Showing 306 changed files with 1,026 additions and 266 deletions.
183 changes: 183 additions & 0 deletions scripts/rake/maps.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
namespace :maps do
task :download do
[
"http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/cultural/ne_50m_admin_1_states_provinces_lakes.zip",
"http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_0_countries_lakes.zip",
"http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_0_map_units.zip",
"http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/cultural/ne_50m_admin_0_countries_lakes.zip",
"http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/cultural/ne_50m_admin_0_map_units.zip",
"http://dev.maxmind.com/static/csv/codes/iso3166.csv",
].each do |url|
path = File.join($input_dir, File.basename(url))
unless(File.exist?(path))
sh("curl", "-L", "-o", path, url)
end

if(url.end_with?(".zip"))
dir = path.chomp(".zip")
unless(Dir.exist?(dir))
sh("unzip", path, "-d", dir)
end
end
end
end

task :world do
require "csv"
require "oj"

maxmind_countries = {}
CSV.foreach(File.join($input_dir, "iso3166.csv")) do |row|
maxmind_countries[row[0]] = row[1]
end

[
"110m",
"50m",
].each do |scale|
sovereignties_path = File.join($input_dir, "tmp/world-#{scale}-sovereignties.json")
sh("ogr2ogr", "-f", "GeoJSON", "-where", "iso_a2 NOT IN('AQ')", "-t_srs", "EPSG:4326", sovereignties_path, File.join($input_dir, "ne_#{scale}_admin_0_map_units/ne_#{scale}_admin_0_map_units.shp"))

countries_path = File.join($input_dir, "tmp/world-#{scale}-countries.json")
sh("ogr2ogr", "-f", "GeoJSON", "-where", "iso_a2 NOT IN('AQ')", "-t_srs", "EPSG:4326", countries_path, File.join($input_dir, "ne_#{scale}_admin_0_countries_lakes/ne_#{scale}_admin_0_countries_lakes.shp"))

sovereignties = Oj.load(File.read(sovereignties_path))
countries = Oj.load(File.read(countries_path))

# Add countries, like United Kingdom, as a single country that are
# represented as separate sovereignties (so we align with MaxMind's
# country mappings).
sovereignties["features"] += countries["features"].find_all { |f| ["GB", "PG", "RS", "BA", "BE", "GE", "PT"].include?(f["properties"]["iso_a2"]) }

sovereignties["features"].each do |feature|
# Consider Metropolitan France the "FR" country.
if(feature["properties"]["iso_a2"] == "-99" && feature["properties"]["adm0_a3"] == "FRA")
feature["properties"]["iso_a2"] = "FR"
end
end

# Remove non-country sovereignties.
sovereignties["features"].reject! do |feature|
if(feature["properties"]["iso_a2"] == "-99")
puts "#{scale} Ignoring #{feature["properties"]["adm0_a3"]}: #{feature["properties"]["formal_en"] || feature["properties"]["name_long"]}"
true
else
false
end
end

countries_in_map = []
sovereignties["features"].each do |feature|
countries_in_map << feature["properties"]["iso_a2"]
end

# Compare the countries in the map to MaxMind's ISO3166 data to make sure
# we have all the expected countries.
missing_countries = (maxmind_countries.keys - countries_in_map).map { |k| maxmind_countries[k] }
extra_countries = (countries_in_map - maxmind_countries.keys).map { |k| maxmind_countries[k] }
puts "#{scale} Missing Countries: #{missing_countries.inspect}"
puts "#{scale} Extra Countries: #{extra_countries.inspect}"

combined_path = File.join($input_dir, "tmp/world-#{scale}-combined.json")
File.open(combined_path, "w") { |f| f.write(Oj.dump(sovereignties)) }
end

# Use the low resolution version for the globe.
FileUtils.cp(File.join($input_dir, "tmp/world-110m-combined.json"), File.join($output_dir, "world.json"))

# Use the medium resolution version to generate specific files for each
# individual country.
countries = Oj.load(File.read(File.join($input_dir, "tmp/world-50m-combined.json")))
countries["features"].each do |feature|
File.open(File.join($output_dir, "#{feature["properties"]["iso_a2"]}.json"), "w") do |f|
country = countries.dup
country["features"] = [country["features"].detect { |f| f["properties"]["iso_a2"] == feature["properties"]["iso_a2"] }]
f.write(Oj.dump(country))
end
end
end

task :us do
require "oj"

output_path = File.join($output_dir, "US.json")
FileUtils.rm_f(output_path)
sh("ogr2ogr", "-f", "GeoJSON", "-where", "iso_a2 = 'US'", "-t_srs", "EPSG:4326", output_path, File.join($input_dir, "ne_50m_admin_1_states_provinces_lakes/ne_50m_admin_1_states_provinces_lakes.shp"))

data = Oj.load(File.read(output_path))
data["features"].each do |feature|
case(feature["properties"]["iso_3166_2"])
when "US-HI"
# Remove Midway from Hawaii, since it's not one of the main islands and
# makes Hawaii's display much wider than normal.
feature["geometry"]["coordinates"].reject! { |c| c[0][0][0] < -177 }
end

File.open(File.join($output_dir, "#{feature["properties"]["iso_3166_2"]}.json"), "w") do |f|
state_data = data.dup
state_data["features"] = [state_data["features"].detect { |f| f["properties"]["iso_3166_2"] == feature["properties"]["iso_3166_2"] }]
f.write(Oj.dump(state_data))
end
end
File.open(output_path, "w") { |f| f.write(Oj.dump(data)) }
end

task :simplify do
require "oj"
require "open3"

Dir.glob(File.join($output_dir, "*.json")).each do |path|
simplify = "0.5"
if(path.end_with?("US.json"))
simplify = "0.2"
end

puts "Simplifying #{path}"
statuses = Open3.pipeline(
["geo2topo", "boundaries=#{path}"],
["toposimplify", "-P", simplify],
["topo2geo", "boundaries=#{path}"],
)
statuses.each do |status|
unless(status.success?)
puts "Simplifying failed: #{statuses.inspect}"
exit 1
end
end

data = Oj.load(File.read(path))
data["_labels"] = {}
data["features"].each do |feature|
if(File.basename(path).start_with?("US"))
code = feature["properties"]["iso_3166_2"]
else
code = feature["properties"]["iso_a2"]
end

data["_labels"][code] ||= feature["properties"]["name"]

feature["properties"] = {
"name" => code,
}
end
File.open(path, "w") { |f| f.write(Oj.dump(data, :float_precision => 9)) }
end
end

task :generate do
require "fileutils"

$input_dir = ENV["INPUT_DIR"] || File.join(API_UMBRELLA_SRC_ROOT, "build/work/maps")
FileUtils.rm_rf(File.join($input_dir, "tmp"))
FileUtils.mkdir_p(File.join($input_dir, "tmp"))

$output_dir = File.join(API_UMBRELLA_SRC_ROOT, "src/api-umbrella/admin-ui/public/maps")
FileUtils.rm_rf($output_dir)
FileUtils.mkdir_p($output_dir)

Rake::Task["maps:download"].invoke
Rake::Task["maps:world"].invoke
Rake::Task["maps:us"].invoke
Rake::Task["maps:simplify"].invoke
end
end
1 change: 0 additions & 1 deletion src/api-umbrella/admin-ui/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ module.exports = {
'_': true,
'ace': true,
'bootbox': true,
'google': true,
'inflection': true,
'jstz': true,
'marked': true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,101 +1,123 @@
import Ember from 'ember';
import echarts from 'npm:echarts';

export default Ember.Component.extend({
chartOptions: {
pointSize: 0,
lineWidth: 1,
focusTarget: 'category',
width: '100%',
chartArea: {
width: '95%',
height: '88%',
top: 0,
},
fontSize: 12,
isStacked: true,
areaOpacity: 0.2,
vAxis: {
gridlines: {
count: 4,
},
textStyle: {
fontSize: 11,
},
textPosition: 'in',
},
hAxis: {
format: 'MMM d',
baselineColor: 'transparent',
gridlines: {
color: 'transparent',
},
},
legend: {
position: 'none',
},
},

chartData: {
cols: [
{id: 'date', label: 'Date', type: 'datetime'},
{id: 'hits', label: 'Hits', type: 'number'},
],
rows: [],
},
classNames: ['stats-drilldown-results-chart'],

didInsertElement() {
google.charts.setOnLoadCallback(this.renderChart.bind(this));
this.renderChart();
},

renderChart() {
this.chart = new google.visualization.AreaChart(this.$()[0]);

// On first load, refresh the data. Afterwards the observer should handle
// refreshing.
if(!this.dataTable) {
this.refreshData();
}
this.chart = echarts.init(this.$()[0], 'api-umbrella-theme');
this.draw();

$(window).on('resize', _.debounce(this.draw.bind(this), 100));
$(window).on('resize', _.debounce(this.chart.resize, 100));
},

refreshData: Ember.observer('hitsOverTime', function() {
// Defer until Google Charts is loaded if this got called earlier from the
// observer.
if(!google || !google.visualization || !google.visualization.DataTable) {
return;
}
refreshData: Ember.on('init', Ember.observer('hitsOverTime', function() {
let data = []
let labels = [];

this.chartData = this.get('hitsOverTime');
for(let i = 0; i < this.chartData.rows.length; i++) {
this.chartData.rows[i].c[0].v = new Date(this.chartData.rows[i].c[0].v);
let hits = this.get('hitsOverTime');
for(let i = 1; i < hits.cols.length; i++) {
data.push({
name: hits.cols[i].label,
type: 'line',
sampling: 'average',
stack: 'hits',
areaStyle: {
normal: {},
},
lineStyle: {
normal: {
width: 1,
},
},
data: [],
});
}

// Show hours on the axis when viewing minutely date.
switch(this.get('controller.query.params.interval')) {
case 'minute':
this.chartOptions.hAxis.format = 'MMM d h a';
break;
default:
this.chartOptions.hAxis.format = 'MMM d';
break;
}
for(let i = 0; i < hits.rows.length; i++) {
labels.push(hits.rows[i].c[0].f);

// Show hours on the axis when viewing less than 2 days of hourly data.
if(this.get('controller.query.params.interval') === 'hour') {
let start = moment(this.get('controller.query.params.start_at'));
let end = moment(this.get('controller.query.params.end_at'));
let maxDuration = 2 * 24 * 60 * 60; // 2 days
if(end.unix() - start.unix() <= maxDuration) {
this.chartOptions.hAxis.format = 'MMM d h a';
for(let j = 1; j < hits.rows[i].c.length; j++) {
data[j - 1].data.push(hits.rows[i].c[j].v);
}
}

this.dataTable = new google.visualization.DataTable(this.chartData);
this.setProperties({
chartData: data,
chartLabels: labels,
});

this.draw();
}),
})),

draw() {
this.chart.draw(this.dataTable, this.chartOptions);
if(!this.chart || !this.get('chartData')) {
return;
}

this.chart.setOption({
tooltip: {
trigger: 'axis',
},
toolbox: {
orient: 'vertical',
iconStyle: {
emphasis: {
textPosition: 'left',
textAlign: 'right',
},
},
feature: {
saveAsImage: {
title: 'save as image',
name: 'api_umbrella_chart',
excludeComponents: ['toolbox', 'dataZoom'],
pixelRatio: 2,
},
dataZoom: {
yAxisIndex: 'none',
title: {
zoom: 'zoom',
back: 'restore zoom',
},
},
},
},
yAxis: {
type: 'value',
min: 0,
minInterval: 1,
splitNumber: 3,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: this.get('chartLabels'),
},
series: this.get('chartData'),
title: {
show: false,
},
legend: {
show: false,
},
grid: {
show: false,
left: 90,
top: 10,
right: 30,
},
dataZoom: [
{
type: 'slider',
start: 0,
end: 100,
},
],
});
},
});
Loading

0 comments on commit 10140f6

Please sign in to comment.