Skip to content

Geo Map

The Geo Map allows you to visualize geospatial data. Please use the Geo Map GL visualization instead as it provides this functionality plus additional features and an improved API.

Initialization

The Geo Map needs access to several external libraries that are included automatically when a Geo Map visualization is used. They are loaded from their default location. If the data application does not have extranet access however, you can specify an alternate location to load these resources. The table below shows the Geo Map dependencies.

Variable Default Url
leafletLibUrl https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.1/leaflet.js
leafletCssUrl https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.1/leaflet.css
leafletClusterUrl https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/leaflet.markercluster.js
leafletClusterCssUrl https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/MarkerCluster.css
leafletClusterCssDefaultUrl https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/MarkerCluster.Default.css
heatMapUrl https://cdn.jsdelivr.net/npm/heatmap.js@2.0.5/build/heatmap.min.js

Note

The public repositories for these dendencies are below:

Define the global variables above to update the location of these resources. The following example shows how to inject the location of a Geo Map dependency:

1
2
3
4
    <script>
        window.leafletLibUrl = 'https://unpkg.com/leaflet@1.7.0/dist/leaflet.js';
    </script>
    <script src="../../lib/CFToolkit.min.js"></script>

Leaflet can also be added to the project by setting the references directly to the html files as shown below: leaflet-libraries

In this case, chartfactor will detect internally if the window.L variable exists, otherwise it will proceed to define it with the default urls, so that it can be used properly.

External Leaflet Plugins

Leaflet also has a large amount of plugins that are usually distributed as separated libraries. In case that a plugin is required, then the leaflet library has to be included as dependency in the html page as a script tag instead of a variable and before the plugin:

1
2
3
4
5
    <script src="https://unpkg.com/leaflet@1.7.0/dist/leaflet.js"></script>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.0/dist/leaflet.css" />

    <script src="https://unpkg.com/leaflet-plugin@1.4.0/dist/leaflef-plugin.min.js"></script>
    <script src="../../lib/cftoolkit.min.js"></script>

Most of these plugins are passed as a configuration / option for the leaflet map object at creation time. In order to do this through the ChartFactor Geo Map, the leafletMapOptions property must be used:

1
2
3
4
5
chart.set('leafletMapOptions', {
    crs: crs,
    maxZoom: window.crs.options.resolutions.length,
    // other options
})

Shape layers (GeoJSON)

Similar to the Vector Map, data can be represented in represented through shapes. The following example renders a Geo Map with shapes for the different drop-off community areas in Chicago (taken from the Chicago Taxi Trips dataset) showing the fare and count for each area:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    var metrics = [
        cf.Metric('fare', 'sum'),
        cf.Metric('count')
    ];

    let color = cf.Color();

    color.metric(metrics[0]);
    color.palette(['#ccece6', '#99d8c9', '#66c2a4', '#41ae76', '#238b45', '#006d2c', '#00441b']);

    let myChart = cf.provider('Elasticsearch')
        .source('chicago_taxi_trips')
        .rows('dropoff_community_area_desc')
        .metrics(metrics)
        .set('shape', 'https://chartfactor.com/resources/chicago.json')
        .set('color', color)
        .set("zoom", 10)
        .set('center', [41.877741, -87.637939])
        .graph('Geo Map')
        .element('chart')
        .execute();

The two requirements to render this visualization are:

  • Provide the attribute to group by using the rows() function. In the example above, the attribute is dropoff_community_area_desc.
  • Provide the shape file using the shape or shapes options explained below. .set('shape', chicagoShape) function. The shape file must be in GeoJSON format.

Note that the default limit for this query is 1000. Use the .limit(x) function to change this default.

Each shape will be colored depending on the color metric values (e.g. fare). The color metric is the metric defined in the cf.Color() object or the first metric in the metrics array if no Color object is defined.

The previous code will render a Geo Map like the one below: geo map shapes

Adding shape layers

Shapes can be specified in three ways, two of them are used as options at declaration time. The other one can be used to add new shapes when the map is executed.

shape

This option takes a resouce URL or a GeoJSON object.

1
2
3
  .set('shape', 'https://chartfactor.com/resources/us-states.json')
  // or
  .set('shape', usStatesShapes)

shapes

This is a recommended option because it is more flexible and it allows more than one shape when needed. The option takes an array of shapes configurations:

1
2
3
4
5
6
7
  .set('shapes', [
      {
          name: 'State Name',
          shape: 'https://chartfactor.com/resources/us-states.json',
          options: { /* shape options described below */ },
      }
  ])

The name property of the shape should match the label of the field specified in the rows query function of the map. This allows the map to synchronize the different geometric objects in the shape file and the field values in the data. The example above enables users to select the different geometric objects (e.g. States) available in the shape file and trigger "State Name" filters.

addShapeLayer

The two previous options should only be used at declaration time. After the map has been executed the first time (built and rendered), if we use the options above again, they will cause a re-render of the map (not a re-query). To avoid this behavior, we can inject the shape to the map directly.

Let's say that we set the US map shape using either the shape or shapes options described above to render an initial map like the following:

us-states

After the map is rendered, to add the Texas shape with county geometries, we would do the following:

1
2
3
4
5
6
7
  const map = cf.getVisualization('geo-map-div-chart')

  map.get('addShapeLayer')({
      name: 'County Name',
      shape: 'https://chartfactor.com/resources/texas.json',
      options: { /* Any shape option */ }
  })

Another variant is that instead of getting the addShapeLayer function, we can invoke it by using directly the visual object:

1
2
3
4
5
6
7
  const mapvisual = cf.getVisualization('geo-map-div-chart').get('visual')

  mapvisual.addShapeLayer({
      name: 'County Name',
      shape: 'https://chartfactor.com/resources/texas.json',
      options: { /* Any shape option */ }
  })

