new: Added new statistics page, fixes #1648, fixes #1557

- brought back the quick organisation overview as it's a much missed feature
- added treemap for tags
- brought attribute histogram into statistics page

- more coming in the future
pull/1651/head
Iglocska 2016-11-04 13:14:03 +01:00
parent 1d63edc1cf
commit c76d358535
16 changed files with 571 additions and 28 deletions

View File

@ -33,7 +33,9 @@ class ACLComponent extends Component {
'add_threatconnect' => array('perm_add'),
'attributeReplace' => array('perm_add'),
'attributeStatistics' => array('*'),
'bro' => array('*'),
'checkComposites' => array('perm_admin'),
'checkOrphanedAttributes' => array(),
'delete' => array('perm_add'),
'deleteSelected' => array('perm_add'),
'describeTypes' => array('perm_add'),
@ -133,6 +135,7 @@ class ACLComponent extends Component {
'edit' => array(),
'enable' => array(),
'fetchFromFeed' => array(),
'fetchSelectedFromFreetextIndex' => array(),
'getEvent' => array(),
'index' => array(),
'previewEvent' => array(),
@ -338,6 +341,7 @@ class ACLComponent extends Component {
'request_API' => array('*'),
'routeafterlogin' => array('*'),
'statistics' => array('*'),
'tagStatisticsGraph' => array('*'),
'terms' => array('*'),
'updateLoginTime' => array('*'),
'verifyCertificate' => array(),

View File

@ -809,10 +809,6 @@ class UsersController extends AppController {
}
}
public function attributehistogram() {
//all code is called via JS
}
public function histogram($selected = null) {
if (!$this->request->is('ajax')) throw new MethodNotAllowedException('This function can only be accessed via AJAX.');
if ($selected == '[]') $selected = null;
@ -1045,7 +1041,21 @@ class UsersController extends AppController {
}
// shows some statistics about the instance
public function statistics() {
public function statistics($page = 'data') {
$this->set('page', $page);
$this->set('pages', array('data' => 'Usage data', 'orgs' => 'Organisations', 'tags' => 'Tags', 'attributehistogram' => 'Attribute histogram'));
if ($page == 'data') {
$this->__statisticsData($this->params['named']);
} else if ($page == 'orgs') {
$this->__statisticsOrgs($this->params['named']);
} else if ($page == 'tags') {
$this->__statisticsTags($this->params['named']);
} else if ($page == 'attributehistogram') {
$this->render('statistics_histogram');
}
}
private function __statisticsData($params = array()) {
// set all of the data up for the heatmaps
$orgs = $this->User->Organisation->find('all', array('fields' => array('DISTINCT (name) AS name'), 'recursive' => -1));
$this->loadModel('Log');
@ -1060,27 +1070,27 @@ class UsersController extends AppController {
$this_month = strtotime('first day of this month');
$stats[0] = $this->User->Event->find('count', null);
$stats[1] = $this->User->Event->find('count', array('conditions' => array('Event.timestamp >' => $this_month)));
$stats[2] = $this->User->Event->Attribute->find('count', array('conditions' => array('Attribute.deleted' => 0)));
$stats[3] = $this->User->Event->Attribute->find('count', array('conditions' => array('Attribute.timestamp >' => $this_month, 'Attribute.deleted' => 0)));
$this->loadModel('Correlation');
$this->Correlation->recursive = -1;
$stats[4] = $this->Correlation->find('count', null);
$stats[4] = $stats[4] / 2;
$stats[5] = $this->User->Event->ShadowAttribute->find('count', null);
$stats[6] = $this->User->find('count', null);
$stats[7] = count($orgs);
$this->loadModel('Thread');
$stats[8] = $this->Thread->find('count', array('conditions' => array('Thread.post_count >' => 0)));
$stats[9] = $this->Thread->find('count', array('conditions' => array('Thread.date_created >' => date("Y-m-d H:i:s",$this_month), 'Thread.post_count >' => 0)));
$stats[10] = $this->Thread->Post->find('count', null);
$stats[11] = $this->Thread->Post->find('count', array('conditions' => array('Post.date_created >' => date("Y-m-d H:i:s",$this_month))));
$this->set('stats', $stats);
$this->set('orgs', $orgs);
$this->set('start', strtotime(date('Y-m-d H:i:s') . ' -5 months'));
@ -1088,6 +1098,108 @@ class UsersController extends AppController {
$this->set('startDateCal', $year . ', ' . $month . ', 01');
$range = '[5, 10, 50, 100]';
$this->set('range', $range);
$this->render('statistics_data');
}
private function __statisticsOrgs($params = array()) {
$this->loadModel('Organisation');
$conditions = array();
if (!isset($params['scope']) || $params['scope'] == 'local') {
$params['scope'] = 'local';
$conditions['Organisation.local'] = 1;
} elseif ($params['scope'] == 'external') {
$conditions['Organisation.local'] = 0;
}
$orgs = array();
$orgs = $this->Organisation->find('all', array(
'recursive' => -1,
'conditions' => $conditions,
'fields' => array('id', 'name', 'description', 'local', 'contacts', 'type', 'sector', 'nationality'),
));
$orgs = Set::combine($orgs, '{n}.Organisation.id', '{n}.Organisation');
$users = $this->User->find('all', array(
'group' => 'User.org_id',
'conditions' => array('User.org_id' => array_keys($orgs)),
'recursive' => -1,
'fields' => array('org_id', 'count(*)')
));
foreach ($users as $user) {
$orgs[$user['User']['org_id']]['userCount'] = $user[0]['count(*)'];
}
unset($users);
$events = $this->User->Event->find('all', array(
'group' => 'Event.orgc_id',
'conditions' => array('Event.orgc_id' => array_keys($orgs)),
'recursive' => -1,
'fields' => array('Event.orgc_id', 'count(*)')
));
foreach ($events as $event) {
$orgs[$event['Event']['orgc_id']]['eventCount'] = $event[0]['count(*)'];
}
unset($events);
$orgs = Set::combine($orgs, '{n}.name', '{n}');
// f*** php
uksort($orgs, 'strcasecmp');
foreach ($orgs as $k => $value) {
if (file_exists(APP . 'webroot' . DS . 'img' . DS . 'orgs' . DS . $k . '.png')) {
$orgs[$k]['logo'] = true;
}
}
$this->set('scope', $params['scope']);
$this->set('orgs', $orgs);
$this->render('statistics_orgs');
}
public function tagStatisticsGraph() {
$top_ten_tags = array();
$trending_tags = array();
$all_tags = array();
$this->loadModel('EventTag');
$tags = $this->EventTag->getSortedTagList();
$this->loadModel('Taxonomy');
$taxonomies = $this->Taxonomy->find('list', array(
'conditions' => array('enabled' => true),
'fields' => array('Taxonomy.namespace')
));
$flatData = array();
foreach ($tags as $key => $value) {
$name = explode(':', $value['name']);
$tags[$key]['taxonomy'] = 'custom';
if (count($name) > 1) {
if (in_array($name[0], $taxonomies)) {
$tags[$key]['taxonomy'] = $name[0];
}
}
$flatData[$tags[$key]['taxonomy']][$value['name']] = array('name' => $value['name'], 'size' => $value['eventCount']);
}
$treemap = array(
'name' => 'tags',
'children' => array()
);
foreach ($flatData as $key => $value) {
$newElement = array(
'name' => $key,
'children' => array()
);
foreach ($value as $tag) {
$newElement['children'][] = array('name' => $tag['name'], 'size' => $tag['size']);
}
$treemap['children'][] = $newElement;
}
$taxonomyColourCodes = array();
$taxonomies = array_merge(array('custom'), $taxonomies);
$this->set('taxonomyColourCodes', $taxonomyColourCodes);
$this->set('taxonomies', $taxonomies);
$this->set('flatData', $flatData);
$this->set('treemap', $treemap);
$this->set('tags', $tags);
$this->layout = 'treemap';
$this->render('ajax/tag_statistics_graph');
}
private function __statisticsTags($params = array()) {
$this->render('statistics_tags');
}
public function verifyGPG() {

View File

@ -63,4 +63,30 @@ class EventTag extends AppModel {
}
return true;
}
public function getSortedTagList($context = false) {
$conditions = array();
$tag_counts = $this->find('all', array(
'recursive' => -1,
'fields' => array('tag_id', 'count(*)'),
'group' => array('tag_id'),
'conditions' => $conditions,
'contain' => array('Tag.name')
));
$temp = array();
$tags = array();
foreach ($tag_counts as $tag_count) {
$temp[$tag_count['Tag']['name']] = array(
'tag_id' => $tag_count['Tag']['id'],
'eventCount' => $tag_count[0]['count(*)'],
'name' => $tag_count['Tag']['name'],
);
$tags[$tag_count['Tag']['name']] = $tag_count[0]['count(*)'];
}
arsort($tags);
foreach ($tags as $k => $v) {
$tags[$k] = $temp[$k];
}
return $tags;
}
}

View File

@ -0,0 +1,11 @@
<div class="btn-toolbar">
<div class="btn-group">
<?php
foreach ($pages as $key => $value):
?>
<a class="btn <?php echo $page == $key ? 'btn-primary' : 'btn-inverse'?> qet" href="<?php echo $baseurl . '/users/statistics/' . h($key); ?>"><?php echo h($value); ?></a>
<?php
endforeach;
?>
</div>
</div>

View File

@ -88,7 +88,6 @@
<li><a href="<?php echo $baseurl;?>/pages/display/doc/quickstart">User Guide</a></li>
<li><a href="<?php echo $baseurl;?>/users/terms">Terms &amp; Conditions</a></li>
<li><a href="<?php echo $baseurl;?>/users/statistics">Statistics</a></li>
<li><a href="<?php echo $baseurl;?>/users/attributehistogram">Attribute Histogram</a></li>
</ul>
</li>

View File

@ -155,7 +155,6 @@
<li id='liuserGuide'><a href="<?php echo $baseurl;?>/pages/display/doc/general">User Guide</a></li>
<li id='literms'><a href="<?php echo $baseurl;?>/users/terms">Terms &amp; Conditions</a></li>
<li id='listatistics'><a href="<?php echo $baseurl;?>/users/statistics">Statistics</a></li>
<li id='limembers'><a href="<?php echo $baseurl;?>/users/attributehistogram">Attribute Histogram</a></li>
<?php
break;

View File

@ -0,0 +1,3 @@
<?php
echo $this->Html->css('treemap');
echo $content_for_layout;

View File

@ -0,0 +1,28 @@
<?php
echo $this->Html->script('d3');
?>
<div id="treemapSettings">
<div class="row">
<?php
foreach ($taxonomies as $k => $taxonomy):
?>
<div class="span2" style="cursor: pointer;">
<span id="<?php echo $taxonomy . '-colour'?>" class="attributehistogram-legend-box" style="display: block;float: left;margin: 4px 6px 0 0;background-color:white;">&nbsp;</span>
<span class="treemap-selector bold" data-treemap-selector="<?php echo h($taxonomy); ?>"><?php echo h($taxonomy); ?></span>
</div>
<?php
if (($k+1) % 12 == 0) {
echo '</div><div class="row">';
}
endforeach;
?>
</div>
</div>
<div id="treemapGraph"></div>
<script>
var root = <?php echo json_encode($treemap); ?>;
var flatData = <?php echo json_encode($flatData); ?>;
var taxonomies = <?php echo json_encode($taxonomies); ?>;
var hiddenTaxonomies = [];
</script>
<?php echo $this->Html->script('treemap'); ?>

View File

@ -1,14 +0,0 @@
<div class="users index">
<div id = "histogram"></div>
<?php //echo $this->element('histogram');?>
<br /><br />
</div>
<?php
echo $this->element('side_menu', array('menuList' => 'globalActions', 'menuItem' => 'members'));
?>
<script type="text/javascript">
// tooltips
$(document).ready(function () {
updateHistogram('');
});
</script>

View File

@ -0,0 +1,128 @@
<?php
echo $this->Html->script('d3');
echo $this->Html->script('cal-heatmap');
echo $this->Html->css('cal-heatmap');
?>
<div class = "index">
<h2>Statistics</h2>
<?php
echo $this->element('Users/statisticsMenu');
?>
<p>Some statistics about this instance. The changes since the beginning of this month are noted in brackets wherever applicable</p>
<div style="width:250px;">
<dl>
<dt>Events</dt>
<dd><?php echo h($stats[0]);
if ($stats[1]) echo ' <span style="color:green">(+' . h($stats[1]) . ')</span>&nbsp;';
else echo ' <span style="color:red">(0)</span>&nbsp;';?>
</dd>
<dt><?php echo 'Attributes'; ?></dt>
<dd><?php echo h($stats[2]);
if ($stats[1]) echo ' <span style="color:green">(+' . h($stats[3]) . ')</span>&nbsp;';
else echo ' <span style="color:red">(0)</span>&nbsp;';?>
</dd>
<dt><?php echo 'Correlations found'; ?></dt>
<dd><?php echo h($stats[4]); ?>&nbsp;</dd>
<dt><?php echo 'Proposals active'; ?></dt>
<dd><?php echo h($stats[5]); ?>&nbsp;</dd>
<dt><?php echo 'Users'; ?></dt>
<dd><?php echo h($stats[6]); ?>&nbsp;</dd>
<dt><?php echo 'Organisations'; ?></dt>
<dd><?php echo h($stats[7]); ?>&nbsp;</dd>
<dt><?php echo 'Discussion threads'; ?></dt>
<dd><?php echo h($stats[8]);
if ($stats[9]) echo ' <span style="color:green">(+' . h($stats[9]) . ')</span>&nbsp;';
else echo ' <span style="color:red">(0)</span>&nbsp;';?>
</dd>
<dt><?php echo 'Discussion posts'; ?></dt>
<dd><?php echo h($stats[10]);
if ($stats[11]) echo ' <span style="color:green">(+' . h($stats[11]) . ')</span>&nbsp;';
else echo ' <span style="color:red">(0)</span>&nbsp;';?>
</dd>
</dl>
</div>
<br />
<h3>Activity Heatmap</h3>
<p>A heatmap showing user activity for each day during this month and the 4 months that preceded it. Use the buttons below to only show the heatmap of a specific organisation.</p>
<div id="orgs">
<select onchange="updateCalendar(this.options[this.selectedIndex].value);">
<option value="all">All organisations</option>
<?php
foreach ($orgs as $org):
?>
<option value="<?php echo h($org['Organisation']['name']); ?>"><?php echo h($org['Organisation']['name']); ?></option>
<?php
endforeach;
?>
</select>
</div>
<div>
<table>
<tr>
<td style="vertical-align:top;">
<div style="margin-right:5px;margin-top:40px;"><button id="goLeft" class="btn" onClick="goLeft()"><span class="icon-arrow-left"></span></button></div>
</td>
<td>
<div id="cal-heatmap"></div>
</td>
<td style="vertical-align:top;">
<div style="margin-left:5px;margin-top:40px;"><button id="goRight" class="btn" onClick="goRight()"><span class="icon-arrow-right"></span></button></div>
</td>
</tr>
</table>
</div>
<script type="text/javascript">
var cal = new CalHeatMap();
var orgSelected = "all";
cal.init({
range: 5,
domain:"month",
subDomain:"x_day",
start: new Date(<?php echo $startDateCal; ?>),
data: "<?php echo Configure::read('MISP.baseurl'); ?>/logs/returnDates.json",
highlight: "now",
domainDynamicDimension: false,
cellSize: 20,
cellPadding: 1,
domainGutter: 10,
legend: <?php echo $range;?>,
legendCellSize: 15,
});
function updateCalendar(org) {
if (org == "all") {
cal.update("<?php echo Configure::read('MISP.baseurl'); ?>/logs/returnDates/all.json");
orgSelected = "all";
} else {
cal.update("<?php echo Configure::read('MISP.baseurl'); ?>/logs/returnDates/"+org+".json");
orgSelected = org;
}
}
function goRight() {
cal.options.data = "<?php echo Configure::read('MISP.baseurl'); ?>/logs/returnDates/"+orgSelected+".json";
cal.next();
}
function goLeft() {
cal.options.data = "<?php echo Configure::read('MISP.baseurl'); ?>/logs/returnDates/"+orgSelected+".json";
cal.previous();
}
</script>
<?php
if (preg_match('/(?i)msie [2-9]/',$_SERVER['HTTP_USER_AGENT']) && !strpos($_SERVER['HTTP_USER_AGENT'], 'Opera')) {
if (preg_match('%(?i)Trident/(.*?).0%', $_SERVER['HTTP_USER_AGENT'], $matches) && isset($matches[1]) && $matches[1] > 5) {
?>
<br /><br /><p style="color:red;font-size:11px;">The above graph will not work correctly in Compatibility mode. Please make sure that it is disabled in your Internet Explorer settings.</p>
<?php
} else {
?>
<br /><br /><p style="color:red;font-size:11px;">The above graph will not work correctly on Internet Explorer 9.0 and earlier. Please download Chrome, Firefox or upgrade to a newer version of Internet Explorer.</p>
<?php
}
}
?>
</div>
<?php
echo $this->element('side_menu', array('menuList' => 'globalActions', 'menuItem' => 'statistics'));
?>

View File

@ -0,0 +1,18 @@
<?php
echo $this->Html->script('d3');
echo $this->Html->script('cal-heatmap');
echo $this->Html->css('cal-heatmap');
?>
<div class = "index">
<h2>Statistics</h2>
<?php echo $this->element('Users/statisticsMenu'); ?>
<div id = "histogram"></div>
</div>
<?php
echo $this->element('side_menu', array('menuList' => 'globalActions', 'menuItem' => 'statistics'));
?>
<script type="text/javascript">
$(document).ready(function () {
updateHistogram('');
});
</script>

View File

@ -0,0 +1,72 @@
<?php
echo $this->Html->script('d3');
echo $this->Html->script('cal-heatmap');
echo $this->Html->css('cal-heatmap');
?>
<div class = "index">
<h2>Statistics</h2>
<?php
echo $this->element('Users/statisticsMenu');
$types = array(
'local' => array('selected' => false, 'text' => 'Local organisations'),
'external' => array('selected' => false, 'text' => 'Known remote organisations'),
'all' => array('selected' => false, 'text' => 'All organisations')
);
$types[$scope]['selected'] = true;
?>
<p>Quick overview over the organisations residing on or known by this instance.</p>
<div class="tabMenuFixedContainer" style="display:inline-block;">
<?php
foreach ($types as $key => $value):
?>
<span class="tabMenuFixed tabMenuFixedCenter tabMenuSides useCursorPointer <?php if ($value['selected']) echo 'tabMenuActive'; ?>" onclick="window.location='/users/statistics/orgs/scope:<?php echo h($key);?>'"><?php echo h($value['text']); ?></span>
<?php
endforeach;
?>
</div>
<table class="table table-striped table-hover table-condensed" style="width:50%;">
<tr>
<th>Name</th>
<th>Logo</th>
<th>Users</th>
<th>Events</th>
<th>Nationality</th>
<th>Type</th>
<th>Sector</th>
</tr>
<?php
foreach ($orgs as $data):
?>
<tr class="org_row" data-orgid="<?php echo h($data['id']); ?>">
<td class="short"><?php echo h($data['name']); ?></td>
<td class="short">
<?php
if (isset($data['logo'])):
?>
<img src="<?php echo $baseurl; ?>/img/orgs/<?php echo h($data['name']); ?>.png" height="24" width="24" />
<?php
else:
echo '&nbsp;';
endif;
?>
</td>
<td class="short"><span class="<?php echo isset($data['userCount']) ? 'blue bold' : 'grey'; ?>"><?php echo isset($data['userCount']) ? h($data['userCount']) : '0';?></span></td>
<td class="short"><span class="<?php echo isset($data['eventCount']) ? 'blue bold' : 'grey'; ?>"><?php echo isset($data['eventCount']) ? h($data['eventCount']) : '0';?></span></td>
<td class="shortish"><?php echo isset($data['nationality']) && $data['nationality'] !== 'Not specified' ? h($data['nationality']) : '&nbsp;'; ?></td>
<td class="shortish"><?php echo isset($data['type']) ? h($data['type']) : '&nbsp;'; ?></td>
<td class="shortish"><?php echo isset($data['sector']) ? h($data['sector']) : '&nbsp;'; ?></td>
</tr>
<?php
endforeach;
?>
</table>
</div>
<?php
echo $this->element('side_menu', array('menuList' => 'globalActions', 'menuItem' => 'statistics'));
?>
<script type="text/javascript">
$('.org_row').click(function() {
window.location = "<?php echo $baseurl; ?>/organisations/view/" + $(this).data('orgid');
});
</script>

View File

@ -0,0 +1,21 @@
<?php
echo $this->Html->script('d3');
echo $this->Html->script('cal-heatmap');
echo $this->Html->css('cal-heatmap');
?>
<div class = "index">
<h2>Statistics</h2>
<?php
echo $this->element('Users/statisticsMenu');
?>
<p>A treemap of the currently used event tags. Click on any of the taxonomies to hide it and click it again to show it.</p>
<div id="treemapdiv" class="treemapdiv"></div>
</div>
<?php
echo $this->element('side_menu', array('menuList' => 'globalActions', 'menuItem' => 'statistics'));
?>
<script type="text/javascript">
$(document).ready(function () {
loadTagTreemap();
});
</script>

View File

@ -0,0 +1,35 @@
@CHARSET "UTF-8";
.treemapdiv {
margin: 0px;
}
#treemapSettings {
margin-top:10px;
margin-bottom:10px;
border: 1px solid black;
width: 960px;
}
#taxonomydt {
width: 20px;
}
#taxonomydd {
width: 100%;
}
#treemapGraph {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
width: 960px;
}
.node {
border: solid 1px white;
font: 10px sans-serif;
line-height: 12px;
overflow: hidden;
position: absolute;
text-indent: 2px;
}

View File

@ -2638,3 +2638,21 @@ function checkOrphanedAttributes() {
url: "/attributes/checkOrphanedAttributes/",
});
}
function loadTagTreemap() {
$.ajax({
async:true,
beforeSend: function (XMLHttpRequest) {
$(".loading").show();
},
success:function (data, textStatus) {
$(".treemapdiv").html(data);
},
complete:function() {
$(".loading").hide();
},
type:"get",
cache: false,
url: "/users/tagStatisticsGraph",
});
}

