diff --git a/frontend/src/components/Map.vue b/frontend/src/components/Map.vue
index 38e6a94..48637cb 100644
--- a/frontend/src/components/Map.vue
+++ b/frontend/src/components/Map.vue
@@ -2,6 +2,11 @@
   <main>
     <div class="loading" v-if="loading">Loading...</div>
     <div class="map-body" v-else>
+      <MapSelectOverlay
+        @close="showSelectOverlay = false"
+        @select="onAreaSelect"
+        v-if="showSelectOverlay" />
+
       <div id="map">
         <div class="time-range" v-if="oldestPoint && newestPoint">
           <div class="row">
@@ -51,6 +56,16 @@
           <FilterButton @input="showControls = !showControls"
                         :value="showControls" />
         </div>
+
+        <div class="floating-buttons-container">
+          <div class="floating-buttons">
+            <FloatingButton
+                :icon="'fas ' + (hasSelectionBox ? 'fa-remove' : 'fa-object-ungroup')"
+                :title="showSelectOverlay || hasSelectionBox ? 'Reset selection' : 'Filter by area'"
+                :primary="showSelectOverlay || hasSelectionBox"
+                @click="onSelectOverlayButtonClick" />
+          </div>
+        </div>
       </div>
 
       <div class="timeline">
@@ -94,8 +109,11 @@ import Dates from '../mixins/Dates.vue';
 import Feature from 'ol/Feature';
 import FilterButton from './filter/ToggleButton.vue';
 import FilterForm from './filter/Form.vue';
+import FloatingButton from '../elements/FloatingButton.vue';
 import GPSPoint from '../models/GPSPoint';
 import LocationQuery from '../models/LocationQuery';
+import LocationQueryMixin from '../mixins/LocationQuery.vue';
+import MapSelectOverlay from './MapSelectOverlay.vue';
 import MapView from '../mixins/MapView.vue';
 import Paginate from '../mixins/Paginate.vue';
 import Points from '../mixins/Points.vue';
