diff --git a/app/controllers/AdminController.scala b/app/controllers/AdminController.scala
index 0201bd5b67..442acb0e8a 100644
--- a/app/controllers/AdminController.scala
+++ b/app/controllers/AdminController.scala
@@ -13,7 +13,7 @@ import models.daos.slick.DBTableDefinitions.UserTable
import models.label.LabelTable.LabelMetadata
import models.label.{LabelPointTable, LabelTable}
import models.mission.MissionTable
-import models.region.RegionTable
+import models.region.{RegionCompletionTable, RegionTable}
import models.street.{StreetEdge, StreetEdgeTable}
import models.user.User
import org.geotools.geometry.jts.JTS
@@ -21,6 +21,7 @@ import org.geotools.referencing.CRS
import play.api.libs.json.{JsArray, JsObject, JsValue, Json}
import play.extras.geojson
+
import scala.concurrent.Future
/**
@@ -102,29 +103,36 @@ class AdminController @Inject() (implicit val env: Environment[User, SessionAuth
* @return
*/
def getNeighborhoodCompletionRate = UserAwareAction.async { implicit request =>
- if (isAdmin(request.identity)) {
-
- // http://docs.geotools.org/latest/tutorials/geometry/geometrycrs.html
- val CRSEpsg4326 = CRS.decode("epsg:4326")
- val CRSEpsg26918 = CRS.decode("epsg:26918")
- val transform = CRS.findMathTransform(CRSEpsg4326, CRSEpsg26918)
-
+ RegionCompletionTable.initializeRegionCompletionTable()
+
+ val neighborhoods = RegionCompletionTable.selectAllNamedNeighborhoodCompletions
+ val completionRates: List[JsObject] = for (neighborhood <- neighborhoods) yield {
+ Json.obj("region_id" -> neighborhood.regionId,
+ "total_distance_m" -> neighborhood.totalDistance,
+ "completed_distance_m" -> neighborhood.auditedDistance,
+ "rate" -> (neighborhood.auditedDistance / neighborhood.totalDistance),
+ "name" -> neighborhood.name
+ )
+ }
- val neighborhoods = RegionTable.selectAllNamedNeighborhoods
- val completionRates: List[JsObject] = for (neighborhood <- neighborhoods) yield {
- val streets: List[StreetEdge] = StreetEdgeTable.selectStreetsByARegionId(neighborhood.regionId)
- val auditedStreets: List[StreetEdge] = StreetEdgeTable.selectAuditedStreetsByARegionId(neighborhood.regionId)
+ Future.successful(Ok(JsArray(completionRates)))
+ }
- val completedDistance = auditedStreets.map(s => JTS.transform(s.geom, transform).getLength).sum
- val totalDistance = streets.map(s => JTS.transform(s.geom, transform).getLength).sum
- Json.obj("region_id" -> neighborhood.regionId,
- "total_distance_m" -> totalDistance,
- "completed_distance_m" -> completedDistance,
- "name" -> neighborhood.name
+ /**
+ * Returns DC coverage percentage by Date
+ *
+ * @return
+ */
+ def getCompletionRateByDate = UserAwareAction.async { implicit request =>
+ if (isAdmin(request.identity)) {
+ val streets: Seq[(String, Float)] = StreetEdgeTable.streetDistanceCompletionRateByDate(1)
+ val json = Json.arr(streets.map(x => {
+ Json.obj(
+ "date" -> x._1, "completion" -> x._2
)
- }
+ }))
- Future.successful(Ok(JsArray(completionRates)))
+ Future.successful(Ok(json))
} else {
Future.successful(Redirect("/"))
}
diff --git a/app/controllers/TaskController.scala b/app/controllers/TaskController.scala
index 1a82cd029e..800442f431 100644
--- a/app/controllers/TaskController.scala
+++ b/app/controllers/TaskController.scala
@@ -107,11 +107,19 @@ class TaskController @Inject() (implicit val env: Environment[User, SessionAuthe
def updateAuditTaskCompleteness(auditTaskId: Int, auditTask: TaskSubmission, incomplete: Option[IncompleteTaskSubmission]): Unit = {
if (auditTask.completed.isDefined && auditTask.completed.get) {
AuditTaskTable.updateCompleted(auditTaskId, completed=true)
- StreetEdgeAssignmentCountTable.incrementCompletion(auditTask.streetEdgeId)
+ val updatedCount: Int = StreetEdgeAssignmentCountTable.incrementCompletion(auditTask.streetEdgeId)
+ // if this was the first completed audit of this street edge, increase total audited distance of that region.
+ if (updatedCount == 1) {
+ RegionCompletionTable.updateAuditedDistance(auditTask.streetEdgeId)
+ }
} else if (incomplete.isDefined && incomplete.get.issueDescription == "GSVNotAvailable") {
// If the user skipped with `GSVNotAvailable`, mark the task as completed and increment the task completion
AuditTaskTable.updateCompleted(auditTaskId, completed=true)
- StreetEdgeAssignmentCountTable.incrementCompletion(auditTask.streetEdgeId) // Increment task completion
+ val updatedCount: Int = StreetEdgeAssignmentCountTable.incrementCompletion(auditTask.streetEdgeId) // Increment task completion
+ // if this was the first completed audit of this street edge, increase total audited distance of that region.
+ if (updatedCount == 1) {
+ RegionCompletionTable.updateAuditedDistance(auditTask.streetEdgeId)
+ }
}
}
diff --git a/app/models/audit/AuditTaskTable.scala b/app/models/audit/AuditTaskTable.scala
index 45ca36b677..a1c5c4f961 100644
--- a/app/models/audit/AuditTaskTable.scala
+++ b/app/models/audit/AuditTaskTable.scala
@@ -109,10 +109,15 @@ object AuditTaskTable {
auditTasks.list
}
+ /**
+ * Returns a count of the number of audits performed on each day since the tool was launched (11/17/2015).
+ *
+ * @return
+ */
def auditCounts: List[AuditCountPerDay] = db.withSession { implicit session =>
val selectAuditCountQuery = Q.queryNA[(String, Int)](
"""SELECT calendar_date::date, COUNT(audit_task_id) FROM (SELECT current_date - (n || ' day')::INTERVAL AS calendar_date
- |FROM generate_series(0, 30) n) AS calendar
+ |FROM generate_series(0, current_date - '11/17/2015') n) AS calendar
|LEFT JOIN sidewalk.audit_task
|ON audit_task.task_start::date = calendar_date::date
|GROUP BY calendar_date
diff --git a/app/models/label/LabelTable.scala b/app/models/label/LabelTable.scala
index 337e4f807f..efda8014fd 100644
--- a/app/models/label/LabelTable.scala
+++ b/app/models/label/LabelTable.scala
@@ -464,10 +464,15 @@ object LabelTable {
// labelLocationList
}
+ /**
+ * Returns a count of the number of labels placed on each day since the tool was launched (11/17/2015).
+ *
+ * @return
+ */
def selectLabelCountsPerDay: List[LabelCountPerDay] = db.withSession { implicit session =>
- val selectAuditCountQuery = Q.queryNA[(String, Int)](
+ val selectLabelCountQuery = Q.queryNA[(String, Int)](
"""SELECT calendar_date::date, COUNT(label_id) FROM (SELECT current_date - (n || ' day')::INTERVAL AS calendar_date
- |FROM generate_series(0, 30) n) AS calendar
+ |FROM generate_series(0, current_date - '11/17/2015') n) AS calendar
|LEFT JOIN sidewalk.audit_task
|ON audit_task.task_start::date = calendar_date::date
|LEFT JOIN sidewalk.label
@@ -475,7 +480,7 @@ object LabelTable {
|GROUP BY calendar_date
|ORDER BY calendar_date""".stripMargin
)
- selectAuditCountQuery.list.map(x => LabelCountPerDay.tupled(x))
+ selectLabelCountQuery.list.map(x => LabelCountPerDay.tupled(x))
}
}
diff --git a/app/models/region/RegionCompletionTable.scala b/app/models/region/RegionCompletionTable.scala
new file mode 100644
index 0000000000..cece008a67
--- /dev/null
+++ b/app/models/region/RegionCompletionTable.scala
@@ -0,0 +1,138 @@
+package models.region
+
+import java.util.UUID
+
+import com.vividsolutions.jts.geom.Polygon
+import org.geotools.geometry.jts.JTS
+import org.geotools.referencing.CRS
+
+import math._
+import models.street.{StreetEdgeAssignmentCountTable, StreetEdgeRegionTable, StreetEdgeTable, StreetEdge}
+import models.user.UserCurrentRegionTable
+import models.utils.MyPostgresDriver
+import models.utils.MyPostgresDriver.simple._
+import play.api.Play.current
+
+import scala.slick.jdbc.{GetResult, StaticQuery => Q}
+import scala.slick.lifted.ForeignKeyQuery
+
+case class RegionCompletion(regionId: Int, totalDistance: Double, auditedDistance: Double)
+case class NamedRegionCompletion(regionId: Int, name: Option[String], totalDistance: Double, auditedDistance: Double)
+
+class RegionCompletionTable(tag: Tag) extends Table[RegionCompletion](tag, Some("sidewalk"), "region_completion") {
+ def regionId = column[Int]("region_id", O.PrimaryKey)
+ def totalDistance = column[Double]("total_distance")
+ def auditedDistance = column[Double]("audited_distance")
+
+ def * = (regionId, totalDistance, auditedDistance) <> ((RegionCompletion.apply _).tupled, RegionCompletion.unapply)
+}
+
+/**
+ * Data access object for the sidewalk_edge table
+ */
+object RegionCompletionTable {
+ import MyPostgresDriver.plainImplicits._
+
+ implicit val regionCompletionConverter = GetResult[RegionCompletion](r => {
+ RegionCompletion(r.nextInt, r.nextDouble, r.nextDouble)
+ })
+
+// implicit val namedRegionConverter = GetResult[NamedRegion](r => {
+// NamedRegion(r.nextInt, r.nextStringOption, r.nextGeometry[Polygon])
+// })
+
+ case class StreetCompletion(regionId: Int, regionName: String, streetEdgeId: Int, completionCount: Int, distance: Double)
+ implicit val streetCompletionConverter = GetResult[StreetCompletion](r => {
+ StreetCompletion(r.nextInt, r.nextString, r.nextInt, r.nextInt, r.nextDouble)
+ })
+
+ val db = play.api.db.slick.DB
+ val regionCompletions = TableQuery[RegionCompletionTable]
+ val regions = TableQuery[RegionTable]
+ val regionTypes = TableQuery[RegionTypeTable]
+ val regionProperties = TableQuery[RegionPropertyTable]
+ val streetEdges = TableQuery[StreetEdgeTable]
+ val streetEdgeAssignmentCounts = TableQuery[StreetEdgeAssignmentCountTable]
+ val streetEdgeRegion = TableQuery[StreetEdgeRegionTable]
+ val userCurrentRegions = TableQuery[UserCurrentRegionTable]
+
+ val regionsWithoutDeleted = regions.filter(_.deleted === false)
+ val streetEdgesWithoutDeleted = streetEdges.filter(_.deleted === false)
+ val neighborhoods = regionsWithoutDeleted.filter(_.regionTypeId === 2)
+ val streetEdgeNeighborhood = for { (se, n) <- streetEdgeRegion.innerJoin(neighborhoods).on(_.regionId === _.regionId) } yield se
+
+
+ /**
+ * Returns a list of all neighborhoods with names
+ * @return
+ */
+ def selectAllNamedNeighborhoodCompletions: List[NamedRegionCompletion] = db.withSession { implicit session =>
+ val namedRegionCompletions = for {
+ (_neighborhoodCompletions, _regionProperties) <- regionCompletions.leftJoin(regionProperties).on(_.regionId === _.regionId)
+ if _regionProperties.key === "Neighborhood Name"
+ } yield (_neighborhoodCompletions.regionId, _regionProperties.value.?, _neighborhoodCompletions.totalDistance, _neighborhoodCompletions.auditedDistance)
+
+ namedRegionCompletions.list.map(x => NamedRegionCompletion.tupled(x))
+ }
+ /**
+ *
+ */
+
+
+ /**
+ * Increments the `audited_distance` column of the corresponding region by the length of the specified street edge.
+ * Reference: http://slick.lightbend.com/doc/2.0.0/queries.html#updating
+ *
+ * @param streetEdgeId street edge id
+ * @return
+ */
+ def updateAuditedDistance(streetEdgeId: Int) = db.withTransaction { implicit session =>
+ val distToAdd: Float = streetEdgesWithoutDeleted.filter(_.streetEdgeId === streetEdgeId).groupBy(x => x).map(_._1.geom.transform(26918).length).list.head
+ val regionId: Int = streetEdgeNeighborhood.filter(_.streetEdgeId === streetEdgeId).groupBy(x => x).map(_._1.regionId).list.head
+
+ val q = for { regionCompletion <- regionCompletions if regionCompletion.regionId === regionId } yield regionCompletion
+
+ val updatedDist = q.firstOption match {
+ case Some(rC) => q.map(_.auditedDistance).update(rC.auditedDistance + distToAdd)
+ case None => -1
+ }
+ updatedDist
+ }
+
+ def initializeRegionCompletionTable() = db.withTransaction { implicit session =>
+
+ if (regionCompletions.length.run == 0) {
+ // http://docs.geotools.org/latest/tutorials/geometry/geometrycrs.html
+ val CRSEpsg4326 = CRS.decode("epsg:4326")
+ val CRSEpsg26918 = CRS.decode("epsg:26918")
+ val transform = CRS.findMathTransform(CRSEpsg4326, CRSEpsg26918)
+
+ val neighborhoods = RegionTable.selectAllNamedNeighborhoods
+ for (neighborhood <- neighborhoods) yield {
+ val streets: List[StreetEdge] = StreetEdgeTable.selectStreetsByARegionId(neighborhood.regionId)
+ val auditedStreets: List[StreetEdge] = StreetEdgeTable.selectAuditedStreetsByARegionId(neighborhood.regionId)
+
+ val auditedDistance = auditedStreets.map(s => JTS.transform(s.geom, transform).getLength).sum
+ val totalDistance = streets.map(s => JTS.transform(s.geom, transform).getLength).sum
+
+ regionCompletions += RegionCompletion(neighborhood.regionId, totalDistance, auditedDistance)
+ }
+ }
+ }
+
+//
+// /**
+// * Update the `task_end` column of the specified audit task row
+// *
+// * @param auditTaskId
+// * @param timestamp
+// * @return
+// */
+// def updateTaskEnd(auditTaskId: Int, timestamp: Timestamp) = db.withTransaction { implicit session =>
+// val q = for { task <- auditTasks if task.auditTaskId === auditTaskId } yield task.taskEnd
+// q.update(Some(timestamp))
+// }
+
+
+
+}
diff --git a/app/models/street/StreetEdgeAssignmentCountTable.scala b/app/models/street/StreetEdgeAssignmentCountTable.scala
index 4706f83620..582e52cd44 100644
--- a/app/models/street/StreetEdgeAssignmentCountTable.scala
+++ b/app/models/street/StreetEdgeAssignmentCountTable.scala
@@ -65,7 +65,7 @@ object StreetEdgeAssignmentCountTable {
def incrementCompletion(edgeId: Int): Int = db.withTransaction { implicit session =>
val q = for {counts <- streetEdgeAssignmentCounts if counts.streetEdgeId === edgeId} yield counts
val count = q.firstOption match {
- case Some(c) => q.map(_.completionCount).update(c.completionCount + 1)
+ case Some(c) => q.map(_.completionCount).update(c.completionCount + 1); c.completionCount + 1 // returns incremented completion count
case None => 0
}
count
diff --git a/app/models/street/StreetEdgeTable.scala b/app/models/street/StreetEdgeTable.scala
index 3bd0f0be05..6fd230d883 100644
--- a/app/models/street/StreetEdgeTable.scala
+++ b/app/models/street/StreetEdgeTable.scala
@@ -2,6 +2,8 @@ package models.street
import java.sql.Timestamp
import java.util.UUID
+import java.util.Calendar
+import java.text.SimpleDateFormat
import com.vividsolutions.jts.geom.LineString
import models.audit.AuditTaskTable
@@ -101,6 +103,18 @@ object StreetEdgeTable {
countAuditedStreets(auditCount).toFloat / allEdges.length
}
+ /**
+ * Calculate the proportion of the total miles of DC that have been audited at least auditCount times.
+ *
+ * @param auditCount
+ * @return Float between 0 and 1
+ */
+ def streetDistanceCompletionRate(auditCount: Int): Float = db.withSession { implicit session =>
+ val auditedDistance = auditedStreetDistance(auditCount)
+ val totalDistance = totalStreetDistance()
+ auditedDistance / totalDistance
+ }
+
/**
* Get the total distance in miles
* Reference: http://gis.stackexchange.com/questions/143436/how-do-i-calculate-st-length-in-miles
@@ -130,6 +144,70 @@ object StreetEdgeTable {
(distances.sum * 0.000621371).toFloat
}
+
+ /**
+ * Computes percentage of DC audited over time.
+ *
+ * author: Mikey Saugstad
+ * date: 06/16/2017
+ *
+ * @param auditCount
+ * @return List[(String,Float)] representing dates and percentages
+ */
+ def streetDistanceCompletionRateByDate(auditCount: Int): Seq[(String, Float)] = db.withSession { implicit session =>
+ // join the street edges and audit tasks
+ // TODO figure out how to do this w/out doing the join twice
+ val edges = for {
+ (_streetEdges, _auditTasks) <- streetEdgesWithoutDeleted.innerJoin(completedAuditTasks).on(_.streetEdgeId === _.streetEdgeId)
+ } yield _streetEdges
+ val audits = for {
+ (_streetEdges, _auditTasks) <- streetEdgesWithoutDeleted.innerJoin(completedAuditTasks).on(_.streetEdgeId === _.streetEdgeId)
+ } yield _auditTasks
+
+ // get distances of street edges associated with their edgeId
+ val edgeDists: Map[Int, Float] = edges.groupBy(x => x).map(g => (g._1.streetEdgeId, g._1.geom.transform(26918).length)).list.toMap
+
+ // Filter out group of edges with the size less than the passed `auditCount`, picking 1 rep from each group
+ // TODO pick audit with earliest timestamp
+ val uniqueEdgeDists: List[(Option[Timestamp], Option[Float])] = (for ((eid, groupedAudits) <- audits.list.groupBy(_.streetEdgeId)) yield {
+ if (auditCount > 0 && groupedAudits.size >= auditCount) {
+ Some((groupedAudits.head.taskEnd, edgeDists.get(eid)))
+ } else {
+ None
+ }
+ }).toList.flatten
+
+ // round the timestamps down to just the date (year-month-day)
+ val dateRoundedDists: List[(Calendar, Double)] = uniqueEdgeDists.map({
+ pair => {
+ var c : Calendar = Calendar.getInstance()
+ c.setTimeInMillis(pair._1.get.getTime)
+ c.set(Calendar.HOUR_OF_DAY, 0)
+ c.set(Calendar.MINUTE, 0)
+ c.set(Calendar.SECOND, 0)
+ c.set(Calendar.MILLISECOND, 0)
+ (c, pair._2.get * 0.000621371)
+ }})
+
+ // sum the distances by date
+ val distsPerDay: List[(Calendar, Double)] = dateRoundedDists.groupBy(_._1).mapValues(_.map(_._2).sum).view.force.toList
+
+ // sort the list by date
+ val sortedEdges: Seq[(Calendar, Double)] =
+ scala.util.Sorting.stableSort(distsPerDay, (e1: (Calendar,Double), e2: (Calendar, Double)) => e1._1.getTimeInMillis < e2._1.getTimeInMillis).toSeq
+
+ // get the cumulative distance over time
+ val cumDistsPerDay: Seq[(Calendar, Double)] = sortedEdges.map({var dist = 0.0; pair => {dist += pair._2; (pair._1, dist)}})
+
+ // calculate the completion percentage for each day
+ val totalDist = totalStreetDistance()
+ val ratePerDay: Seq[(Calendar, Float)] = cumDistsPerDay.map(pair => (pair._1, (100.0 * pair._2 / totalDist).toFloat))
+
+ // format the calendar date in the correct format and return the (date,completionPercentage) pair
+ val format1 = new SimpleDateFormat("yyyy-MM-dd")
+ ratePerDay.map(pair => (format1.format(pair._1.getTime), pair._2))
+ }
+
/**
* Count the number of streets that have been audited at least a given number of times
*
diff --git a/app/views/admin/index.scala.html b/app/views/admin/index.scala.html
index 8ce19f59e0..fcd1f46e73 100644
--- a/app/views/admin/index.scala.html
+++ b/app/views/admin/index.scala.html
@@ -132,25 +132,19 @@
Coverage
Coverage
Total
+ Rate
Audited Streets
@StreetEdgeTable.countAuditedStreets()
@StreetEdgeTable.countTotalStreets()
+ @("%.0f".format(StreetEdgeTable.auditCompletionRate(1) * 100))%
Distance
@("%.1f".format(StreetEdgeTable.auditedStreetDistance(1))) miles
@("%.1f".format(StreetEdgeTable.totalStreetDistance())) miles
-
-
-
- Rate
-
-
- @("%.0f".format(StreetEdgeTable.auditCompletionRate(1) * 100))%
-
-
+ @("%.0f".format(StreetEdgeTable.streetDistanceCompletionRate(1) * 100))%
@@ -259,57 +253,94 @@ Comments
-
-
Activities
-
-
-
- Daily Audits
-
-
-
-
-
-
-
- Daily Label Counts
-
-
-
-
-
-
-
+
+
+
+ Percent of Neighborhood Complete
+
+
+
+
+
+
+
+
+
+
+
+ 100%
+
+
+
+
+
+
+
+
+ 0%
+
+
+
-
Completed Missions
+
+
Coverage Percentage by Neighborhood
-
Onboarding Completion Time
+
Sort By Completion %
+
Sort Alphabetically
+
Neighborhood Coverage Percentages
- Bounce Rate:
+ Standard Deviation:
-
-
-
Neighborhood Statistics
+
DC Coverage Progress
-
Coverage Rate per Neighborhood (%)
-
-
+
-
-
+
+
Daily Audits
-
Coverage (m)
-
+
+ Standard Deviation:
+
+
+
+
+
Daily Label Counts
+
+
+ Standard Deviation:
+
+
+
+
+
Severity Ratings by Label Type
+
+
Onboarding Completion Time
+
+
+ Bounce Rate (Percent who did NOT finish tutorial):
+ Standard Deviation:
+ Note: Final column represents all completion times > 9 minutes.
+
+
+
+
@@ -438,6 +469,13 @@
Anonymous Users
+
+
+
+
+
+
+
-}
\ No newline at end of file
+}
diff --git a/app/views/developer.scala.html b/app/views/developer.scala.html
index 24aca22d9b..93a8e8f39d 100644
--- a/app/views/developer.scala.html
+++ b/app/views/developer.scala.html
@@ -259,7 +259,7 @@
Warning
APIs' design (e.g., URL, response formats) could change
we are focusing on collecting data only from Washington, D.C. at the moment
- since we have only covered @("%.0f".format(StreetEdgeTable.auditCompletionRate(1) * 100))% of the entire area of DC,
+ since we have only covered @("%.0f".format(StreetEdgeTable.streetDistanceCompletionRate(1) * 100))% of the entire area of DC,
you would find neighborhoods where we don't have the accessibility data yet
(note you can help us by contributing to data collection too ;) ).
diff --git a/app/views/index.scala.html b/app/views/index.scala.html
index cf24634211..022ce3f242 100644
--- a/app/views/index.scala.html
+++ b/app/views/index.scala.html
@@ -141,6 +141,47 @@
-->
+
+
+
Pick a Neighborhood
+
+
+
+ You are automatically assigned a neighborhood when you click 'Start Mapping', but you can also choose one by clicking on a region below!
+
+
+
+
+
+
+ Percent of Neighborhood Complete
+
+
+
+
+
+
+
+
+
+
+
+ 100%
+
+
+
+
+
+
+
+
+ 0%
+
+
+
+
+
@@ -182,7 +223,7 @@
@@ -291,4 +332,17 @@
+
+
+
+
+
+
+
+
+
}
diff --git a/conf/application.conf b/conf/application.conf
index 5b6cb161b0..b7c93f53eb 100644
--- a/conf/application.conf
+++ b/conf/application.conf
@@ -53,8 +53,15 @@ slick.default="models.*"
# Evolutions
# ~~~~~
# You can disable evolutions if needed
-evolutionplugin=disabled
-# applyEvolutions.default=true
+evolutionplugin=enabled
+
+play.evolutions.autoApplyDown=true
+
+applyEvolutions.default=true
+
+# Transactional DDL - causes all statements to be executed in one transaction only
+evolutions.autocommit=false
+
# https://www.playframework.com/documentation/2.3.x/HTTPServer
trustxforwarded=true
diff --git a/conf/evolutions/default/1.sql b/conf/evolutions/default/1.sql
new file mode 100644
index 0000000000..1fc03a2b01
--- /dev/null
+++ b/conf/evolutions/default/1.sql
@@ -0,0 +1,13 @@
+
+# --- !Ups
+CREATE TABLE region_completion
+(
+ region_id INTEGER NOT NULL,
+ total_distance REAL,
+ audited_distance REAL,
+ PRIMARY KEY (region_id)
+);
+
+# --- !Downs
+
+DROP TABLE region_completion;
diff --git a/conf/routes b/conf/routes
index 7598144a7f..55584d2f1d 100644
--- a/conf/routes
+++ b/conf/routes
@@ -30,6 +30,7 @@ GET /admin/task/:taskId @controllers.AdminController.tas
GET /adminapi/missionsCompletedByUsers @controllers.AdminController.getMissionsCompletedByUsers
GET /adminapi/neighborhoodCompletionRate @controllers.AdminController.getNeighborhoodCompletionRate
+GET /adminapi/completionRateByDate @controllers.AdminController.getCompletionRateByDate
GET /adminapi/tasks/:username @controllers.AdminController.getSubmittedTasksWithLabels(username: String)
GET /adminapi/interactions/:username @controllers.AdminController.getAuditTaskInteractionsOfAUser(username: String)
GET /adminapi/onboardingInteractions @controllers.AdminController.getOnboardingTaskInteractions
diff --git a/public/css/main.css b/public/css/main.css
index 97a4800250..234f61af13 100644
--- a/public/css/main.css
+++ b/public/css/main.css
@@ -618,6 +618,12 @@ video#bgvid {
min-height: 800px;
}
+#choropleth-container {
+ background-color: rgba(249, 241, 227, 1);
+ width: 100%;
+ min-height: 1100px;
+}
+
#content{
position:relative;
top:0px;
diff --git a/public/javascripts/Admin/src/Admin.js b/public/javascripts/Admin/src/Admin.js
index 00019c269c..f068a9a7c1 100644
--- a/public/javascripts/Admin/src/Admin.js
+++ b/public/javascripts/Admin/src/Admin.js
@@ -12,6 +12,8 @@ function Admin(_, $, c3, turf) {
self.mapLoaded = false;
self.graphsLoaded = false;
+ var neighborhoodPolygonLayer;
+
for (i = 0; i < 5; i++) {
self.curbRampLayers[i] = [];
self.missingCurbRampLayers[i] = [];
@@ -32,312 +34,48 @@ function Admin(_, $, c3, turf) {
L.mapbox.accessToken = 'pk.eyJ1Ijoia290YXJvaGFyYSIsImEiOiJDdmJnOW1FIn0.kJV65G6eNXs4ATjWCtkEmA';
- // Construct a bounding box for this map that the user cannot move out of
+ // Construct a bounding box for these maps that the user cannot move out of
// https://www.mapbox.com/mapbox.js/example/v1.0.0/maxbounds/
- var southWest = L.latLng(38.761, -77.262),
- northEast = L.latLng(39.060, -76.830),
- bounds = L.latLngBounds(southWest, northEast),
-
- // var tileUrl = "https://a.tiles.mapbox.com/v4/kotarohara.mmoldjeh/page.html?access_token=pk.eyJ1Ijoia290YXJvaGFyYSIsImEiOiJDdmJnOW1FIn0.kJV65G6eNXs4ATjWCtkEmA#13/38.8998/-77.0638";
- tileUrl = "https:\/\/a.tiles.mapbox.com\/v4\/kotarohara.8e0c6890\/{z}\/{x}\/{y}.png?access_token=pk.eyJ1Ijoia290YXJvaGFyYSIsImEiOiJDdmJnOW1FIn0.kJV65G6eNXs4ATjWCtkEmA",
- mapboxTiles = L.tileLayer(tileUrl, {
- attribution: 'Terms & Feedback '
- }),
- map = L.mapbox.map('admin-map', "kotarohara.8e0c6890", {
+ var southWest = L.latLng(38.761, -77.262);
+ var northEast = L.latLng(39.060, -76.830);
+ var bounds = L.latLngBounds(southWest, northEast);
+
+ // var tileUrl = "https://a.tiles.mapbox.com/v4/kotarohara.mmoldjeh/page.html?access_token=pk.eyJ1Ijoia290YXJvaGFyYSIsImEiOiJDdmJnOW1FIn0.kJV65G6eNXs4ATjWCtkEmA#13/38.8998/-77.0638";
+ var tileUrl = "https:\/\/a.tiles.mapbox.com\/v4\/kotarohara.8e0c6890\/{z}\/{x}\/{y}.png?access_token=pk.eyJ1Ijoia290YXJvaGFyYSIsImEiOiJDdmJnOW1FIn0.kJV65G6eNXs4ATjWCtkEmA";
+ var mapboxTiles = L.tileLayer(tileUrl, {
+ attribution: 'Terms & Feedback '
+ });
+ var map = L.mapbox.map('admin-map', "kotarohara.8e0c6890", {
+ // set that bounding box as maxBounds to restrict moving the map
+ // see full maxBounds documentation:
+ // http://leafletjs.com/reference.html#map-maxbounds
+ maxBounds: bounds,
+ maxZoom: 19,
+ minZoom: 9
+ })
+ .fitBounds(bounds)
+ .setView([38.892, -77.038], 12);
+
+ // a grayscale tileLayer for the choropleth
+ L.mapbox.accessToken = 'pk.eyJ1IjoibWlzYXVnc3RhZCIsImEiOiJjajN2dTV2Mm0wMDFsMndvMXJiZWcydDRvIn0.IXE8rQNF--HikYDjccA7Ug';
+ var choropleth = L.mapbox.map('admin-choropleth', "kotarohara.8e0c6890", {
// set that bounding box as maxBounds to restrict moving the map
// see full maxBounds documentation:
// http://leafletjs.com/reference.html#map-maxbounds
maxBounds: bounds,
maxZoom: 19,
- minZoom: 9
+ minZoom: 9,
+ legendControl: {
+ position: 'bottomleft'
+ }
})
- // .addLayer(mapboxTiles)
.fitBounds(bounds)
- .setView([38.892, -77.038], 12),
- popup = L.popup().setContent('Hello world! This is a nice popup.
');
-
- // Draw an onboarding interaction chart
- $.getJSON("/adminapi/onboardingInteractions", function (data) {
- function cmp(a, b) {
- return a.timestamp - b.timestamp;
- }
+ .setView([38.892, -77.038], 12);
+ choropleth.scrollWheelZoom.disable();
- // Group the audit task interaction records by audit_task_id, then go through each group and compute
- // the duration between the first time stamp and the last time stamp.
- var grouped = _.groupBy(data, function (x) {
- return x.audit_task_id;
- });
- var completionDurationArray = [];
- var record1;
- var record2;
- var duration;
- for (var auditTaskId in grouped) {
- grouped[auditTaskId].sort(cmp);
- record1 = grouped[auditTaskId][0];
- record2 = grouped[auditTaskId][grouped[auditTaskId].length - 1];
- duration = (record2.timestamp - record1.timestamp) / 1000; // Duration in seconds
- completionDurationArray.push(duration);
- }
- completionDurationArray.sort(function (a, b) {
- return a - b;
- });
+ L.mapbox.styleLayer('mapbox://styles/mapbox/light-v9').addTo(choropleth);
- // Bounce rate
- var zeros = _.countBy(completionDurationArray, function (x) {
- return x == 0;
- });
- var bounceRate = zeros['true'] / (zeros['true'] + zeros['false']);
-
- // Histogram of duration
- completionDurationArray = completionDurationArray.filter(function (x) {
- return x != 0;
- }); // Remove zeros
- var numberOfBins = 10;
- var histogram = makeAHistogramArray(completionDurationArray, numberOfBins);
- // console.log(histogram);
- var counts = histogram.histogram;
- counts.unshift("Count");
- var bins = histogram.histogram.map(function (x, i) {
- return (i * histogram.stepSize).toFixed(1) + " - " + ((i + 1) * histogram.stepSize).toFixed(1);
- });
-
- $("#onboarding-bounce-rate").html((bounceRate * 100).toFixed(1) + "%");
-
- var chart = c3.generate({
- bindto: '#onboarding-completion-duration-histogram',
- data: {
- columns: [
- counts
- ],
- type: 'bar'
- },
- axis: {
- x: {
- label: "Onboarding Completion Time (s)",
- type: 'category',
- categories: bins
- },
- y: {
- label: "Count",
- min: 0,
- padding: {top: 50, bottom: 10}
- }
- },
- legend: {
- show: false
- }
- });
- });
-
- $.getJSON('/adminapi/missionsCompletedByUsers', function (data) {
- var i,
- len = data.length;
-
- // Todo. This code double counts the missions completed for different region. So it should be fixed in the future.
- var missions = {};
- var printedMissionName;
- for (i = 0; i < len; i++) {
- // Set the printed mission name
- if (data[i].label == "initial-mission") {
- printedMissionName = "Initial Mission (1000 ft)";
- } else if (data[i].label == "distance-mission") {
- if (data[i].level <= 2) {
- printedMissionName = "Distance Mission (" + data[i].distance_ft + " ft)";
- } else {
- printedMissionName = "Distance Mission (" + data[i].distance_mi + " mi)";
- }
- } else {
- printedMissionName = "Onboarding";
- }
-
- // Create a counter for the printedMissionName if it does not exist yet.
- if (!(printedMissionName in missions)) {
- missions[printedMissionName] = {
- label: data[i].label,
- level: data[i].level,
- printedMissionName: printedMissionName,
- count: 0
- };
- }
- missions[printedMissionName].count += 1;
- }
- var arrayOfMissions = Object.keys(missions).map(function (key) {
- return missions[key];
- });
- arrayOfMissions.sort(function (a, b) {
- if (a.count < b.count) {
- return 1;
- }
- else if (a.count > b.count) {
- return -1;
- }
- else {
- return 0;
- }
- });
-
- var missionCountArray = ["Mission Counts"];
- var missionNames = [];
- for (i = 0; i < arrayOfMissions.length; i++) {
- missionCountArray.push(arrayOfMissions[i].count);
- missionNames.push(arrayOfMissions[i].printedMissionName);
- }
- var chart = c3.generate({
- bindto: '#completed-mission-histogram',
- data: {
- columns: [
- missionCountArray
- ],
- type: 'bar'
- },
- axis: {
- x: {
- type: 'category',
- categories: missionNames
- },
- y: {
- label: "# Users Completed the Mission",
- min: 0,
- padding: {top: 50, bottom: 10}
- }
- },
- legend: {
- show: false
- }
- });
- });
-
- $.getJSON('/adminapi/neighborhoodCompletionRate', function (data) {
- var i,
- len = data.length,
- completionRate,
- row,
- rows = "";
- var coverageRateColumn = ["Neighborhood Coverage Rate (%)"];
- var coverageDistanceArray = ["Neighborhood Coverage (m)"];
- var neighborhoodNames = [];
- for (i = 0; i < len; i++) {
- completionRate = data[i].completed_distance_m / data[i].total_distance_m * 100;
- coverageRateColumn.push(completionRate);
- coverageDistanceArray.push(data[i].completed_distance_m);
-
- neighborhoodNames.push(data[i].name);
- // row = "" + data[i].region_id + " " + data[i].name + " " + completionRate + "% "
- // rows += row;
- }
-
- var coverageChart = c3.generate({
- bindto: '#neighborhood-completion-rate',
- data: {
- columns: [
- coverageRateColumn
- ],
- type: 'bar'
- },
- axis: {
- x: {
- type: 'category',
- categories: neighborhoodNames
- },
- y: {
- label: "Neighborhood Coverage Rate (%)",
- min: 0,
- max: 100,
- padding: {top: 50, bottom: 10}
- }
- },
- legend: {
- show: false
- }
- });
-
- var coverageDistanceChart = c3.generate({
- bindto: '#neighborhood-completed-distance',
- data: {
- columns: [
- coverageDistanceArray
- ],
- type: 'bar'
- },
- axis: {
- x: {
- type: 'category',
- categories: neighborhoodNames
- },
- y: {
- label: "Coverage Distance (m)",
- min: 0,
- padding: {top: 50, bottom: 10}
- }
- },
- legend: {
- show: false
- }
- });
-
- });
-
- $.getJSON("/contribution/auditCounts/all", function (data) {
- var dates = ['Date'].concat(data[0].map(function (x) {
- return x.date;
- })),
- counts = ['Audit Count'].concat(data[0].map(function (x) {
- return x.count;
- }));
- var chart = c3.generate({
- bindto: "#audit-count-chart",
- data: {
- x: 'Date',
- columns: [dates, counts],
- types: {'Audit Count': 'line'}
- },
- axis: {
- x: {
- type: 'timeseries',
- tick: {format: '%Y-%m-%d'}
- },
- y: {
- label: "Street Audit Count",
- min: 0,
- padding: {top: 50, bottom: 10}
- }
- },
- legend: {
- show: false
- }
- });
- });
-
- $.getJSON("/userapi/labelCounts/all", function (data) {
- var dates = ['Date'].concat(data[0].map(function (x) {
- return x.date;
- })),
- counts = ['Label Count'].concat(data[0].map(function (x) {
- return x.count;
- }));
- var chart = c3.generate({
- bindto: "#label-count-chart",
- data: {
- x: 'Date',
- columns: [dates, counts],
- types: {'Audit Count': 'line'}
- },
- axis: {
- x: {
- type: 'timeseries',
- tick: {format: '%Y-%m-%d'}
- },
- y: {
- label: "Label Count",
- min: 0,
- padding: {top: 50, bottom: 10}
- }
- },
- legend: {
- show: false
- }
- });
- });
+ var popup = L.popup().setContent('Hello world! This is a nice popup.
');
// Initialize the map
/**
@@ -406,7 +144,7 @@ function Admin(_, $, c3, turf) {
}
$.getJSON("/neighborhoods", function (data) {
- L.geoJson(data, {
+ neighborhoodPolygonLayer = L.geoJson(data, {
style: function (feature) {
return $.extend(true, {}, neighborhoodPolygonStyle);
},
@@ -417,7 +155,166 @@ function Admin(_, $, c3, turf) {
}
/**
- * This function queries the streets that the user audited and visualize them as segmetns on the map.
+ * Takes a completion percentage, bins it, and returns the appropriate color for a choropleth.
+ *
+ * @param p {float} represents a completion percentage, between 0 and 100
+ * @returns {string} color in hex
+ */
+ function getColor(p) {
+ return p > 80 ? '#08519c' :
+ p > 60 ? '#3182bd' :
+ p > 40 ? '#6baed6' :
+ p > 20 ? '#bdd7e7' :
+ '#eff3ff';
+ }
+ function getColor2(p) {
+ return p > 90 ? '#08306b' :
+ p > 80 ? '#08519c' :
+ p > 70 ? '#08719c' :
+ p > 60 ? '#2171b5' :
+ p > 50 ? '#4292c6' :
+ p > 40 ? '#6baed6' :
+ p > 30 ? '#9ecae1' :
+ p > 20 ? '#c6dbef' :
+ p > 10 ? '#deebf7' :
+ '#f7fbff';
+ }
+ function getColor3(p) {
+ return p > 90 ? '#023858' :
+ p > 80 ? '#045a8d' :
+ p > 70 ? '#0570b0' :
+ p > 60 ? '#3690c0' :
+ p > 50 ? '#74a9cf' :
+ p > 40 ? '#a6bddb' :
+ p > 30 ? '#d0d1e6' :
+ p > 20 ? '#ece7f2' :
+ p > 10 ? '#fff7fb' :
+ '#ffffff';
+ }
+ function getColor4(p) {
+ return p > 80 ? '#045a8d' :
+ p > 60 ? '#2b8cbe' :
+ p > 40 ? '#74a9cf' :
+ p > 20 ? '#bdc9e1' :
+ '#f1eef6';
+ }
+ function getOpacity(p) {
+ return p > 90 ? 1.0 :
+ p > 80 ? 0.9 :
+ p > 70 ? 0.8 :
+ p > 60 ? 0.7 :
+ p > 50 ? 0.6 :
+ p > 40 ? 0.5 :
+ p > 30 ? 0.4 :
+ p > 20 ? 0.3 :
+ p > 10 ? 0.2 :
+ 0.1;
+ }
+
+ /**
+ * render the neighborhood polygons, colored by completion percentage
+ */
+ function initializeChoroplethNeighborhoodPolygons(map, rates) {
+ var neighborhoodPolygonStyle = { // default bright red, used to check if any regions are missing data
+ color: '#888',
+ weight: 1,
+ opacity: 0.25,
+ fillColor: "#f00",
+ fillOpacity: 1.0
+ },
+ layers = [],
+ currentLayer;
+
+ // finds the matching neighborhood's completion percentage, and uses it to determine the fill color
+ function style(feature) {
+ for (var i=0; i < rates.length; i++) {
+ if (rates[i].region_id === feature.properties.region_id) {
+ return {
+ color: '#888',
+ weight: 1,
+ opacity: 0.25,
+ fillColor: getColor2(rates[i].rate),
+ fillOpacity: 0.25 + (0.5 * rates[i].rate / 100.0)
+ }
+ }
+ }
+ return neighborhoodPolygonStyle; // default case (shouldn't happen, will be bright red)
+ }
+
+ function onEachNeighborhoodFeature(feature, layer) {
+
+ var regionId = feature.properties.region_id,
+ regionName = feature.properties.region_name,
+ compRate = -1.0,
+ milesLeft = -1.0,
+ url = "/audit/region/" + regionId,
+ popupContent = "???";
+ for (var i=0; i < rates.length; i++) {
+ if (rates[i].region_id === feature.properties.region_id) {
+ compRate = Math.round(rates[i].rate);
+ milesLeft = Math.round(0.000621371 * (rates[i].total_distance_m - rates[i].completed_distance_m));
+ if (compRate === 100) {
+ popupContent = "" + regionName + " : " + compRate + "\% Complete!";
+ }
+ else if (milesLeft === 0) {
+ popupContent = "" + regionName + " : " + compRate +
+ "\% Complete Less than a mile left! " +
+ "Click here " +
+ " to help finish this neighborhood!";
+ }
+ else if (milesLeft === 1) {
+ var popupContent = "" + regionName + " : " + compRate + "\% Complete Only " +
+ milesLeft + " mile left! " +
+ "Click here " +
+ " to help finish this neighborhood!";
+ }
+ else {
+ var popupContent = "" + regionName + " : " + compRate + "\% Complete Only " +
+ milesLeft + " miles left! " +
+ "Click here " +
+ " to help finish this neighborhood!";
+ }
+ break;
+ }
+ }
+ layer.bindPopup(popupContent);
+ layers.push(layer);
+
+ layer.on('mouseover', function (e) {
+ this.setStyle({opacity: 1.0, weight: 3, color: "#000"});
+
+ });
+ layer.on('mouseout', function (e) {
+ for (var i = layers.length - 1; i >= 0; i--) {
+ if (currentLayer !== layers[i])
+ layers[i].setStyle({opacity: 0.25, weight: 1});
+ }
+ //this.setStyle(neighborhoodPolygonStyle);
+ });
+ layer.on('click', function (e) {
+ var center = turf.center(this.feature),
+ coordinates = center.geometry.coordinates,
+ latlng = L.latLng(coordinates[1], coordinates[0]),
+ zoom = map.getZoom();
+ zoom = zoom > 14 ? zoom : 14;
+
+ map.setView(latlng, zoom, {animate: true});
+ currentLayer = this;
+ });
+ }
+
+ // adds the neighborhood polygons to the map
+ $.getJSON("/neighborhoods", function (data) {
+ neighborhoodPolygonLayer = L.geoJson(data, {
+ style: style,
+ onEachFeature: onEachNeighborhoodFeature
+ })
+ .addTo(map);
+ });
+ }
+
+ /**
+ * This function queries the streets that the user audited and visualize them as segments on the map.
*/
function initializeAuditedStreets(map) {
var distanceAudited = 0, // Distance audited in km
@@ -618,27 +515,6 @@ function Admin(_, $, c3, turf) {
}
}
- // A helper method to make an histogram of an array.
- function makeAHistogramArray(arrayOfNumbers, numberOfBins) {
- arrayOfNumbers.sort(function (a, b) {
- return a - b;
- });
- var stepSize = arrayOfNumbers[arrayOfNumbers.length - 1] / numberOfBins;
- var dividedArray = arrayOfNumbers.map(function (x) {
- return x / stepSize;
- });
- var histogram = Array.apply(null, Array(numberOfBins)).map(Number.prototype.valueOf, 0);
- for (var i = 0; i < dividedArray.length; i++) {
- var binIndex = Math.floor(dividedArray[i] - 0.0000001);
- histogram[binIndex] += 1;
- }
- return {
- histogram: histogram,
- stepSize: stepSize,
- numberOfBins: numberOfBins
- };
- }
-
function initializeAdminGSVLabelView() {
self.adminGSVLabelView = AdminGSVLabel();
}
@@ -663,6 +539,99 @@ function Admin(_, $, c3, turf) {
self.mapLoaded = true;
}
else if (e.target.id == "analytics" && self.graphsLoaded == false) {
+
+
+ $.getJSON("/adminapi/completionRateByDate", function (data) {
+ var chart = {
+ // "height": 800,
+ "height": 300,
+ "width": 875,
+ "mark": "area",
+ "data": {"values": data[0], "format": {"type": "json"}},
+ "encoding": {
+ "x": {
+ "field": "date",
+ "type": "temporal",
+ "axis": {"title": "Date", "labelAngle": 0}
+ },
+ "y": {
+ "field": "completion",
+ "type": "quantitative", "scale": {
+ "domain": [0,100]
+ },
+ "axis": {
+ "title": "DC Coverage (%)"
+ }
+ }
+ },
+ // this is the slightly different code for the interactive version
+ // "vconcat": [
+ // {
+ // "width": 800,
+ // "height": 150,
+ // "mark": "area",
+ // "selection": {
+ // "brush": {
+ // "type": "interval", "encodings": ["x"]
+ // }
+ // },
+ // "encoding": {
+ // "x": {
+ // "field": "date",
+ // "type": "temporal",
+ // "axis": {"title": "Date", "labelAngle": 0}
+ // },
+ // "y": {
+ // "field": "completion",
+ // "type": "quantitative", "scale": {
+ // "domain": [0,100]
+ // },
+ // "axis": {
+ // "title": "DC Coverage (%)"
+ // }
+ // }
+ // }
+ // },
+ // {
+ // "width": 800,
+ // "height": 400,
+ // "mark": "area",
+ // "encoding": {
+ // "x": {
+ // "field": "date",
+ // "type": "temporal",
+ // "scale": {
+ // "domain": {
+ // "selection": "brush", "encoding": "x"
+ // }
+ // },
+ // "axis": {
+ // "title": "", "labelAngle": 0
+ // }
+ // },
+ // "y": {
+ // "field": "completion","type": "quantitative", "scale": {
+ // "domain": [0,100]
+ // },
+ // "axis": {
+ // "title": "DC Coverage (%)"
+ // }
+ // }
+ // }
+ // }
+ // ],
+ "config": {
+ "axis": {
+ "titleFontSize": 16
+ }
+ }
+ };
+ var opt = {
+ "mode": "vega-lite",
+ "actions": false
+ };
+ vega.embed("#completion-progress-chart", chart, opt, function(error, results) {});
+ });
// Draw an onboarding interaction chart
$.getJSON("/adminapi/onboardingInteractions", function (data) {
function cmp(a, b) {
@@ -674,273 +643,562 @@ function Admin(_, $, c3, turf) {
var grouped = _.groupBy(data, function (x) {
return x.audit_task_id;
});
- var completionDurationArray = [];
+ var onboardingTimes = [];
var record1;
var record2;
var duration;
+ var bounceCount = 0;
+ var sum = 0;
for (var auditTaskId in grouped) {
grouped[auditTaskId].sort(cmp);
record1 = grouped[auditTaskId][0];
record2 = grouped[auditTaskId][grouped[auditTaskId].length - 1];
- duration = (record2.timestamp - record1.timestamp) / 1000; // Duration in seconds
- completionDurationArray.push(duration);
+ if(record2.note === "from:outro" || record2.note === "onboardingTransition:outro"){
+ duration = (record2.timestamp - record1.timestamp) / 60000; // Duration in minutes
+ onboardingTimes.push({duration: duration, binned: Math.min(10.0, duration)});
+ sum += duration;
+ }
+ else bounceCount++;
}
- completionDurationArray.sort(function (a, b) {
- return a - b;
- });
-
- // Bounce rate
- var zeros = _.countBy(completionDurationArray, function (x) {
- return x == 0;
- });
- var bounceRate = zeros['true'] / (zeros['true'] + zeros['false']);
-
- // Histogram of duration
- completionDurationArray = completionDurationArray.filter(function (x) {
- return x != 0;
- }); // Remove zeros
- var numberOfBins = 10;
- var histogram = makeAHistogramArray(completionDurationArray, numberOfBins);
- // console.log(histogram);
- var counts = histogram.histogram;
- counts.unshift("Count");
- var bins = histogram.histogram.map(function (x, i) {
- return (i * histogram.stepSize).toFixed(1) + " - " + ((i + 1) * histogram.stepSize).toFixed(1);
- });
-
+ var bounceRate = bounceCount / (bounceCount + onboardingTimes.length);
$("#onboarding-bounce-rate").html((bounceRate * 100).toFixed(1) + "%");
- var chart = c3.generate({
- bindto: '#onboarding-completion-duration-histogram',
- data: {
- columns: [
- counts
- ],
- type: 'bar'
- },
- axis: {
- x: {
- label: "Onboarding Completion Time (s)",
- type: 'category',
- categories: bins
+ var mean = sum / onboardingTimes.length;
+ onboardingTimes.sort(function(a, b) {return (a.duration > b.duration) ? 1 : ((b.duration > a.duration) ? -1 : 0);} );
+ var i = onboardingTimes.length / 2;
+ var median = i % 1 == 0 ? (onboardingTimes[i - 1].duration + onboardingTimes[i].duration) / 2 : onboardingTimes[Math.floor(i)].duration;
+
+ var std = 0;
+ for(var j = 0; j < onboardingTimes.length; j++) {
+ std += Math.pow(onboardingTimes[j].duration - mean, 2);
+ }
+ std /= onboardingTimes.length;
+ std = Math.sqrt(std);
+ $("#onboarding-std").html((std).toFixed(1) + " minutes");
+
+ var chart = {
+ "width": 400,
+ "height": 250,
+ "layer": [
+ {
+ "data": {"values": onboardingTimes},
+ "mark": "bar",
+ "encoding": {
+ "x": {
+ "bin": {"maxbins": 10},
+ "field": "binned", "type": "quantitative",
+ "axis": {
+ "title": "Onboarding Completion Time (minutes)", "labelAngle": 0,
+ "scale": {"domain": [0,10]}
+ }
+ },
+ "y": {
+ "aggregate": "count", "field": "*", "type": "quantitative",
+ "axis": {
+ "title": "Counts"
+ }
+ }
+ }
},
- y: {
- label: "Count",
- min: 0,
- padding: {top: 50, bottom: 10}
+ { // creates lines marking summary statistics
+ "data": {"values": [
+ {"stat": "mean", "value": mean}, {"stat": "median", "value": median}]
+ },
+ "mark": "rule",
+ "encoding": {
+ "x": {
+ "field": "value", "type": "quantitative",
+ "axis": {"labels": false, "ticks": false, "title": ""},
+ "scale": {"domain": [0,10]}
+ },
+ "color": {
+ "field": "stat", "type": "nominal", "scale": {"range": ["pink", "orange"]},
+ "legend": {
+ "title": "Summary Stats"
+ }
+ },
+ "size": {
+ "value": 2
+ }
+ }
+ }
+ ],
+ "resolve": {"x": {"scale": "independent"}},
+ "config": {
+ "axis": {
+ "titleFontSize": 16
}
- },
- legend: {
- show: false
}
- });
+ };
+ var opt = {
+ "mode": "vega-lite",
+ "actions": false
+ };
+ vega.embed("#onboarding-completion-duration-histogram", chart, opt, function(error, results) {});
});
- $.getJSON('/adminapi/missionsCompletedByUsers', function (data) {
- var i,
- len = data.length;
-
- // Todo. This code double counts the missions completed for different region. So it should be fixed in the future.
- var missions = {};
- var printedMissionName;
- for (i = 0; i < len; i++) {
- // Set the printed mission name
- if (data[i].label == "initial-mission") {
- printedMissionName = "Initial Mission (1000 ft)";
- } else if (data[i].label == "distance-mission") {
- if (data[i].level <= 2) {
- printedMissionName = "Distance Mission (" + data[i].distance_ft + " ft)";
- } else {
- printedMissionName = "Distance Mission (" + data[i].distance_mi + " mi)";
+ $.getJSON('/adminapi/labels/all', function (data) {
+ for (var i = 0; i < data.features.length; i++) {
+ data.features[i].label_type = data.features[i].properties.label_type;
+ data.features[i].severity = data.features[i].properties.severity;
+ }
+ var curbRamps = data.features.filter(function(label) {return label.properties.label_type === "CurbRamp"});
+ var noCurbRamps = data.features.filter(function(label) {return label.properties.label_type === "NoCurbRamp"});
+ var surfaceProblems = data.features.filter(function(label) {return label.properties.label_type === "SurfaceProblem"});
+ var obstacles = data.features.filter(function(label) {return label.properties.label_type === "Obstacle"});
+
+ var subPlotHeight = 200;
+ var subPlotWidth = 199;
+ var chart = {
+ "hconcat": [
+ {
+ "height": subPlotHeight,
+ "width": subPlotWidth,
+ "data": {"values": curbRamps},
+ "mark": "bar",
+ "encoding": {
+ "x": {"field": "severity", "type": "ordinal", "axis": {"title": "Curb Ramp Severity"}},
+ "y": {"aggregate": "count", "type": "quantitative", "axis": {"title": "# of labels"}}
+ }
+ },
+ {
+ "height": subPlotHeight,
+ "width": subPlotWidth,
+ "data": {"values": noCurbRamps},
+ "mark": "bar",
+ "encoding": {
+ "x": {"field": "severity", "type": "ordinal", "axis": {"title": "Missing Curb Ramp Severity"}},
+ "y": {"aggregate": "count", "type": "quantitative", "axis": {"title": ""}}
+ }
+ },
+ {
+ "height": subPlotHeight,
+ "width": subPlotWidth,
+ "data": {"values": surfaceProblems},
+ "mark": "bar",
+ "encoding": {
+ "x": {"field": "severity", "type": "ordinal", "axis": {"title": "Surface Problem Severity"}},
+ "y": {"aggregate": "count", "type": "quantitative", "axis": {"title": ""}}
+ }
+ },
+ {
+ "height": subPlotHeight,
+ "width": subPlotWidth,
+ "data": {"values": obstacles},
+ "mark": "bar",
+ "encoding": {
+ "x": {"field": "severity", "type": "ordinal", "axis": {"title": "Obstacle Severity"}},
+ "y": {"aggregate": "count", "type": "quantitative", "axis": {"title": ""}}
+ }
}
- } else {
- printedMissionName = "Onboarding";
- }
+ ]
+ };
+ var opt = {
+ "mode": "vega-lite",
+ "actions": false
+ };
+ vega.embed("#severity-histograms", chart, opt, function(error, results) {});
+ });
+ $.getJSON('/adminapi/neighborhoodCompletionRate', function (data) {
- // Create a counter for the printedMissionName if it does not exist yet.
- if (!(printedMissionName in missions)) {
- missions[printedMissionName] = {
- label: data[i].label,
- level: data[i].level,
- printedMissionName: printedMissionName,
- count: 0
- };
- }
- missions[printedMissionName].count += 1;
+ // make a choropleth of neighborhood completion percentages
+ initializeChoroplethNeighborhoodPolygons(choropleth, data);
+ choropleth.legendControl.addLegend(document.getElementById('legend').innerHTML);
+ setTimeout(function () {
+ choropleth.invalidateSize(false);
+ }, 1);
+
+ // make charts showing neighborhood completion rate
+ data.sort(function(a, b) {return (a.rate > b.rate) ? 1 : ((b.rate > a.rate) ? -1 : 0);} );
+ var sum = 0;
+ for (var j = 0; j < data.length; j++) {
+ data[j].rate *= 100.0;
+ sum += data[j].rate;
}
- var arrayOfMissions = Object.keys(missions).map(function (key) {
- return missions[key];
- });
- arrayOfMissions.sort(function (a, b) {
- if (a.count < b.count) {
- return 1;
- }
- else if (a.count > b.count) {
- return -1;
- }
- else {
- return 0;
- }
- });
+ var mean = sum / data.length;
+ var i = data.length / 2;
+ var median = (data.length / 2) % 1 == 0 ? (data[i - 1].rate + data[i].rate) / 2 : data[Math.floor(i)].rate;
- var missionCountArray = ["Mission Counts"];
- var missionNames = [];
- for (i = 0; i < arrayOfMissions.length; i++) {
- missionCountArray.push(arrayOfMissions[i].count);
- missionNames.push(arrayOfMissions[i].printedMissionName);
+ var std = 0;
+ for(var k = 0; k < data.length; k++) {
+ std += Math.pow(data[k].rate - mean, 2);
}
- var chart = c3.generate({
- bindto: '#completed-mission-histogram',
- data: {
- columns: [
- missionCountArray
- ],
- type: 'bar'
+ std /= data.length;
+ std = Math.sqrt(std);
+ $("#neighborhood-std").html((std).toFixed(0) + "%");
+
+ var coverageRateChartSortedByCompletion = {
+ "width": 810,
+ "height": 800,
+ "data": {
+ "values": data, "format": {
+ "type": "json"
+ }
},
- axis: {
- x: {
- type: 'category',
- categories: missionNames
+ "mark": "bar",
+ "encoding": {
+ "x": {
+ "field": "rate", "type": "quantitative",
+ "axis": {"title": "Neighborhood Completion (%)"}
},
- y: {
- label: "# Users Completed the Mission",
- min: 0,
- padding: {top: 50, bottom: 10}
+ "y": {
+ "field": "name", "type": "nominal",
+ "axis": {"title": "Neighborhood"},
+ "sort": {"field": "rate", "op": "max", "order": "ascending"}
}
},
- legend: {
- show: false
+ "config": {
+ "axis": {"titleFontSize": 16, "labelFontSize": 8},
+ "bar": {"binSpacing": 2}
}
- });
- });
- $.getJSON('/adminapi/neighborhoodCompletionRate', function (data) {
- var i,
- len = data.length,
- completionRate,
- row,
- rows = "";
- var coverageRateColumn = ["Neighborhood Coverage Rate (%)"];
- var coverageDistanceArray = ["Neighborhood Coverage (m)"];
- var neighborhoodNames = [];
- for (i = 0; i < len; i++) {
- completionRate = data[i].completed_distance_m / data[i].total_distance_m * 100;
- coverageRateColumn.push(completionRate);
- coverageDistanceArray.push(data[i].completed_distance_m);
-
- neighborhoodNames.push(data[i].name);
- // row = "" + data[i].region_id + " " + data[i].name + " " + completionRate + "% "
- // rows += row;
- }
+ };
- var coverageChart = c3.generate({
- bindto: '#neighborhood-completion-rate',
- data: {
- columns: [
- coverageRateColumn
- ],
- type: 'bar'
+ var coverageRateChartSortedAlphabetically = {
+ "width": 810,
+ "height": 800,
+ "data": {
+ "values": data, "format": {
+ "type": "json"
+ }
},
- axis: {
- x: {
- type: 'category',
- categories: neighborhoodNames
+ "mark": "bar",
+ "encoding": {
+ "x": {
+ "field": "rate", "type": "quantitative",
+ "axis": {"title": "Neighborhood Completion (%)"}
},
- y: {
- label: "Neighborhood Coverage Rate (%)",
- min: 0,
- max: 100,
- padding: {top: 50, bottom: 10}
+ "y": {
+ "field": "name", "type": "nominal",
+ "axis": {"title": "Neighborhood"},
+ "sort": {"field": "name", "op": "max", "order": "descending"}
}
},
- legend: {
- show: false
+ "config": {
+ "axis": {"titleFontSize": 16, "labelFontSize": 8},
+ "bar": {"binSpacing": 2}
}
+ };
+ var opt = {
+ "mode": "vega-lite",
+ "actions": false
+ };
+ vega.embed("#neighborhood-completion-rate", coverageRateChartSortedByCompletion, opt, function(error, results) {});
+
+ document.getElementById("neighborhood-completion-sort-button").addEventListener("click", function() {
+ vega.embed("#neighborhood-completion-rate", coverageRateChartSortedByCompletion, opt, function(error, results) {});
+ });
+ document.getElementById("neighborhood-alphabetical-sort-button").addEventListener("click", function() {
+ vega.embed("#neighborhood-completion-rate", coverageRateChartSortedAlphabetically, opt, function(error, results) {});
});
- var coverageDistanceChart = c3.generate({
- bindto: '#neighborhood-completed-distance',
- data: {
- columns: [
- coverageDistanceArray
- ],
- type: 'bar'
- },
- axis: {
- x: {
- type: 'category',
- categories: neighborhoodNames
+ var coverageRateHist = {
+ "width": 400,
+ "height": 250,
+ "layer": [
+ {
+ "data": {"values": data},
+ "mark": "bar",
+ "encoding": {
+ "x": {
+ "bin": {
+ "maxbins": 10
+ },
+ "field": "rate", "type": "quantitative",
+ "axis": {
+ "title": "Neighborhood Completion (%)", "labelAngle": 0
+ }
+ },
+ "y": {
+ "aggregate": "count", "field": "*", "type": "quantitative",
+ "axis": {
+ "title": "Counts"
+ }
+ }
+ }
},
- y: {
- label: "Coverage Distance (m)",
- min: 0,
- padding: {top: 50, bottom: 10}
+ { // creates lines marking summary statistics
+ "data": {"values": [
+ {"stat": "mean", "value": mean}, {"stat": "median", "value": median}]
+ },
+ "mark": "rule",
+ "encoding": {
+ "x": {
+ "field": "value", "type": "quantitative",
+ "axis": {"labels": false, "ticks": false, "title": ""},
+ "scale": {"domain": [0,100]}
+ },
+ "color": {
+ "field": "stat", "type": "nominal", "scale": {"range": ["pink", "orange"]},
+ "legend": {
+ "title": "Summary Stats"
+ }
+ },
+ "size": {
+ "value": 2
+ }
+ }
+ }
+ ],
+ "resolve": {"x": {"scale": "independent"}},
+ "config": {
+ "axis": {
+ "titleFontSize": 16
}
- },
- legend: {
- show: false
}
- });
+ };
+ vega.embed("#neighborhood-completed-distance", coverageRateHist, opt, function(error, results) {});
});
$.getJSON("/contribution/auditCounts/all", function (data) {
- var dates = ['Date'].concat(data[0].map(function (x) {
- return x.date;
- })),
- counts = ['Audit Count'].concat(data[0].map(function (x) {
- return x.count;
- }));
- var chart = c3.generate({
- bindto: "#audit-count-chart",
- data: {
- x: 'Date',
- columns: [dates, counts],
- types: {'Audit Count': 'line'}
- },
- axis: {
- x: {
- type: 'timeseries',
- tick: {format: '%Y-%m-%d'}
+ data[0].sort(function(a, b) {return (a.count > b.count) ? 1 : ((b.count > a.count) ? -1 : 0);} );
+ var sum = 0;
+ for (var j = 0; j < data[0].length; j++) {
+ sum += data[0][j].count;
+ }
+ var mean = sum / data[0].length;
+ var i = data[0].length / 2;
+ var median = (data[0].length / 2) % 1 == 0 ? (data[0][i - 1].count + data[0][i].count) / 2 : data[0][Math.floor(i)].count;
+
+ var std = 0;
+ for(var k = 0; k < data[0].length; k++) {
+ std += Math.pow(data[0][k].count - mean, 2);
+ }
+ std /= data[0].length;
+ std = Math.sqrt(std);
+ $("#audit-std").html((std).toFixed(1) + " Street Audits");
+
+ var chart = {
+ "data": {"values": data[0]},
+ "hconcat": [
+ {
+ "height": 300,
+ "width": 550,
+ "layer": [
+ {
+ "mark": "area",
+ "encoding": {
+ "x": {
+ "field": "date",
+ "type": "temporal",
+ "axis": {"title": "Date", "labelAngle": 0}
+ },
+ "y": {
+ "field": "count",
+ "type": "quantitative",
+ "axis": {
+ "title": "# Street Audits per Day"
+ }
+ }
+ }
+ },
+ { // creates lines marking summary statistics
+ "data": {"values": [
+ {"stat": "mean", "value": mean}, {"stat": "median", "value": median}]
+ },
+ "mark": "rule",
+ "encoding": {
+ "y": {
+ "field": "value", "type": "quantitative",
+ "axis": {"labels": false, "ticks": false, "title": ""},
+ "scale": {"domain": [0, data[0][data[0].length-1].count]}
+ },
+ "color": {
+ "field": "stat", "type": "nominal", "scale": {"range": ["pink", "orange"]},
+ "legend": false
+ },
+ "size": {
+ "value": 1
+ }
+ }
+ }
+ ],
+ "resolve": {"y": {"scale": "independent"}}
},
- y: {
- label: "Street Audit Count",
- min: 0,
- padding: {top: 50, bottom: 10}
+ {
+ "height": 300,
+ "width": 250,
+ "layer": [
+ {
+ "mark": "bar",
+ "encoding": {
+ "x": {
+ "field": "count",
+ "type": "quantitative",
+ "axis": {"title": "# Street Audits per Day", "labelAngle": 0},
+ "bin": {"maxbins": 20}
+ },
+ "y": {
+ "aggregate": "count",
+ "field": "*",
+ "type": "quantitative",
+ "axis": {
+ "title": "Counts"
+ }
+ }
+ }
+ },
+ { // creates lines marking summary statistics
+ "data": {"values": [
+ {"stat": "mean", "value": mean}, {"stat": "median", "value": median}]
+ },
+ "mark": "rule",
+ "encoding": {
+ "x": {
+ "field": "value", "type": "quantitative",
+ "axis": {"labels": false, "ticks": false, "title": ""},
+ "scale": {"domain": [0, data[0][data[0].length-1].count]}
+ },
+ "color": {
+ "field": "stat", "type": "nominal", "scale": {"range": ["pink", "orange"]},
+ "legend": {
+ "title": "Summary Stats"
+ }
+ },
+ "size": {
+ "value": 1
+ }
+ }
+ }
+ ],
+ "resolve": {"x": {"scale": "independent"}}
+ }
+ ],
+ "config": {
+ "axis": {
+ "titleFontSize": 16
}
- },
- legend: {
- show: false
}
- });
+ };
+ var opt = {
+ "mode": "vega-lite",
+ "actions": false
+ };
+ vega.embed("#audit-count-chart", chart, opt, function(error, results) {});
});
-
$.getJSON("/userapi/labelCounts/all", function (data) {
- var dates = ['Date'].concat(data[0].map(function (x) {
- return x.date;
- })),
- counts = ['Label Count'].concat(data[0].map(function (x) {
- return x.count;
- }));
- var chart = c3.generate({
- bindto: "#label-count-chart",
- data: {
- x: 'Date',
- columns: [dates, counts],
- types: {'Audit Count': 'line'}
- },
- axis: {
- x: {
- type: 'timeseries',
- tick: {format: '%Y-%m-%d'}
+ data[0].sort(function(a, b) {return (a.count > b.count) ? 1 : ((b.count > a.count) ? -1 : 0);} );
+ var sum = 0;
+ for (var j = 0; j < data[0].length; j++) {
+ sum += data[0][j].count;
+ }
+ var mean = sum / data[0].length;
+ var i = data[0].length / 2;
+ var median = (data[0].length / 2) % 1 == 0 ? (data[0][i - 1].count + data[0][i].count) / 2 : data[0][Math.floor(i)].count;
+
+ var std = 0;
+ for(var k = 0; k < data[0].length; k++) {
+ std += Math.pow(data[0][k].count - mean, 2);
+ }
+ std /= data[0].length;
+ std = Math.sqrt(std);
+ $("#label-std").html((std).toFixed(0) + " Labels");
+
+ var chart = {
+ "data": {"values": data[0]},
+ "hconcat": [
+ {
+ "height": 300,
+ "width": 550,
+ "layer": [
+ {
+ "mark": "area",
+ "encoding": {
+ "x": {
+ "field": "date",
+ "type": "temporal",
+ "axis": {"title": "Date", "labelAngle": 0}
+ },
+ "y": {
+ "field": "count",
+ "type": "quantitative",
+ "axis": {
+ "title": "# Labels per Day"
+ }
+ }
+ }
+ },
+ { // creates lines marking summary statistics
+ "data": {"values": [
+ {"stat": "mean", "value": mean}, {"stat": "median", "value": median}]
+ },
+ "mark": "rule",
+ "encoding": {
+ "y": {
+ "field": "value", "type": "quantitative",
+ "axis": {"labels": false, "ticks": false, "title": ""},
+ "scale": {"domain": [0, data[0][data[0].length-1].count]}
+ },
+ "color": {
+ "field": "stat", "type": "nominal", "scale": {"range": ["pink", "orange"]},
+ "legend": false
+ },
+ "size": {
+ "value": 2
+ }
+ }
+ }
+ ],
+ "resolve": {"y": {"scale": "independent"}}
},
- y: {
- label: "Label Count",
- min: 0,
- padding: {top: 50, bottom: 10}
+ {
+ "height": 300,
+ "width": 250,
+ "layer": [
+ {
+ "mark": "bar",
+ "encoding": {
+ "x": {
+ "field": "count",
+ "type": "quantitative",
+ "axis": {"title": "# Labels per Day", "labelAngle": 0},
+ "bin": {"maxbins": 20}
+ },
+ "y": {
+ "aggregate": "count",
+ "field": "*",
+ "type": "quantitative",
+ "axis": {
+ "title": "Counts"
+ }
+ }
+ }
+ },
+ { // creates lines marking summary statistics
+ "data": {"values": [
+ {"stat": "mean", "value": mean}, {"stat": "median", "value": median}]
+ },
+ "mark": "rule",
+ "encoding": {
+ "x": {
+ "field": "value", "type": "quantitative",
+ "axis": {"labels": false, "ticks": false, "title": ""},
+ "scale": {"domain": [0, data[0][data[0].length-1].count]}
+ },
+ "color": {
+ "field": "stat", "type": "nominal", "scale": {"range": ["pink", "orange"]},
+ "legend": {
+ "title": "Summary Stats"
+ }
+ },
+ "size": {
+ "value": 2
+ }
+ }
+ }
+ ],
+ "resolve": {"x": {"scale": "independent"}}
+ }
+ ],
+ "config": {
+ "axis": {
+ "titleFontSize": 16
}
- },
- legend: {
- show: false
}
- });
+ };
+ var opt = {
+ "mode": "vega-lite",
+ "actions": false
+ };
+ vega.embed("#label-count-chart", chart, opt, function(error, results) {});
});
self.graphsLoaded = true;
}
diff --git a/public/javascripts/Choropleth.js b/public/javascripts/Choropleth.js
new file mode 100644
index 0000000000..c6698bee4c
--- /dev/null
+++ b/public/javascripts/Choropleth.js
@@ -0,0 +1,167 @@
+function Choropleth(_, $, turf) {
+ var neighborhoodPolygonLayer;
+
+// Construct a bounding box for these maps that the user cannot move out of
+// https://www.mapbox.com/mapbox.js/example/v1.0.0/maxbounds/
+ var southWest = L.latLng(38.761, -77.262);
+ var northEast = L.latLng(39.060, -76.830);
+ var bounds = L.latLngBounds(southWest, northEast);
+
+// var tileUrl = "https://a.tiles.mapbox.com/v4/kotarohara.mmoldjeh/page.html?access_token=pk.eyJ1Ijoia290YXJvaGFyYSIsImEiOiJDdmJnOW1FIn0.kJV65G6eNXs4ATjWCtkEmA#13/38.8998/-77.0638";
+ var tileUrl = "https:\/\/a.tiles.mapbox.com\/v4\/kotarohara.8e0c6890\/{z}\/{x}\/{y}.png?access_token=pk.eyJ1Ijoia290YXJvaGFyYSIsImEiOiJDdmJnOW1FIn0.kJV65G6eNXs4ATjWCtkEmA";
+ var mapboxTiles = L.tileLayer(tileUrl, {
+ attribution: 'Terms & Feedback '
+ });
+
+// a grayscale tileLayer for the choropleth
+ L.mapbox.accessToken = 'pk.eyJ1IjoibWlzYXVnc3RhZCIsImEiOiJjajN2dTV2Mm0wMDFsMndvMXJiZWcydDRvIn0.IXE8rQNF--HikYDjccA7Ug';
+ var choropleth = L.mapbox.map('choropleth', "kotarohara.8e0c6890", {
+ // set that bounding box as maxBounds to restrict moving the map
+ // see full maxBounds documentation:
+ // http://leafletjs.com/reference.html#map-maxbounds
+ maxBounds: bounds,
+ maxZoom: 19,
+ minZoom: 9,
+ legendControl: {
+ position: 'bottomleft'
+ }
+ })
+ .fitBounds(bounds)
+ .setView([38.892, -77.038], 12);
+ choropleth.scrollWheelZoom.disable();
+
+ L.mapbox.styleLayer('mapbox://styles/mapbox/light-v9').addTo(choropleth);
+
+
+ /**
+ * Takes a completion percentage, bins it, and returns the appropriate color for a choropleth.
+ *
+ * @param p {float} represents a completion percentage, between 0 and 100
+ * @returns {string} color in hex
+ */
+
+ function getColor(p) {
+ return p > 90 ? '#08306b' :
+ p > 80 ? '#08519c' :
+ p > 70 ? '#08719c' :
+ p > 60 ? '#2171b5' :
+ p > 50 ? '#4292c6' :
+ p > 40 ? '#6baed6' :
+ p > 30 ? '#9ecae1' :
+ p > 20 ? '#c6dbef' :
+ p > 10 ? '#deebf7' :
+ '#f7fbff';
+ }
+
+ /**
+ * render the neighborhood polygons, colored by completion percentage
+ */
+ function initializeChoroplethNeighborhoodPolygons(map, rates) {
+ var neighborhoodPolygonStyle = { // default bright red, used to check if any regions are missing data
+ color: '#888',
+ weight: 1,
+ opacity: 0.25,
+ fillColor: "#f00",
+ fillOpacity: 1.0
+ },
+ layers = [],
+ currentLayer;
+
+ // finds the matching neighborhood's completion percentage, and uses it to determine the fill color
+ function style(feature) {
+ for (var i = 0; i < rates.length; i++) {
+ if (rates[i].region_id === feature.properties.region_id) {
+
+ return {
+ color: '#888',
+ weight: 1,
+ opacity: 0.25,
+ fillColor: getColor(100.0 * rates[i].rate),
+ fillOpacity: 0.25 + (0.5 * rates[i].rate)
+ }
+ }
+ }
+ return neighborhoodPolygonStyle; // default case (shouldn't happen, will be bright red)
+ }
+
+ function onEachNeighborhoodFeature(feature, layer) {
+
+ var regionId = feature.properties.region_id,
+ regionName = feature.properties.region_name,
+ compRate = -1.0,
+ milesLeft = -1.0,
+ url = "/audit/region/" + regionId,
+ popupContent = "???";
+ for (var i = 0; i < rates.length; i++) {
+ if (rates[i].region_id === feature.properties.region_id) {
+ compRate = Math.round(100.0 * rates[i].rate);
+ milesLeft = Math.round(0.000621371 * (rates[i].total_distance_m - rates[i].completed_distance_m));
+ if (compRate === 100) {
+ popupContent = "" + regionName + " : " + compRate + "\% Complete!";
+ }
+ else if (milesLeft === 0) {
+ popupContent = "" + regionName + " : " + compRate +
+ "\% Complete Less than a mile left! " +
+ "Click here " +
+ " to help finish this neighborhood!";
+ }
+ else if (milesLeft === 1) {
+ var popupContent = "" + regionName + " : " + compRate + "\% Complete Only " +
+ milesLeft + " mile left! " +
+ "Click here " +
+ " to help finish this neighborhood!";
+ }
+ else {
+ var popupContent = "" + regionName + " : " + compRate + "\% Complete Only " +
+ milesLeft + " miles left! " +
+ "Click here " +
+ " to help finish this neighborhood!";
+ }
+ break;
+ }
+ }
+ layer.bindPopup(popupContent);
+ layers.push(layer);
+
+ layer.on('mouseover', function (e) {
+ this.setStyle({opacity: 1.0, weight: 3, color: "#000"});
+
+ });
+ layer.on('mouseout', function (e) {
+ for (var i = layers.length - 1; i >= 0; i--) {
+ if (currentLayer !== layers[i])
+ layers[i].setStyle({opacity: 0.25, weight: 1});
+ }
+ });
+ layer.on('click', function (e) {
+ var center = turf.center(this.feature),
+ coordinates = center.geometry.coordinates,
+ latlng = L.latLng(coordinates[1], coordinates[0]),
+ zoom = map.getZoom();
+ zoom = zoom > 14 ? zoom : 14;
+
+ map.setView(latlng, zoom, {animate: true});
+ currentLayer = this;
+ });
+ }
+
+ // adds the neighborhood polygons to the map
+ $.getJSON("/neighborhoods", function (data) {
+ neighborhoodPolygonLayer = L.geoJson(data, {
+ style: style,
+ onEachFeature: onEachNeighborhoodFeature
+ })
+ .addTo(map);
+ });
+ }
+
+
+ $.getJSON('/adminapi/neighborhoodCompletionRate', function (data) {
+ // make a choropleth of neighborhood completion percentages
+ initializeChoroplethNeighborhoodPolygons(choropleth, data);
+ choropleth.legendControl.addLegend(document.getElementById('legend').innerHTML);
+ setTimeout(function () {
+ choropleth.invalidateSize(false);
+ }, 1);
+ });
+}
\ No newline at end of file
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index 799876b31f..030337626a 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -19,6 +19,23 @@
height: 800px;
}
+#admin-choropleth {
+ width: 100%;
+ height: 800px;
+}
+
+.legend label,
+.legend span {
+ display:block;
+ float:right;
+ height:15px;
+ width:10%;
+ text-align:left;
+ font-size:8px;
+ color:#808080;
+}
+
+
#map-label-legend {
position: absolute;
background: rgba(255, 255, 255, 0.75);
diff --git a/public/stylesheets/choropleth.css b/public/stylesheets/choropleth.css
new file mode 100644
index 0000000000..15184acb97
--- /dev/null
+++ b/public/stylesheets/choropleth.css
@@ -0,0 +1,15 @@
+#choropleth {
+ width: 100%;
+ height: 800px;
+}
+
+.legend label,
+.legend span {
+ display:block;
+ float:right;
+ height:15px;
+ width:10%;
+ text-align:left;
+ font-size:8px;
+ color:#808080;
+}
\ No newline at end of file