83
app/webroot/js/treemap.js Normal file
View File

@ -0,0 +1,83 @@
var margin = {top: 0, right: 0, bottom: 0, left: 0},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var color = d3.scale.category20c();
var treemap = d3.layout.treemap()
.size([width, height])
.sticky(true)
.value(function(d) { return d.size; });
var div = d3.select("#treemapGraph").append("div")
.style("position", "relative")
.style("width", (width + margin.left + margin.right) + "px")
.style("height", (height + margin.top + margin.bottom) + "px")
.style("left", margin.left + "px")
.style("top", margin.top + "px");
var node = div.datum(root).selectAll(".node")
.data(treemap.nodes)
.enter().append("div")
.attr("class", "node")
.attr("title", function(d) {return d.name + ': ' + d.size})
.attr("id", function(d) { return d.name + '-node'})
.call(position)
.style("background", function(d) { return d.children ? color(d.name) : null; })
.text(function(d) { return d.children ? null : d.name; });
taxonomies.forEach(function(taxonomy) {
$("#" + taxonomy + "-colour").css("background-color", $("#" + taxonomy + "-node").css('background-color'));
});
d3.selectAll("input").on("change", function change() {
var value = this.value === "count" ? function() { return 1; } : function(d) { return d.size; };
node
.data(treemap.value(value).nodes)
.transition()
.duration(1500)
.call(position);
});
function position() {
this.style("left", function(d) { return d.x + "px"; })
.style("top", function(d) { return d.y + "px"; })
.style("width", function(d) { return Math.max(0, d.dx - 1) + "px"; })
.style("height", function(d) { return Math.max(0, d.dy - 1) + "px"; });
}
function updateTaxonomies() {
var value = function(d) {
tagTaxonomy = d.name.split(':')[0];
if (taxonomies.indexOf(tagTaxonomy) == -1) {
tagTaxonomy = 'custom';
}
if ($.inArray(tagTaxonomy, hiddenTaxonomies) > -1) {
return 0;
} else {
return flatData[tagTaxonomy][d.name]['size'];
}
};
node
.data(treemap.value(value).nodes)
.transition()
.duration(1500)
.call(position);
}
$('.treemap-selector').click(function() {
var taxonomy = $( this ).data("treemap-selector");
var index = hiddenTaxonomies.indexOf(taxonomy);
if ($( this ).hasClass("bold")) {
$( this ).removeClass("bold");
if (index < 0) {
hiddenTaxonomies.push(taxonomy);
}
} else {
$( this ).addClass("bold");
if (index > -1) {
hiddenTaxonomies.splice(index, 1);
}
}
updateTaxonomies();
});