diff --git a/src/Plugin/views/argument/BBoxArgument.php b/src/Plugin/views/argument/BBoxArgument.php
new file mode 100644
index 0000000..1a97841
--- /dev/null
+++ b/src/Plugin/views/argument/BBoxArgument.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Drupal\views_geojson\Plugin\views\argument;
+
+use Drupal\views\Plugin\views\join\JoinPluginBase;
+use Drupal\views\Plugin\views\argument\StringArgument;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Argument handler for Bounding Boxes.
+ *
+ * @ingroup views_argument_handlers
+ *
+ * @ViewsArgument("views_geojson_bbox_argument")
+ */
+class BBoxArgument extends StringArgument implements ContainerFactoryPluginInterface {
+
+  /**
+   * Is this correct?
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+    $options['round_coordinates'] = ['default' => TRUE];
+
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+    parent::buildOptionsForm($form, $form_state);
+    $form['description']['#markup'] .= $this->t('<br/>The format should be<strong>"left,bottom,right,top"</strong>');
+    $form['round_coordinates'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Round coordinates'),
+      '#default_value' => $this->options['title'],
+      '#description' => $this->t('Round coordinates to two decimal places. This helps somewhat in cacheing results in that requests for the bounding boxes -0.542,51.344,-0.367,51.368 and -0.541,51.343,-0.368,51.369 will use the same SQL query.'),
+    ];
+
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * If we've been passed a bounding box, and it's parseable, and the view style
+   * has a geofield then we work out which fields to add to the query and add a
+   * where clause.
+   */
+  public function query($group_by = FALSE) {
+
+    if ($bbox = $this->getParsedBoundingBox()) {
+
+      $geojson_options = $this->view->getStyle()->options;
+
+      $geofield_type = NULL;
+      $geofield_name = NULL;
+      if (!empty($geojson_options['data_source']['value'])) {
+        $geofield_type = $geojson_options['data_source']['value'];
+        if (!empty($geojson_options['data_source'][$geofield_type])) {
+          $geofield_name = $geojson_options['data_source'][$geofield_type];
+        }
+      }
+
+      if (empty($geofield_type) || empty($geofield_name)) {
+        return;
+      }
+
+      // @todo - get geo_table from field definition?
+      $geo_table = "node__{$geofield_name}";
+
+      if ($geofield_type == 'geolocation') {
+        $geo_table_entity_id_field = 'entity_id';
+        $field_lat = "{$geo_table}.field_geolocation_lat";
+        $field_lng = "{$geo_table}.field_geolocation_lng";
+      }
+      elseif ($geofield_type == 'geofield') {
+        $geo_table_entity_id_field = 'entity_id';
+        $field_lat = "{$geo_table}.field_geofield_lat";
+        $field_lng = "{$geo_table}.field_geofield_lon";
+      }
+
+      if (!empty($geo_table)) {
+        $this->query->ensureTable($geo_table, NULL, new JoinPluginBase([
+          'table' => $geo_table,
+          'field' => $geo_table_entity_id_field,
+          'left_table' => 'node_field_data',
+          'left_field' => 'nid',
+        ],
+          NULL, NULL));
+      }
+
+      $this->query->addWhere('bbox', $field_lat, $bbox['bottom'], '>=');
+      $this->query->addWhere('bbox', $field_lat, $bbox['top'], '<=');
+      $this->query->addWhere('bbox', $field_lng, $bbox['left'], '>=');
+      $this->query->addWhere('bbox', $field_lng, $bbox['right'], '<=');
+    }
+  }
+
+  /**
+   * Parses the bounding box argument.
+   *
+   * Parses the bounding box argument. Returns an array keyed 'top', 'left',
+   * 'bottom', 'right' or FALSE if the argument was not parsed succesfully.
+   *
+   * @return array|bool
+   *   The calculated values.
+   */
+  public function getParsedBoundingBox() {
+    static $values;
+
+    if (!isset($values)) {
+      $exploded_values = explode(',', $this->getValue());
+      if (count($exploded_values) == 4) {
+        $values['left'] = (float) $exploded_values[0];
+        $values['bottom'] = (float) $exploded_values[1];
+        $values['right'] = (float) $exploded_values[2];
+        $values['top'] = (float) $exploded_values[3];
+
+        if ($this->options['round_coordinates']) {
+          $values['left'] -= 0.005;
+          $values['bottom'] -= 0.005;
+          $values['right'] += 0.005;
+          $values['top'] += 0.005;
+          foreach ($values as $k => $v) {
+            $values[$k] = round($values[$k], 2);
+          }
+        }
+
+      }
+      else {
+        $values = FALSE;
+      }
+    }
+    return $values;
+  }
+
+}
diff --git a/src/Plugin/views/style/GeoJson.php b/src/Plugin/views/style/GeoJson.php
index 566ea4d..04df974 100644
--- a/src/Plugin/views/style/GeoJson.php
+++ b/src/Plugin/views/style/GeoJson.php
@@ -261,6 +261,7 @@ class GeoJson extends StylePluginBase {
 
     // Attributes and variable styling description.
     $form['attributes'] = array(
+      
       '#type' => 'fieldset',
       '#title' => t('Attributes and Styling'),
       '#description' => t('Attributes are field data attached to each feature.  This can be used with styling to create Variable styling.'),
diff --git a/views/views_geojson.views.inc b/views/views_geojson.views.inc
deleted file mode 100644
index d09cdab..0000000
--- a/views/views_geojson.views.inc
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-/**
- * @file
- * Views plugin definition.
- */
-
-/**
- * Implements hook_views_data().
- *
- * Adds bounding box contextual filter.
- */
-function views_geojson_views_data() {
-  $data = array();
-
-  $data['views']['bbox_argument'] = array(
-    'group' => t('Custom'),
-    'real field' => 'bbox_argument',
-    'title' => t('Bounding box'),
-    'help' => t('Filter locations within a bounding box.'),
-    'argument' => array(
-      'handler' => 'views_geojson_bbox_argument',
-    ),
-  );
-
-  return $data;
-}
diff --git a/views/views_geojson_bbox_argument.inc b/views/views_geojson_bbox_argument.inc
deleted file mode 100644
index 63afd5f..0000000
--- a/views/views_geojson_bbox_argument.inc
+++ /dev/null
@@ -1,230 +0,0 @@
-<?php
-
-/**
- * @file
- *
- */
-
-/**
- * Custom filter to return only points within a provided bounding box.
- */
-class views_geojson_bbox_argument extends views_handler_argument {
-
-  /**
-   * Filter options definition.
-   */
-  function option_definition() {
-    $options = parent::option_definition();
-    $options['arg_id'] = array('default' => 'bbox');
-    $options['empty_result'] = array(
-      'default' => FALSE,
-      'bool' => TRUE,
-    );
-    $options['bbox_wrap'] = array('default' => TRUE, 'bool' => TRUE);
-    return $options;
-  }
-
-  /**
-   * Override the default argument form.
-   */
-  public function default_argument_form(&$form, &$form_state) {
-    parent::default_argument_form($form, $form_state);
-    // Clarify this, since we're treating pulling from the query string as
-    // different than a normal arg.
-    $form['no_argument']['#title'] = 'When the filter value is NOT in the URL <em>as a normal Drupal argument</em>';
-    $form['empty_result'] = array(
-      '#type' => 'checkbox',
-      '#title' => t('Empty result on missing bounding box value'),
-      '#description' => t('When the argument is not found you may choose to show no result.'),
-      '#default_value' => $this->options['empty_result'],
-    );
-
-  }
-
-  /**
-   * Provide additional form options.
-   */
-  public function options_form(&$form, &$form_state) {
-    parent::options_form($form, $form_state);
-
-    $form['bbox_wrap'] = array(
-      '#type' => 'checkbox',
-      '#title' => t('Wrap the bounding box around 360 degrees.'),
-      '#description' => t('Depending on the projection of the data it is possible that the bounding box wraps around the edges of the map. This requires extra conditions in the views query. By default wrapping is enabled. Turn it of if the provided coordinates never wrap.'),
-      '#default_value' => $this->options['bbox_wrap'],
-    );
-  }
-
-  /**
-   * Use the argument to modify the query.
-   */
-  public function query($group_by = FALSE) {
-    // Apply no bbox filtering if the argument is absent or if this is a
-    // search_api based view (which doesn't support sql specific query changes).
-    if ((empty($this->argument)) || ($this->view->base_field == 'search_api_id')) {
-      // @TODO: Implement Search API filtering if Solr BBOX is not an option.
-      // @see: https://www.drupal.org/project/search_api_location_solr_bboxfield
-      return NULL;
-    }
-
-    $this->ensure_my_table();
-
-    // Process coordinates: split string and make sure coordinates are clean.
-    $bbox = $this->_explode_bbox_coords($this->argument);
-
-    if (empty($bbox)) {
-      // Return no values if arg is invalid.
-      $this->argument = FALSE;
-      $this->view->built = TRUE;
-      $this->view->executed = TRUE;
-      return NULL;
-    }
-
-    if (isset($this->view->display_handler->display->display_options['style_options'])) {
-      $data_source = $this->view->display_handler->display->display_options['style_options']['data_source'];
-    }
-    else {
-      $data_source = $this->view->display['default']->display_options['style_options']['data_source'];
-    }
-
-    // We handle latlon and geofield data_source types.
-    // No support yet for WKT.
-    if ($data_source['value'] == 'latlon') {
-      $lat_field_obj = $this->view->field[$data_source['latitude']];
-      $lon_field_obj = $this->view->field[$data_source['longitude']];
-      $lat_field_table = $lat_field_obj->table;
-      $lon_field_table = $lon_field_obj->table;
-    }
-    elseif ($data_source['value'] == 'geofield') {
-      // Geofield includes both Lat and Lon fields.
-      // We pretend they're distinct fields so we can use same code as for
-      // latlon, since we're just going to get lat & lon from geofield anyway.
-      $lat_field_obj = $lon_field_obj = $this->view->field[$data_source['geofield']];
-      $lat_field_table = $lon_field_table = $lat_field_obj->table;
-    }
-    else {
-      // @TODO: Implement WKT (and possibly other data source types).
-      $this->argument = FALSE;
-      $this->view->built = TRUE;
-      $this->view->executed = TRUE;
-      return NULL;
-    }
-
-    // If the Latitude / Longitude fields are really geofields,
-    // we need to dig a bit deeper to find field names.
-    if (isset($lat_field_obj->field_info) && $lat_field_obj->field_info['type'] == 'geofield') {
-      // @TODO: Maybe the field name can really just be "${lat_field_obj}_lat"?
-      $lat_field_name = $lat_field_obj->field_info['storage']['details']['sql']['FIELD_LOAD_CURRENT'][$lat_field_table]['lat'];
-    }
-    else {
-      $lat_field_name = $lat_field_obj->real_field;
-    }
-    if (isset($lon_field_obj->field_info) && $lon_field_obj->field_info['type'] == 'geofield') {
-      $lon_field_name = $lon_field_obj->field_info['storage']['details']['sql']['FIELD_LOAD_CURRENT'][$lon_field_table]['lon'];
-    }
-    else {
-      $lon_field_name = $lon_field_obj->real_field;
-    }
-
-    // Add JOIN(s) to query.
-    $this->query->ensure_table($lat_field_table);
-    if ($lon_field_table != $lat_field_table) {
-      $this->query->ensure_table($lon_field_table);
-    }
-
-    // Add WHERE(s) to query.
-    $left = $bbox['left'];
-    $bottom = $bbox['bottom'];
-    $right = $bbox['right'];
-    $top = $bbox['top'];
-
-    if ($this->options['bbox_wrap']) {
-      // OpenLayers' longitude bbox values can be >180 or <-180 when the map
-      // wraps around. We need to turn these into BETWEENs with OR.
-      if ($right - $left < 360) {
-        $group = $this->query->set_where_group('AND');
-        if ($left > -180) {
-          $this->query->add_where($group, "$lon_field_table.$lon_field_name", $left, '>=');
-        }
-        else {
-          $this->query->set_where_group('OR', $group);
-          $left = -1 * ($left % 180);
-          $this->query->add_where($group, "$lon_field_table.$lon_field_name", array(
-            $left,
-            0,
-          ), 'BETWEEN');
-        }
-        if ($right < 180) {
-          $this->query->add_where($group, "$lon_field_table.$lon_field_name", $right, '<=');
-        }
-        else {
-          $this->query->set_where_group('OR', $group);
-          $right = -1 * ($right % 180);
-          $this->query->add_where($group, "$lon_field_table.$lon_field_name", array(
-            $right,
-            0,
-          ), 'BETWEEN');
-        }
-      }
-    }
-    else {
-      $group = $this->query->set_where_group('AND');
-      $this->query->add_where($group, "$lon_field_table.$lon_field_name", $left, '>=');
-      $this->query->add_where($group, "$lon_field_table.$lon_field_name", $right, '<=');
-    }
-
-    $group = $this->query->set_where_group('AND');
-    $this->query->add_where($group, "$lat_field_table.$lat_field_name", $bottom, '>=');
-    $this->query->add_where($group, "$lat_field_table.$lat_field_name", $top, '<=');
-  }
-
-
-  /**
-   * Split BBOX string into {left, bottom, right, top}.
-   *
-   * @param string $bbox_coords_str
-   *   4 comma delimited coordinates.
-   *
-   * @return array
-   *   Array of 4 valid bbox coordinates or
-   *   empty array if coordinates are invalid.
-   */
-  private function _explode_bbox_coords($bbox_coords_str) {
-    if (!is_string($bbox_coords_str)) {
-      return array();
-    }
-
-    $bbox_coords = explode(',', $bbox_coords_str);
-    if (count($bbox_coords) == 4) {
-      if (!$this->_check_bbox_coords($bbox_coords)) {
-        return array();
-      }
-      $bbox = array(
-        'left' => $bbox_coords[0],
-        'bottom' => $bbox_coords[1],
-        'right' => $bbox_coords[2],
-        'top' => $bbox_coords[3],
-      );
-      return $bbox;
-    }
-    else {
-      return array();
-    }
-  }
-
-  /**
-   * Check if the BBOX coordinates are valid coordinates.
-   *
-   * Currently only checks if coordinates are numeric.
-   *
-   * @TODO: if supported by projection this could include degree checking.
-   */
-  private function _check_bbox_coords($bbox_coords) {
-    foreach ($bbox_coords as $coord) {
-      if (!is_numeric($coord)) {
-        return FALSE;
-      }
-    }
-    return TRUE;
-  }
-}
diff --git a/views/views_plugin_argument_default_bboxquery.inc b/views/views_plugin_argument_default_bboxquery.inc
deleted file mode 100644
index 3b0c05e..0000000
--- a/views/views_plugin_argument_default_bboxquery.inc
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-/**
- * @file
- * Contains the BBOX query string argument default plugin.
- */
-
-// @TODO: Delete this file since it's being replaced by src/Plugin/views/argument_default/BBoxQuery.php?
-
-/**
- * The BBOX query string argument default handler.
- *
- * @TODO: Write an options_validate() method.
- */
-class views_plugin_argument_default_bboxquery extends views_plugin_argument_default {
-  function option_definition() {
-    $options = parent::option_definition();
-    $options['argument'] = array('default' => '');
-    $options['arg_id'] = array('default' => 'bbox');
-
-    return $options;
-  }
-
-  function options_form(&$form, &$form_state) {
-    parent::options_form($form, $form_state);
-
-    $form['info'] = array(
-      '#markup' => '<p class="description">Attempt to pull bounding box info
-      directly from the query string, bypassing Drupal\'s normal argument
-      handling. If the argument does not exist, all values will be shown.</p>',
-    );
-    $form['arg_id'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Query argument ID'),
-      '#size' => 60,
-      '#maxlength' => 64,
-      '#default_value' => $this->options['arg_id'] ? $this->options['arg_id'] : t('bbox'),
-      '#description' => t('The ID of the query argument.<br />For OpenLayers use <em>bbox</em>, (as in "<em>?bbox=left,bottom,right,top</em>".)'),
-    );
-  }
-
-  /**
-   * Return the default argument.
-   */
-  function get_argument() {
-    $query = drupal_get_query_parameters();
-    if (isset($query[$this->options['arg_id']])) {
-      // Just set the argument here. We'll validate and sanitize the input in
-      // views_geojson_bbox_argument::query().
-      return $query[$this->options['arg_id']];
-    }
-    else {
-      if ($this->argument->options['empty_result']) {
-        // Return no values if arg not present and empty result option is set.
-        $this->view->built = TRUE;
-        $this->view->executed = TRUE;
-        return FALSE;
-      }
-      else {
-        // Return all values if arg not present.
-        return TRUE;
-      }
-    }
-  }
-}
diff --git a/views_geojson.views.inc b/views_geojson.views.inc
new file mode 100644
index 0000000..887cf2d
--- /dev/null
+++ b/views_geojson.views.inc
@@ -0,0 +1,16 @@
+<?php
+
+function views_geojson_views_data() {
+  // Add argument
+  $data['views']['views_geojson_bbox_argument'] = [
+    'group' => 'Views GeoJSON',
+    'title' => t('Bounding Box'),
+    'help' => t('Limit the results to a bounding box. Can be used in conjunction with Leaflet.'),
+    'argument' => [
+      'id' => 'views_geojson_bbox_argument',
+      'label' => t('Bounding box'),
+      'empty field name' => '- No value -',
+    ],
+  ];
+  return $data;
+}
