2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements. See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership. The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License. You may obtain a copy of the License at
10 http://www.apache.org/licenses/LICENSE-2.0
12 Unless required by applicable law or agreed to in writing,
13 software distributed under the License is distributed on an
14 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 KIND, either express or implied. See the License for the
16 specific language governing permissions and limitations
20 --[[ mod_lua implementation of the server-status page ]]
21 local ssversion = "0.11" -- verion of this script
22 local redact_ips = true -- whether to replace the last two bits of every IP with 'x.x'
23 local warning_banner = [[
24 <div style="float: left; color: #222; margin-bottom: 8px; margin-top: 24px; text-align: center; width: 200px; font-size: 0.7rem; border: 1px dashed #333; background: #F8C940;">
25 <h3 style="margin: 4px; font-size: 1rem;">Don't be alarmed - this page is here for a reason!</h3>
26 <p style="font-weight: bolder; font-size: 0.8rem;">This is an example server status page for the Apache HTTP Server. Nothing on this server is secret, no URL tokens, no sensitive passwords. Everything served from here is static data.</p>
29 local show_warning = true -- whether to display the above warning/notice on the page
30 local show_modules = false -- Whether to list loaded modules or not
31 local show_threads = true -- whether to list thread information or not
33 -- pre-declare some variables defined at the bottom of this script:
34 local status_js, status_css, quokka_js
36 -- quick and dirty JSON conversion
37 local function quickJSON(input)
38 if type(input) == "table" then
40 for k, v in pairs(input) do
41 if type(k) ~= "number" then
50 for k, v in pairs(input) do
51 local kv = ([["%s": %s]]):format(k, quickJSON(v))
54 return "{" .. table.concat(tbl, ", ") .. "}"
57 for k, v in pairs(input) do
58 table.insert(tbl, quickJSON(v))
60 return "[" .. table.concat(tbl, ", ") .. "]"
62 elseif type(input) == "string" then
63 return ([["%s"]]):format(input:gsub('"', '\\"'):gsub("[\r\n\t]", " "))
64 elseif type(input) == "number" then
65 return tostring(input)
66 elseif type(input) == "boolean" then
67 return (input and "true" or "false")
73 -- Module information callback
74 local function modInfo(r, modname)
80 <meta charset="utf-8">
86 <title>Module information</title>
91 r:puts( ("<h3>Details for module %s</h3>\n"):format(r:escape_html(modname)) )
92 -- Queries the server for information about a module
93 local mod = r.module_info(modname)
95 for k, v in pairs(mod.commands) do
96 -- print out all directives accepted by this module
97 r:puts( ("<b>%s:</b> %s<br>\n"):format(r:escape_html(k), v))
108 -- Function for generating server stats
109 function getServerState(r, verbose)
113 type = "prefork", -- default to prefork until told otherwise
116 maxServers = r.mpm_query(12),
119 if r.mpm_query(14) == 1 then
120 state.mpm.type = "event" -- this is event mpm
121 elseif r.mpm_query(3) >= 1 then
122 state.mpm.type = "worker" -- it's not event, but it's threaded, we'll assume worker mpm (could be motorz??)
123 elseif r.mpm_query(2) == 1 then
124 state.mpm.type = "winnt" -- it's threaded, but not worker nor event, so it's probably winnt
126 if state.mpm.type ~= "prefork" then
127 state.mpm.threaded = true -- it's threaded
128 state.mpm.threadsPerChild = r.mpm_query(6) -- get threads per child proc
131 state.processes = {} -- list of child procs
132 state.connections = { -- overall connection info
136 -- overall server stats
140 built = r.server_built,
141 localtime = os.time(),
142 uptime = os.time() - r.started,
144 host = r.server_name,
146 extended = show_threads, -- whether extended status is available or not
149 -- if show_modules is true, add list of modules to the JSON
151 state.server.modules = {}
152 for k, module in pairs(r:loaded_modules()) do
153 table.insert(state.server.modules, module)
157 -- Fetch process/thread data
158 for i=0,state.mpm.maxServers-1,1 do
159 local server = r.scoreboard_process(r, i);
171 state.connections.idle = state.connections.idle + (server.keepalive or 0)
173 if server.pid > 0 then
174 state.mpm.activeServers = state.mpm.activeServers + 1
178 for j = 0, state.mpm.threadsPerChild-1, 1 do
179 local worker = r.scoreboard_worker(r, i, j)
181 s.stime = s.stime + (worker.stimes or 0);
182 s.utime = s.utime + (worker.utimes or 0);
183 if verbose and show_threads then
184 s.threads = s.threads or {}
185 table.insert(s.threads, {
186 bytes = worker.bytes_served,
187 thread = ("0x%x"):format(worker.tid),
188 client = redact_ips and (worker.client or "???"):gsub("[a-f0-9]+[.:]+[a-f0-9]+$", "x.x") or worker.client or "???",
189 cost = ((worker.utimes or 0) + (worker.stimes or 0)),
190 count = worker.access_count,
191 vhost = worker.vhost:gsub(":%d+", ""),
192 request = worker.request,
193 last_used = math.floor(worker.last_used/1000000)
196 state.server.connections = state.server.connections + worker.access_count
197 s.bytes = s.bytes + worker.bytes_served
198 s.connections = s.connections + worker.access_count
199 if server.pid > 0 then
200 tstates[worker.status] = (tstates[worker.status] or 0) + 1
207 keepalive = (server.keepalive > 0) and server.keepalive or tstates[5] or 0,
208 closing = tstates[8] or 0,
209 idle = tstates[2] or 0,
210 writing = tstates[4] or 0,
211 reading = tstates[3] or 0,
212 graceful = tstates[9] or 0
214 table.insert(state.processes, s)
215 state.server.bytes = state.server.bytes + s.bytes
216 state.connections.active = state.connections.active + (tstates[8] or 0) + (tstates[4] or 0) + (tstates[3] or 0)
225 -- Parse GET data, if any, and set content type
226 local GET = r:parseargs()
228 if GET['module'] then
229 modInfo(r, GET['module'])
234 -- If we only need the stats feed, compact it and hand it over
235 if GET['view'] and GET['view'] == "json" then
236 local state = getServerState(r, GET['extended'] == 'true')
237 r.content_type = "application/json"
238 r:puts(quickJSON(state))
242 if not GET['resource'] then
244 local state = getServerState(r, show_threads)
246 -- Print out the HTML for the front page
247 r.content_type = "text/html"
252 <meta charset="utf-8">
254 <link href="?resource=css" rel="stylesheet">
257 <script type="text/javascript" src="?resource=js"></script>
259 <title>Server status for %s</title>
262 <body onload="refreshCharts(false);">
263 <div class="wrapper" id="wrapper">
264 <div class="navbarLeft">
265 <img align='absmiddle' src='?resource=feather' width="15" height="30"/>
268 <div class="navbarRight">Status for %s on %s</div>
269 <div style="clear: both;"></div>
270 <div class="serverinfo" id="leftpane">
273 <a class="btn active" id="dashboard_button" href="javascript:void(showPanel('dashboard'));">Dashboard</a>
276 <a class="btn" id="misc_button" href="javascript:void(showPanel('misc'));">Server Info</a>
279 <a class="btn" id="threads_button" style="display: none;" href="javascript:void(showPanel('threads'));">Show thread information</a>
282 <a class="btn" id="modules_button" style="display: none;" href="javascript:void(showPanel('modules'));">Show loaded modules</a>
286 <!-- warning --> %s <!-- /warning -->
291 <div class="charts" id="dashboard_panel">
293 <div class="infobox_wrapper" style="clear: both; width: 100%%;">
294 <div class="infobox_title">Quick Stats</div>
295 <div class="infobox" id="general_stats">
298 <div class="infobox_wrapper" style="width: 100%%;">
299 <div class="infobox_title">Charts</div>
300 <div class="infobox">
301 <!--Div that will hold the pie chart-->
302 <canvas id="actions_div" width="1400" height="400" class="canvas_wide"></canvas>
303 <canvas id="status_div" width=580" height="400" class="canvas_narrow"></canvas>
304 <canvas id="traffic_div" width="1400" height="400" class="canvas_wide"></canvas>
305 <canvas id="idle_div" width="580" height="400" class="canvas_narrow"></canvas>
306 <canvas id="connection_div" width="1400" height="400" class="canvas_wide"></canvas>
307 <canvas id="cpu_div" width="580" height="400" class="canvas_narrow"></canvas>
308 <div style="clear: both"></div>
313 <!-- misc server info -->
314 <div class="charts" id="misc_panel" style="display: none;">
315 <div class="infobox_wrapper" style="clear: both; width: 100%%;">
316 <div class="infobox_title">General server information</div>
317 <div class="infobox" style='padding: 16px; width: calc(100%% - 32px);' id="server_breakdown">
323 <div class="charts" id="threads_panel" style="display: none;">
324 <div class="infobox_wrapper" style="clear: both; width: 100%%;">
325 <div class="infobox_title">Thread breakdown</div>
326 <div class="infobox" style='padding: 16px; width: calc(100%% - 32px);' id="threads_breakdown">
332 <div class="charts" id="modules_panel" style="display: none;">
333 <div class="infobox_wrapper" style="clear: both; width: 100%%;">
334 <div class="infobox_title">Modules loaded</div>
335 <div class="infobox" style='padding: 16px; width: calc(100%% - 32px);' id="modules_breakdown">
349 show_warning and warning_banner or ""
357 -- Resource documents (CSS, JS, PNG)
358 if GET['resource'] == 'js' then
359 r.content_type = "application/javascript"
362 elseif GET['resource'] == 'css' then
363 r.content_type = "text/css"
365 elseif GET['resource'] == 'feather' then
366 r.content_type = "image/png"
367 r:write(r:base64_decode('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'))
374 ------------------------------------
375 -- JavaScript and CSS definitions --
376 ------------------------------------
378 -- Set up some JavaScripts:
380 Number.prototype.pad = function(size) {
381 var str = String(this);
382 while (str.length < size) {
388 function getAsync(theUrl, xstate, callback) {
390 if (window.XMLHttpRequest) {
391 xmlHttp = new XMLHttpRequest();
393 xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
395 xmlHttp.open("GET", theUrl, true);
397 xmlHttp.onreadystatechange = function(state) {
398 if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
400 callback(JSON.parse(xmlHttp.responseText));
407 var actionCache = [];
408 var connectionCache = [];
409 var trafficCache = [];
412 var lastConnections = 0;
413 var negativeBytes = 0; // cache for proc reloads, which skews traffic
414 var updateSpeed = 5; // How fast do charts update?
415 var maxRecords = 24; // How many records to show per chart
416 var cpumax = 1000000; // random cpu max(?)
418 function refreshCharts(json, state) {
419 if (json && json.processes) {
423 // general server info box
424 var gs = document.getElementById('server_breakdown');
426 gs.innerHTML += "<b>Server version: </b>" + json.server.version + "<br/>";
427 gs.innerHTML += "<b>Server built: </b>" + json.server.built + "<br/>";
428 gs.innerHTML += "<b>Server MPM: </b>" + json.mpm.type + " <span id='mpminfo'></span><br/>";
432 var now = new Date();
433 var ts = now.getHours().pad(2) + ":" + now.getMinutes().pad(2) + ":" + now.getSeconds().pad(2);
438 // Construct state based on proc details
448 for (var i in json.processes) {
449 var proc = json.processes[i];
451 state.closing += proc.workerStates.closing||0;
452 state.idle += proc.workerStates.idle||0;
453 state.writing += proc.workerStates.writing||0;
454 state.reading += proc.workerStates.reading||0;
455 state.keepalive += proc.workerStates.keepalive||0;
456 state.graceful += proc.workerStates.graceful||0;
462 // Push action state entry into action cache with timestamp
463 // Shift if more than 10 entries in cache
464 actionCache.push(state);
465 if (actionCache.length > maxRecords) {
469 // construct array for QuokkaLines
471 for (var i in actionCache) {
472 var el = actionCache[i];
473 if (json.mpm.type == 'event') {
474 arr.push([el.timestamp, el.closing, el.idle, el.writing, el.reading, el.graceful]);
476 arr.push([el.timestamp, el.keepalive, el.closing, el.idle, el.writing, el.reading, el.graceful]);
479 var states = ['Keepalive', 'Closing', 'Idle', 'Writing', 'Reading', 'Graceful']
480 if (json.mpm.type == 'event') {
482 if (document.getElementById('mpminfo')) {
483 document.getElementById('mpminfo').innerHTML = "(" + fn(parseInt(json.connections.idle)) + " connections in idle keepalive)";
487 quokkaLines("actions_div", states, arr, { lastsum: true, hires: true, nosum: true, stack: true, curve: true, title: "Thread states" } );
490 // Get traffic, figure out how much it was this time (0 if just started!)
491 var bytesThisTurn = 0;
492 var connectionsThisTurn = 0;
493 for (var i in json.processes) {
494 var proc = json.processes[i];
496 // if we haven't seen this proc before, ignore its bytes first time
497 if (!processes[pid]) {
500 connections: proc.connections,
503 bytesThisTurn += proc.bytes - processes[pid].bytes;
505 x = proc.connections - processes[pid].connections;
506 connectionsThisTurn += (x > 0) ? x : 0;
508 processes[pid].bytes = proc.bytes;
509 processes[pid].connections = proc.connections;
513 if (lastBytes == 0 ) {
518 // Push a new element into cache, prune cache
521 bytes: bytesThisTurn/updateSpeed
523 trafficCache.push(el);
524 if (trafficCache.length > maxRecords) {
525 trafficCache.shift();
528 // construct array for QuokkaLines
530 for (var i in trafficCache) {
531 var el = trafficCache[i];
532 arr.push([el.timestamp, el.bytes]);
535 quokkaLines("traffic_div", ['Traffic'], arr, { traffic: true, hires: true, nosum: true, stack: true, curve: true, title: "Traffic per second" } );
538 // Get connections per second
539 // Push a new element into cache, prune cache
542 connections: (connectionsThisTurn+1)/updateSpeed
544 connectionCache.push(el);
545 if (connectionCache.length > maxRecords) {
546 connectionCache.shift();
549 // construct array for QuokkaLines
551 for (var i in connectionCache) {
552 var el = connectionCache[i];
553 arr.push([el.timestamp, el.connections]);
555 // Draw connection chart
556 quokkaLines("connection_div", ['Connections/sec'], arr, { traffic: false, hires: true, nosum: true, stack: true, curve: true, title: "Connections per second" } );
560 quokkaCircle("status_div", [
561 { title: 'Active', value: (json.mpm.threadsPerChild*json.mpm.activeServers)},
562 { title: 'Reserve', value: (json.mpm.threadsPerChild*(json.mpm.activeServers-json.mpm.maxServers))}
564 { title: "Worker pool", hires: true});
566 // Idle vs active connections
567 var idlecons = json.connections.idle;
568 var activecons = json.connections.active;
569 quokkaCircle("idle_div", [
570 { title: 'Idle', value: idlecons},
571 { title: 'Active', value: activecons},
573 { hires: true, title: "Idle vs active connections"});
577 while ( (stime+utime) > cpumax ) {
581 quokkaCircle("cpu_div", [
582 { title: 'Idle', value: (cpumax - stime - utime) / (cpumax/100)},
583 { title: 'System', value: stime/(cpumax/100)},
584 { title: 'User', value: utime/(cpumax/100)}
586 { hires: true, title: "CPU usage", pct: true});
593 // General stats infobox
594 var gstats = document.getElementById('general_stats');
595 gstats.innerHTML = ''; // wipe the box
597 // Days since restart
598 var u_f = Math.floor(json.server.uptime/8640.0) / 10;
599 var u_d = Math.floor(json.server.uptime/86400);
600 var u_h = Math.floor((json.server.uptime%86400)/3600);
601 var u_m = Math.floor((json.server.uptime%3600)/60);
602 var u_s = Math.floor(json.server.uptime %60);
603 var str = u_d + " day" + (u_d != 1 ? "s, " : ", ") + u_h + " hour" + (u_h != 1 ? "s, " : ", ") + u_m + " minute" + (u_m != 1 ? "s" : "");
604 var ubox = document.createElement('div');
605 ubox.setAttribute("class", "statsbox");
606 ubox.innerHTML = "<span style='font-size: 2rem;'>" + u_f + " days</span><br/><i>since last (re)start.</i><br/><small>" + str;
607 gstats.appendChild(ubox);
610 // Bytes transferred in total
611 var MB = fnmb(json.server.bytes);
612 var KB = (json.server.bytes > 0) ? fnmb(json.server.bytes/json.server.connections) : 0;
613 var KBs = fnmb(json.server.bytes/json.server.uptime);
614 var mbbox = document.createElement('div');
615 mbbox.setAttribute("class", "statsbox");
616 mbbox.innerHTML = "<span style='font-size: 2rem;'>" + MB + "</span><br/><i>transferred in total.</i><br/><small>" + KBs + "/sec, " + KB + "/request";
617 gstats.appendChild(mbbox);
619 // connections in total
620 var cons = fn(json.server.connections);
621 var cps = Math.floor(json.server.connections/json.server.uptime*100)/100;
622 var conbox = document.createElement('div');
623 conbox.setAttribute("class", "statsbox");
624 conbox.innerHTML = "<span style='font-size: 2rem;'>" + cons + " conns</span><br/><i>since server started.</i><br/><small>" + cps + " requests per second";
625 gstats.appendChild(conbox);
628 var tpc = json.mpm.threadsPerChild;
629 var activeThreads = fn(json.mpm.activeServers * json.mpm.threadsPerChild);
630 var maxThreads = json.mpm.maxServers * json.mpm.threadsPerChild;
631 var tbox = document.createElement('div');
632 tbox.setAttribute("class", "statsbox");
633 tbox.innerHTML = "<span style='font-size: 2rem;'>" + activeThreads + " threads</span><br/><i>currently at work (" + json.mpm.activeServers + "x" + tpc+" threads).</i><br/><small>" + maxThreads + " (" + json.mpm.maxServers + "x"+tpc+") threads allowed.";
634 gstats.appendChild(tbox);
638 window.setTimeout(waitTwo, updateSpeed*1000);
641 document.getElementById('leftpane').style.height = document.getElementById('wrapper').getBoundingClientRect().height + "px";
643 // Do we have extended info and module lists??
644 if (json.server.extended) document.getElementById('threads_button').style.display = 'block';
645 if (json.server.modules && json.server.modules.length > 0) {
646 var panel = document.getElementById('modules_breakdown');
648 for (var i in json.server.modules) {
649 var mod = json.server.modules[i];
650 list += "<li>" + mod + "</li>";
653 panel.innerHTML = list;
655 document.getElementById('modules_button').style.display = 'block';
659 } else if (json === false) {
664 function refreshThreads(json, state) {
665 var box = document.getElementById('threads_breakdown');
667 for (var i in json.processes) {
668 var proc = json.processes[i];
669 var phtml = '<div style="color: #DDF">';
670 if (!proc.active) phtml = '<div title="this process is inactive" style="color: #999;">';
671 phtml += "<h3>Process " + i + ":</h3>";
672 phtml += "<b>PID:</b> " + (proc.pid||"None (not active)") + "<br/>";
673 if (proc.threads && proc.active) {
674 phtml += "<table style='width: 800px; color: #000;'><tr><th>Thread ID</th><th>Access count</th><th>Bytes served</th><th>Last Used</th><th>Last client</th><th>Last request</th></tr>";
675 for (var j in proc.threads) {
676 var thread = proc.threads[j];
677 thread.request = (thread.request||"(Unknown)").replace(/[<>]+/g, "");
678 phtml += "<tr><td>"+thread.thread+"</td><td>"+thread.count+"</td><td>"+thread.bytes+"</td><td>"+thread.last_used+"</td><td>"+thread.client+"</td><td>"+thread.request+"</td></tr>";
682 phtml += "<p>No thread information avaialable</p>";
685 box.innerHTML += phtml;
690 getAsync(location.href + "?view=json&rnd=" + Math.random(), null, refreshCharts)
693 function showPanel(what) {
694 var items = ['dashboard','misc','threads','modules'];
695 for (var i in items) {
697 var btn = document.getElementById(item+'_button');
698 var panel = document.getElementById(item+'_panel');
700 btn.setAttribute("class", "btn active");
701 panel.style.display = 'block';
703 btn.setAttribute("class", "btn");
704 panel.style.display = 'none';
708 // special constructors
709 if (what == 'threads') {
710 getAsync(location.href + "?view=json&extended=true&rnd=" + Math.random(), null, refreshThreads)
716 num = num.replace(/(\d)(\d{9})$/, '$1,$2');
717 num = num.replace(/(\d)(\d{6})$/, '$1,$2');
718 num = num.replace(/(\d)(\d{3})$/, '$1,$2');
726 if (num > 1024) { add = "KB"; mul= 1024; }
727 if (num > (1024*1024)) { add = "MB"; mul= 1024*1024; }
728 if (num > (1024*1024*1024)) { add = "GB"; mul= 1024*1024*1024; }
729 if (num > (1024*1024*1024*1024)) { add = "TB"; mul= 1024*1024*1024*1024; }
731 if (add != "bytes") {
732 dec = "." + Math.floor( (num - Math.floor(num)) * 100 );
734 return ( fn(Math.floor(num)) + dec + " " + add );
739 var sort_reverse = false;
740 var sortWay = a.getAttribute("sort_" + b);
741 if (sortWay && sortWay == "forward") {
742 a.setAttribute("sort_" + b, "reverse");
746 a.setAttribute("sort_" + b, "forward");
751 d=a.rows[1].cells.length;
759 cell_text=a.rows[h].cells[i].textContent;
760 if(cell_text===undefined){cell_text=a.rows[h].cells[i].innerText;}
767 if(b!=lastcol) lastseq="A";
769 if(lastseq=="A") lastseq="D";
781 var gt = (m[b]>n[b]) ? true : false;
782 var lt = (m[b]<n[b]) ? true : false;
783 if (n[b].match(/^(\d+)$/)) { gt = parseInt(m[b], 10) > parseInt(n[b], 10) ? true : false; lt = parseInt(m[b], 10) < parseInt(n[b], 10) ? true : false; }
784 if (sort_reverse) {gt = (!gt); lt = (!lt);}
807 if(a.rows[f].cells[i].innerText!==undefined){
808 a.rows[f].cells[i].innerText=m[i];
811 a.rows[f].cells[i].textContent=m[i];
820 var CPUmax = 1000000;
824 function showDetails() {
825 for (i=1; i < 1000; i++) {
826 var obj = document.getElementById("srv_" + i);
828 if (showing) { obj.style.display = "none"; }
829 else { obj.style.display = "block"; }
832 var link = document.getElementById("show_link");
833 showing = (!showing);
834 if (showing) { link.innerHTML = "Hide thread information"; }
835 else { link.innerHTML = "Show thread information"; }
838 var showing_modules = false;
839 function show_modules() {
841 var obj = document.getElementById("modules");
843 if (showing_modules) { obj.style.display = "none"; }
844 else { obj.style.display = "block"; }
846 var link = document.getElementById("show_modules_link");
847 showing_modules = (!showing_modules);
848 if (showing_modules) { link.innerHTML = "Hide loaded modules"; }
849 else { link.innerHTML = "Show loaded modules"; }
856 * Licensed under the Apache License, Version 2.0 (the "License");
857 * you may not use this file except in compliance with the License.
858 * You may obtain a copy of the License at
860 * http://www.apache.org/licenses/LICENSE-2.0
862 * Unless required by applicable law or agreed to in writing, software
863 * distributed under the License is distributed on an "AS IS" BASIS,
864 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
865 * See the License for the specific language governing permissions and
866 * limitations under the License.
870 function quokka_fnmb(num) {
874 if (num > 1024) { add = "KB"; mul= 1024; }
875 if (num > (1024*1024)) { add = "MB"; mul= 1024*1024; }
876 if (num > (1024*1024*1024)) { add = "GB"; mul= 1024*1024*1024; }
877 if (num > (1024*1024*1024*1024)) { add = "TB"; mul= 1024*1024*1024*1024; }
879 if (add != "b" && num < 10) {
880 dec = "." + Math.floor( (num - Math.floor(num)) * 100 );
882 return ( Math.floor(num) + dec + " " + add );
885 // Hue, Saturation and Lightness to Red, Green and Blue:
886 function quokka_internal_hsl2rgb (h,s,l)
888 var min, sv, switcher, fract, vsf;
892 var v = (l <= 0.5) ? (l * (1 + s)) : (l + s - l * s);
894 return { r: 0, g: 0, b: 0 };
898 var sh = (6 * h) % 6;
899 switcher = Math.floor(sh);
900 fract = sh - switcher;
901 vsf = v * sv * fract;
905 case 0: return { r: v, g: min + vsf, b: min };
906 case 1: return { r: v - vsf, g: v, b: min };
907 case 2: return { r: min, g: v, b: min + vsf };
908 case 3: return { r: min, g: v - vsf, b: v };
909 case 4: return { r: min + vsf, g: min, b: v };
910 case 5: return { r: v, g: min, b: v - vsf };
912 return {r:0, g:0, b: 0};
915 // RGB to Hex conversion
916 function quokka_internal_rgb2hex(r, g, b) {
917 return "#" + ((1 << 24) + (Math.floor(r) << 16) + (Math.floor(g) << 8) + Math.floor(b)).toString(16).slice(1);
921 // Generate color list used for charts
924 var numColorRows = 6;
925 var numColorColumns = 20;
926 for (var x=0;x<numColorRows;x++) {
927 for (var y=0;y<numColorColumns;y++) {
928 var rnd = [[148, 221, 119], [0, 203, 171], [51, 167, 215] , [35, 160, 253], [218, 54, 188], [16, 171, 246], [110, 68, 206], [21, 49, 248], [142, 104, 210]][y]
929 var color = quokka_internal_hsl2rgb(y > 8 ? (Math.random()) : (rnd[0]/255), y > 8 ? (0.75+(y*0.05)) : (rnd[1]/255), y > 8 ? (0.42 + (y*0.05*(x/numColorRows))) : (0.1 + rnd[2]/512));
931 // Light (primary) color:
932 var hex = quokka_internal_rgb2hex(color.r*255, color.g*255, color.b*255);
934 // Darker variant for gradients:
935 var dhex = quokka_internal_rgb2hex(color.r*131, color.g*131, color.b*131);
937 // Medium variant for legends:
938 var mhex = quokka_internal_rgb2hex(color.r*200, color.g*200, color.b*200);
940 colors.push([hex, dhex, color, mhex]);
945 /* Function for drawing pie diagrams
947 * quokkaCircle("canvasName", [ { title: 'ups', value: 30}, { title: 'downs', value: 70} ] );
950 function quokkaCircle(id, tags, opts) {
951 // Get Canvas object and context
952 var canvas = document.getElementById(id);
953 var ctx=canvas.getContext("2d");
955 // Calculate the total value of the pie
959 tags[k].value = Math.abs(tags[k].value);
960 total += tags[k].value;
965 // Draw the empty pie
968 var radius = (canvas.height*0.75)/2;
969 ctx.clearRect(0, 0, canvas.width, canvas.height);
972 ctx.shadowOffsetX = 6;
973 ctx.shadowOffsetY = 6;
974 ctx.shadowColor = "#555";
975 ctx.lineWidth = (opts && opts.hires) ? 6 : 2;
976 ctx.strokeStyle = "#222";
977 ctx.arc((canvas.width-140)/2,canvas.height/2,radius, 0, Math.PI * 2);
982 ctx.shadowOffsetY = 0;
983 ctx.shadowOffsetX = 0;
986 // Draw a title if set:
987 if (opts && opts.title) {
988 ctx.font= (opts && opts.hires) ? "28px Sans-Serif" : "15px Sans-Serif";
989 ctx.fillStyle = "#000000";
990 ctx.textAlign = "center";
991 ctx.fillText(opts.title,(canvas.width-140)/2, (opts && opts.hires) ? 30:15);
992 ctx.textAlign = "left";
997 var left = 120 + ((canvas.width-140)/2) + ((opts && opts.hires) ? 40 : 25)
999 var val = tags[k].value;
1000 stop = stop + (2 * Math.PI * (val / total));
1002 // Make a pizza slice
1004 ctx.lineCap = 'round';
1005 ctx.arc((canvas.width-140)/2,canvas.height/2,radius,begin,stop);
1006 ctx.lineTo((canvas.width-140)/2,canvas.height/2);
1011 // Add color gradient
1012 var grd=ctx.createLinearGradient(0,canvas.height*0.2,0,canvas.height);
1013 grd.addColorStop(0,colors[k % colors.length][1]);
1014 grd.addColorStop(1,colors[k % colors.length][0]);
1015 ctx.fillStyle = grd;
1019 // Make color legend
1020 ctx.fillRect(left, posY-((opts && opts.hires) ? 15 : 10), (opts && opts.hires) ? 14 : 7, (opts && opts.hires) ? 14 : 7);
1023 ctx.shadowColor = "rgba(0,0,0,0)"
1024 ctx.font= (opts && opts.hires) ? "22px Sans-Serif" : "12px Sans-Serif";
1025 ctx.fillStyle = "#000";
1026 ctx.fillText(tags[k].title + " (" + Math.floor(val) + (opts && opts.pct ? "%" : "") + ")",left+20,posY);
1028 posY += (opts && opts.hires) ? 28 : 14;
1034 /* Function for drawing line charts
1036 * quokkaLines("myCanvas", ['Line a', 'Line b', 'Line c'], [ [x1,a1,b1,c1], [x2,a2,b2,c2], [x3,a3,b3,c3] ], { stacked: true, curve: false, title: "Some title" } );
1038 function quokkaLines(id, titles, values, options, sums) {
1039 var canvas = document.getElementById(id);
1040 var ctx=canvas.getContext("2d");
1041 // clear the canvas first
1042 ctx.clearRect(0, 0, canvas.width, canvas.height);
1047 ctx.lineWidth = 0.25;
1048 ctx.strokeStyle = "#000000";
1052 wspace = (options && options.hires) ? 110 : 55;
1053 var rectwidth = canvas.width - lwidth - wspace;
1054 var stack = options ? options.stack : false;
1055 var curve = options ? options.curve : false;
1056 var title = options ? options.title : null;
1057 var spots = options ? options.points : false;
1058 var noX = options ? options.nox : false;
1059 var verts = options ? options.verts : true;
1065 // calc rectwidth if titles are large
1067 for (var k in titles) {
1068 ctx.font= (options && options.hires) ? "24px Sans-Serif" : "12px Sans-Serif";
1069 ctx.fillStyle = "#00000";
1075 for (var y in values) {
1078 var t = titles[k] + (!options.nosum ? " (" + ((sums && sums[k]) ? sums[k] : sum.toFixed(0)) + ")" : "");
1079 var w = ctx.measureText(t).width + 48;
1080 if (w > lwidth && w > nlwidth) {
1084 rectwidth -= nlwidth - lwidth
1090 ctx.lineWidth = 0.5;
1091 ctx.strokeRect((wspace*0.75), 30, rectwidth, canvas.height - lheight - 40);
1093 // Draw a title if set:
1094 if (title != null) {
1095 ctx.font= (options && options.hires) ? "24px Sans-Serif" : "15px Sans-Serif";
1096 ctx.fillStyle = "#00000";
1097 ctx.textAlign = "center";
1098 ctx.fillText(title,rectwidth/2, 20);
1102 ctx.textAlign = "left";
1104 for (var k in titles) {
1110 for (var y in values) {
1114 var title = titles[k] + (!options.nosum ? (" (" + ((sums && sums[k]) ? sums[k] : sum.toFixed(0)) + ")") : "");
1115 if (options && options.lastsum) {
1116 title = titles[k] + " (" + values[values.length-1][x].toFixed(0) + ")";
1118 ctx.fillStyle = colors[k % colors.length][3];
1119 ctx.fillRect(wspace + rectwidth + 75 , posY-((options && options.hires) ? 18:9), (options && options.hires) ? 20:10, (options && options.hires) ?20:10);
1122 ctx.font= (options && options.hires) ? "24px Sans-Serif" : "14px Sans-Serif";
1123 ctx.fillStyle = "#00000";
1124 ctx.fillText(title,canvas.width - lwidth + ((options && options.hires) ? 100:60), posY);
1126 posY += (options && options.hires) ? 30:15;
1135 for (y in values[x]) {
1138 if (max === null || max < values[x][y]) {
1141 if (min === null || min > values[x][y]) {
1146 if (stacked === null || stacked < s) {
1150 if (min == max) max++;
1157 // Set number of lines to draw and each step
1159 var step = (max-min) / (numLines+1);
1161 // Prettify the max value so steps aren't ugly numbers
1163 step = (Math.round(step+0.5));
1164 max = step * (numLines+1);
1167 // Draw horizontal lines
1169 for (x = -1; x <= numLines; x++) {
1171 var y = 30 + (((canvas.height-40-lheight) / (numLines+1)) * (x+1));
1172 ctx.moveTo(wspace*0.75, y);
1173 ctx.lineTo(wspace*0.75 + rectwidth, y);
1174 ctx.lineWidth = 0.25;
1178 ctx.font= (options && options.hires) ? "20px Sans-Serif" : "12px Sans-Serif";
1179 ctx.fillStyle = "#000000";
1181 var val = Math.round( ((max-min) - (step*(x+1))) );
1182 if (options && options.traffic) {
1183 val = quokka_fnmb(val);
1185 ctx.textAlign = "left";
1186 ctx.fillText( val,canvas.width - lwidth - 20, y+8);
1187 ctx.textAlign = "right";
1188 ctx.fillText( val,wspace-32, y+8);
1194 // Draw vertical lines
1196 var numLines = values.length-1;
1197 var step = (canvas.width - lwidth - wspace*0.75) / values.length;
1206 for (var x = 1; x < values.length; x++) {
1208 var y = (wspace*0.75) + (step * (x/sx));
1210 ctx.lineTo(y, canvas.height - 10 - lheight);
1211 ctx.lineWidth = 0.25;
1220 // Some pre-calculations of steps
1221 var step = (rectwidth) / (values.length > 1 ? values.length-1:1);
1223 // Draw X values if noX isn't set:
1226 for (var i = 0; i < values.length; i++) {
1228 var x = (wspace*0.75) + ((step) * i);
1229 var y = canvas.height - lheight + 5;
1231 ctx.translate(x, y);
1235 ctx.rotate(45*Math.PI/180);
1236 ctx.textAlign = "left";
1237 var val = values[i][0];
1238 if (val.constructor.toString().match("Date()")) {
1239 val = val.toDateString();
1241 ctx.fillText(val.toString(), 0, 0);
1242 ctx.rotate(-45*Math.PI/180);
1243 ctx.translate(-x,-y);
1256 for (k in values) { if (k > 0) { stacks[k] = 0; pstacks[k] = canvas.height - 40 - lheight; }}
1259 var maxY = 0, minY = 99999;
1261 var color = colors[k % colors.length][0];
1262 var f = parseInt(k) + 1;
1266 var value = values[0][f];
1267 var step = rectwidth / numLines;
1268 var x = (wspace*0.75);
1269 var y = (canvas.height - 10 - lheight) - (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
1272 stacks[0] = stacks[0] ? stacks[0] : 0
1274 pstacks[0] = stacks[0];
1275 stacks[0] += (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
1282 for (var i in values) {
1284 x = (wspace*0.75) + (step*i);
1285 var f = parseInt(k) + 1;
1289 value = values[i][f];
1290 y = (canvas.height - 10 - lheight) - (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
1293 pstacks[i] = stacks[i];
1294 stacks[i] += (((value-min) / (max-min)) * (canvas.height - 40- lheight));
1296 if (y > maxY) maxY = y;
1297 if (y < minY) minY = y;
1298 // Draw curved lines??
1299 /* We'll do: (x1,y1)-----(x1.5,y1)
1301 * (x1.5,y2)-----(x2,y2)
1302 * with a quadratic beizer thingy
1305 ctx.bezierCurveTo((pvalX + x) / 2, pvalY, (pvalX + x) / 2, y, x, y);
1309 // Nope, just draw straight lines
1314 ctx.fillStyle = color;
1315 ctx.translate(x-2, y-2);
1316 ctx.rotate(-45*Math.PI/180);
1317 ctx.fillRect(-2,1,4,4);
1318 ctx.rotate(45*Math.PI/180);
1319 ctx.translate(-x+2, -y+2);
1325 ctx.strokeStyle = color;
1329 if (minY == maxY) maxY++;
1333 ctx.globalAlpha = 0.65;
1336 var f = parseInt(k) + 1;
1340 x = (wspace*0.75) + (step*i);
1341 value = values[i][f];
1342 y = (canvas.height - 10 - lheight) - (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
1348 if (y > maxY) maxY = y;
1349 if (y < minY) minY = y;
1351 var l = values.length - i - 1;
1352 x = (wspace*0.75) + (step*l);
1353 y = canvas.height - 10 - lheight - pstacks[l];
1354 if (y > maxY) maxY = y;
1355 if (y < minY) minY = y;
1357 ctx.bezierCurveTo((pvalX + x) / 2, pvalY, (pvalX + x) / 2, y, x, y);
1365 ctx.lineTo((wspace*0.75), py - pstacks[0]);
1367 var grad = ctx.createLinearGradient(0, minY, 0, maxY);
1368 grad.addColorStop(0.25, colors[k % colors.length][0])
1369 grad.addColorStop(1, colors[k % colors.length][1])
1370 ctx.strokeStyle = colors[k % colors.length][0];
1371 ctx.fillStyle = grad;
1373 ctx.fillStyle = "#000"
1374 ctx.strokeStyle = "#000"
1375 ctx.globalAlpha = 1;
1381 base_image = new Image();
1382 base_image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAAEACAYAAAB7+X6nAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACJQAAAiUBweyXgQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7Z13vBXF2ce/z+65hUuxYUNEuFyaF0EEwRaDPRobKsYSW0w0auwKWBKPFUEs0ddujJpEDWo0aiyxYBcEgqKXJsJFEQuitFvP2XneP07b0+utnN/nA/fs7MyzszO/eZ5nnpndFYrocNC7q7s1SMNvUY40qsNQa3NVvkGZbYw+02Ndl6fEW9OciSxp6coWUVg03Fd5hqreoipboaAKqAT/Bv6pUmtUzt78kqX/TSevSIAOgrX39Nmi1PI8BhwORHe4ESJkCP9Vo/xlM2m6QC5Z2ZBMbpEAHQAbHqiqttU8h1IVTozq8BgCmMh5VX3H+Dlyy0nL1iWSXSRAO0f9fVV7gvkPsEXUibgRL7EawEUGmec4Zr9EJLBa/haKyBUN9/ffD8yrxHY+BIauxCcRmyyA6AjLtp7TO6vKYsUUCdBOUX9f5TGq+jLQPVkeCf4nCciQAGPXNzI5oYwi2hc23tv/dEv0IcBOmTGdGTBucyCoomI4dLNJS18NiShqgHaGjfdWnizwF9J1fhrEaYfAUBcVHvj2lmFdQ/mKBGhHqLuv6ghB/kqm/eLq3IT2Pwoaytun3DScG0otEqCdYMO9VWNRnQ6UQHBqlwHE/SN6tCeH6oV6/8gSKBKgXaDu7srdbfR5oDw/SRr5mUo7CDv8tOang6BIgDZH4wOVA7HkRU3h7adEgtGeoMND08FwolgyHsCT00WLKAjqH6zq7fj1TWCbvARJ5I9ClCJInl+OUA34iEW0AfTu6m71dsO7qOyaKp9Iut5Eg9M+SRcWJmaqaDlmUNEEtAHUi1VvNf49XednAAM0EWUIogkTmgYmChb5LWt0kQBtgLptK6cBR+UpRoF1QHki7z+pahd1O4cjiwRoZdTdV3WmIBfnK0dgAa41grDjl+lUMJC3d17RpiKyw8b7+h0kKk+Q1exLEnhqMhMYQjBmkKxcXIqGJQKgsLZIgFbChrsHDLEsXgEqsi0bQ4D/AdsCPaMzEWP+JcEvV0rAEviLJqAV8NXF1Vs2fCc3omyep6gvAr4+fRKedTt8ZBQW7lkkQAtDx2OXlvr+bpoY529gXk4yAmHhDcBHKCOT5YvfB0BSfyCYZIoEaGF8XzngJoVDARpX2yZHMUZV7wCOy7xITPwgRjsE4RQJ0IL4dsKgIxW5PHSshpH+OvkoWzmCTBVLTiWl00e2u4RAij5Ai+H7y4cMQPQxYtq+cY10RclGEzxv0EqUnTLJnGQfQLK8RQK0BL66uHcXx3L+CWwWe06NVDdvsD7IUNRSFZkpcHymy8MxVwv8SU6GjcXFoBZAaVnFvaqMSHa+6UfZobQHPlKr9AYR/RPKw4WqlwAqkRmjwrdFDVBgfDNx4HmqnJYmW7+mn6z3U+ZQuUBVLiHbPQJZ7BISdFWRAAXENxMGjsZwa2gFLhWa18lglPqEJ1X/ArodMCo6ubC7hASWFQlQIHx1cfWWOPJPVMrCT+WEiJCYDNs1rrFnJkhfatk8gvDHwtQsyS4hARUWFwlQAKgXy8b/hCp9CTyJE/6nMU/quOGrY7ga1rqSmsSyfm2M9X9Aac4VSrVLSDHGx0qnQf6njnxWdAILgFU/DbpchIPB1ccanIoFvS4NnolSz8pWjT9Yb3TZxhwAICJXqjFHAMPzrZOq1pkm62vjkzW+Rmk2zZSpw1ao9AF6A1tt+1P3T4sEyBOrLhg8UpXrFA2stiXYmiGQlAj+ehmjDt+JzceIvI/qe6mupyqBXUKKUSOrjY/VpknWOc3i8zdLifrpoYbtCSwWDUwuiP/JA3N9RQLkgdUTBnVvbuBJoJTgKg1uIgT/JiSCashT79bwnfV2t16+PxjDSwoGP98ZR9aroc7x0ag+mo0fNX6rBIduGLbUwIpg6F/WUJgJxU2heaGpjruBKgTX6I8QgVBSIiKE1uRV8TXx8PoVpaersiMB259zx2YOKRIgH6z8/aDjUTkldBzu3PDoT6DyQ0QI/hVAVR7H0oVqeALJw/HLDurxBOIQxV3BOaD2t4P62R7rY6AHouH4uzvyEk4LnwsSITo6s9o2zbuY0pLHgf1jzrUkZm83ZcloKD4YkjXUO9YjlvUPlB6RbdYSfhoX11O5UVNAE5wWmqi0c9Qu/SVG9o87l3Y3eO4QlRdCv4smIEt8tfLba0Vkz7AaT2D7wwM4pe3nSUv97xiPvTC0QcftF0TJLrRGMPw79LNoArLAl2cO2VfhTZHgo9spVX6QErFqPRCF+8H2+apNecktoKfGnHPldz3KVTgi1Gw3ZcnQ0EFRA2SIFSftsgXG+RuCHRn9kQ6KHuWBBA33pkb1naLnaknJEFU9JaoMLgdRcbFBw4985esnKPKY+7hIgAyhHv9DwShaJC2kuENE0IDzD9FEQII+QsBhfLrBcp6vwPMJRiQmJoC7WEA2uPdxJYwoZg7HUedxd0KRABlgxak7/1rhmLCDFmX7XUQIdZuLCODuI/mh2Wf/obycKxQGRc6F/YIASaJkE/6RNrScHi/2nrp0pTuhSIA0+PKEQb0clT8HuyicHj/vj6h8NxHc837gDyW200MME4MZo2ICRJXOMLQcqz1SEEGE/4tNK04D08DxlDyIYcvA8m5gyodrmTdutc8QnBZKJC3Qc//a8f6F0xHuVZXyqDLBpeOoqWSS6WX8NRNMLxNOIfXzbW5e8kZsalEDpMCyk4aeqY4eFvtoVqa2XyVIAtF14jfnf332kFNQDkjgF7hkB+XkEVpONIVUlTslATWKBEiCz08e1hu/My3c0GEbTEa2P5oz8scusKHBYUpI50ap/BS2P9vQcpgIrjqJxXoP8mii+yyagARQEPGZh1Rlc1yqPCq6F2UGQipfoiOBgc0hc/tsWHBPo1hXAduF1Xisyk6k8mOvqUQiirGmJ86MROpjVB7ZeuriDYnutRgISoBlx1afp5YEHCZ3IAZwv2cneQAomA+MKntZJfZqcBYglCUNDrnKh66TSHZ0XYCgvLj4QCSvT7EG9Lp14YpE91rUADFYNH5YP4PcHLWfL2oUSnAUxqYTM/oF1Lq7398WzBI101DKwg6ikfjRG6dZXBrBJTu5ExivPYJ5H03W+VDUAFFQL9ayT3aZocK+uEaqJBiFMSM9ZvQD8C0lJUPE+EYg+mZCbREsn2j0Jh79SfImkhk4dhxjDdnxzoWfJ7vnogZwYen/drnIKPtqjA2NtsHEjNrEtl/g0r51Azag3B7vF7j/ScbTy4RaIcUU0ihPpOp8KBIgjIXjhg8EbojqgBARTAoihJd+XURQ3t3pHzVP1JbV/FaV4THnwrLjzUAMEQzJiZDejKhHrSnp7rtoAggo+88P3+U1hODuXBI5U8EfaZxAMFiMwSn73CppXIIE3wHoluM2G7FyotI0gRlIqfLdPfp077sXjU9370UNACw5bNiZqBwQpfKTRt9cTmCiEarc3//Jz+aIp/FPqmyTcJSaaI2Q0AlMFFHMfArpWOr8KZN73+Q1wLIjh27r91kLVQJv3IqM/tROYJRGIJz+k1PaPNAypZvZUINometcQs0SNdLTTi8zm0KqcF+fexeek8n9b/IawNcsd6qyRba2X+Nsv6CqVw96YskPluEOVcqS2f5oJzBz25/hFLLOOFyX6f1v0hpg4cHDDxf0hWQ2GGJHv8anRf5+WlUyeMQXLPi5qLyRUE6M7U8+vYy3/YmnkPHaQy25bqf7F1yTaRtsshrgk4OHdRXlzsjolTS2n8QbO0P/0MuofkoxckfCMG0C258wtBxl++Onl6m0h6h+X+FzpmXTDpvsYlCpXyar0A8hauUNIOGCS7CcuP4Prbyp8sqgFz7771IZdiroLgQe3QosDCnuDT1BrohrYUkj6UQGd7g8rtc6hssHMsZuSjFY12/9cE3CmH8ybJImoGb/4aMt+ACwUzteyVW+K81xjI4wXc3i0iZrIRaV0bJinMmkJsZlHpKo/DRTyJrVJV1GjHpgri+bttjkTMCMsWM9YuR+NWIXYuVNDH8d8tKnn5Y22ucClfEqO6LKU5mYhCo/lRmJrreqI+dn2/mwCWqA+fsMv8Rjy61A4kBK6G8iRy04ml1pdeq3BzqbN68vabC+QNgmxapc6uml67oSrFPi8sSMfgWRv/V9rObUrBoiiE1KA8wbMmaAs8bzezU057fyFk6bNvi1eatK6q0JqGzjPpdwNTHV9DJquhejEZJoj6ATuB5jJubaJpuUBvjfoFH/QTjM09W8a1eYnwE5234RvtPyxgG2r6Lc4HyB0D2jhzoKZPtD5dXi/Mq/18Rt9swUm4wGmDdo1FEoh6Hg32hXYmiOC6RkYfvV0asGP794g1FzDUr3qBBtFrafbG1/dJ0/XbFy6/vyaZdNQgN80HvPLmVdfJ8BlaGRY3c179gVum/mQZeotfgF3zpbDN++9McdgUUIpfna/iSh5VTaw6/oXv3/WTM7m7aIxSahAcrKnYkYqXSPUqfe7o+hKdGCS9xsIOQnRHbdXLLfW2/5Ra0bUSmN8gtytP2JQsvJwseqYGBKvp0Pm4AGmFM5sg9YC4GKWBttdeGdkm7OvuG0sFftzke07UfeHPL2vAMWHTB0mGLPQ7ASzhRcMt2ysggtJ9Ueqiy0u2/crd8jtY05NksYm0AkUP6MBr7SoUqkcxBMA4O1gkaR4Ns4lfine4IIljXG5hIANfYUJKBBw3KVcG9qWCBR28lDUgP9GqhMomcDwzIUJPohUZ+onlaIzodObgJm9xt1ECpHRyW61a1hG3+dPSvTlTdR/jl0xrxPFu434ueq8otMNnbGOZMZTC9TTSFVrYv6/yt/1R9CpyVATXV1qSB3Jc0QbFxTL0Mw1CfzwEO2H8Xx++3rANTRaxP4BcmJkGhWkXBzRxrbjz424F/z7ylkO3VaAjQ0VFyiwSdwU0GVbXz19uw4JzB+182jwz6cu6hm32H7q/LzNDtyooiQTWg5afgYPqSx8axCt1OnJMAHvffcQVWugkgbp4JTJ0NVpS7ugc5Ix/gcy3NjILN1TWynZf1AZ6plZU1EBPm4udEcPuDlpU0FbSg6KQFKPM4toN3caamIIMJWzkZrjtv2R+/ZkweHvTd72YI9RhyoKvtmYvs1B9ufZAo5w24sGVv9as2PBW8oOiEBZvcbNRrRE5KdT0YEf6PsooaNsdE3VWk0+CcDGMEbZ/tTESFL2x+3KQXu8Xd1ftH/9bnrQvWcM3Jk3FdI8kGnmwYq1rTIjCq58g+dCU+9hS2dBustu4sZG7WBQ/Tu4bPmr/x0zPCD1bB39HQtzZO9ESGuKWDM4+DBcjGbUlYb9JwhL81/JpRv3phdB9iOfS0W04HnMmmLTNCpCDBrp9HjBH4WSYnMn5PBfcY0yTC7jA1qBWL7ImykyZkayGh5w2VCnea6TrjTg3P6QEwhpiYKad4LYFD5u+W3Lx381twfAOaPGLGnipwjPk5E1CNSkvd3h93oNJHAGWPHerqtqJ9P4Ju6KZDaJbQ9+pbd1YwFUJEbdp0394+f7L7bL0R5uUUf6kBfN7ZMqH79k3mfDt1tmPFYR6DmFIRBLjkLh//vfzunvr/s0Gk0QLcV9b9XGJKe0am1guOXEZZjrRVLpcRXejuAGLzhEkF17o4URkY/rtEfE1EMnovVHoq87zRb9/nX4UHk3I+HjjzEEXbERGyKBDWGIs9n0BRZoVMQ4L1Be3fXZt/VEN2tqcmQlAibOY3yttWF16prPvxx/q6jDlNjxsS9JiZEBI2M9HjbjzHIahH9AcNGNdKEQdWRElW6Gb90x5FhCH+LeAhuc+GWK1hiwm/4LBQ6BQFKm5qvQCTu9epxHZIQ8UQwfrbv2lR2ZyDVuQYVV1w+qvA6g3yLQx1Kszrix6FUjXQ1PtkKpSewLcK2hEyC22SE/mow3h8e6a6aRYiwYpd58xJ9YygvdHgCfNB7zx3AuTBVnqyJoHrv4MXvb5g7ePQh6jd+Vd5WQB3K8Ut3VXpi6Engw5DJp2Xi/hMazpp8uzggxBMBQAyPSgJ1lS86PAFKPM5NChWZ9HKGRPhOms0DAKZJrzNNMjpl7lQnQ+Y/yu5HrexlRAQU9VtEveK1UOjQBJjVb/RwVX4dlZgnEUSYOmrV3Pq5fUcfYmB0ukGXGakIB4qiVH7wRBQRQs5iWK4AvD7qszlfpLtELujQBBDVW0ASRzNzI8IPXbpUPADgCFdJ7NksAkvpMkY6PYYIIRmuWYNY+ud0YnNFhyXAzH5jDlbVgyAzNZwJEUCmVNe8tXF23z3GgvlZfM7MA0s5ESGREwhLd1049+V04nJFhyWAqIYfgc6o0dNn+qFrRZf7AFT0qhynkFlcLjpjQidQQUTvFLL63HxW6JCLQTP7jjkKGBObnmyhJ5NMqtxSXfPWxjl99xgDemBGsqLCf1ldLmnGmO3ia+wuvoczKZ4rOhwBFCyBa9PkyZwIgYw/dOtacQ+AEXNV1rJagggGMHrb8Pnz6zIplis6HAFm991jPDC8kI2uhjuqa97aOLPfmGHA4bnLyrxOGdRrnaX+gm7/SoQORYDpjLcVvSY6Ne9GX18qzXcDWKoTY1Zrk8pKjfR1SidLVG8fUfvx2iSnC4YORYC+fb86haSrfUIuWkGEu0bUfrx2dp/dK1GOT5gpAzmFrBPwrc9j355WfAHQYQgwZ+TIEoP+MbNYaMaNXm/bvj8DqCWXITGzooIRIas6IXDhHktnrc9IbJ7oMARw1ti/ASohGzuautEFfXC3pfNWz+w3Zlvg9KQZsyBC3uZB5eGRy2dPTyumQOgQBJjRd2y5IlclOpdHo/t8UnJ7UMhFCl1ynUIWsE6LSxsaL0h/hcKhQxCgXBp/B+yYKk8Ojf7E3ss/WDGzakwPQcMvVcxhClmoOjVg9MTh37XstC8W7Z4AL1UdWoZhQhrnPIwMG10t29wCII6eTYIl3YxVemGI4IjKSbuvmDMv3eUKjXZPgC38P52B0BtAkXSztDBSN7q+uPsXcz77vOrQMlG5KD9ZmWdKkqVZ0ZNG1X5UsJ2+2aBdE2A6421VuTS24/MlgmUCr1Ff4//xJIVeeUzXcsrkylKPyLjRrej0xaJdE6D3Tit/pSJVoeOCEEF5b/cvZ74fWGeRS+JzthoRalV1792XzXoJYO5Oewz5qGr36nRiC412SwAFUZHLAr9Td3w2RMDWKQAf9t3zEESHJs/YYkQwIPc0l5YMG107++N3++yzxUd9x0x2LPN47dK+i9LWv8BotwR4r3KfXyoyIpuOz4AIC0cv++glAEvNpa260BPItERh7Ojls86z6kzJrH6jryq3m79AdJJgXXM8Tzlpq1NgtFsCiOqk0O9sOz4ZEUT0ZgHzUd/dd0U4MJK/hYmgrAS90Gp2RliOMbP7jn7U43G+FrhBYQvg1d2Xzyz4nv9M0C43hLxXuc8+atg7dOz6nHJOx8G0r+wtnSdYDkatSxL1pYbzpkLmm0GA5cBTIroQtQ4wpXI1sHVMxnpVMvq4Q0ugXRLAGLks+smbvDo+9PPWUXPn+mZWjemNn6RPDwfKh8qmQnoiANtCIIYhkiSfyDVjamctT3mpFkS7MwHv77hXf+DwkBrPV/UH035s6NrlLwD4uECRkkzqUoAVv4pUsgRm1C7v0yqrfsnQ7gjgWPaFitjutFyJEH7sUuTe/Wre2vjeoL27I/wuUZlUaIkVP+A7UefktnD83GhXBJhZNaaHipwG8Z3oTsv0OJjms23nPgCryZxpsDbPN5ZQAIexCdHjR62Y+01GF25BtCsCNDhdzlKkR74d7yaPoI/vsXTWSgVLkfMzlZEOeRBBRfS3o5fPfifji7Ug2g0BpjPeRvmDO61AGuAOgA/77nkkUJmt+UiHLImgilw8evnsv2ckvBXQbmYBW/ddfQSwUyIPPudZgPDGPsvf/ziYdlFQI2QkI1laMmQwc3CA8/aonXV/WmGtiHZDAEXOgdQdkPWxkdsB3u23z3BV/Xk2MlLlSX0fobJRaFDllD1WfPRMXIE2RrsgwBs77t8f9EDIrAMyPF7ysxXvvhxMuDBbGZnmSQYXEZYgevwetR99krZQG6Bd+ADi0XMVsZLM4bM+DqbdLmDeqfrZ1oqcmKOMjMokgQKP1HftMnLM8kDnv99/r23SFWpttDkBPui9Zxej1umh4wJN/35saOjyNwD8nAOU5+hA5kqEj0X053vUzjpjG1Y3z+o3evzMnca8ZjkmZQSyLdDmBGjwdDkB2LIQHR8+Vu455Lv/1tVUV5cq8vs8tEi29ahR5AzHtg9RlR1m9h3z8Ia6bl+qynSE/R3HfjrnhmohtLkPYLDOgoI6f82C3gPwff3Wx1uY7bORkeN1GxV5B8MqLM63HPMX4gfX2/t89f6qLJqmVdCmBHi98oCBatgDCur8Pb7vine/ARDVC1rC+UtwXA4c7O7yuDwiT2TcMK2INiWAMdaZod+F0gAqgcDPm33221vR3fNZQcy1HgnSfFaz+VeGzdKqaDMfYMbYsR5Fwu/3KZAP8Pb+y2d8AiAWF+YiI8frppShIv8d8/VHa9K1SVugzQjQtLzsEKBXQTtAuQtgRtXY3qqMa0XnL52Mf2bYLK2ONiOAIqcXuAO+7tFz/fMAxmedTfBBz9bUAElIuVFLpU32/GeCNiHAK70P2VJEj4DCdQDKvaPmzvXNGTmyBOE3Be3EDI+T5Hlyn8Xvb8ikXdoCbeIEmhLrWFEtg4I5YU1qyUMAa9dsfizQK1dHLpcyqeqO8JdM26Ut0DYmQCN78gox8gzWkwcuf+O74MnzcpHRQhpg0T617xf8/b6FRKtrgP/0PWw70J8XcuRZlrkb4M0++1U7IvvkIiOXMmllKA9m3jJtg7bQAMera89fAUbezAOXvTEbwG95Mt7xU0jnL4mMZstj2s3Gj2RoCwL8CgrXAUatuwBeqjq0B8rJbaH6E8pQeX7vLz74PrMmaTu0KgGe63/UjorsWcAO+N6UWM8A2H7ndBXploOMXK6bVoZlt9z7fQuJVvUBbMc5kdDmuAL4AAbr3sOWvhz6mOJZuchoER9AmbPPsvfey7BZ2hSt7QQeV8AO8NmO8wDAy31+cYCi1TnIyIuAyequIrdm0SZtilYzAc/sdMz2iowKHeerggX918FfvRZYXrU4NxcZ7uOCOX/Kyu5bbWh3e/+SodUIUGo1Hw6Bj6QVogMM1r0A/93xoF7AEQX24LM6jkm7Y9Tcub7MWqXt0WomQFWOUOK3ZUNOanvhobUvvwPgs0vOErSk4HP4LI5daRsc234oq4ZpY7SKBpjee3wXRQ6Awow8I9a9Ajqd8baonpmLjEKo/gQyHjpo2evh7/x2BLSKBvCU+g9QIxVQkJFXj8PfASr61f9SVXrnIKMgzl+MjAb8dBjnL4TWMQEm8gr2fDtAVR4/4ssXfwqeODsXGe7jXMokkmFh7tp/5YyvM2qPdoRWMQGCHlwoFSyq90EgqAQc0pbOnythY6OnfFpmrdG+0OIa4Nkdj+6vSL8CqeDZh3/5n7kAtt/8TkXstnT+wjKEaYctfXl1lk3TLtDiGsDYVviRr3w1AHAvBPYTIvqbTMq0pAYIpq3xeUra9C0f+aDlfQDlQJXkT+UmSktyvFaa+SfAxtruhyvskEGZFtUAwYObD1v6cqu8278l0KIawIvXAsZCQTTAI0eseqE+kB6I+2dQpqU1QG1XX93dmbVG+0SLaoChlZ/upkZ6QgFGnglsrnih8og+xsjBOckoRD1cxyJ64V4rP2zIrlXaF1pUA6iRg8K/8xt5M47+8t8LABy1zgLiNpRke5yP1ggev3rw8tfa5OWOhUTLEgD5eYGcv/sh6PwpZxSiE3Mp4zpuciy7Vb/s0VJoMQIE7f8eoeM8OmBNqaf5OYB1yzc7QpFeBRi9+WqAa3+57KUlmbZFe0aLEWDnyppqYLN8O0BEHwlt+lCR32VSJpPjPGTMKu/beEs2bdGe0XImQEm49Ss6S/oG9zklDwM8U3VMb0WSRhSzPc5V9VvGnLnfW2/5s2qLdowWI4BRa0/IswOU947/8qkFADicTgGcPzeylqFy5S++fLUmi2Zo92hJJ3CvfEeesawHA+kIyukFst+5lRH5z2ErXuqwEb9kaBECPLrDqVsBAyCvkbeuvKnpaYBndxq3nyL9c5ARd5wjeb5sbi49TcAVAuwcaJFAkFNij1JEII/Ai/CPUOTPiHVmTjISHOdQpkksPfaYr59tl8/354sW0QAbSroO90nJEshdA4jRhwCe7Xv05oqMy0VGATSAqsjvfrnspTnZtkFHQYsQwKg1dL3VY10ezt+c41Y8Mw+gWUtPBrpkLSPJcTZlVOSaI5e/8LfcW6L9o4WcQKn2iWf3ZilZmEsHYEUeqhTR3xSi47OVIehfj1r+/PU53X4HQsEJ4MVrKQwBWG9vVh9Kz6ID6prt0icBpvcdv6siu7nlt4oGEHnmm622P5tNAAUnQEXlj/1BuijgxxrZQOnCbDpAkX/+euk/1gMYtaJ2/LaSBpj+3Zbbnnj23Ac6zN7+fFBwAnjEHqqEPGlhg92jAbLoAA286eOlqkPLgBMK5fxlIkPQJzfvu/bkTaXzoQUIYCzCnz9VwBF7tyarbEGGnbTwpBVPfAiw3t9jHEJP9/mW1ACC3vFx7YiTO1OYNxO0RByg2tWoAKyT7mZr1hCbnuD4gZAQRU4N/c5jDh/V4Uny+BDOG7f8uQeh3b7Mq8VQcA2gyNCg9g+PMCPW0Ear/DNXnkQjsdnnK/kbwBM7ntBLiez6KYQGSJLnR1H9xTHLn233r3JpKaQlwE0DLx2cqbDgHoABGhpbLiKss7qlVsGqz5/29WNrANQjJwN2Th585qr/Axtn5DErnn0z0/vrjEhLABV7+8kDL5t+U9UVW6fL22Pguu0VyoI75ggRQQVUrKFNVtknkLST/ho+VjklRw8+6bErzQGm/LjVlmPHHim3iwAADrJJREFU1T5Xm+g+pg28tGe6e+0sSEuAqxZPnQGyTizfZzcNvPToVHn94ukb+q2EVk4CakCBdXaP0iSd9G2vfqv+C/BY5SmjgF3c5wvo/C0T1X3H1z49KZmnf0fVhed6fH470bnOiIx8gKZm3+UgfsF6dvLAy+/39vWWJ8pnjO4UaezQvD5CBIMMabRKP4ntJIP1SMj7th3ntEKq/iB8wJ10Zfj4FU9/kKju0xlv3z7ggvvBdL9o+Z+/y6RdOgMyIoC39o61oH8I+uJnlZXWv3dz5cQ+sflE2Cn02x0LiBzDBqtHWSRPoJM8+B8FmF49vtRgFfolkm+L0V1/VTv9wuNrntqY6P7u6XPOFl9Xbf8sah2y1r+hQ7zcqVDIeBZwxZJpz6L6dKAjdaR6zKzJgy8d6c5jwm//jiCWCI7I4AYpm+fqpA9Oqn1iEUDzxtJfIvQshAYAPlWVY06sfXJseFdRAtxeddGejaWlcxU5QtVM8tY+0phpm3QGZDUN9JTK+cBaAIXt1Fgzbh5w6b6RHLp1oK8TjdwIEdZ5ulcAqggqEefPEfv0Ajh/i4xYp1m1ZsSJK558Ntm93Fl1ftltVRfeqPAuSD9g5sVf3NluX+veUsiKABNqbvlWkRtcjd7diPXSzQMn7gOAWNuFbX5MLCCEwHlrUL1dPg9oMGo9DfBw1RlbC3poIE/WGkBV5A1VOdZT6x968vLHH0v2VW4FubXqkvE+7BpFrlSwFZow1lkSclc2IWQdCfSVVNxV4qs/B+gf7ICuDubZG/pfspsqW4MggIbaUggeh5o3UGqjdOtRrs1Pn7HikbUAtt85SZGSQJHMoniKrEZ50hJzz0nLA2YkGbxjvZ5uq9YefquKF3Q4GiXHe+my2z/Nti06A7ImgLfG23z9oAmTRHnK1T09xfY8AbodRLz+kPsXGlbhTWIKjlC1xurqfr3baRl2/Ocq8jzw77LlTR8kG+khTBl04SBL7TP4ev2pirV9uHbBSxiVt3daurLT7PPPFpI+SzwU5MZBEz9CdVQiIRKjSaMNQPj428Ze3Xb0vuX1P9rv1GGq8klMeZ8iqxX53of9sVjyjlViXj1z8cNJP73m7eWt6NplY6URxqjoPhbsDTog8c0qoKtK/LrbpjTti0VOi0ECer3oZEGeAVCXanf/iozeULkonfCo9y2vH+Anu/vhJWre9Vmech92D0c8WzpYWwvaC+gF7AqcLoqZPPDyVaDuDqsX2AplB9i4mXGRLKkmCvzfgOpxm3LnQ44aAAJa4IZBkz4GHSauxGgFHrpIvEYQYw2dtHRKjXes1+P5puFLlO1jNETS8pE8GnPsTkpUj3AGn2CNu/zzaf9JfoebBnJeDRRQFW4HV7QvagoYQQIPfu6kpVNqAOxVTQcrbB8qG+ruVOUj10wQcQwnJaoHKOJDOKXY+QHktRy8ub/uiaCdBiKdoCEiSHRnhuf1oq6dtvprd9kwESQXIkg8EaIJWSeiR09YctsmN99PhrwIcMHSu5oUHgF358SOyLjO9Pn8+gTAlEETuqtwVOK1g4Rlib6WOy1+8SkiB0TkKyPsN2HJbS/lc8+dDXlvCFHLeixV6DeOCKKveL+Y9j1Ao9jHABWpyiUhUTBPsogjuIkA8obf59v9iiW3zs73fjsb8iaAd+FNnyGyIN0aQKRj7Ij6N6FPvCQvF1U2CREgWcSRDWCdP3HJLQddFfT2vX0v2jyf++1sKMiWMEWeCvxK6ngR7NB1FXVNLwLcOOTK7RH2z7CcKw0XEeLnB2EiKM8bx6m+YsnU/wvFIG8aePlh5VZZj0Lcc2dBYTaFiryiqtdAeI7tmnNHYgGK9eQlK29vAPDDCep63j82chCJKSQ6G7psMFWjcs4FJl75+S1vuKs4ecCE36M6eNKyKUUfwIWCEGBVxeq5vep61gFdFQ33UHgNAEK/wurfKL+WUDxW44I0cURwHyUMMYsswJEbrvz85ifdizo3DrlsKI411aC7lzpaTRFRyDkQFItrBl/xBsj+EaHRRABq/7R4cqWAequv2FkdCb9pI0rJa2xaID1RRYPXmSMqN/mXlP/bi9d4R3orrLrGIeI4e4nIUQL7A4LIr65aPGV6Ie61M6FgzwUo9jzB7B/4DYE2D55TBZFHw+uBjnWykiRULIEUjSJCvEkJljMgW4owzTOo4bbrmbg5Gxs2FwDLilwbHri62PkJkZQA51edX7aZZ4tTEYyjvucmL5qc+gUJyhcqyWy1qO23QupfFE4KnE+cP0KeWCLEmRQLqDShMsH0KMdQrHcdT/n5FJEQ6UyAXDn46j+JyFUCM0HewTLveUo8M70fe9e6M/5p8JWHgLwSLTzce+9du/CmnwFcNeSqfS3l7UQXTx7zJ7TilKDCKWP+c0p8HDhp2ZQO9RmX1kQ6E6A3Lbrh2qsHX/2pijwK+jOM4G9yuHrI1etRvgLWCrKFotsEi8SPVo18Q9eCE2PduGSriIG0AEIOo8YRIfHMQZH3Sks5ctLim4udnwIZxQFuWHTDv8AMBV6EcAP3UJFqhL1VdGeFnqGuCc3Pg53X1Fyi0wHOGnlWiaoclzJsS5oIX0wwyG02QtcV+Ic2lR90xac3/5Rle2xyyHoWcEW19zgxzq0CUdvCk3j+oOa56xffNA7g6uqrf4GRl935oiuRTM0nXlIOFHFTgA0icsGfFt30SFY3tQkj60jg5Brv0z9UfFelwimKhF+eFI7cScCTD41OseTJcGEjJ8bmc+eNaIQUEb6o6xHWCAgv2pa1S7Hzs0NO08CydWWWVWavMmqeV6QB9GexkbugF7/Bqih5AcDb11vejHN0XD4IzAsgydQvWTAofPa/BuO9ftHkD3O5l00dWRFg4uA/7mKLda4iJzrGbAapQ7iC/Ns711sP0FTOYYL0SBnqTRkDiDIPP4E8Jap3exffOD+beygiGhkRYMLQ64dY6tyIcrRRlUAHJvLkYwM2gXX/AJwTI6WSje7AuSREWA76GvCiZZe86q3xNmd1p0UkREoCXNz7ti4lPdZPFONMUgg80xf8AFRk/SXmOQDCawA/rK747jUAb7W3W5PRw6Li98H/E0T46hRWgsxHmC+WzjdGP7lx4Y0rCnC/RcQgKQEuH+o9XMyGu0D6BrttlQUzjPKBWLLIZzxf2Gb9j1MXT90wYdCE7pZWbC0WFyHm/KD6f/qB4CPYjWqOAqkA62XgNohMAiwx6oj+oLa9xvfjujW3B1cLi2gdxBHAO9JbUV9v36HGnKbwsiBTcKwZUxf/cXEyIVMXT90AbJg0+JoBIY3gIBH1r3JCQC3ooSLyyuQF3jtb4maKyB5RBJi483XVGxq5DPhYPLrjtPne7zMVdOlAb08sDXwkUuWrLgt5L3xSZJkKjqC2ordOHOL9eMpC7zuFuokickfYBztr5P0l3ZpWb3fbZ1d/lYugSTt7f6/BL3uKcMvNNd4J7vMTqq/dT1SfAbYQdMGPXb7Z9YFN6H187RXhQNADc8/25dr5AAZODP124PHY81NrrplhYR0KbFRk583re52X67WKKBwKsiFkwiBvL2z5CrAUXXTLAu+QZHkvG+I90BZeBtb+1GX7Xg/MPbuoBdoQhXlPoC0nKlgKiEjc6Hdj2kLv68BNCj23qv/moFR5i2h5FIQARjkhpEz8jqZ96qbLNlwvyCy/RMxGEW2DvAlw+dDr+2PJyOAiz+zbFnnTflDR+5bX7xf9jYjsnu/1i8gPeRPAqJyowcm/imvunwa31ngXqPJislfOFdE6KIAJ0BOCS7jGeCSrjZeNzXpdk6e0e/51KCJX5DULuHiX63YRI/MBFN68veaPBxSmWkW0FvLTAI51QujxLStq5a+IjoL8ngsQjgdQaG5yPEnfyVdE+0XOGuDi6htGK1IVXNF/9e5FV3bKDyt2duSsAVQY7zpIGfwpov0idwJgHRP8VeexS18oVIWKaF3kZAL+MPSGUQqVgSN5Ydr8y+sKWakiWg85EcASz3gIbwUvPnTZgZGTCVA1xwZDCBs93cteSZe/iPaLrDXAecOm7qZIfwBFX7z9w0uKe/g6MLLWAKJmPBJ8LYvydAvUqYhWRNYEUBgHIEJ9ueUpqv8OjqxMwLnVU3cFGRTcxf9i0fvv+MhOA4geF358Q0KvhiuiIyM7J1Dk2GDot74i8JBHER0cGRPgnKG3DFMYDGCwXiqq/86BjE2ACseFnvpDKar/ToKMNYARjgss/Uu9pRXFt212EmREgLNHTNtZYEjwrRyv3FNzXsIvcBbR8ZCRCVAjx4Z+W2hR/XciZKQBfMY/zsGg0FjSpayo/jsR0mqAk4bcsJMfZ1dLBQvz8l2zLlvfGhUronWQlgDGco5RVbFEELGL6r+TIS0BmnDG2SiWSlNJg7/4pa1OhpQEGDfMu43j+PZSVWzklelLryuq/06GlASo9zUeaQu2JYqR4tJvZ0RKAvjFf4xBsFR9HkuK6r8TIikB9h40obtffftZCJbom69+Oq344uVOiKQEMNJwuF+l3BJBoPjUTydFUgL4xYwTBUvFlCDPt2alimg9JHw6uKrq/LJudtNqC6u7JfLBnEX37t3aFSuidZBQA5RRf7Bf6W6JYhXVf6dGQgI4thmHgqiiYhfVfydGgsWg8TbKEQCq+tmiRQ+mfedPER0XcQQYXNVtH6AnAMK/WrtCRbQu4jWAJUeGftqqRfvfyRFPANUjgr9qaz5/5JPWrU4RrY0oAuxcdVo1woDg4bNEf82tiE6IKAIYS46KnCiq/00BMSZAQup/9YIldR+0em2KaHWECTC032+3BUYDILwITzltVakiWg9hAvhL/EeGjkWl+M6fTQQuExCe/jUZ4fU2qU0RrQ4LYGSvsyoQ9gdQeGPx4oc3tG21imgtWAD13fyHoFQAUFT/mxQsAMWE1L86ar3YhvUpopVhwXgb5JcAKG8vXfrQyjauUxGtCGvQoIo9gK0BI1jXtHWFimhdWBZyFIGP+k5c+Plfih9z3MTgUZVtVfSwxUseKb7xaxPE/wNdTWzU9o0tSgAAAABJRU5ErkJggg==';
1383 base_image.onload = function(){
1384 ctx.globalAlpha = 0.15
1385 ctx.drawImage(base_image, (canvas.width/2) - 64 - (lwidth/2), (canvas.height/2) - 128);
1392 /* Function for drawing line charts
1394 * quokkaLines("myCanvas", ['Line a', 'Line b', 'Line c'], [ [x1,a1,b1,c1], [x2,a2,b2,c2], [x3,a3,b3,c3] ], { stacked: true, curve: false, title: "Some title" } );
1396 function quokkaBars(id, titles, values, options) {
1397 var canvas = document.getElementById(id);
1398 var ctx=canvas.getContext("2d");
1399 // clear the canvas first
1400 ctx.clearRect(0, 0, canvas.width, canvas.height);
1403 var stack = options ? options.stack : false;
1404 var astack = options ? options.astack : false;
1405 var curve = options ? options.curve : false;
1406 var title = options ? options.title : null;
1407 var noX = options ? options.nox : false;
1408 var verts = options ? options.verts : true;
1416 ctx.lineWidth = 0.5;
1417 ctx.strokeRect(25, 30, canvas.width - lwidth - 40, canvas.height - lheight - 40);
1419 // Draw a title if set:
1420 if (title != null) {
1421 ctx.font="15px Arial";
1422 ctx.fillStyle = "#000";
1423 ctx.textAlign = "center";
1424 ctx.fillText(title,(canvas.width-lwidth)/2, 15);
1428 ctx.textAlign = "left";
1430 for (var k in titles) {
1435 var title = titles[k];
1436 if (title && title.length > 0) {
1437 ctx.fillStyle = colors[k % colors.length][0];
1438 ctx.fillRect(canvas.width - lwidth + 20, posY-10, 10, 10);
1441 ctx.font="12px Arial";
1442 ctx.fillStyle = "#000";
1443 ctx.fillText(title,canvas.width - lwidth + 40, posY);
1457 for (y in values[x]) {
1460 if (max == null || max < values[x][y]) {
1463 if (min == null || min > values[x][y]) {
1468 if (stacked == null || stacked < s) {
1481 // Set number of lines to draw and each step
1483 var step = (max-min) / (numLines+1);
1485 // Prettify the max value so steps aren't ugly numbers
1487 step = (Math.round(step+0.5));
1488 max = step * (numLines+1);
1491 // Draw horizontal lines
1492 for (x = numLines; x >= 0; x--) {
1494 var y = 30 + (((canvas.height-40-lheight) / (numLines+1)) * (x+1));
1496 ctx.lineTo(canvas.width - lwidth - 15, y);
1497 ctx.lineWidth = 0.25;
1501 ctx.font="10px Arial";
1502 ctx.fillStyle = "#000";
1503 ctx.textAlign = "right";
1504 ctx.fillText( Math.round( ((max-min) - (step*(x+1))) * 100 ) / 100,canvas.width - lwidth + 12, y-4);
1505 ctx.fillText( Math.round( ((max-min) - (step*(x+1))) * 100 ) / 100,20, y-4);
1509 // Draw vertical lines
1511 var numLines = values.length-1;
1512 var step = (canvas.width - lwidth - 40) / values.length;
1521 for (var x = 1; x < values.length; x++) {
1523 var y = 35 + (step * (x/sx));
1525 ctx.lineTo(y, canvas.height - 10 - lheight);
1526 ctx.lineWidth = 0.25;
1534 // Some pre-calculations of steps
1535 var step = (canvas.width - lwidth - 48) / values.length;
1536 var smallstep = (step / titles.length) - 2;
1538 // Draw X values if noX isn't set:
1541 for (var i = 0; i < values.length; i++) {
1542 smallstep = (step / (values[i].length-1)) - 2;
1544 var x = 35 + ((step) * i);
1545 var y = canvas.height - lheight + 5;
1547 ctx.translate(x, y);
1551 ctx.rotate(45*Math.PI/180);
1552 ctx.textAlign = "left";
1553 var val = values[i][0];
1554 if (val.constructor.toString().match("Date()")) {
1555 val = val.toDateString();
1557 ctx.fillText(val.toString(), 0, 0);
1558 ctx.rotate(-45*Math.PI/180);
1559 ctx.translate(-x,-y);
1573 smallstep = (step / (values[k].length)) - 2;
1575 pstacks[k] = canvas.height - 40 - lheight;
1577 for (i in values[k]) {
1579 var z = parseInt(i);
1582 z = parseInt(i) + 1;
1584 if (z > values[k].length) {
1588 var value = values[k][i];
1589 var title = titles[i];
1590 var color = colors[zz % colors.length][1];
1591 var fcolor = colors[zz % colors.length][2];
1592 if (values[k][2] && values[k][2].toString().match(/^#.+$/)) {
1593 color = values[k][2]
1594 fcolor = values[k][2]
1595 smallstep = (step / (values[k].length-2)) - 2;
1597 var x = ((step) * k) + ((smallstep+2) * zz) + 5;
1598 var y = canvas.height - 10 - lheight;
1599 var mdiff = (max-min);
1600 mdiff = (mdiff == 0) ? 1 : mdiff;
1601 var height = ((canvas.height - 40 - lheight) / (mdiff)) * value * -1;
1602 var width = smallstep - 2;
1609 stacks[k] -= height;
1612 y = canvas.height - 10 - lheight;
1620 ctx.strokeStyle = color;
1621 ctx.strokeRect(27 + x, y, width, height);
1624 ctx.fillStyle = 'rgba('+ [parseInt(fcolor.r*255),parseInt(fcolor.g*255),parseInt(fcolor.b*255),alpha].join(",") + ')';
1626 ctx.fillStyle = fcolor;
1628 ctx.fillRect(27 + x, y, width, height);
1645 background: #272B30;
1649 background-color: #272B30;
1653 font-family: Arial, Helvetica, sans-serif;
1654 font-weight: normal;
1658 background: linear-gradient(to bottom, #F8A900 0%,#D88900 100%);
1664 border-bottom: 2px solid #000;
1670 background: linear-gradient(to bottom, #EFEFEF 0%,#EEE 100%);
1671 width: calc(100% - 240px);
1674 border-bottom: 2px solid #000;
1685 background: #33363F;
1686 min-height: calc(100% - 80px);
1693 height: calc(100% - 34px);
1694 background: #293D4C;
1698 background: rgba(30,30,30,0.3);
1706 background: rgba(30,30,30,0.3);
1709 border-bottom: 1px solid rgba(200,200,200,0.2);
1714 width: calc(100% - 220px);
1724 font-family: "Courier New", Courier, monospace;
1735 /* ====================== */
1739 border: 1px solid #405871;
1740 background-color: inherit;
1742 text-decoration: none;
1748 /* ====================== */
1750 padding: 0.2em 0 0.2em 0.7em;
1751 margin: 0 0 0.5em 0;
1752 text-decoration: none;
1771 background: #F6F6F6;
1773 tr:nth-child(even) {
1774 background: #EBEBEB;
1780 border: 1px solid #333;
1793 border-radius: 10px;
1794 border: 1px solid #999;
1812 background: #3C3E47;
1821 background: linear-gradient(to bottom, #72ca72 0%,#55bf55 100%);
1824 text-decoration: none;
1826 padding-bottom: 6px;
1830 text-shadow: 1px 1px rgba(0,0,0,0.4);
1842 border-top-left-radius: 4px;
1843 border-top-right-radius: 4px;
1844 background: #FAB227;
1846 border: 2px solid #FAB227;
1847 border-bottom: none;
1853 background: #222222;
1854 border: 2px solid #FAB227;
1857 border-bottom-left-radius: 4px;
1858 border-bottom-right-radius: 4px;
1872 .serverinfo ul li .btn {
1873 width: calc(100% - 8px);
1881 background: rgba(0,0,0,0.2);
1882 border-bottom: 1px solid rgba(100,100,100,0.3);
1885 .serverinfo ul li:nth-child(1) {
1886 border-top: 1px solid rgba(100,100,100,0.3);
1888 .serverinfo ul li .btn.active {
1889 background: rgba(30,30,50,0.2);
1890 border-left: 4px solid #27FAB2;
1895 .serverinfo ul li .btn:hover {
1896 background: rgba(50,50,50,0.15);
1897 border-left: 4px solid #FAB227;