Any of the two methods described above will have the same result: The state shape for Texas rendered over the US shape.

us-states

This function can go even further. It allows to also pass custom data so it can be displayed in the shape. The data passed must follow the right data format. Let's say we have an array of data like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const data [
    { 
       group: ['Cars'],
       current: { 
           count: 10,
           metrics: { score: { sum: 100 }}
       }
    },
    ...
]

We could render the shape with data like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  const mapvisual = cf.getVisualization('geo-map-div-chart').get('visual')
  const config = {
      name: 'Cars',
      shape: 'https://chartfactor.com/resources/texas.json',
      options: {
          dataField: { name: Cars, label: Cars, type: 'ATTRIBUTE' },
          shapeMetrics: [cf.Metric('score').label('Score')]
      }
  }

  mapvisual.addShapeLayer(config, data)

A final note about this function is that it returns a promise. This is because the geojson file with the shape data needs to be fetched the first time. This may be useful if we need to do some actions after the shape has been rendered in the map:

1
2
3
4
  mapvisual.addShapeLayer(config, data).then(() => {
        // The shape was fetched and loaded in the map at this point
        // do something else here
  })

Shape layer options

Shape layers can be customized to render shapes with specific colors, borders and others. Three main groups of configuration options exist for shape layers, discussed below:

  • Shape-specific options
  • Interaction options
  • Data options
Shape-specific options
  • featureProperty: This option represents the name of the geojson's feature property that matches the name of the shape. It defaults to "name".
Shape-specific options when not hovered
  • shapeFillColor: The color of the shape when it doesn't contain data. By default is white.
  • shapeOpacity: Applies some opacity to the shape. 0 is completely transparent and 1 is completely opaque.
  • shapeBorderColor: The borders of the shape. Uses a dark color by default.
Shape-specific options when hovered
  • shapeBorderColorHl: The borders of the shape when it is hovered. By default has the same value as shapeBorderColor.
  • shapeOpacityHl: Shape opacity when hovered. By default same as shapeOpacity
  • shapeFillColorHl: The color of the shape when hovered. By default, the shape keeps its current color.
  • shapeBorderWeightHl: The thickness of the border when hovered.

It is also possible to define shape styles using the style property as specified in the Leaflet example for GeoJSON. Let's see the example below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// If you are using the "shapes" property (recommended)
.set("shapes", [{
      name: 'State Name',
      shape: "https://chartfactor.com/resources/us-states.json",
      options:{
        allowMultiSelect: true,
        allowClick: true  
      },
      style:{
        color: "#999", // Equivalent to shapeBorderColor
        weight: 2, // Border weight
        opacity: 1, // Border opacity
        fillOpacity: 0.8, // Equivalent to shapeOpacity
        fillColor: "#B0DE5C" // Equivalent to shapeFillColor
      }
  }])

If you are using the "shape" property to configure a single shape, then use shapeStyle instead of style. Example below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// If you are using the "shape" property to configure a single shape
.set('shape', 'https://chartfactor.com/resources/us-states.json')
// use the "shapeStyle" setting to set style properties
.set("shapeStyle", {
        color: "#999", // Equivalent to shapeBorderColor
        weight: 2, // Border weight
        opacity: 1, // Border opacity
        fillOpacity: 0.8, // Equivalent to shapeOpacity
        fillColor: "#B0DE5C" // Equivalent to shapeFillColor
  })

Either the first or the second code block above renders the shape in Geo Map with the following look:

eo-map-shape-style

There are two things that you have to keep in mind:

  • The described properties defined within the style object override the equivalent properties defined within the options object
  • Data-driven shape coloring (see Data options below) has prescedence over both style fillColor and options shapeFillColor properties
Interaction options
  • allowClick: True by default. Enables or disables the ability to trigger a filter when clicking a shape.
  • allowHover: True by default. Allows to display tooltips when the mouse goes over the shape.
  • allowMultiSelect: False by default. Enables or disables the ability to select multiples shapes.
Data options

These properties can be specified when you use an external data query and then you add the shape to the map using the addShapeLayer function. Refer to Simple multigroup queries for how to perform data queries.

  • dataField: A json representation of the field used by the shape in order to trigger valid filters.
  • shapeMetrics: An array of Metric objects used only with custom data. These metrics must match the names specified in the custom data. The shapeMetrics array is best obtained after performing a ChartFactor query using its Aktive object since each metric is enriched with field type information used to render tooltips. The code example below illustrates how to obtain the shapeMetrics array.
  • color: This is a color object (cf.Color). Its metric must be the first one used in the shapeMetrics configuration.
  • rows: An array of row objects that match the rows used in the external data query. The row properties specified will render in the shape's tooltip. See the example below:

Note that this example is only to show that you can add layers dynamically. You would not do this just to show data on a map as this is easily done defining the query and shape layers declaratively.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// Metadata configuration
metadata: {
  company_location: {
    fields: {
      id: {
          "label": "Identification"
      },
      name: {
          "label": "Name"
      }
    }
  }
}

// Define metric
let metric = new cf.Metric("metric", 'sum').label('Metric').hideFunction();

// Define the shape color
let palette =   ["#006d2c", "#2ca25f", "#66c2a4", "#b2e2e2", "#edf8fb"];
const shapeColor = cf.Color().palette(palette).metric(metric);
shapeColor.autoRange({ dynamic: true });

