diff --git a/site/common/database.php b/site/common/database.php
index 9cc314e..fa054a0 100644
--- a/site/common/database.php
+++ b/site/common/database.php
@@ -337,7 +337,7 @@ function getRegions($dbh, &$output, $userID) {
if(getEpiweekInfo($dbh, $temp) !== 1) {
return getResult($temp);
}
- $result = $dbh->query("SELECT r.`id`, r.`name`, r.`states`, r.`population`, CASE WHEN s.`user_id` IS NULL THEN FALSE ELSE TRUE END `completed` FROM ec_fluv_regions r LEFT JOIN ec_fluv_submissions s ON s.`user_id` = {$userID} AND s.`region_id` = r.`id` AND s.`epiweek_now` = {$temp['epiweek']['round_epiweek']} ORDER BY r.`id` ASC");
+ $result = $dbh->query("SELECT r.`id`, r.`fluview_name`, r.`name`, r.`states`, r.`population`, CASE WHEN s.`user_id` IS NULL THEN FALSE ELSE TRUE END `completed` FROM ec_fluv_regions r LEFT JOIN ec_fluv_submissions s ON s.`user_id` = {$userID} AND s.`region_id` = r.`id` AND s.`epiweek_now` = {$temp['epiweek']['round_epiweek']} ORDER BY r.`id` ASC");
$regions = array();
while($row = $result->fetch_assoc()) {
if ($row['name'] == "ny_minus_jfk") {
@@ -347,7 +347,8 @@ function getRegions($dbh, &$output, $userID) {
}
$region = array(
- 'id' => intval($row['id']),
+ 'id' => intval($row['id']),
+ 'fluview_name' => $row['fluview_name'],
'name' => $region_name,
'states' => $row['states'],
'population' => intval($row['population']),
diff --git a/site/common/header.php b/site/common/header.php
index 9fec30c..27d1d96 100644
--- a/site/common/header.php
+++ b/site/common/header.php
@@ -52,7 +52,7 @@
<COVID-19 Edition>
-
Epidemiological Forecasting by
DELPHI
+ Epidemiological Forecasting by
DELPHI
');
- createLink('DELPHI', 'https://delphi.midas.cs.cmu.edu/', true, 'delphi');
+ createLink('DELPHI', 'https://delphi.cmu.edu/', true, 'delphi');
print('');
?>
diff --git a/site/css/style.php b/site/css/style.php
index 49ecdc0..d50d355 100644
--- a/site/css/style.php
+++ b/site/css/style.php
@@ -351,6 +351,11 @@
padding-top: 10px;
float: right;
}
+
+div.sidebar_entry i.fa {margin-right:5px}
+div.sidebar_region {margin-left:15px}
+div.sidebar_region i.fa {margin-right:5px}
+
div.box_scroll_test {
width: 100%;
height: 100%;
diff --git a/site/forecast.php b/site/forecast.php
index 4d671ba..5f5f909 100644
--- a/site/forecast.php
+++ b/site/forecast.php
@@ -1,5 +1,5 @@
201020) {
- $maxRegionalWILI = max($maxRegionalWILI, $region['history']['wili'][$i]);
- $minRegionalWILI = min($minRegionalWILI, $region['history']['wili'][$i]);
- }
-}
-max($region['history']['wili']); // what is this for? -kmm
-$target = $seasonStart;
-$seasonOffsets = array();
-$seasonYears = array();
-for($i = count($region['history']['date']) - 1; $i >= 0; $i--) {
- if($region['history']['date'][$i] <= $target) {
- array_push($seasonOffsets, $i);
- array_push($seasonYears, intval($target / 100));
- $target -= 100;
- }
-}
-if($seasonOffsets[count($seasonOffsets) - 1] != 0) {
- array_push($seasonOffsets, 0);
- array_push($seasonYears, intval($target / 100));
-}
-$seasonOffsets = array_reverse($seasonOffsets);
-$seasonYears = array_reverse($seasonYears);
-// okay for international and COVID-19 data, we're going to treat them
-// as magic additional seasons with years that start with some absurd
-// number (maybe we have a limited number of other sources and each
-// source can get its own absurd prefix e.g. 9xxx=COVID-19, 8xxx=ECDC,
-// etc). These will be added after the year-based calculations from
-// the block above.
-//
-// We also want to make sure the not-actually-WILI data shakes out in
-// the correct order for when we fill the `pastWili` javascript array
-// with it in a minute.
-//
-// Maybe we want to make the set of additional data selectable in the
-// future, but for now it has to be hard-coded.
-
-// A rough sketch:
-// sources = {
-// "COVID-19":{"Italy":9001, "Spain":9002, "France":9003, "USA":9010},
-// "ECDC":{"Italy":80012019, "Spain":80022019, "France":80032019}, #ECDC publishes the last two seasons, for now we just want 2019-2020 but maybe we'll want both later? Will SK and UK have multiple seasons of ILI data too?
-// "SKorea":{"South Korea":70002019},
-// "UK":{"UK":60002019}
-// }
-
-
-
-$sources = array(
- "ECDC" => array(
- "fn" => "getECDCILI",
- "key" => "ecdc",
- "members" => array(
- //"Italy" => 8001,
- //"Spain" => 8002,
- //"France" => 8003,
- //"Netherlands" => 8004,
- //"Ireland" => 8005,
- //"UK - Scotland" => 8006,
- //"Belgium" => 8007,
- )));
-$sourceIDs = array();
-foreach($sources["ECDC"]["members"] as $country => $cid) {
- $sourceIDs[$cid] = $country.", ECDC";
-}
-
-
-// $lastOffset = $seasonOffsets[end]
-// for $src,$map in $sources {
-// for $name,$rid in $map {
-// push $output['regions'][$rid]['history'] onto the end of $region['history']
-// push $lastOffset + count($output['regions'][$rid]['history']['date'] onto the end of $seasonOffsets
-// push $rid onto the end of $seasonYears
-// }
-// }
-
-
-$lastOffset_i = count($seasonOffsets);
-$currentYear = $seasonYears[$lastOffset_i-1];
-$lastHistory_i = count($region['history']['date']);
-$nextOffset = $lastHistory_i;
-foreach ($sources as $src => $meta) {
- $fn = $meta["fn"];
- foreach ($meta["members"] as $name => $rid) {
- $fn($dbh, $output, $rid, $seasonStart+5); // hard-coded for now; ECDC counts seasons from epiweek 40
-
- $n = count($output[$meta["key"]][$rid]["date"]);
-?>
-
- You can edit any part of your forecast by redrawing just that part.
You can adjust a single point by dragging it up or down.
The animation below demonstrates these actions.
- (If you don't see the animation, click here .)
+ (If you don't see the animation, click here .)
@@ -238,8 +114,23 @@ function getColor($regionID, $seasonID) {
} else {
?>
@@ -263,73 +154,37 @@ function getColor($regionID, $seasonID) {
-
-
-
History
-
+
History
+
+
+
+
-
= htmlspecialchars($r['name']) ?>
-
Seasons:
-
Show all
- $numHHS)) {
- continue;
- }
- }
-
- if($r['id'] == $regionID && $year == $currentYear) { // does this ever actually happen? -kmm
- ?>
-
- = sprintf('current year') ?>
-
-
-
-
- = sprintf('current year') ?>= ($year == 2009 ? ' pandemic' : '') ?>
-
- 3000) {
- // this indicates an international dataset. See preamble for setup.
- ?>
-
= sprintf('%s',$sourceIDs[$year]) ?>
-
-
-
= sprintf('%d-%s', $year, substr((string)($year + 1), 2, 2)) ?>= ($year == 2009 ? ' pandemic' : '') ?>
-
-
-
+
+
+
+
= formatEpiweek($output['epiweek']['data_epiweek']) ?>
latest available data
-
-
= getDeltaWeeks($output['epiweek']['round_epiweek'], $output['epiweek']['season']['end']) ?>
-
weeks remaining in season
-
= season.start; i--) {
+ var point = curves[region].data[i];
+ if (point.epiweek == epiweek) {
+ return point;
+ }
+ if (point.epiweek < epiweek && interp) {
+ return point;
+ }
+ }
+ return false;
+}
+function drawText(g, str, x, y, angle, alignX, alignY, scale, font) {
+ scale = typeof scale !== 'undefined' ? scale : 1;
+ font = typeof font !== 'undefined' ? font : ['', 'Calibri'];
+ var size = Math.round(12 * scale * uiScale);
+ g.font = font[0] + ' ' + size + 'px ' + font[1];
+ var w = g.measureText(str).width;
+ var h = size;
+ var dx = 0;
+ var dy = 0;
+ if(alignX == Align.left) {
+ dx = 0;
+ } else if(alignX == Align.right) {
+ dx = -w;
+ } else if(alignX == Align.center) {
+ dx = -w / 2;
+ } else {
+ g.strokeStyle = '#ff0000';
+ }
+ if(alignY == Align.bottom) {
+ dy = 0;
+ } else if(alignY == Align.top) {
+ dy = h;
+ } else if(alignY == Align.center) {
+ dy = h / 2;
+ } else {
+ g.strokeStyle = '#ff0000';
+ }
+ g.save();
+ g.translate(x, y);
+ g.rotate(angle);
+ g.fillText(str, dx, dy);
+ g.restore();
+ return {x: x + dx, y: y + dy - h, w: w, h: h};
+}
+function drawLine(x1, y1, x2, y2, style) {
+ var g = getGraphics();
+ g.strokeStyle = style.color;
+ g.lineWidth = style.size * uiScale;
+ g.setLineDash(style.dash);
+ g.beginPath();
+ g.moveTo(x1, y1);
+ g.lineTo(x2, y2);
+ g.stroke();
+ g.setLineDash([]);
+}
+function drawPoints(xs, ys, style, g) {
+ if (typeof g == 'undefined') {
+ var g = getGraphics();
+ g.strokeStyle = style.color;
+ g.lineWidth = style.size * uiScale;
+ g.setLineDash([]);
+ }
+ g.lineWidth = 3 * style.size * uiScale;
+ for(var i = 0; i < xs.length; i++) {
+ if(ys[i] >= 0) {
+ g.beginPath();
+ var x = getX(xs[i]);
+ var y = getY(ys[i]);
+ g.moveTo(x, y);
+ g.lineTo(x + 1, y);
+ g.stroke();
+ }
+ }
+}
-//Number of axis tick marks
-var xInterval = 2;
-var yInterval = 1;
-var uiScale = 1;
-var canvas = $('#canvas')[0];
-var dragging = false;
-var hoveringButton = null;
+function drawCurveData(curve, style, do_drawPoints, season, key) {
+ if (typeof season == "undefined") {
+ season = currentSeason;
+ }
+ if (typeof key == "undefined") {
+ key = "wili";
+ }
+ var g = getGraphics();
+ g.strokeStyle = style.color;
+ g.lineWidth = style.size * uiScale;
+ g.setLineDash(style.dash);
+ g.beginPath();
+ var first = true;
+ for(var i = 0; i < curve.length; i++) {
+ if (curve[i][key] < 0) { continue; }
+ var x = getX(curve[i].epiweek, season);
+ var y = getY(curve[i][key]);
+ if(first) {
+ first = false;
+ g.moveTo(x, y);
+ } else {
+ g.lineTo(x, y);
+ }
+
+ }
+ g.stroke();
+ if(do_drawPoints) {
+ g.setLineDash([]);
+ drawPoints(curve.map(function (item) { return item.epiweek; }),
+ curve.map(function (item) { return item[key]; }),
+ style, g);
+ }
+}
-var modifyCounter = 0;
-var submitCounter = 0;
-var modified = false;
-var zoomDownBounds;
-var zoomUpBounds;
-var showLastBounds;
-var snapLastBounds;
-var SubmitStatus = {
- init: 0,
- sent: 1,
- success: 2,
- failure: 3
-};
-var submitStatus = SubmitStatus.init;
+function drawCurve(curve, start, end, epiweekOffset, style) {
+ var g = getGraphics();
+ g.strokeStyle = style.color;
+ g.lineWidth = style.size * uiScale;
+ g.setLineDash(style.dash);
+ g.beginPath();
+ var first = true;
+ var epiweek = addEpiweeks(xRange[0], epiweekOffset);
+ for(var i = start; i < end; i++) {
+ if(curve[i] >= 0) {
+ var x = getX(epiweek);
+ var y = getY(curve[i]);
+ if(first) {
+ first = false;
+ g.moveTo(x, y);
+ } else {
+ g.lineTo(x, y);
+ }
+ }
+ epiweek = addEpiweeks(epiweek, 1);
+ }
+ g.stroke();
+ g.setLineDash([]);
+ if(DRAW_POINTS) drawPoints(curve, start, end, epiweekOffset, style, g);
+}
+function drawCurveXY(xs, ys, start, end, style) {
+ var g = getGraphics();
+ g.strokeStyle = style.color;
+ g.lineWidth = style.size * uiScale;
+ g.setLineDash(style.dash);
+ g.beginPath();
+ var first = true;
+ for(var i = start; i < end; i++) {
+ if(ys[i] >= 0) {
+ var x = getX(modulusEpiweek(xs[i]));
+ var y = getY(ys[i]);
+ if(first) {
+ first = false;
+ g.moveTo(x, y);
+ } else {
+ g.lineTo(x, y);
+ }
+ }
+ }
+ g.stroke();
+ g.setLineDash([]);
+ if(DRAW_POINTS) drawPoints(xs, ys, start, end, style, g);
+}
+function stitchCurves(rid, style, y2, xoffset) {
+ if(forecast[0] < 0) {
+ return;
+ }
+ if (typeof y2 == "undefined") {
+ y2 = getY(forecast[0]);
+ }
+ if (typeof xoffset == "undefined") {
+ xoffset = 1;
+
+ }
+
+ var seasonIndex = seasonIndices[2019];
+ var seasonLength = seasonOffsets[seasonIndex+1] - seasonOffsets[seasonIndex];
+ var x1 = getX(addEpiweeks(xRange[0], seasonLength - 1));
+ var y1 = getY(pastWili[seasonOffsets[seasonIndex + 1] - 1]);
+ var x2 = getX(addEpiweeks(currentWeek, xoffset));
+ drawLine(x1, y1, x2, y2, style);
+}
+function drawTooltip(g, str) {
+ str = ' ' + str;
+ var cx = getChartWidth() / 2;
+ var cy = getChartHeight() / 2;
+ var bt = drawText(g, str, cx, cy, 0, Align.center, Align.center, 1.5);
+ var bi = drawText(g, "\uf05a", bt.x, cy, 0, Align.right, Align.center, 1.5, ['', 'FontAwesome']);
+ var padding = 6;
+ var border = 3;
+ g.fillStyle = '#000';
+ g.fillRect(bi.x - padding - border, bt.y - padding - border, bi.w + bt.w + 2 * (padding + border), bt.h + 2 * (padding + border));
+ g.fillStyle = '#fff';
+ g.fillRect(bi.x - padding, bt.y - padding, bi.w + bt.w + 2 * padding, bt.h + 2 * padding);
+ g.fillStyle = '#000';
+ drawText(g, str, cx, cy, 0, Align.center, Align.center, 1.5);
+ drawText(g, "\uf05a", bt.x, cy, 0, Align.right, Align.center, 1.5, ['', 'FontAwesome']);
+}
+function getStyle(region, season) {
+ var ret;
+
+ if (region==regionID && season==currentSeason) {
+ ret = {color: '#000', size: 2, dash: [], alpha: 1};
+ } else if (region.startsWith("hhs")) {
+ ret = {color: '#66f', size: 1, dash: [], alpha: 0.4};
+ } else if (season==2009) { //pandemic
+ ret = {color: '#666', size: 1, dash: [], alpha: 0.4};
+ } else {
+ ret = {color: '#aaa', size: 0.5, dash: [], alpha: 0.4};
+ }
+
+ if (hoverCurve(region,season)) {
+ ret.size++;
+ }
+
+ return ret;
+}
+function repaint() {
+ var g = getGraphics();
+ //clear the canvas
+ g.clearRect(0, 0, canvas.width, canvas.height);
+ g.fillStyle = '#fff';
+ g.fillRect(0, 0, canvas.width, canvas.height);
+ //past/future
+ var weekX = getX(currentWeek + 0.5);
+ var x1 = getX(xRange[0]);
+ var x2 = getX(xRange[1]);
+ var y1 = getY(yRange[0]);
+ var y2 = getY(yRange[1]);
+ var scale_y0 = 0;
+ var scale_y1 = 0;
+ //past
+ g.fillStyle = '#eee';
+ g.fillRect(x1, y2, weekX - x1, y1 - y2);
+ g.fillStyle = '#888';
+ drawText(g, '< past', weekX - 15, y2, 0, Align.right, Align.top);
+ //future
+ g.fillStyle = '#fff';
+ g.fillRect(weekX, y2, x2 - weekX, y1 - y2);
+ g.fillStyle = '#888';
+ drawText(g, 'future >', weekX + 15, y2, 0, Align.left, Align.top);
+ //axis styles
+ g.lineCap = 'round';
+ g.fillStyle = '#000';
+ g.lineWidth = 1 * uiScale;
+ //y-axis
+ {
+ var row1 = 12.5 * uiScale;
+ var row2 = marginLeft() - 12.5 * uiScale;
+ scale_y0 = getY(yRange[0]);
+ scale_y1 = getY(yRange[0]+yInterval);
+ var scale = scale_y0 - scale_y1;
+ //ticks and lines
+ for(var incidence = yRange[0]; incidence <= yRange[1]; incidence += yInterval) {
+ var y = getY(incidence);
+ drawText(g, '' + incidence, row2, y, 0, Align.right, Align.center);
+ drawLine(marginLeft() - TICK_SIZE, y, marginLeft() - 1, y, AXIS_STYLE);
+ drawLine(getX(xRange[0]), y, getX(xRange[1]), y, GRID_STYLE);
+ }
+ //label
+ drawText(g, LABEL_Y, row1 - 8 * uiScale, canvas.height / 2, -Math.PI / 2, Align.center, Align.center, 1.5, ['bold', 'Calibri']);
+ //drawText(g, "(% of all doctors’ office visits that involve flu-like symptoms)", row1 + 7 * uiScale, canvas.height / 2, -Math.PI / 2, Align.center, Align.center, 1.5, ['', 'Calibri']);
+
+ //zoom controls
+ var x = 16 * uiScale;
+ var dy = BUTTON_SIZE * uiScale;
+ zoomUpBounds = drawText(g, "\uf151", x, y2, 0, Align.center, Align.top, 2, ['', 'FontAwesome']);
+ zoomDownBounds = drawText(g, "\uf150", x, y2 + dy, 0, Align.center, Align.top, 2, ['', 'FontAwesome']);
+ }
+ //x-axis
+ {
+ var row1 = 0.75 * (marginBottom() / 3);
+ var row2 = 1.75 * (marginBottom() / 3);
+ var row3 = 2.5 * (marginBottom() / 3);
+ var axisY = canvas.height - marginBottom();
+ //flu season
+ //ticks
+ var skip = 0;
+ for(var epiweek = xRange[0]; epiweek <= xRange[1]; epiweek = addEpiweeks(epiweek, 1)) {
+ var x = getX(epiweek);
+ if(skip == 0) {
+ drawText(g, 'w' + (epiweek % 100), x, canvas.height - row3, 0, Align.center, Align.center);
+ }
+ skip = (skip + 1) % xInterval;
+ drawLine(x, axisY + TICK_SIZE, x, axisY + 1, AXIS_STYLE);
+ }
+ //months
+ var on = true;
+ var decStart = epiweekToDecimal(xRange[0]-1);
+ var decEnd = epiweekToDecimal(xRange[1]);
+ // run through the year twice so we get everybody no matter how long our current season goes on
+ for (var si = 0; si<2; si++) {
+ for (var li=0; li decEnd || x2 < decStart) {
+ continue;
+ }
+ x1 = decimalToFEpiweek(max(decStart,x1));
+ x2 = decimalToFEpiweek(min(decEnd,x2));
+
+ // skip truncated months that are too short for their label
+ if (x2-x1<1.5) { continue; }
+
+ x1 = getX(x1);
+ x2 = getX(x2);
+ y1 = canvas.height - row3 + row2/4;
+ oldFillStyle=g.fillStyle;
+ g.fillStyle = on ? '#eee' : '#fff'; on = !on;
+ g.fillRect(x1, y1, x2-x1, row2/2);
+ g.fillStyle = oldFillStyle;
+
+ drawText(g, label, 0.5*(x1 + x2), canvas.height - row2, 0, Align.center, Align.center);
+ }
+ }
+ //label
+ drawText(g, LABEL_X, canvas.width / 2, canvas.height - row1, 0, Align.center, Align.center, 1.5, ['bold', 'Calibri']);
+ }
+
+ //other regions or past seasons
+ function repaintSeason(r, s, do_drawPoints) {
+ if (typeof s == "undefined") {
+ i = r;
+ var r = selectedSeasons[i][0];
+ var s = selectedSeasons[i][1];
+ do_drawPoints = false;
+ }
+ var style = getStyle(r, s); //curveStyles[r][s];
+
+ if (typeof curves[r] == "undefined") {
+ console.log("repaint:",r,"not yet available");
+ return;
+ }
+ drawCurveData(
+ curves[r].data.slice(
+ curves[r].season[s].start,
+ curves[r].season[s].end+1), style, do_drawPoints, s);
+ }
+ for(var i = 0; i < selectedSeasons.length; i++) {
+ var isCurrentSeason = (selectedSeasons[i][1] == 2019);
+ if(selectedSeasons[i][0] == regionID && isCurrentSeason) {
+ // Skip the current region's latest season
+ // so it can be plotted on top of everything else
+ continue;
+ }
+ repaintSeason(i);
+ }
+
+ var lfStyle = {color: '#aaa', size: 2, dash: DASH_STYLE};
+ var style = {color: '#000', size: 2, dash: DASH_STYLE};
+ if (regionID in curves) {
+ //last forecast
-//chart bounds
-function marginLeft() { return MARGIN_LEFT * uiScale; }
-function marginRight() { return MARGIN_RIGHT * uiScale; }
-function marginTop() { return MARGIN_TOP * uiScale; }
-function marginBottom() { return MARGIN_BOTTOM * uiScale; }
-function max(x1,x2) { if (x1>x2) { return x1; } return x2; }
-function min(x1,x2) { if (x1 start) ? 1 : -1;
+ var num = 0;
+ while(start != end && num < 1e3) {
+ start = addEpiweeks(start, x);
+ num += x;
+ }
+ return num;
+}
+function addEpiweeks(ew, i) {
+ var year = Math.floor(ew / 100);
+ var week = ew % 100;
+ week += i;
+ var limit = getNumWeeks(year);
+ if(week >= limit + 1) {
+ week -= limit;
+ year += 1;
+ } else if(week < 1) {
+ week += getNumWeeks(year - 1);
+ year -= 1;
+ }
+ return year * 100 + week;
+}
+function modulusEpiweek(ew) {
+ var startingWeek = xRange[0] % 100;
+ var weekOffset = (ew % 100) - startingWeek;
+ if (weekOffset < 0) weekOffset = weekOffset + 100;
+ return xRange[0] + weekOffset;
+}
+function epiweekToDecimal(ew) {
+ var year = Math.floor(ew / 100);
+ var week = ew % 100;
+ return year + (week - 1) / getNumWeeks(year);
+}
+function decimalToFEpiweek(yr) {
+ yr += 0.5 / 52;
+ var year = Math.floor(yr);
+ var wk = yr - year;
+ var week = wk * getNumWeeks(year);
+ return year * 100 + week;
+}
+function decimalToEpiweek(yr) {
+ yr += 0.5 / 52;
+ var year = Math.floor(yr);
+ var wk = yr - year;
+ var week = Math.floor(wk * getNumWeeks(year)) + 1;
+ return year * 100 + week;
+}
+function animate() {
+ repaint();
+ if(dragging) {
+ requestAnimationFrame(animate);
+ } else {
+ repaint();
+ }
+}
+function adjustForecast(x, y) {
+ var epiweek = getEpiweek(x);
+ if(epiweek > currentWeek && epiweek <= xRange[1]) {
+ var wili = Math.min(yRange[1], Math.max(yRange[0], getIncidence(y)));
+ var point = {'epiweek':epiweek, 'wili':wili};
+ curves.forecast[getDeltaWeeks(currentWeek, epiweek) - 1] = point;
+ if(lastDrag != null && epiweek != lastDrag.epiweek) {
+ var direction = (epiweek > lastDrag.epiweek) ? 1 : -1;
+ for(var i = addEpiweeks(lastDrag.epiweek, direction); i != epiweek; i = addEpiweeks(i, direction)) {
+ curves.forecast[getDeltaWeeks(currentWeek, i) - 1] = {'epiweek':i, 'wili':point.wili};
+ }
+ }
+ lastDrag = point;//{epiweek: epiweek, wili: wili};
+ modified = true;
+ } else {
+ lastDrag = null;
+ }
+}
+function contains(bounds, point) {
+ var x1 = bounds.x;
+ var x2 = bounds.x + bounds.w;
+ var y1 = bounds.y;
+ var y2 = bounds.y + bounds.h;
+ return (point.x >= x1 && point.x <= x2 && point.y >= y1 && point.y <= y2);
+}
+//user interaction
+function mouseDown(m) {
+ tooltip = null;
+ if(contains(zoomUpBounds, m)) {
+ zoom(1 / ZOOM_AMOUNT);
+ } else if(contains(zoomDownBounds, m)) {
+ zoom(ZOOM_AMOUNT);
+ //} else if(contains(undoBounds, m)) {
+ // undo();
+ //} else if(contains(redoBounds, m)) {
+ // redo();
+ } else if(contains(showLastBounds, m)) {
+ showLastForecast = !showLastForecast;
+ repaint();
+ } else if(contains(snapLastBounds, m)) {
+ if(confirm('Are you sure you want to reset your current forecast to your previous forecast?')) {
+ snapToLastForecast();
+ }
+ } else {
+ $('#canvas').addClass('canvas_drag');
+ adjustForecast(m.x, m.y);
+ dragging = true;
+ animate();
+ }
+}
+function mouseUp(m) {
+ $('#canvas').removeClass('canvas_drag');
+ if(dragging) {
+ dragging = false;
+ lastDrag = null;
+ if(modified) {
+ ++modifyCounter;
+ setTimeout(submitForecastDelayed, AUTOSAVE_INTERVAL * 1000);
+ }
+ modified = false;
+ }
+}
+function mouseMove(m) {
+ //Drawing ecast
+ if(dragging) {
+ adjustForecast(m.x, m.y);
+ return;
+ }
+ //Interacting with a button
+ var buttons = [
+ {
+ bounds: zoomUpBounds,
+ tooltip: 'Decrease the scale of the Y axis. (Zoom in.)',
+ },{
+ bounds: zoomDownBounds,
+ tooltip: 'Increase the scale of the Y axis. (Zoom out.)',
+ },{
+ bounds: showLastBounds,
+ tooltip: 'Show or hide your forecast from last week.',
+ },{
+ bounds: snapLastBounds,
+ tooltip: 'Pin your current forecast to your forecast from last week.',
+ },
+ ];
+ //Find out which button (if any)
+ var hb = null;
+ tooltip = null;
+ for(var i = 0; i < buttons.length; i++) {
+ if(contains(buttons[i].bounds, m)) {
+ hb = buttons[i].bounds;
+ tooltip = buttons[i].tooltip;
+ break;
+ }
+ }
+ //Update if the hovered button has changed
+ if(hoveringButton != hb) {
+ if(hoveringButton != null && hb == null) {
+ //back to the normal cursor
+ $('#canvas').removeClass('canvas_button');
+ } else if(hoveringButton == null && hb != null) {
+ //use the button cursor
+ $('#canvas').addClass('canvas_button');
+ }
+ hoveringButton = hb;
+ repaint();
+ }
+}
+function mousePosition(e) {
+ if(e.type.toLowerCase().indexOf('touch') == 0) {
+ e = e.originalEvent.changedTouches[0];
+ }
+ var canvas = $('#canvas');
+ return {
+ x: e.pageX - canvas.offset().left,
+ y: e.pageY - canvas.offset().top
+ };
+}
+function zoom(scale) {
+ yRange[1] = Math.min(WILI_MAX, Math.max(WILI_MIN, yRange[1] * scale));
+ repaint();
+}
+function submitForecastDelayed() {
+ ++submitCounter;
+ if(modifyCounter == submitCounter && !dragging) {
+ //No modifications in the last AUTOSAVE_INTERVAL seconds
+ submitForecast(false);
+ }
+}
+function submitForecast(commit) {
+ if(commit && $('#button_submit').hasClass('box_button_disabled')) {
+ return;
+ }
+ var foundZero = false;
+ var f = [];
+ for(var i = 0; i < 52; i++) {
+ if (!curves.forecast[i]) { continue; }
+ if (curves.forecast[i].wili == 0) {
+ foundZero = curves.forecast[i].epiweek;
+ break;
+ }
+ f[i] = curves.forecast[i].wili;
+ }
+ if(commit) {
+ if(foundZero) {
+ alert('Some points are still at zero (for '+Math.floor(foundZero/100)+' w'+foundZero%100+'; maybe others). Please double check your forecast and try again.');
+ return;
+ }
+ timeoutID = setTimeout(submitTimeout, 10000);
+ submitStatus = SubmitStatus.sent;
+ updateStatus();
+ $('#button_submit').addClass('box_button_disabled');
+ }
+ var params = {
+ 'action': commit ? 'forecast' : 'autosave',
+ 'hash': userHash,
+ 'region_id': regionNo,
+ 'f[]': f,
+ };
+ $.get("api.php", params, handleResponse, 'json');
+}
+function updateStatus() {
+ $('#box_status').removeClass('any_success any_failure any_neutral');
+ if(submitStatus == SubmitStatus.sent) {
+ $('#status_icon').html(' ');
+ $('#status_message').html('Uploading forecast...');
+ $('#box_status').addClass('any_neutral');
+ } else if(submitStatus == SubmitStatus.success) {
+ $('#status_icon').html(' ');
+ $('#status_message').html('Forecast submitted successfully!');
+ $('#box_status').addClass('any_success');
+ //Move to the next missing region, or go home
+ submit('forecast');
+ } else if(submitStatus == SubmitStatus.failure) {
+ $('#status_icon').html(' ');
+ $('#status_message').html('Uh oh, something went wrong. Please try again later.');
+ $('#box_status').addClass('any_failure');
+ }
+}
+//other events
+function submitTimeout() {
+ handleResponse({result: 0, action: 'forecast'});
+}
+function handleResponse(data) {
+ if(data.action != 'forecast') {
+ //don't really care what the result was unless it has to do with the submit forecast button
+ return;
+ }
+ clearTimeout(timeoutID);
+ //$('#stat_completed').removeClass();
+ $('#button_submit').removeClass('box_button_disabled');
+ if(data.result == 1) {
+ //$('#stat_completed').addClass('any_success');
+ //$('#stat_completed').html('Submitted');
+ submitStatus = SubmitStatus.success;
+ } else {
+ submitStatus = SubmitStatus.failure;
+ }
+ updateStatus();
+}
+function resize() {
+ //Find the right fit for the canvas
+ var w = $('body').innerWidth() - $('#box_histories').width() - 48;
+ var h = $(window).height();
+ w = Math.floor(w - 24);
+ h = Math.floor((h - (56 + 24 + 47 + 24 + 33)) * 0.98);
+ //Get the drawing scale
+ uiScale = ((w * 2 + h * 1) / 3) / 1000;
+ //Apply the resize
+ canvas.width = w;
+ canvas.height = h;
+ $('#box_canvas').width(w);
+ $('#box_canvas').height(h);
+ $('#box_side_bar').height(h);
+ $('#box_histories').height(h - 8);
+ //Finally, repaint the canvas
+ repaint();
+}
+function hoverCurve(rid, season) {
+ return curves[rid]
+ && curves[rid].season
+ && curves[rid].season[season]
+ && curves[rid].season[season].hover;
+}
+function hoverCurveOn(rid, season) {
+ curves[rid].season[season].hover = true;
+ repaint();
+}
+function hoverCurveOff(rid, season) {
+ curves[rid].season[season].hover = false;
+ repaint();
+}
+function toggleSeasonList(rid) {
+ var closedClass = 'fa-plus-square-o';
+ var openedClass = 'fa-minus-square-o';
+ var checkbox = $('#checkbox_region_' + rid);
+ if(checkbox.hasClass(closedClass)) {
+ //Expand region
+ checkbox.removeClass(closedClass);
+ checkbox.addClass(openedClass);
+ $('#container_' + rid + '_all').removeClass('any_hidden');
+ } else {
+ //Shrink region
+ checkbox.removeClass(openedClass);
+ checkbox.addClass(closedClass);
+ $('#container_' + rid + '_all').addClass('any_hidden');
+ }
+ repaint();
+}
+function toggleAllSeasons(rid) {
+ var uncheckedClass = 'fa-square-o';
+ var checkedClass = 'fa-check-square-o';
+ var checkbox = $('#checkbox_' + rid + '_all');
+ if(checkbox.hasClass(uncheckedClass)) {
+ //Enable history
+ checkbox.removeClass(uncheckedClass);
+ checkbox.addClass(checkedClass);
+ for(var season in curves[rid].season) {
+ if($('#checkbox_' + rid + '_' + season).hasClass(uncheckedClass)) {
+ toggleSeason(rid, season);
+ }
+ }
+ } else {
+ //Disable history
+ checkbox.removeClass(checkedClass);
+ checkbox.addClass(uncheckedClass);
+ for(var season in curves[rid].season) {
+ if($('#checkbox_' + rid + '_' + season).hasClass(checkedClass)) {
+ toggleSeason(rid, season);
+ }
+ }
+ }
+ repaint();
+}
+function toggleSeason(rid, seasonID) {
+ var uncheckedClass = 'fa-square-o';
+ var checkedClass = 'fa-check-square-o';
+ var checkbox = $('#checkbox_' + rid + '_' + seasonID);
+ if(checkbox.hasClass(uncheckedClass)) {
+ //Enable history
+ checkbox.removeClass(uncheckedClass);
+ checkbox.addClass(checkedClass);
+ selectedSeasons.push([rid, seasonID]);
+ } else {
+ //Disable history
+ checkbox.removeClass(checkedClass);
+ checkbox.addClass(uncheckedClass);
+ var index = -1;
+ for(var i = 0; i < selectedSeasons.length; i++) {
+ if(selectedSeasons[i][0] == rid && selectedSeasons[i][1] == seasonID) {
+ index = i;
+ break;
+ }
+ }
+ if(index > -1) {
+ selectedSeasons.splice(index, 1);
+ }
+ }
+ repaint();
+}
+function snapToLastForecast() {
+ var extra = curves.lastForecast.length - curves.forecast.length;
+ for(var i = 0; i < Math.min(curves.forecast.length, curves.lastForecast.length - extra); i++) {
+ curves.forecast[i] = curves.lastForecast[i + extra];
+ }
+ repaint();
+ ++modifyCounter;
+ setTimeout(submitForecastDelayed, AUTOSAVE_INTERVAL * 1000);
+ modified = false;
+}
+
+
diff --git a/site/js/mustache.min.js b/site/js/mustache.min.js
new file mode 100644
index 0000000..4369a58
--- /dev/null
+++ b/site/js/mustache.min.js
@@ -0,0 +1 @@
+(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i