
CARTO + Angular
Getting started
The CARTO platform is framework-agnostic so you can build applications using CARTO with your framework of choice. When you build an application with CARTO it is common to use deck.gl as the visualization library. This guide describes an approach for integrating CARTO for deck.gl within applications developed using the Angular framework, but you can use CARTO with Angular and other visualization libraries.
We have created an example that you can download and execute in your own local machine, available in the viz-doc repository:
This guide follows a step-by-step approach using the mentioned example as reference. To download and execute the example, start by cloning the repository:
|
|
Then change the current directory to the Angular example for CARTO 2 or CARTO 3:
|
|
Install the packages using the following command:
|
|
Now you are ready to start the application in your local development environment:
|
|
And you will be able to access the application using the following URL:
https://localhost:4200
Creating your application
We are going to start by creating a new Angular application using the Angular CLI. If you haven’t installed it already, you need to execute the following command:
|
|
Then we create our application using the ng new
command:
|
|
The tool will ask us to select some options. We are going to select the following:
- Answer ‘N’ to strict type checking question
- Answer ‘Y’ to use angular routing
- Select ‘CSS’ as the stylesheet format
Now we are going to add the deck.gl packages using the following commands:
|
|
Basic layout
At this point we have the default Angular project structure with the addition of the deck.gl packages. Now we start by defining our application layout. We will have a simple but versatile layout with a header, a left sidebar, and a map area.
The first task we are going to perform is to generate a header component in the src/app/components
folder with this command:
|
|
In the example, the header component includes a logo and a title. It also includes custom CSS styles. Please take a look at the HTML template and component styles definition.
Then we specify the overall layout for the application in the App component HTML template. The layout includes the header and a <router-outlet>
component that will be used to plug the different views into the outlet:
|
|
Home module
We start by adding the module into the modules\home
folder using the CLI:
|
|
Inside this module we are going to have one main component (home
):
|
|
This component will contain the <router-outlet>
component to plug it into the main app component layout and a <div>
element that will contain our map:
|
|
In the <router-outlet>
element we are going to inject a new component (SidebarComponent
) that will contain the user interface elements for this view:
|
|
We are going to leave this component empty for now but we are going to configure routing so this component will be loaded when we request the homepage for the application. We need to open the app-routing.module.ts
file and configure the routes
property to load the HomeComponent and its SidebarComponent child:
|
|
Map component
Now that we have our basic layout and routing, we are going to add a map component to the container that we have defined in the home
component. This component could be potentially shared by different modules/views, so it is going to be included in a new module called shared
:
|
|
Now we create the map
component:
|
|
The HTML template for this component will include a main <div>
element with one inner <div>
for the basemap and a canvas
element for deck.gl:
|
|
The style definition includes the CSS styles to ensure the map takes almost all of the remaining space:
|
|
Finally we must add the code to include our map in the component TypeScript file. There are three main tasks we need to perform:
- Add our map when the component has been initialized (
ngAfterViewInit
). We call a private functionlaunchMap
with the initial center coordinates and zoom level:
|
|
- In the
launchMap
function, create the Mapbox GL JSMapboxMap
object that will manage the basemap specifying the initial center coordinates and zoom level. Here we are using our Positron basemap.
|
|
- In the same function, right after the
MapboxMap
, create the deck.gl instance to draw our layers. We use theonBeforeRender
event handler to synchronize the view state between the deck.gl instance and the basemap using thejumpTo
andredrawMapbox
methods.
|
|
In order to have optimal performance, we run both the Mapbox GL JS map and deck.gl instantiations outside Angular zone, to ensure Angular is not running any change detection code (zone.runOutsideAngular
).
Layers
If we run our application in this moment, we will hopefully see our homepage with an empty sidebar on the left and our Positron basemap in the map area. We are ready to start adding our layers to the map.
The process for adding layers with datasets from the CARTO 3 platform involves these steps:
- Create a connection in the Workspace with access to the datasets
- Set the credentials to connect with the CARTO platform
- Create a layer that uses the connection and the credentials to retrieve the data and render it
- Add the layer to the map
Creating a connection in the workspace
In the Workspace we need to define a connection with access to those datasets and use the connection name when instancing the layers.
Setting the credentials
The Map component is also responsible for setting the credentials we will use for connecting with the CARTO platform and retrieving the data for the layers we are going to add in the next section.
First we need to create a token using the Tokens API with access to the datasets we want to visualize:
|
|
Then we set the credentials by using the setDefaultCredentials
method from the CARTO for deck.gl module.
|
|
Creating the layers
Once we have established the credentials for connecting to the CARTO platform, we can add layers to visualize the datasets.
We are going to define a base class for layers from which all the layers will inherit. The Layer objects will include an id
property in order to identify the layer and a visible
property initialized by default to true
. The base layer implements the show
and hide
methods and defines the getLayer
method that must be implemented by derived layers:
|
|
We want the layers to be provided and injected as dependencies so we define them as classes with the Injectable
decorator. First we create a folder named layers
within the modules/home
folder and then we create a class for each of the layers we want to use in the application.
In the example we have created three layers to showcase some of the options for working with CARTO datasets and tilesets. The Buildings and Railroads layers use the CartoLayer
:
-
BuildingsLayer. This is a polygon layer created from a BigQuery tileset.
-
RailroadsLayer. This is a line layer created from a CARTO dataset.
-
StoresLayer. This is a point layer created from a GeoJSON dataset extracted from the CARTO platform using the
getData
function that uses deck.glGeoJsonLayer
. We could have directly added theCartoLayer
withtype: MAP_TYPES.TABLE
but we wanted to illustrate thegetData
functionality. For CARTO 2 you can follow a similar approach with the SQL API.
The structure for all the layers is similar: we define a class that extends from the base Layer
class. We need to specify a unique id
for the layer and set the visible
property to true
or false
depending on if we want to show the layer or not when the view is loaded. The most important part is creating the deck.gl object that we must return in the getLayer
method. Here we specify the data source for the layer, the id
, and visible
properties and the styling properties like getFillColor
or getLineColor
. Below you can see the implementation for the getLayer
method in the RailroadsLayer
:
|
|
Adding the layers to the map
Now that we have created our layers, we are ready to add them to the map. We want the layers to be added or removed when we load a new view (plug a component in the <router-outlet>
). So we need to go to the component class definition and add the corresponding code.
In this case, we must add it to the SidebarComponent
. First we must inject the layer objects in the constructor:
|
|
Then we can use the ngOnInit
event handler to add the layers to the map and the ngOnDestroy
event handler to remove the layers from the map when we switch to a different view.
Instead of adding or removing directly the layers in those event handlers, we are going to define an Angular service that will implement the most common operations found in Location Intelligence applications. The service is going to be called MapService
and will take care of adding, removing, or updating layers, amongst other operations:
|
|
The first task we need to perform is to add to the service the deck.gl instance created by the MapComponent
. We define a setDeckInstance
public method that will be called after we have created the deck.gl instance in the launchMap
private method:
|
|
Then we need to add a couple of private arrays to manage the layers efficiently. The layers
array will contain the collection of deck.gl layers and the layersIdx
array will contain the ids for these layers so we can efficiently retrieve, update, or remove layers.
For example, adding a layer to the map will be an asynchronous operation that includes adding the layer id
to the layersIdx
array, adding the deck.gl layer to the layers
array and calling the updateDeck
private method to update the deck.gl instance layers
property:
|
|
Please review the MapService
code to discover how we are implementing the different layer operations.
Now that we have the MapService
defined, we must inject the dependency in the SidebarComponent
and use the service to add and remove layers in the event handlers:
|
|
One important functionality of this service is the ability to keep the different applications components in sync. In order to implement this functionality with a clear design we take advantage of the Observable
, Subject
, and BehaviorSubject
types defined in the Angular RxJS library. With this library we can use reactive programming concepts that provide a sound approach to Location Intelligence applications where you need to maintain several elements in sync (map, layers, user interface components…)
Please take a look at the onLayerChange
event handler and the onViewStateChange
property to understand how we are synchronizing layer changes (for example when we are filtering data) or viewstate changes.
Manage layer visibility
We already have an application where we are showing layers with data coming from the CARTO platform. In Location Intelligence applications, one basic functionality is the ability to show or hide layers on demand. We are going to create a new component called ToggleComponent
to control the visibility of layers.
|
|
In the HTML template for the component we are going to use the <mat-slide-toggle>
component from the Angular Material UI library. We will include a slide toggle component for each layer. We will use the change
event to update the layer visibility when the user interacts with the toggle and the checked
event to keep the layer visibility status up-to-date.
|
|
This component must access both the layers and the map service so we must inject them in the constructor. When the component is initialized (ngOnInit
), we will initialize the layersStatus array with the values from the layers' visible
property and we will create a Subscription
to the map service onLayerChange
event so we can update the layer status when it changes.
When the user interacts with the toggle to update the visibility for a layer, the component will call the MapService
UpdateLayer
method and the layers' show
or hide
method:
|
|
Finally we must add the component to the SidebarComponent
HTML template:
|
|
Interactivity
Another important functionality commonly found in Location Intelligence applications is interactivity. In our example we are using two different features to make our application more interactive and have an improved user experience:
-
Highlighting the features when the user moves the pointer over them
-
Showing a tooltip with feature information when the user moves the pointer over it
Highlighting features is very easy using deck.gl. For instance, if we want to highlight the railroad lines with a green color, we just need to add the following properties to the deck.gl layer instantiation:
|
|
Implementing a tooltip with deck.gl is also straight forward. First we need to set the pickable
property to true
for all the layers we want to show a tooltip for (in the layer instantiation). Then we need to add an event handler for the getTooltip
property in the deck.gl instance instantiation in the MapComponent
:
|
|
The tooltip
method must first check if we have picked any object. If that’s the case, the function must return the picked object and two properties: html
and style
, that define the content and style for the tooltip. We have made a generic implementation that shows all the properties from the object except for the layerName
and cartodb_id
:
|
|
Charts
We are going to finish this guide by adding a chart to our application. This is another functionality that we usually see in Location Intelligence applications that allows users to better analyze the information displayed in the map.
In the example we have created a component that shows a bar chart with the number of the stores layer features by store type in the current viewport. To implement the chart we are using the Apache ECharts library.
First we must add the echarts
package and the ngx-echarts
package, that facilitates the use of ECharts with Angular, to our project. We must import the NgxEchartsModule
and the different objects from the echarts
package in our HomeModule
:
|
|
Then we create our component:
|
|
In the component HTML template, we add the div
element that will contain our chart:
|
|
We set the chart height in the component styles definition file and then we are ready to add the charting code to the component class. The chart is updated automatically with every viewstate change. In order to achieve this, the MapService
is injected in the component constructor and the component subscribes to the map service onViewStateChange
event in the ngOnInit
handler. When this event is fired, we retrieve the stores layer features in the current viewport and update the chart:
|
|
To retrieve the features in the current viewport we use the booleanIntersects
function from the Turf.js library:
|
|
Then the features are grouped by store type and counted using the groupValuesByColumn
method. This method returns an array of key-value properties with the store type (category) as the key and the number of stores as the value. We call this method using the store type as the keys column and the store revenue as the values column.
This method uses a map-reduce approach: in the map stage, we assign the store to its category (store type) and in the reduce stage we accumulate the store revenue. Instead of summing the revenues for each of the store types, we are simplifying the calculation and just returning the number of elements (stores) per category (store type).
|
|
Finally we must add the chart component to the SidebarComponent
HTML template with a divider separating it from the toggle component:
|
|