// Defining the map and performing the external query
cf.provider("Elasticsearch").source('company_location')
  .limit(1000)
  .graph("Geo Map")
  .set("center", [54.4664606400153,-101.35230305777141])
  .set('zoomDelta', 0.2)
  .set('zoomSnap', 0.2)
  .set("zoom", 3.8000000000000003)
  .element('element-id-of-viz')
  .execute()
  .then(() => {   
    // When the Geo Map visualization execution it's done , then performs the external query
    cf.provider("Elasticsearch").source('company_location')
      .limit(1000)
      // Specify the main field name (state), and the additionals ones we want to show in tooltip
        .rows('state',"name", "id")
      .metrics(metric)
      .on('execute:stop', (event) => {

        // Getting the query rows to pass to shape options
        const rows = cf.getVisualization(event.element).get().rows;

        // Getting the metrics json array to pass to shape options
        // The geo-map uses the "type" property included in each metric json
        // object to properly format metric values when rendering tooltips.
        const metricsJson = cf.getVisualization(event.element).get().config.metrics;

        let shape = {
            "name": "State",
            "shape": "https://chartfactor.com/resources/us-states.json",
            "options": {
                "featureProperty": "name",                
                "allowClick": false,
                "shapeOpacity": 2,
                "shapeOpacityHl": 1,
                "shapeBorderColor": "black",
                "shapeBorderWeightHl": 4,
                "color": shapeColor,
                "shapeMetrics": [cf.Metric().fromJSON(metricsJson[0])],
                "dataField": {
                    "name": "state",
                    "type": "ATTRIBUTE",
                    "originName": "state",
                    "originalType": "keyword",
                    "label": "State",
                    "keyword": true
                },
                "rows": rows
            }
        };

        const viz = cf.getVisualization('element-id-of-viz').get('visual');

        viz.removeShapeLayer(shape.name);
        viz.addShapeLayer(shape, event.data);

      })
      .execute();
  });

The previous code will render a Geo Map that will show the tooltips with the additional name and id rows contained inside.

geo-map-data-options-ext-layer

Removing shape layers

Shape layers can be removed from the map at any moment like this:

1
  mapvisual.removeShapeLayer(shapeName);

Markers

ChartFactor supports Geo Maps that render markers in specific latitudes and longitudes. Two types of queries are supported:

  • Agregate metrics arround a point (lat, lng) and an attribute. In this case, one marker represents one or many events happening on the marker location and the attribute value. Use the rows() function to specify the latitute, longitude, and additional attribute to group by. Note that the default limit for this query is 1000. Use the .limit(x) function to change this default.

  • Geohash queries that requires a location point. You can learn more about this type of query here. The markers shown for this query represent the center of an area. The area size is determined by a precision value.

  • Raw query that includes latitude, longitude, and any other fields. In this case, each marker represents a single event. Multiple events on the same location will render multiple markers. Use the fields() function to specify all the attributes the marker should include in its tooltip. Note that the default limit for this query is 100. Use the .limit(x) function to change this default.

In the example below, we have Geo Map Markers from the Chicago Taxi Trip datasource with an aggregate query with the pickup_latitude and pickup_longitude, also gruped by company and colored by the fare metric.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    var metrics = [
        cf.Metric('fare', 'sum'),
        cf.Metric('count')
    ];

    let myChart = cf.provider('Elasticsearch')
        .source('chicago_taxi_trips')
        .rows('pickup_latitude', 'pickup_longitude', 'company')
        .metrics(metrics)
        .set('zoom', 12)
        .set('center', [42.149151, -87.807541])
        .set('min', 0)
        .set('max', 20)
        .limit(100)  // 1000 by default 
        .graph('Geo Map')
        .element('chart')
        .execute();

The previous code will render a Geo Map like the one below: geo map markers

Custom markers

To define a Geo Map custom marker you have to define a function that accepts the object representing the data for that marker and produces a valid html code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let markerHtml = (value) => {
    return `<div style="
        background-color: blue;
        width: 30px;
        height: 30px;
        display: block;
        left: -15px;
        top: -12px;
        position: relative;
        transform: rotate(45deg);
        border: 1px solid #FFFFFF;
  "></div>`;
};

let myChart = cf.provider('Elasticsearch')
    .source('chicago_taxi_trips')
    .fields('pickup_latitude', 'pickup_longitude', 'company')
    .set('zoom', 12)
    .set('center', [41.877741, -87.637939])
    .set('minZoom', 3)
    .set('markerHtml', markerHtml)
    .set('layersControl', true)
    .set('ignoreCords', [0,0])
    .set('legend', 'right')
    .graph('Geo Map')
    .element('chart')
    .execute();

The previous code will render a Geo Map like the one below: geo map markers

Marker events

Normally, hovering a pin pin will show the latitude and longitude information for the pin, and if clicked a filter with these two values will be applied. This behaviour can be removed by setting disableMarkerEvents to true:

1
.set('disableMarkerEvents', true)

Fixed markers

We can define markers that are independent from the data. This is usefull to display for example fixed locations that are always going to be visible and are not affected by filters. Since these markers are not part of the data queried but more like static data, the won't trigger any filters when clicked.

1
2
3
4
5
6
7
  .set('fixedMarkers', [
      {
          pos: [40.16827581021202, -75.54729095036286],
          label: '<div style="color: blue; font-size: 20px">My custom fixed pin</div>',
          color: 'red'
      }
  ])

As seen in the example above, the fixedMarkers option takes an array of objects, where each object is a fixed marker configuration. The label of the marker could be either a simple text or a string representing html code. The color used for the pin is red. This will display the following:

attribution

Markers within shapes

Map markers can be rendered together with geo shapes using the .fields() query function with latitude and longitude to obtain the marker points, along with the shapes setting as shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
map.fields('latitude', 'longitude')
map.set('shapes',
    [{
        'name': 'Counties',
        'shape': 'https://shape-url',
        'style': {
            "color": "blue",
            "weight": 2,
            "opacity": 0.65,
            "fillOpacity": .2
        }
    }]      
)