@@ -111,6 +129,7 @@ export default {
   mixins: [
     Api,
     Dates,
+    LocationQueryMixin,
     MapView,
     Paginate,
     Points,
@@ -122,6 +141,8 @@ export default {
     ConfirmDialog,
     FilterButton,
     FilterForm,
+    FloatingButton,
+    MapSelectOverlay,
     PointInfo,
     Timeline,
   },
@@ -135,7 +156,6 @@ export default {
       pointToRemove: null as Optional<GPSPoint>,
       pointsLayer: null as Optional<VectorLayer>,
       popup: null as Optional<Overlay>,
-      queryInitialized: false,
       refreshPoints: 0,
       routesLayer: null as Optional<VectorLayer>,
       selectedFeature: null as Optional<Feature>,
@@ -143,6 +163,7 @@ export default {
       selectedPointIndex: null as Optional<number>,
       showControls: false,
       showMetrics: new TimelineMetricsConfiguration(),
+      showSelectOverlay: false,
     }
   },
 
@@ -160,6 +181,13 @@ export default {
       return this.groupPoints(this.gpsPoints)
     },
 
+    hasSelectionBox(): boolean {
+      return this.locationQuery.minLongitude != null &&
+        this.locationQuery.minLatitude != null &&
+        this.locationQuery.maxLongitude != null &&
+        this.locationQuery.maxLatitude != null
+    },
+
     mappedPoints(): Record<string, Point> {
       // Reference refreshPoints to force reactivity
       this.refreshPoints;
@@ -378,26 +406,72 @@ export default {
     setShowMetrics(metrics: any) {
       Object.assign(this.showMetrics, metrics)
     },
+
+    onAreaSelect(selectionBox: number[][]) {
+      this.showSelectOverlay = false
+      if (!selectionBox.length) {
+        return
+      }
+
+      let [start, end] = selectionBox
+      if (!(start && end)) {
+        return
+      }
+
+      start = this.map.getCoordinateFromPixel(start)
+      end = this.map.getCoordinateFromPixel(end)
+      const [startLon, startLat, endLon, endLat] = [
+        Math.min(start[0], end[0]),
+        Math.min(start[1], end[1]),
+        Math.max(start[0], end[0]),
+        Math.max(start[1], end[1]),
+      ]
+
+      this.locationQuery = {
+        ...this.locationQuery,
+        startDate: null,
+        endDate: null,
+        minLongitude: startLon,
+        minLatitude: startLat,
+        maxLongitude: endLon,
+        maxLatitude: endLat,
+      }
+    },
+
+    onSelectOverlayButtonClick() {
+      if (!this.hasSelectionBox) {
+        this.showSelectOverlay = true
+      } else {
+        this.locationQuery = {
+          ...this.locationQuery,
+          minLongitude: null,
+          minLatitude: null,
+          maxLongitude: null,
+          maxLatitude: null,
+        }
+      }
+    },
   },
 
   watch: {
     locationQuery: {
-      async handler(newQuery, oldQuery) {
-        const isFirstQuery = !this.queryInitialized
+      async handler(newQuery: LocationQuery, oldQuery: LocationQuery) {
+        if (!this.isQueryChanged({
+          newValue: newQuery,
+          oldValue: oldQuery,
+        })) {
+          return
+        }
 
         // If startDate/endDate have changed, reset minId/maxId
-        if (!isFirstQuery &&
-          (newQuery.startDate !== oldQuery.startDate || newQuery.endDate !== oldQuery.endDate)
-        ) {
+        if (newQuery.startDate !== oldQuery.startDate || newQuery.endDate !== oldQuery.endDate) {
           newQuery.minId = null
           newQuery.maxId = null
           this.hasNextPage = true
           this.hasPrevPage = true
         }
 
-        // Results with maxId should be retrieved in descending order,
-        // otherwise all results should be retrieved in ascending order
-        newQuery.order = newQuery.maxId ? 'desc' : 'asc'
+        newQuery.order = 'desc'
         this.setQuery(
           {
             ...newQuery,
@@ -406,33 +480,29 @@ export default {
           }
         )
 
-        this.queryInitialized = true
+        const gpsPoints = await this.fetch()
 
-        if (!isFirstQuery) {
-          const gpsPoints = await this.fetch()
-
-          // If there are no points, and minId/maxId are set, reset them
-          // and don't update the map (it means that we have reached the
-          // start/end of the current window)
-          if (gpsPoints.length < 2 && (newQuery.minId || newQuery.maxId)) {
-            if (newQuery.minId) {
-              this.hasNextPage = false
-            }
-
-            if (newQuery.maxId) {
-              this.hasPrevPage = false
-            }
-
-            newQuery.minId = oldQuery.minId
-            newQuery.maxId = oldQuery.maxId
-            return
+        // If there are no points, and minId/maxId are set, reset them
+        // and don't update the map (it means that we have reached the
+        // start/end of the current window)
+        if (gpsPoints.length < 2 && (newQuery.minId || newQuery.maxId)) {
+          if (newQuery.minId) {
+            this.hasNextPage = false
           }
 
-          this.gpsPoints = gpsPoints
-          this.hasNextPage = gpsPoints.length > 1
-          this.hasPrevPage = gpsPoints.length > 1
+          if (newQuery.maxId) {
+            this.hasPrevPage = false
+          }
+
+          newQuery.minId = oldQuery.minId
+          newQuery.maxId = oldQuery.maxId
+          return
         }
 
+        this.gpsPoints = gpsPoints
+        this.hasNextPage = gpsPoints.length > 1
+        this.hasPrevPage = gpsPoints.length > 1
+
         if (this.mapView) {
           this.refreshMap()
         }
@@ -474,10 +544,9 @@ export default {
 
 <style lang="scss" scoped>
 @use "@/styles/common.scss" as *;
+@use "./vars.scss" as *;
 @import "ol/ol.css";
 
-$timeline-height: 10rem;
-
 main {
   width: 100%;
   height: 100%;
@@ -556,6 +625,25 @@ main {
   box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.5);
 }
 
+.floating-buttons-container {
+  width: 100%;
+  height: 5em;
+  position: absolute;
+  bottom: 0;
+  right: 0;
+
+  .floating-buttons {
+    height: 100%;
+    position: relative;
+    display: flex;
+    justify-content: flex-end;
+
+    :deep(button) {
+      position: absolute;
+    }
+  }
+}
+
 :deep(.ol-viewport) {
   .ol-attribution {
     display: none;
diff --git a/frontend/src/components/MapSelectOverlay.vue b/frontend/src/components/MapSelectOverlay.vue
new file mode 100644
index 0000000..a6fb20d
--- /dev/null
+++ b/frontend/src/components/MapSelectOverlay.vue
@@ -0,0 +1,192 @@
+<template>
+  <div class="overlay-container">
+    <div class="overlay-popup" @click.stop>
+      <p>Filter location points in the selected area.</p>
+      <button @click="$emit('close')">Close</button>
+    </div>
+    <div class="overlay"
+         ref="overlay"
+         @mousedown="onOverlayDragStart"
+         @mouseup="onOverlayDragEnd"
+         @mousemove="onOverlayMove"
+         @touchstart="onOverlayDragStart"
+         @touchend="onOverlayDragEnd"
+         @touchmove="onOverlayMove"
+         @click="onOverlayDragEnd">
+      <div class="box"
+           :style="selectionBoxStyle"
+           v-if="selectionBox.length > 1" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+export default {
+  emits: ['close', 'select'],
+
+  data() {
+    return {
+      overlayDragging: false,
+      selectionBox: [] as number[][],
+    }
+  },
+
+  computed: {
+    selectionBoxStyle(): Record<string, string> {
+      if (this.selectionBox.length < 2) {
+        return {}
+      }
+
+      const scaledCoords = [
+        this.scaledPointerCoordinates(...this.selectionBox[0]),
+        this.scaledPointerCoordinates(...this.selectionBox[1]),
+      ]
+
+      const [minX, minY, maxX, maxY] = this.sorted(scaledCoords).flat()
+      return {
+        top: minY + 'px',
+        left: minX + 'px',
+        width: `${maxX - minX}px`,
+        height: `${maxY - minY}px`,
+      }
+    },
+
+    hasDistinctPoints(): boolean {
+      return this.selectionBox.length > 1 && this.selectionBox[1] && (
+        this.selectionBox[0][0] !== this.selectionBox[1][0] || this.selectionBox[0][1] !== this.selectionBox[1][1]
+      )
+    },
+  },
+
+  methods: {
+    sorted(coords: number[][]): number[][] {
+      if ((coords?.length || 0) < 2) {
+        return coords
+      }
+
+      return [
+        [Math.min(coords[0][0], coords[1][0]), Math.min(coords[0][1], coords[1][1])],
+        [Math.max(coords[0][0], coords[1][0]), Math.max(coords[0][1], coords[1][1])],
+      ]
+    },
+
+    scaledPointerCoordinates(x: number, y: number): number[] {
+      const offsetLeft = this.$refs.overlay?.getBoundingClientRect().left || 0
+      const offsetTop = this.$refs.overlay?.getBoundingClientRect().top || 0
+
+      return [
+        x - offsetLeft,
+        y - offsetTop,
+      ]
+    },
+
+    setSelectionBoxCoordinates(event: MouseEvent) {
+      const coords = [event.clientX, event.clientY]
+      let newBox = JSON.parse(JSON.stringify(this.selectionBox)) as number[][]
+
+      if (newBox.length === 1 || !newBox[1]) {
+        newBox.push(coords)
+      } else {
+        newBox[1] = coords
+        newBox = newBox.sort((a, b) => a[0] - b[0])
+      }
+
+      this.selectionBox = newBox
+    },
+
+    onOverlayDragStart(event: MouseEvent) {
+      this.selectionBox = []
+      this.setSelectionBoxCoordinates(event)
+      this.overlayDragging = true
+    },
+
+    onOverlayDragEnd(event: MouseEvent) {
+      if (this.selectionBox.length < 1) {
+        this.selectionBox = []
+        return
+      }
+
+      if (!this.hasDistinctPoints) {
+        this.selectionBox = []
+      }
+
+      this.setSelectionBoxCoordinates(event)
+      this.overlayDragging = false
+
+      if (this.hasDistinctPoints) {
+        this.$emit(
+          'select',
+          [
+            this.scaledPointerCoordinates(...this.selectionBox[0]),
+            this.scaledPointerCoordinates(...this.selectionBox[1])
+          ]
+        )
+      }
+    },
+
+    onOverlayMove(event: MouseEvent) {
+      if (!this.overlayDragging || this.selectionBox.length < 1) {
+        return
+      }
+
+      this.setSelectionBoxCoordinates(event)
+    },
+
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@use "./vars.scss" as *;
+
+.overlay-container {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: calc(100% - #{$timeline-height});
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 1000;
+
+  .overlay-popup {
+    position: absolute;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.15);
+    color: white;
+    display: flex;
+    flex-direction: column;
+    padding: 1em;
+    border-radius: 0.25em;
+    box-shadow: 0 0 0.25em rgba(0, 0, 0, 0.5);
+    opacity: 0.75;
+    z-index: 1002;
+
+    button {
+      background: transparent;
+      color: white;
+      margin-top: 0.5em;
+      cursor: pointer;
+
+      &:hover {
+        text-decoration: underline;
+      }
+    }
+  }
+
+  .overlay {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    z-index: 1001;
+
+    .box {
+      position: absolute;
+      background: rgba(0, 0, 0, 0.25);
+      border: 1px solid var(--color-accent);
+    }
+  }
+}
+</style>
diff --git a/frontend/src/components/Timeline.vue b/frontend/src/components/Timeline.vue
index caf2719..cf568d3 100644
--- a/frontend/src/components/Timeline.vue
+++ b/frontend/src/components/Timeline.vue
@@ -210,6 +210,12 @@ export default {
         }
       }
 
+      const xTicks = {} as { min?: Date, max?: Date }
+      if (this.points.length > 1) {
+        xTicks.min = this.points[0].timestamp
+        xTicks.max = this.points[this.points.length - 1].timestamp
+      }
+
       return {
         responsive: true,
         maintainAspectRatio: false,
@@ -245,9 +251,9 @@ export default {
               drawTicks: true,
             },
             time: {
-              tooltipFormat: 'MMM dd, HH:mm',
-              unit: 'minute',
+              tooltipFormat: 'MMM dd yyyy, HH:mm',
             },
+            ticks: xTicks,
             title: {
               display: true,
               text: 'Date'
diff --git a/frontend/src/components/filter/Form.vue b/frontend/src/components/filter/Form.vue
index 0fc884e..671805c 100644
--- a/frontend/src/components/filter/Form.vue
+++ b/frontend/src/components/filter/Form.vue
@@ -2,7 +2,16 @@
   <form class="filter-view" @submit.prevent.stop="handleSubmit">
     <h2>Filter</h2>
 
-    <div class="date-selectors">
+    <div class="date-range-toggle">
+      <input type="checkbox"
+             id="date-range-toggle"
+             name="date-range-toggle"
+             v-model="enableDateRange"
+             :disabled="disabled" />
+      <label for="date-range-toggle">Enable Date Range</label>
+    </div>
+
+    <div class="date-selectors" v-if="enableDateRange">
       <div class="date-selector">
         <label for="start-date">Start Date</label>
         <input type="datetime-local"
@@ -160,9 +169,12 @@
 <script lang="ts">
 import _ from 'lodash'
 
+import LocationQuery from '../../models/LocationQuery'
+import LocationQueryMixin from '../../mixins/LocationQuery.vue'
 import UserDevice from '../../models/UserDevice'
 
 export default {
+  mixins: [LocationQueryMixin],
   emit: [
     'next-page',
     'prev-page',
@@ -172,7 +184,7 @@ export default {
   ],
 
   props: {
-    value: Object,
+    value: LocationQuery,
     devices: {
       type: Array as () => UserDevice[],
       default: () => [],
@@ -204,51 +216,13 @@ export default {
   data() {
     return {
       changed: false,
+      enableDateRange: false,
       newFilter: {...this.value},
       newResolution: this.resolution,
     }
   },
 
   methods: {
-    hasChanged(oldValue: any, newValue: any): boolean {
-      return !_.isEqual(
-        {
-          ...oldValue,
-          startDate: this.normalizeDate(this.value?.startDate),
-          endDate: this.normalizeDate(this.value?.endDate),
-        },
-        {
-          ...newValue,
-          startDate: this.normalizeDate(this.newFilter.startDate),
-          endDate: this.normalizeDate(this.newFilter.endDate),
-        }
-      )
-    },
-
-    normalizeDate(date: any): Date | null {
-      if (!date) {
-        return null
-      }
-
-      if (typeof date === 'number' || typeof date === 'string') {
-        date = new Date(date)
-      }
-
-      // Round to the nearest minute
-      return new Date(Math.floor(date.getTime() / 60000) * 60000)
-    },
-
-    toLocalString(date: Date | string | number | null): string {
-      const d = this.normalizeDate(date)
-      if (!d) {
-        return ''
-      }
-
-      return new Date(
-        d.getTime() - (d.getTimezoneOffset() * 60000)
-      ).toISOString().slice(0, -8)
-    },
-
     startPlusHours(date: Date | number | Event | undefined | null, hours: number): Date | null {
       if ((date as any)?.target?.value) {
         date = (date as any).target.value
@@ -316,11 +290,19 @@ export default {
     setResolution(event: Event) {
       this.newResolution = Number((event.target as HTMLInputElement).value)
     },
+
+    initDateRange(value: LocationQuery) {
+      this.enableDateRange = !!(value.startDate && value.endDate)
+    },
+  },
+
+  mounted() {
+    this.initDateRange(this.value)
   },
 
   watch: {
     value: {
-      handler(value) {
+      handler(value: LocationQuery) {
         this.newFilter = {...value}
         this.changed = false
       },
@@ -329,16 +311,35 @@ export default {
     },
 
     newFilter: {
-      handler(value) {
-        this.changed = this.hasChanged(this.value, value)
+      handler(value: LocationQuery) {
+        this.changed = this.isQueryChanged({
+          newValue: value,
+          oldValue: this.value
+        })
+        this.initDateRange(value)
       },
       immediate: true,
       deep: true,
     },
 
-    newResolution(value) {
+    newResolution(value: number) {
       this.changed = this.changed || value !== this.resolution
     },
+
+    enableDateRange(value: boolean) {
+      if (!value) {
+        this.newFilter.startDate = null
+        this.newFilter.endDate = null
+      } else {
+        if (!this.newFilter.startDate) {
+          this.newFilter.startDate = new Date(new Date().getTime() - 24 * 60 * 60 * 1000)
+        }
+
+        if (!this.newFilter.endDate) {
+          this.newFilter.endDate = new Date()
+        }
+      }
+    },
   },
 }
 </script>
@@ -408,6 +409,16 @@ export default {
     }
   }
 
+  .date-range-toggle {
+    display: flex;
+    align-items: center;
+    margin: 0.5em 0 -0.5em 0;
+
+    input {
+      margin-right: 0.25em;
+    }
+  }
+
   .pagination-container {
     display: flex;
     flex-direction: row;
diff --git a/frontend/src/components/vars.scss b/frontend/src/components/vars.scss
new file mode 100644
index 0000000..af5a733
--- /dev/null
+++ b/frontend/src/components/vars.scss
@@ -0,0 +1 @@
+$timeline-height: 10rem;
diff --git a/frontend/src/elements/FloatingButton.vue b/frontend/src/elements/FloatingButton.vue
index e85491d..dc79495 100644
--- a/frontend/src/elements/FloatingButton.vue
+++ b/frontend/src/elements/FloatingButton.vue
@@ -1,7 +1,9 @@
 <template>
   <button class="floating-button"
+          :class="{ primary }"
           @click="$emit('click')"
           :title="title"
+          :style="style"
           :aria-label="title">
     <font-awesome-icon :icon="icon" />
   </button>
@@ -16,6 +18,16 @@ export default {
       required: true,
     },
 
+    primary: {
+      type: Boolean,
+      default: false,
+    },
+
+    style: {
+      type: Object,
+      default: () => ({}),
+    },
+
     title: {
       type: String,
     },
@@ -28,8 +40,6 @@ button.floating-button {
   position: fixed;
   bottom: 1em;
   right: 1em;
-  background: var(--color-accent);
-  color: var(--color-background);
   width: 4em;
   height: 4em;
   font-size: 1em;
@@ -42,10 +52,20 @@ button.floating-button {
   z-index: 100;
 
   &:hover {
-    background: var(--color-accent) !important;
-    color: var(--color-background) !important;
     font-weight: bold;
     filter: brightness(1.2);
   }
+
+  &.primary {
+    background: var(--color-accent);
+    color: var(--color-background);
+
+    &:hover {
+      background: var(--color-accent) !important;
+      color: var(--color-background) !important;
+      font-weight: bold;
+      filter: brightness(1.2);
+    }
+  }
 }
 </style>
diff --git a/frontend/src/mixins/Dates.vue b/frontend/src/mixins/Dates.vue
index 60bae27..e23eaa4 100644
--- a/frontend/src/mixins/Dates.vue
+++ b/frontend/src/mixins/Dates.vue
@@ -8,6 +8,30 @@ export default {
 
       return new Date(date).toString().replace(/GMT.*/, '')
     },
+
+    normalizeDate(date: any): Date | null {
+      if (!date) {
+        return null
+      }
+
+      if (typeof date === 'number' || typeof date === 'string') {
+        date = new Date(date)
+      }
+
+      // Round to the nearest minute
+      return new Date(Math.floor(date.getTime() / 60000) * 60000)
+    },
+
+    toLocalString(date: Date | string | number | null): string {
+      const d = this.normalizeDate(date)
+      if (!d) {
+        return ''
+      }
+
+      return new Date(
+        d.getTime() - (d.getTimezoneOffset() * 60000)
+      ).toISOString().slice(0, -8)
+    },
   }
 }
 </script>
diff --git a/frontend/src/mixins/LocationQuery.vue b/frontend/src/mixins/LocationQuery.vue
new file mode 100644
index 0000000..541212d
--- /dev/null
+++ b/frontend/src/mixins/LocationQuery.vue
@@ -0,0 +1,32 @@
+<script lang="ts">
+import _ from 'lodash'
+
+import Dates from './Dates.vue'
+import LocationQuery from '../models/LocationQuery'
+
+export default {
+  mixins: [Dates],
+  methods: {
+    isQueryChanged({
+      newValue,
+      oldValue,
+    }: {
+      newValue?: LocationQuery,
+      oldValue?: LocationQuery,
+    }): boolean {
+      return !_.isEqual(
+        {
+          ...(oldValue || {}),
+          startDate: this.normalizeDate(oldValue?.startDate),
+          endDate: this.normalizeDate(oldValue?.endDate),
+        },
+        {
+          ...(newValue || {}),
+          startDate: this.normalizeDate(newValue?.startDate),
+          endDate: this.normalizeDate(newValue?.endDate),
+        }
+      )
+    },
+  }
+}
+</script>
diff --git a/frontend/src/mixins/Paginate.vue b/frontend/src/mixins/Paginate.vue
index 1cc62b7..16f7aa4 100644
--- a/frontend/src/mixins/Paginate.vue
+++ b/frontend/src/mixins/Paginate.vue
@@ -8,7 +8,7 @@ export default {
       gpsPoints: [] as GPSPoint[],
       hasNextPage: true,
       hasPrevPage: true,
-      locationQuery: new LocationQuery({}),
+      locationQuery: new LocationQuery({}) as LocationQuery,
     }
   },
 
diff --git a/frontend/src/mixins/SelectionBox.vue b/frontend/src/mixins/SelectionBox.vue
new file mode 100644
index 0000000..104431d
--- /dev/null
+++ b/frontend/src/mixins/SelectionBox.vue
@@ -0,0 +1,86 @@
+<script lang="ts">
+export default {
+  data() {
+    return {
+      selectionBox: [] as number[][],
+    }
+  },
+
+  computed: {
+    selectionBoxStyle(): Record<string, string> {
+      if (this.selectionBox.length < 2) {
+        return {}
+      }
+
+      const [minX, minY, maxX, maxY] = [
+        Math.min(this.selectionBox[0][0], this.selectionBox[1][0]),
+        Math.min(this.selectionBox[0][1], this.selectionBox[1][1]),
+        Math.max(this.selectionBox[0][0], this.selectionBox[1][0]),
+        Math.max(this.selectionBox[0][1], this.selectionBox[1][1]),
+      ]
+
+      return {
+        top: minY + 'px',
+        left: minX + 'px',
+        width: `${maxX - minX}px`,
+        height: `${maxY - minY}px`,
+      }
+    },
+  },
+
+  methods: {
+    scaledPointerCoordinates(event: MouseEvent): number[] {
+      const offsetLeft = this.$refs.overlay?.getBoundingClientRect().left || 0
+      const offsetTop = this.$refs.overlay?.getBoundingClientRect().top || 0
+
+      return [
+        event.clientX - offsetLeft,
+        event.clientY - offsetTop,
+      ]
+    },
+
+    setSelectionBoxCoordinates(event: MouseEvent) {
+      const coords = this.scaledPointerCoordinates(event)
+      let newBox = JSON.parse(JSON.stringify(this.selectionBox)) as number[][]
+
+      if (newBox.length === 1 || !newBox[1]) {
+        newBox.push(coords)
+      } else {
+        newBox[1] = coords
+      }
+
+      newBox = newBox.sort((a: number[], b: number[]) => a[0] - b[0])
+      this.selectionBox = newBox
+    },
+
+    onOverlayDragStart(event: MouseEvent) {
+      this.setSelectionBoxCoordinates(event)
+      this.overlayDragging = true
+    },
+
+    onOverlayDragEnd(event: MouseEvent) {
+      if (this.selectionBox.length < 1) {
+        this.selectionBox = []
+        return
+      }
+
+      this.setSelectionBoxCoordinates(event)
+      if (this.selectionBox.length > 1 && (
+        this.selectionBox[0][0] === this.selectionBox[1][0] && this.selectionBox[0][1] === this.selectionBox[1][1])
+      ) {
+        this.selectionBox = []
+      }
+
+      this.overlayDragging = false
+    },
+
+    onOverlayMove(event: MouseEvent) {
+      if (!this.overlayDragging || this.selectionBox.length < 1) {
+        return
+      }
+
+      this.setSelectionBoxCoordinates(event)
+    },
+  },
+}
+</script>
diff --git a/frontend/src/mixins/URLQueryHandler.vue b/frontend/src/mixins/URLQueryHandler.vue
index c4f3aa9..10f8c4d 100644
--- a/frontend/src/mixins/URLQueryHandler.vue
+++ b/frontend/src/mixins/URLQueryHandler.vue
@@ -1,6 +1,8 @@
 <script lang="ts">
 import _ from 'lodash'
 
+import LocationQueryMixin from './LocationQuery.vue'
+
 function isDate(key: string, value: string): boolean {
   return (
     (
@@ -37,6 +39,7 @@ function encodeValue(value: string | number | boolean | Date): string {
 }
 
 export default {
+  mixins: [LocationQueryMixin],
   data() {
     return {
       query: this.parseQuery(window.location.href),
@@ -60,10 +63,6 @@ export default {
         }, {})
     },
 
-    isQueryChanged(oldQuery: Record<string, any>, newQuery: Record<string, any>): boolean {
-      return !_.isEqual(oldQuery, newQuery)
-    },
-
     toQueryString(values: Record<string, any>) {
       return Object.entries(values)
         .filter(([_, value]) => value != null && value.toString() !== '[object Object]')
@@ -78,10 +77,10 @@ export default {
   },
 
   watch: {
-    $route(newRoute, oldRoute) {
+    $route(newRoute: { fullPath: string }, oldRoute: { fullPath: string }) {
       const oldQuery = this.parseQuery(oldRoute.fullPath)
       const newQuery = this.parseQuery(newRoute.fullPath)
-      if (this.isQueryChanged(oldQuery, newQuery)) {
+      if (this.isQueryChanged({oldValue: oldQuery, newValue: newQuery})) {
         this.query = newQuery
       }
     },
diff --git a/frontend/src/models/LocationQuery.ts b/frontend/src/models/LocationQuery.ts
index cb0db7b..f7211e1 100644
--- a/frontend/src/models/LocationQuery.ts
+++ b/frontend/src/models/LocationQuery.ts
@@ -8,10 +8,14 @@ class LocationQuery {
   public endDate: Optional<Date> = null;
   public minId: Optional<number> = null;
   public maxId: Optional<number> = null;
+  public minLatitude: Optional<number> = null;
+  public maxLatitude: Optional<number> = null;
+  public minLongitude: Optional<number> = null;
+  public maxLongitude: Optional<number> = null;
   public country: Optional<string> = null;
   public locality: Optional<string> = null;
   public postalCode: Optional<string> = null;
-  public order: string = 'asc';
+  public order: string = 'desc';
 
   constructor(data: {
     limit?: Optional<number>;
@@ -21,6 +25,10 @@ class LocationQuery {
     endDate?: Optional<Date>;
     minId?: Optional<number>;
     maxId?: Optional<number>;
+    minLatitude?: Optional<number>;
+    maxLatitude?: Optional<number>;
+    minLongitude?: Optional<number>;
+    maxLongitude?: Optional<number>;
     country?: Optional<string>;
     locality?: Optional<string>;
     postalCode?: Optional<string>;
@@ -33,16 +41,14 @@ class LocationQuery {
     this.endDate = data.endDate || this.endDate;
     this.minId = data.minId || this.minId;
     this.maxId = data.maxId || this.maxId;
+    this.minLatitude = data.minLatitude || this.minLatitude;
+    this.maxLatitude = data.maxLatitude || this.maxLatitude;
+    this.minLongitude = data.minLongitude || this.minLongitude;
+    this.maxLongitude = data.maxLongitude || this.maxLongitude;
     this.country = data.country || this.country;
     this.locality = data.locality || this.locality;
     this.postalCode = data.postalCode || this.postalCode;
     this.order = data.order || this.order;
-
-    if (!(this.startDate && this.endDate)) {
-      // Default to the past 24 hours
-      this.endDate = new Date();
-      this.startDate = new Date(this.endDate.getTime() - 24 * 60 * 60 * 1000);
-    }
   }
 }
 
diff --git a/frontend/src/views/API.vue b/frontend/src/views/API.vue
index ac3cf7a..d1b68b9 100644
--- a/frontend/src/views/API.vue
+++ b/frontend/src/views/API.vue
@@ -19,6 +19,7 @@
     <FloatingButton
         icon="fas fa-plus"
         title="Create a new API token"
+        :primary="true"
         @click="showTokenForm = true" />
   </div>
 </template>
diff --git a/frontend/src/views/Devices.vue b/frontend/src/views/Devices.vue
index 29271e2..46049a6 100644
--- a/frontend/src/views/Devices.vue
+++ b/frontend/src/views/Devices.vue
@@ -23,6 +23,7 @@
     <FloatingButton
         icon="fas fa-plus"
         title="Register a new device"
+        :primary="true"
         @click="showDeviceForm = true" />
   </div>
 </template>
diff --git a/src/requests/LocationRequest.ts b/src/requests/LocationRequest.ts
index 90207d7..44b3a90 100644
--- a/src/requests/LocationRequest.ts
+++ b/src/requests/LocationRequest.ts
@@ -15,6 +15,10 @@ class LocationRequest {
   endDate: Optional<Date> = null;
   minId: Optional<number> = null;
   maxId: Optional<number> = null;
+  minLatitude: Optional<number> = null;
+  maxLatitude: Optional<number> = null;
+  minLongitude: Optional<number> = null;
+  maxLongitude: Optional<number> = null;
   country: Optional<string> = null;
   locality: Optional<string> = null;
   postalCode: Optional<string> = null;
@@ -31,6 +35,10 @@ class LocationRequest {
     endDate?: Date;
     minId?: number;
     maxId?: number;
+    minLatitude?: number;
+    maxLatitude?: number;
+    minLongitude?: number;
+    maxLongitude?: number;
     country?: string;
     locality?: string;
     postalCode?: string;
@@ -46,6 +54,10 @@ class LocationRequest {
     this.initDate('endDate', req);
     this.initNumber('minId', req);
     this.initNumber('maxId', req);
+    this.initNumber('minLatitude', req, parseFloat);
+    this.initNumber('maxLatitude', req, parseFloat);
+    this.initNumber('minLongitude', req, parseFloat);
+    this.initNumber('maxLongitude', req, parseFloat);
     this.country = req.country;
     this.locality = req.locality;
     this.postalCode = req.postalCode;
@@ -54,9 +66,9 @@ class LocationRequest {
     this.order = (req.order || this.order).toUpperCase() as Order;
   }
 
-  private initNumber(key: string, req: any): void {
+  private initNumber(key: string, req: any, parser: (s: string) => number = parseInt): void {
     if (req[key] != null) {
-      const numValue = (this as any)[key] = parseInt(req[key]);
+      const numValue = (this as any)[key] = parser(req[key]);
       if (isNaN(numValue)) {
         throw new ValidationError(`Invalid value for ${key}: ${req[key]}`);
       }
@@ -127,6 +139,30 @@ class LocationRequest {
       where[colMapping.description || 'description'] = {[Op.like]: `%${this.description}%`};
     }
 
+    if (this.minLatitude != null || this.maxLatitude != null) {
+      const column = colMapping.latitude || 'latitude';
+      const where_lat: any = where[column] = {};
+      if (this.minLatitude == null && this.maxLatitude != null) {
+        where_lat[Op.lte] = this.maxLatitude;
+      } else if (this.minLatitude != null && this.maxLatitude == null) {
+        where_lat[Op.gte] = this.minLatitude;
+      } else {
+        where_lat[Op.between] = [this.minLatitude, this.maxLatitude];
+      }
+    }
+
+    if (this.minLongitude != null || this.maxLongitude != null) {
+      const column = colMapping.longitude || 'longitude';
+      const where_lon: any = where[column] = {};
+      if (this.minLongitude == null && this.maxLongitude != null) {
+        where_lon[Op.lte] = this.maxLongitude;
+      } else if (this.minLongitude != null && this.maxLongitude == null) {
+        where_lon[Op.gte] = this.minLongitude;
+      } else {
+        where_lon[Op.between] = [this.minLongitude, this.maxLongitude];
+      }
+    }
+
     queryMap.where = where;
     queryMap.order = [[colMapping[this.orderBy], this.order.toUpperCase()]];
     return queryMap;