-
Notifications
You must be signed in to change notification settings - Fork 328
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #333 from NREL/echarts
Replace charting library in admin
- Loading branch information
Showing
306 changed files
with
1,026 additions
and
266 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
178 changes: 100 additions & 78 deletions
178
src/api-umbrella/admin-ui/app/components/stats/drilldown/results-chart.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
], | ||
}); | ||
}, | ||
}); |
Oops, something went wrong.