We can see the markers and the geoJSON shape specified:

geoJSONLayers

Geohash clusters

Geohash clusters represent the center of areas for an aggregated geohash query result. This type of geo visualization can be extremely powerful specially when dealing with big data.

As described in the geoqueries documentation, a Geo Map using geohash queries can be defined like this:

1
2
3
4
5
6
const map = cf.provider('Elasticsearch')
              .source(source)
              .location('people-location')
              .precision(4)
              .element('map-chart')
map.execute()

Let's imagine the above code is aggregating the location of people by area. It may render something like this:

When hovering one of the clusters we can see a rectangle that defines the area where this cluster is the center. Imagine using a raw query instead of using a geohash query. The amount of raw markers and clusters could potentially tear down the map since we can see there are over half million people and that is only in the visible portion of the picture.

Now let's add an additional configuration to the map before executing it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
map.set('precisionLevels', {
    raw: { zoom: 18 },
    levels: [
    { zoom: 9, precision: 4 },
    { zoom: 12, precision: 5 },
    { zoom: 15, precision: 6 },
    { zoom: 17, precision: 7 }
    ]
})

map.execute()

The above configuration takes care of the magic: It allows to automatically trigger a new geohash query with a new level of precision every time a specific zoom level is reached by using the bounding box filter. This will allow to zoom-in step by step until it hits the last zoom level represented by the raw property. This is the level the Geo Map considers safe enough to do a raw query. By default if no fields were configured for the raw level query, all fields are included and shown on the markers tooltips. Use the fields property to query only some of them:

1
2
3
4
5
6
7
map.set('precisionLevels', {
    raw: { 
        zoom: 18,
        fields: ['latitude', 'longitude', 'street_name', 'person_name'] 
      },
    levels: [ ... ]
})

Here is an example of a map changing the query at different precision levels:

Cluster and marker color

You can color clusters depending on their value. You can define your custom color ranges or take advantage of our automatic color ranges by simply providing a color palette. See the Color Range documentation. After defining your color instance, just set it by calling .set('color', colorInstance) in your Map configuration. See the following example:

1
2
3
4
5
6
7
// color configuration
let color = cf.Color();
color.palette(['#ffffb2','#fed976','#feb24c','#fd8d3c','#fc4e2a','#e31a1c','#b10026'].reverse());
color.autoRange({ dynamic: false });
//...
.set("color", color)
//...

The previous code will render the color of the clusters like the image below.

geo map marker icons html

You can also color the clusters of your map when adding cluster layers dynamically, that is when using an external query to obtain the data and then dynamically calling the addMarkerLayer function. Take a look at the example below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Using an external query and the addMarkerLayer function
cf.provider('Elasticsearch').source('source_index_name')
      .limit(1000)
        .fields(
        cf.Field('latitude', 'Latitude'),
                cf.Field('longitude', 'Longitude'),
                cf.Field('name', 'Company')
      )
      .metrics(metrics)
      .on('execute:stop', (event) => {

          const color = cf.Color();
          color.range([
              {min: 0, max: 10, color: 'red'},
              {min: 10, max: 20, color: 'gray'},
              {min: 20, max: 30, color: 'blue'},
              {min: 30, max: 40, color: 'yellow'},
              {min: 40, max: 50, color: '#006d2c'},
              {min: 50, max: 60, color: '#2ca25f'},
              {min: 60, max: 70, color: '#66c2a4'},
              {min: 70, max: 100, color: '#b2e2e2'},
          ]);

          const config = {
              clusterColor: color
          };

          const viz = cf.getVisualization('map-viz-id').get('visual');

          viz.removeMarkerLayer('source_index_name');
          viz.addMarkerLayer('source_index_name', event.data, config);

      })
      .execute();

Cluster and marker colors support the following properties:

  • color: applies the provided color definition to markers and clusters. For markers, it uses the first color in the palette. For clusters, it uses all colors in the palette to color individual clusters depending on their value. The color property is optional. It defaults to a standard map color definition.
  • clusterColor: applies the provided color definition to clusters, overriding the value provided in the color property if any. This property is useful when you need to provide separate color definitions for clusters and markers. For example, you may want to color clusters according to their value and markers with color blue. In this case, you would provide a color definition with a single color palette to the color property (for markers) and a color definition with a multi-color palette to the clusterColor property (for clusters). The clusterColor property is optional. It defaults to the value of the color property if provided, otherwise, to a standard map color definition.

Cluster icons

The geohashMarkerHtml setting allows you to add icons to the standard representation of geohash clusters. This is useful to distinguish what clusters belong to what datasets when rendering multiple datasets on the Geo Map. The following example renders a Geo Map with marker icons representing several US hospitals and their number of beds.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
let metric = new cf.Metric('count');
let geohashIconCustomMarker = (value) => {
  return `<img
          class="icon-image"
          style="width: 34px !important; height: 34px !important;"
          src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAABmJLR0QA/wD/AP+gvaeTAAAE5klEQVRYhe2YW2xURRjH/9+cy+52r0CvFuhVKhYM8UGUQLzEoBLCg8TEaCQxRh+MMSY8+uCLifKivhj1TVATjTGNRg1GiSBRAyEiSImF0rvtblvavffcZj4fKt3tUtpuIUjC/l/OmTPz/87vzHwzc84hZsatJ/F/AyysClY5qmCVowpWOapglaMKVjm6HbCUx1aq9CLLK7X52cNyIunXi8IsL/0o//xEDhzj1DAA6H6xuk3r2K1t2Sdq74achDuBQCenD1PsSc7+RqHtYAdkLhL1unpLxc9aH221Dz7mnfkUuYQWq9GqG7VwhJOX3OMHrPfvcbpeYCvNyc/hDCN/El4C2Z8B5mTX4pFpxe9b8nyX/eUz8CwRXmWs79Bi1QAViDPT7vAFOT0u1rQbO8Ki+RVOf0fBB8Ae9HqAafU+zp+g4LYbiSX7jtgHH4fyjLV3Gus7QLRgM2+s3+nrpqBp7tpCPgaZMJthX6TGAzz1mah9Ff5NUBaEv8S4kkHkmSnni6ehPGNtu9F017WYAOgNLWZrJ+ds9/cegME27B6AOfE26atABicOQE5fbVwJlnv0Tc5PiqqIsa6jCFaxY6lciu0ZqMJ00xtatFiNGkmqRHquKbzLbPepoRcpuhtGw5VJWvQ8S0LI7q/cX95S8TPQ/dqGXcbDb8jTHwPQ17ZDCPYcLz4kpxMqM41CPpAIRbVVNXpdE/kCxroNMjnh9YybdZFCXGeQQg9ysgvyIFW/BF978U2XyC3v13ecw/sBkGGy6wIMzYB0IUTgvp3eSK871g8pr+knodeuM5o3WqePsnL8T90Lbd6IU3QP1e6HCJT6FsHi1PDMu+1E7Ou8X4RiKpe2u39n15mlJNOvculSjxAUNImgMhauBCZfFYjYypl7NotIUXaTBr0eZFD4EYrthV4zV7PYIMqebyEdrbFVBGPeaJ872jfLBIBdZ+58npTijIWQT9tQJyI+zrtqIqMu5yEVAMw4KMZiSUYD1b8Oo7EkzGJYbGcAQLHTf84b61+kZakxa8uehCSIuojWXms8FOWUpSay8BsA4CmVtUEQYT/nT/HAsxR5gmJ7i9NrYSxODan4Wc6OAfASg+BlbWRXRYGKp1U8LRqixrYWvSYEQF4Yd08NQTEAMjVt0x5j5wcUrCuxluaWSpxzv39N9h1ZCUdJ6IChtVaL1moRNNVoSsbT+sZ6CLK/OQtZuCnFmv3P/0Sr266JpQaPW4d2wcmKyBqjsVUEo+w5Kj3ljvSyYy0XR5BojOlt1Qj5eCwtR5Mqkfmve6pM49EO2Tshz8fnOeo2+1/+A6IwdAUstpLWex2cG9fvaDGbN83ubzI1KRPDMj3FdumKtwBPNCBa11CVqS7n1EiSs/bVbchvGDva3GMX2Zm3rJh7D+lbnpsrFgC9kx9yblyEomZLJwAwO71nvPHhJWnI1KgmTAGDbdf7axTeYonIluse76WGKA9OFV+Xf3+9MJbq/QGAXt80+yLg9HcvzaRpFKiCrql/kkvSF5F5PFK6D/L0QHGxgMWZMQAiEAag8hlvbABLSkrOZpYPVGQsXcPJHy0uFrZqijQCmE1tOTkK3NT/XmLt1uJiIeXVyAlODpE7DS/NZjW04E3FatpO4YYFsG4p3Q4fZDdOFaxyVMEqRxWsclTBKkcVrHJUwSpHtyjWv3ZmSTCYpkOiAAAAAElFTkSuQmCC"
          />`;
};

cf.provider('Elastic')
.graph('Geo Map')
.source('chicago_taxi_trips')
.limit(10000)
.metrics(metric)
.location('dropoff_location')
.precision(3)
.set('precisionLevels', {
  raw: { zoom: 16, fields: ['dropoff_latitude', 'dropoff_longitude'] },
  levels: [
    { zoom: 6, precision: 4 },         
    { zoom: 10, precision: 5 },
    { zoom: 13, precision: 8 },
    { zoom: 15, precision: 11 },
  ]
})
.set('geohashMarkerHtml', geohashIconCustomMarker)    
.set('tileLayers', [
  {
      Base: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
      attribution: `Map data &copy; 
      <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors,
    <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>`
  }
])
.set("center", [41.76055653463573,-87.56172180175781])
.set("zoom", 11)
.element('v1')
.execute();

The important parts to render this visualization are:

  • Define a function that contains the marker icon to render. In the example above, we define the function geohashIconCustomMarker. Note that the defined function must have a parameter tu set the value inside the circle.
  • Provide the "location" field name by using the location() function. In the example above, the location field name is hospital_location.
  • Provide the default precision level by using the precision() function. In the example above, it's configured with the value of 3.
  • Provide the attribute precisionLevels by using the set() function, in order to set the diferents precisions levels according to the zoom levels, together the max zoom value and the fields names corresponding to the latitude and longitude values. In the example above, the fields names are latitude, longitude.
  • Provide the attribute geohashMarkerHtml by using the set() function. In the example above, it is set with the geohashIconCustomMarker function noted in the first point.

The previous code renders a Geo Map like the one below.

geo map marker icons html

Heat Map

This mode allows to render data with latitude and longitude information (geohash and raw) as a heat map instead of markers. For that we only need to add the following to the map aql:

1
2
3
...
.set('useHeatMap', true)
...

And the default result will be something like this:

Since this mode uses the leaflet's heatmap.js plugin, we can specify custom configuration described in the plugin's documentation by passing an object instead of a boolean:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.set('useHeatMap', {
  maxOpacity: 0.8,
  baseRadius: 45,
  gradient: {
    ".5": "blue",
    ".8": "red",
    ".95": "white",
  },
  valueField: 'count',
  rescale: { min: 40, max: 100 },
})

The above will increase the radius of the heatmap area and change the color of it:

All the properties accepted in the configuration are described in their documentation. The only property that is specific from ChartFactor is baseRadius, which acts like the property radius but is used along with the zoom level value to calculate the new size of the heatmap when the user performs a zoom in or out. The value of baseRadius is 40 by default.

Advanced settings

The advanced settings below allow you to render heat maps with sparse data that would otherwise not be visible.

  • rescale: This is an object with min and max values. Example:

    1
    { min: 40, max: 100 }
    

    When this setting is configured, two out-of-the-box properties are added to the data collection elements: __cf_cluster_count_percent__ and __cf_rescale__. The first one contains the normalized value of the cluster count from 0 to 1. The second one contains this normalized value but rescaled within the range specified by rescale.min and rescale.max. The rescale setting works together with valueField setting (below).

  • valueField: This setting specifies the field that drives the heatmap intensity (e.g. __cf_cluster_count_percent__ or __cf_rescale__)

As an example, imagine that your geohash query returns data as shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    const data = [
      {
          "geohash": "dp3",
          "count": 9680
      },
      {
          "geohash": "9q8",
          "count": 10
      }
    ]

Because the count difference between the two elements in the array above is too big, only one cluster will be visible in your heatmap as shown in the image bellow:

Now let's apply the following configuration:

1
2
3
4
5
.set("useHeatMap", {
    "rescale": { min: 40, max: 100 },
    "valueField": "__cf_rescale__" 
    // ...
})

With the configuration above, the __cf_rescale__ field will now drive the heat map intensity as specified by the valueField setting. This field will be populated with values from 40 to 100 as specified by the the rescale property. Your heat map data will look as the array shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[
    {
        // ...
        "count": 9680,
        "__cf_cluster_count_percent__": 1,
        "__cf_rescale__": 100
    },
    {
        // ...
        "count": 10,
        "__cf_cluster_count_percent__": 0,
        "__cf_rescale__": 40
    }
]

And your heatmap will look as the image below.

Heat Map and precision levels

The useHeatMap configuration works together with the precisionLevels configuration (when defined). This allows you to have a Heat Map of higher and higher precision as users zoom into the map. See the Geohash clusters section for more details on precisionLevels.

Additionally, the useHeatMap configuration supports the switchToMarkersAtRaw setting. This property is false by default. When true, the Heat Map automatically switches to a Markers layer when executing raw queries. This would occur when users zoom-in enough to reach the raw query point according to the precisionLevels configuration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.set('useHeatMap',   {
    maxOpacity: 0.9,
    baseRadius: 60,
    gradient: {
        '.3': '#4484CD',
        '.65': '#D0E0F2',
        '.95': 'white',
    },
    switchToMarkersAtRaw: true
})

The code above would render a map that will change to markers layers when the zoom level is high enough to execute a raw query. See the following animation:

External data layers

The Geo Map visualization also allows to render external data in the map by using an exposed utility called addMarkerLayer. This functionality may seem similar to the fixed markers since both allow basically to show markers using the latitude and longitude information passed in the data for each marker. The main features of addMarkerLayer are:

  1. You can specify multiple data layers
  2. The addMarkerLayer utility allows geohash queries in addition to raw data queries
  3. Precision levels can also be used with this method

Let's take a look at some examples:

Rendering raw markers from static data

1
2
3
4
5
6
7
8
9
const map = cf.getVisualization('map-chart')
const data = [
  { latitude: 42.149151, longitude: -87.807541 },
  { latitude: 42.239151, longitude: -86.207622 },
  ...
]
const options = {}

map.get('addMarkerLayer')('Layer Name', data, options)

Rendering raw markers from a query

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cf.provider('Elasticsearch')
.source(source)
.fields('latitude', 'longitude')
.element('dummy')
.on('execute:stop', (event) => {
    if (!event.error) {
        map.get('addMarkerLayer')(source, event.data, { color: red });
    }
})
.execute()

Rendering markers from a geohash query

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
cf.provider('Elasticsearch')
.source(source)
.location('location_field')
.precision(3)
.element('dummy')
.on('execute:stop', (event) => {
    if (!event.error) {
        map.get('addMarkerLayer')(source, event.data, { color: blue });
    }
})
.execute()

The addMarkerLayer takes 3 parameters: The first one is the source name, the second one is the data, and the third is a configuration object with any of the following options:

  • color: The color of the markers, by default is #034e7b.
  • precisionLevels: If the precisionLevels configuration is specified to the query, it also has to be passed here.
  • maxSpiderifyMarkers: Same as maxSpiderifyMarkers but for external layers.
  • fields: An array of field objects. If we are using a raw query and this configuration is not set, no tooltips will be shown. A field object example: { name: 'field-name', label: 'Field Label', type: 'NUMBER'}
  • geoHashMarkerClickEvent: A function that will be fired when the user clicks a geohash marker. It takes an event as parameter with the information of the marker.
  • allowClickInRawMarker: True by default. It will enable or disable triggering filter events when clicking a raw marker (a pin).
  • marker: An object with one property: icon where the value is a function that returns a marker html. See custom markers.

Similar to addMarkerLayer we can use addHeatMapData to render similar data in heatmap mode:

Rendering a heatmap from a query

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cf.provider('Elasticsearch')
.source(source)
.fields('latitude', 'longitude')
.element('dummy')
.on('execute:stop', (event) => {
        if (!event.error) {
        map.get('addHeatMapData')(source, event.data, { baseRadius: 15 });
    }
})
.execute()

As mentioned, the same zoom functionality achieved using the precisionLevels configuration for geohashes can be obtained for external data. However this requires the usage of additional utilities with the combination of the mapmove and mapzoom events.

Similar to addMarkerLayer all the map utilities can be accessed through the map instance:

1
2
3
4
const map = cf.getVisualization('map-chart')
const utility = map.get('utilityName') 

utility(params)

The available utilities are:

getGeoHashPrecisionByZoomLevel(levels, currentZoom)

Given the an array of levels (ie: precisionLevels.levels) and a zoom level, it returns the proper precision value.

changeGeoHashPrecisionLevel(aktive, zoomLevel)

Changes the precision level of an existing query object (aktive instance) by triggering a new query using a bounding box filter at the specified zoom level. The query object must have a precisionLevels configured.

changeMapBoundariesFilter(aktive, event)

This function is meant to be used within a callback subscribed to the mapmove event. It takes the query object used to bring the data and the nativeData property from the mapmove event. The function will trigger a query using the new boundaries of the map's viewport when the user is panning the map.

isGeoHashData(aktive), isRawData(aktive), isAggregatedData(aktive)

Any of these three functions allows to determine the current state of the query object (aktive instance). They can be also applicable to the map instance.

For a better understanding on how to integrate these utilities take a look at this demo.

Proportional circles

With this option, data can be represented through proportional circles based on the value of a metric. The following example renders a Geo Map with proportional circles representing several US cities and their populations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const metric = cf.Metric('population', 'sum').label('Population');
const circlesOptions = {
    name: 'City',
    color: 'red',
    metrics: [metric]
}
cf.provider('Elastic')
  .source('usa_cities_populations')
  .rows('latitude', 'longitude', 'city.keyword')
  .metrics(metric)
  .graph('Geo Map')
  .set('proportionalCircles', circlesOptions)
  .set('tileLayers', [
      {
          Base: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
          attribution: `Map data &copy; 
          <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors,
        <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>`
      }
])
.set('center', [35.199165, -101.845276])
.set('zoom', 2)
.element('element1')

The requirements to render this visualization are:

  • Define at least one metric. In the example above, we define the metric population.
  • Define an object with the Proportional Circles options. In the example above, the object is circlesOptions.
  • Provide the three attributes to group by using the rows() function. In the example above, the attributes are latitude, longitude, city.keyword.
  • Provide the Proportional Circles options using the .set('proportionalCircles', circlesOptions) function.

The previous code renders a Geo Map like the one below. Note how circles are colored depending on the color defined on circlesOptions object.

geo map proportional circles

Options

The proportional circles options as you can see in the example above is an object that can contains several properties.

  • name An string that represents the label of the main field defined after the latitude and longitude fields that will be use to show in the circles tooltips.
  • showLocation Toggles the visibility of the "Position" in the tooltip. Go to the Custom Configuration for Maps section to see how it works.
  • allowClick True by default. Enables or disables the ability to trigger a filter when clicking a circle.
  • color An string that specify the circles color.
  • metrics An array of metric objects. The first metric will be used to calculate the circles radius. All metrics will be rendered in the circles tooltip.
  • rows An array of row objects. This property only applies when you are using an external data layer query. It works just like the rows property specified in Shapes Data Options section.

addProportionalCircles

Proportional circles can be added as external data as well using the function addProportionalCircles in a similar fashion that addShapeLayer is used. The important thing to have in mind here is that the data must contain latitude and longitude information. This will not be a problem if the data was obtained using a cf aktive instance to query the data. However, if the data was extracted from somewhere else, then it must have the specific format as described below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const data = [
    // Latitud 1st position
    // Longitude 2nd position
    // Additional group 3rd position
    { group: ["42.149151", "-87.807541", "Chicago"],
        current: { metrics: { Population: { 2706000 } } } }
    ....
]
const map = cf.getVisualization('geo-map-div-chart')

map.get('addProportionalCircles')(data, {
    name: 'City',
    color: 'red',
    metrics: [metric]
})

Options parameters are the same as specified in the previous example.

Legend

We can position the legend in different ways. Available positions are:

  • right
  • left
  • top-right
  • top-left
  • bottom-right
  • bottom-left

For more information you can read the Legend section.

Custom configurations

enableZoom

Enables or disables the zoom and pan on the map. Example: .set('enableZoom', false). Zoom is enabled by default.

zoom

Sets the initial zoom value of the map. Example: .set('zoom', 0.5). It is 1 by default.

center

Sets the initial center of the map. Example: .set('center', [0,0]). It is null by default which translates to the center of the shape.

showLocation

Toggles the visibility of the "Position" in the tooltip. Markers, shapes and or circles that contain information about the latitude and longitude, will display these values as a "Position" in the tooltip. But for security reasons, this information may be hidden in some cases. True by default.

layersControl

Turns the layer control on or off. Example: .set('layersControl', false). It is true by default.

ignoreCords

Ignores markers when they match a specified location. Example: .set('ignoreCords', [0, 0]). None by default. This is useful when the dataset includes invalid points as 0,0 for example.

maxZoom and minZoom

Sets the max and min zoom levels. Example: .set('maxZoom', 10). Default maxZoom is 18 and default minZoom is 0.

zoomSnap

Forces the map's zoom level to always be a multiple of this value. Its default value is 1. The zoom level snaps to the nearest integer; lower values (e.g. 0.5 or 0.1) allow for greater granularity. A value of 0 means the zoom level will not be snapped after fitBounds or a pinch-zoom.

zoomDelta

Controls how much the map's zoom level will change after zomming in or zooming out by pressing + or - on the keyboard, or using the zoom controls. Its default value is 1. Values smaller than 1 (e.g. 0.5) allow for greater granularity.

fitBounds

Fits a map that contains geographical bounds with the maximum zoom level possible. Valid values are true and false. It is false by default. (Geo Map with GeoJSON shapes only)

markerHtml

This configuration allows you to provide a function that receives an object and returns a string that represent a valid html code representing the marker.

maxSpiderifyMarkers

Allows to define the maximum number of markers that can be spiderified when clicking a cluster. It's default value is 500. This is useful when a large number of markers have the same lat/long:

1
.set('maxSpiderifyMarkers', 100)

The above configuration will render clusters of up to 100 underlying markers using the spider effect. Beyond that, it will display a table with the list:

tileLayers

Specifies one or many custom tile layer servers to be used in the map. By default the Geo Map uses Wikimedia maps as tile server.

To use a different one for example OpenStreetMap add the following:

1
2
3
.set('tileLayers', {
    Base: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
})
WMS (Web Map Services) can also be used as tile layers. When using a WMS we need to specify the type and the layers properties:
1
2
3
4
5
.set('tileLayers', {
    Base: 'https://ows.terrestris.de/osm/service',
    type: 'wms',
    layers: 'OSM-WMS', // 'TOPO-WMS', 'SRTM30-Colored-Hillshade'
})
Since most tile servers require attribution, in the above configuration we can add the attribution property so it will displayed at the bottom right corner of the map.
1
2
3
4
5
.set('tileLayers', {
    Base: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
    attribution:
        'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
})
attribution

It we need to use several layer servers, we'll use an array instead:

1
2
3
4
5
6
.set('tileLayers', [{
    Base: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
}, {
    Base: 'https://ows.terrestris.de/osm/service',
    type: 'wms',
}])

Same as shape layers, tiles can be added after the map has been rendered. We can add a new tile layer like this:

1
2
3
4
5
6
const mapvisual = cf.getVisualization('geo-map-div-chart').get('visual')

mapvisual.addTileLayer({
    Base: 'https://ows.terrestris.de/osm/service',
    type: 'wms'
})
WMS headers

Sometimes, it is necessary to send headers in your WMS service requests so that they can return the response successfully.

Suppose our WMS service requires an authorization header (Bearer Token). To achieve this, you can set the Headers property of your WMS tile configurations as shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
.set('tileLayers', [{
    Base: 'http://localhost:8080/geoserver/topp/wms',
    Headers: getHeaders(),
    type: "wms",
    format: "image/png8",
    updateWhenZooming: false,
    layers: "topp:states",    
    transparent: true,
    tileSize: 512,
    tiled: true,
    maxZoom: 21
},{
    Base: 'http://localhost:8080/geoserver/exmpl_basemap/wms',
    Headers: getHeaders(),
    type: "wms",
    format: "image/png8",
    updateWhenZooming: false,
    layers: "exmpl_basemap:countries",    
    transparent: true,    
    tiled: true
}])

The Headers property can be a function that returns an array of header configurations as shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const getHeaders = () => {
    // Getting the token from localStorage for example
    const authData = JSON.parse(localStorage.getItem('auth'));
    if (authData && authData.access_token) {
        const bearerToken = 'Bearer ' + authData.access_token;
        const headers = [
            { header: 'Authorization', value: bearerToken },
            { header: 'Content-Type', value: 'image/png' },
        ];

        return headers;
    }

    return [];
};

Now, your WMS requests will contain headers that include the Authorization and Content-Type items configured as specified in the function above. See below a screenshot of the browser's network tab showing the request headers after configuring the WMS tiles as specified above.

wms-tile-layers-headers

zoomPosition

The zoomPosition property allows you to set the position of the zoom control. Those positions can have these 6 possible configurations:

  • topleft
  • topright
  • bottomleft
  • bottomright
  • verticalcenterleft
  • verticalcenterright

To change the position of the zoom control use the following:

1
.set("zoomPosition", "topright")

An example of how the zoom control positions would look, can be seen in the following image:

zoomPositions

Another way to accomplish the same is to access the Leaflet control object which accepts different configurations as described in their documentation. To modify the position of the Zoom control we could specify their allowed values: topleft, topright, bottomleft or bottomright once the map has been rendered as shown below:

1
2
3
map.on('execute:stop', () => {
    map.get('visual').map.zoomControl.setPosition('topright');
})

animationDuration

The animationDuration property applies to shape layers and it allows you to set the duration of the animation effect when the map is fitting the bounds depending on the map's zoom and center. For example .set('animationDuration', 1.5) would change the animation duration to 1.5 seconds. By defult the animation duration time is 0.5 seconds.

Listening to custom events

ChartFactor Toolkit Maps have special events to which you can subscribe to obtain current zoom information or the position where the map is located, here are some examples.

1
2
3
4
5
6
7
    myChart.on('mapzoom', (e)=>{
        console.log("Current map zoom: ", e.data);
    });

    myChart.on('mapmove', (e)=>{
        console.log("Current map center: ", e.data);
    });

As specified in the events documentation, you can also subscribe to click events in the Geo Map. The click event is dispatched as soon as users click on a specific layer, be it a marker, a proportional circle, a shape or any other type of layer. See the following example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
  const handleMapClick = (e) => { 
      // Get the current map visualization
      const map = cf.getVisualization(e.chart);
      // Get the leaflet marker icon container div
      const markerIcon = e.nativeData.target._icon;
      // Get the field metadata definitions
      const dataField = e.data.dataField;
      // Get the group name and coordinates of the marker layer
      const group = e.data.group;

      const lat = group[0];
      const lng = group[1];
      const groupName = group[2];

      // Some other logic...
  }

  const map = cf.getVisualization(mapId);
  map.on('click', (e) => handleMapClick(e));

The callback function receives the e object as a parameter with the following properties:

  • name: the current event name
  • chart: the current visualization id
  • data: the current zoom value when the event is mapzoom, the current map center when is mapmove and an object with the layer internal information when it is a click event
  • nativeData: the whole leaflet native information correspondent to the specific type of layer