How to Build a Responsive Dashboard Application in Angular - Part 3

In the previous article, we built out the information cards displayed at the top of the dashboard, the FlexCharts that display session, browser, and platform information, and Linear Gauges that display information regarding the percentage of visitors per country. In this blog, we'll be implementing our FlexMap which will display visual information for users per country, and FlexGrids which show additional information when a user clicks on different countries on the map.

FlexMap

While many of the other controls that we've implemented in the dashboard serve purely to display data for users to view, FlexMap will act as the primary interactive piece of the dashboard. Users will be able to hover over each country that has data associated with it. If a user clicks on a country, the FlexGrids (which we will implement later in this article) will update to display information for that country.

First, we'll need to generate the component that will contain the FlexMap:

ng g c map-info

Next, we'll need to implement the markup within our map-info.component.html:

<wj-flex-map #flexMap header="Breakdown by Country" [tooltipContent]="tooltipContent" [selectionMode]="2" style="height: 100%;">
    <wj-geo-map-layer #geoLayer url="../assets/custom.geo.json" (itemsSourceChanged)="initializeMap(geoLayer)">
      <wj-color-scale #colorScale [colors]="colors" [binding]="binding" [scale]="scale"></wj-color-scale>
    </wj-geo-map-layer>
</wj-flex-map>

As you can see, this is a very small amount of markup for a control as complex as an interactive map. While this markup creates our FlexMap control, there's actually three separate components that are used in this map: FlexMap, GeoMapLayer, and ColorScale.

  • FlexMap creates the window that contains the map that is being displayed, as well as the tooltips that display when users hover over different sections of the map
  • GeoMapLayer uses GeoJSON to draw the map, as well as initialize the control
  • ColorScale is used to bind countries that will be colored, as well as the colors that will be bound to the grid

For our application, we've stored the GeoJSON that we're using to build the map within the assets/custom.geo.json file. It is a lot of data which we won't be showing in the article, but if you'd like to see it you can download the application here [link needed].

Now that we've got the markup written, we're going to move over to the map-info.component.ts file to write the TypeScript that will allow users to interact with the FlexMap, and we're going to break it down section-by-section.

import { Component, OnInit, Output, ViewChild, EventEmitter } from '@angular/core';
 
import { Palettes } from "@grapecity/wijmo.chart";
import { FlexMap } from '@grapecity/wijmo.chart.map';
import { DataService } from '../data.service';
 
@Component({
  selector: 'app-map-info',
  templateUrl: './map-info.component.html',
  styleUrls: ['./map-info.component.css']
})
export class MapInfoComponent implements OnInit {}

Here, we're importing all the different packages we'll be using. From Wijmo, we're using Palettes to color our map and FlexMap so that we can access the control to retrieve data from the map. We're importing our DataService to retrieve data that we can use to populate our tooltips. Finally, we're importing ViewChildEventEmitter, and Output to pull information from the markup and emit the data to other components (in our case, the FlexGrids that we will be implementing down the line).

Next, we're going to look at the properties that we'll be using in our component:

@ViewChild('flexMap') flexMap: FlexMap;
@Output() countryName = new EventEmitter<string>();
flexMapData: any;
dataMap = new Map();
pageViewMap = new Map();
issuesReportedMap = new Map();
colors = Palettes.Diverging.RdYlGn;
selectedColor: '#188d9b';
selectedCountry: '';
selectedID: any;
hitTestInfo: any;
binding = (o: any) => this.dataMap.get(o.properties.name);
scale = (v: number) => 1 - v;
tooltipContent = (f: any) => this.getCountryToolTip(f);
  • flexMap holds the reference to the FlexMap control
  • countryName will be used to emit the selected country name to the FlexGrid controls
  • flexMapData will be used to hold country-specific data from our data service
  • dataMap will be used to build our array of data that will be used to assign each country a color value
  • pageViewMap will hold the total page views for each country; this will be displayed in the tooltip
  • issuesReportedMap will hold the number of issues reported for each country; this will be displayed in the tooltip
  • colors hold our array of color values that get passed to the ColorScale component and be used to color countries
  • selectedColor is used to set the color value of a country that the user clicks on
  • selectedCountry is used to get the string value of the country that the user clicks on
  • selectedID is used to get the ID value of the country that the user clicks on
  • binding holds an array of values that are used to bind the different countries that will be colored in the FlexMap
  • scale is used to select the color that will get assigned to each country
  • tooltipContent builds out the tooltip for each country that will be displayed when a user hovers over the country

The next section of code to go over in this file are the four methods that will be called when the component initializes, when the FlexMap control is created, to create the tooltips, and to emit the country name value when a user clicks on a country. First, we'll cover the ngOnInit() method:

ngOnInit() {
    this.flexMapData = this.dataService.getCountryData();
    Array.prototype.forEach.call(this.flexMapData, el => {
        this.dataMap.set(el.Country, parseFloat(el.AverageResponseTime)); 
        this.pageViewMap.set(el.Country, el.PageViews);
        this.issuesReportedMap.set(el.Country, parseInt(el.IssuesReported));
    });
}

Inside of the ngOnInit() method, we're setting our FlexMap's data source, which will be used to assign data to each of the different regions of our map control. This will allow the map control to color each region of the map based on the data that we want to measure (in the case of this sample, we're using response time).

We're also setting the value of our three data maps: dataMappageViewMap, and issuesReportedMap. These three maps will be used to color the map and build out the tooltips that get displayed when a user hovers over a country.

Next, we'll set up the method that gets called when the FlexMap is initialized:

initializeMap(layer: any) {
    this.flexMap.zoomTo(layer.getGeoBBox());
    this.flexMap.hostElement.addEventListener('mousedown', (e) => {
      this.hitTestInfo = this.flexMap.hitTest(e);
      if(hitTestInfo._item !== undefined) {
        this.emitCountryName(hitTestInfo._item.name);
        let el = document.elementFromPoint(e.x, e.y);
        let id = el ? el.getAttribute('wj-map:id') : undefined;
        this.selectedID = id;
        this.flexMap.invalidate(true);
      }
    });
    this.flexMap.rendered.addHandler((s, a) => {
        const layer = this.flexMap.layers[0];
        const g = layer._g;
        if(g && this.selectedID && this.validCountry(this.hitTestInfo._item.name)) {
            let list = [];
            for(let i = 0; i < g.childNodes.length; i++) {
                const node = g.childNodes[i];
                let id = node.getAttribute('wj-map:id');
                if(id === this.selectedID) {
                    node.setAttribute('fill', this.selectedColor);
                    list.push(node);
                }
            }
            list.forEach((el) => el.parentNode.appendChild(el));
        }
    });
}

The initializeMap() method gets used to tie the mousedown event to the map, which will then use the hitTest() method to track where the user clicks on the map. If the country that the user clicks on has a value associated with it, we'll call the emitCountryName() method to emit an event containing the country's name that the user clicked on.

We're also tying an event to the map's rendered event; we'll use this to set the selected country based on the country's value that the user clicks on. In this method, we're just checking that the chosen country has a valid ID and checking to see if the country selected is valid (a country that contains values within our data). If it is, we fill the area with the selectedColor that we defined earlier in the application.

Finally, we'll create our emitCountry()getCountryToolTip(), and validCountry() methods:

emitCountryName(value: string) {
    this.countryName.emit(value);
}
 
getCountryToolTip(val: any): string {
    if(this.dataService.isValidCountry(val.name)) {
        return `<b>` + val.name + `</b><br>` + 'Average Response Time: ' + this.dataMap.get(val.name) + 's' + `<br>` + 'Page Views: ' + this.pageViewMap.get(val.name) + `<br>` + 'Issues Reported: ' + this.issuesReportedMap.get(val.name);
    }
    return `<b>` + val.name + `</b><br>` + 'No data available';
}
 
validCountry(value: string) {
    for(var i = 0; i < this.flexMapData.length; i++) {
        if(this.flexMapData[i].Country == value) {
            return true;
        }
    }
    return false;
}

The emitCountry() method takes the country name value and emits it; we'll use this with the components that use FlexGrid to get data associated with that country. The getCountryTooltip() method is used to build out HTML that will get rendered when a user hovers over different countries on the map. The validCountry()method takes a value and determines if the selected country is a "valid" country; this checks to see if it is one of the countries that have data associated with it.

Finally, the getCountryToolTip() method is used to build out the displayed tooltips. Inside of these tooltips, we're displaying the country name, the average response time for the country, the number of page views for the country, and the number of issues reported for the country.

Here is the completed version of our TypeScript file:

import { Component, OnInit, Output, ViewChild, EventEmitter } from '@angular/core';
 
import { Palettes } from "@grapecity/wijmo.chart";
import { FlexMap } from '@grapecity/wijmo.chart.map';
import { DataService } from '../data.service';
 
@Component({
  selector: 'app-map-info',
  templateUrl: './map-info.component.html',
  styleUrls: ['./map-info.component.css']
})
export class MapInfoComponent implements OnInit {
  @ViewChild('flexMap') flexMap: FlexMap;
  @Output() countryName = new EventEmitter<string>();
  flexMapData: any;
  dataMap = new Map();
  pageViewMap = new Map();
  issuesReportedMap = new Map();
  colors = Palettes.Diverging.RdYlGn;
  selectedColor: '#188d9b';
  selectedCountry: '';
  selectedID: any;
  hitTestInfo: any;   binding = (o: any) => this.dataMap.get(o.properties.name);
  scale = (v: number) => 1 - v;
  tooltipContent = (f: any) => this.getCountryToolTip(f);
 
  constructor(private dataService: DataService){}
   
  ngOnInit() {
    this.flexMapData = this.dataService.getCountryData();
    Array.prototype.forEach.call(this.flexMapData, el => {
      this.dataMap.set(el.Country, parseFloat(el.AverageResponseTime));
      this.pageViewMap.set(el.Country, el.PageViews);
      this.issuesReportedMap.set(el.Country, parseInt(el.IssuesReported));
    });
  }
 
  initializeMap(layer: any) {
    this.flexMap.zoomTo(layer.getGeoBBox());
    this.flexMap.hostElement.addEventListener('mousedown', (e) => {
      this.hitTestInfo = this.flexMap.hitTest(e);
      if(hitTestInfo._item !== undefined) {
        this.emitCountryName(hitTestInfo._item.name);
      let el = document.elementFromPoint(e.x, e.y);
      let id = el ? el.getAttribute('wj-map:id') : undefined;
      this.selectedID = id;
      this.flexMap.invalidate(true);
    }});
    this.flexMap.rendered.addHandler((s, a) => {
      const layer = this.flexMap.layers[0];
      const g = layer._g;
      if(g && this.selectedID && this.validCountry(this.hitTestInfo._item.name)) {
        let list = [];
        for(let i = 0; i < g.childNodes.length; i++) {
          const node = g.childNodes[i];
          let id = node.getAttribute('wj-map:id');
          if(id === this.selectedID) {
            node.setAttribute('fill', this.selectedColor);
            list.push(node);
          }
        }
        list.forEach((el) => el.parentNode.appendChild(el));
      }
    });
  }
     
  emitCountryName(value: string) {
    this.countryName.emit(value);
  }
 
  getCountryToolTip(val: any): string {
    if(this.dataService.isValidCountry(val.name)) {
      return `<b>` + val.name + `</b><br>` + 'Average Response Time: ' + this.dataMap.get(val.name) + 's' + `<br>` + 'Page Views: ' + this.pageViewMap.get(val.name) + `<br>` + 'Issues Reported: ' + this.issuesReportedMap.get(val.name);
    }
    return `<b>` + val.name + `</b><br>` + 'No data available';
  }
 
  validCountry(value: string) {
    for(var i = 0; i < this.flexMapData.length; i++) {
      if(this.flexMapData[i].Country == value) {
        return true;
      }
    }
    return false;
  }
}

Now all we need to do is drop the component inside of our app.component.html file:

<main class="livemap-grid">
  <div class="livemap load-info">
    <app-current-info></app-current-info>
  </div>
  <div class="livemap map">
    <app-map-info (countryName)="countryName($event)"></app-map-info>
  </div>
  <div class="livemap user-info">
  </div>
  <div class="livemap sessions">
    <app-session-info></app-session-info>
  </div>
  <div class="livemap issue-info">
  </div>
  <div class="livemap top-countries">
    <app-top-country-info></app-top-country-info>
  </div>
  <div class="livemap top-platforms">
    <app-top-platform-info></app-top-platform-info>
  </div>
  <div class="livemap browsers">
    <app-top-browser></app-top-browser>
  </div>
</main>

And when we run our application, we'll see the following:

FlexMap

And if a user hovers over a country, they'll see a tooltip with the associated data:

FlexMap Tooltip

And that's how we implement a FlexMap control! Next, we'll be going over the process of implementing our dashboard cards that contain our FlexGrids.

FlexGrid

Now, the final thing that we have to do is implement the two components that we'll use to display FlexGrid and the data associated with the selected country. To generate the components, run the following commands:

ng g c user-info
ng g c issue-info

The first component that we'll look at is the user-info component. This component will gather and display data on the last 200 users that have visited our site from the user-selected country. By default, we'll display the data from users visiting from the United States.

First, we'll look at the user-info.component.html file:

<div class="users-container">
    <div class="users-header">RECENT USERS <i class="bi bi-question-circle" [wjTooltip]="usersTooltip" [wjTooltipPosition]="6"></i></div>
    <wj-flex-grid [itemsSource]="selectedCountryData" [isReadOnly]="true" [selectionMode]="'Row'" [headersVisibility]="'Column'">
        <wj-flex-grid-column binding="country" header="Country" [width]="'*'"></wj-flex-grid-column>
        <wj-flex-grid-column binding="sessionDuration" header="Session Duration"></wj-flex-grid-column>
        <wj-flex-grid-column binding="ipAddress" header="IP Address"></wj-flex-grid-column>
        <wj-flex-grid-column binding="platform" header="Platform">
            <ng-template wjFlexGridCellTemplate [cellType]="'Cell'" let-cell="cell">
                <i *ngIf="cell.item.platform == 'Desktop'" class="bi bi-display-fill"></i>
                <i *ngIf="cell.item.platform == 'Mobile'" class="bi bi-phone-fill"></i>
                <i *ngIf="cell.item.platform == 'Tablet'" class="bi bi-tablet-landscape-fill"></i>
                <i *ngIf="cell.item.platform == 'Other'" class="bi bi-terminal-fill"></i>
                {{cell.item.platform}}
            </ng-template>
        </wj-flex-grid-column>
        <wj-flex-grid-column binding="browser" header="Browser">
            <ng-template wjFlexGridCellTemplate [cellType]="'Cell'" let-cell="cell">
                <img *ngIf="cell.item.browser == 'Chrome'" src="../../assets/chrome.png" alt="" height="16">
                <img *ngIf="cell.item.browser == 'Firefox'" src="../../assets/firefox.png" alt="" height="16">
                <img *ngIf="cell.item.browser == 'Edge'" src="../../assets/edge.png" alt="" height="16">
                <img *ngIf="cell.item.browser == 'Safari'" src="../../assets/safari.png" alt="" height="16">
                <i *ngIf="cell.item.browser == 'Other'" class="bi bi-cloud-fill"></i>
                {{cell.item.browser}}
            </ng-template>
        </wj-flex-grid-column>
    </wj-flex-grid>
    <br>
    <wj-collection-view-navigator headerFormat="Page {currentPage:n0} of {pageCount:n0}" [byPage]="true" [cv]="selectedCountryData"> </wj-collection-view-navigator>
</div>

Here, we're setting up two controls: the FlexGrid and the CollectionViewNavigator. For FlexGrid, we create the control itself using the wj-flex-grid tag, and then we create each column that will be displayed using the wj-flex-grid-column tag. Usually, you wouldn't need to set each individual column, but we're going to be using some templates to help us style some cells using the FlexGridCellTemplate. Inside our Platform and Browser columns, we're creating ng-template elements; the FlexGridCellTemplate control will use these to style the interior of the cells that reside in that column.

We can include the markup that we want to display inside each cell. For these cells, the markup that we want to display will be dependent on the value that is within the cell, so we'll use *ngIf to screen the cell value and then select the appropriate markup that we want to display.

The CollectionViewNavigator allows us to include pagination with FlexGrid; for this to work, we need to supply it with the same CollectionView that our grid is using. FlexGrid and the CollectionViewNavigator will then both have access to the CollectionView. Any changes made to the CollectionView by either of the controls will be mimicked across both controls, including the ability to apply paging.

Next, we'll jump over to the user-info.component.css file to apply some styling:

.users-container {
    padding: 1rem;
}
 
.users-header {
    font-size: 1.05em;
    color: gray;
}
 
.wj-flexgrid {
    max-height: 440px;
    margin-top: 10px;
}
 
@media (max-width: 799px) {
    .wj-collectionview-navigator {
        width: 300px;
    }
}

All we're doing here is setting some padding and font size for our header, setting a width and margin on the FlexGrid control, and setting a width on our CollectionViewNavigator when the screen size is smaller than 800px.

The last thing that we need to do for this component is to create the TypeScript that will handle collecting data that we'll use to hand to the FlexGrid control to populate it. This will be done inside of the user-info.component.ts file:

@Component({
  selector: 'app-user-info',
  templateUrl: './user-info.component.html',
  styleUrls: ['./user-info.component.css']
})
export class UserInfoComponent implements OnInit, OnChanges {
  @Input('selectedCountryName') selectedCountryName: string;
  selectedCountryData: wjCore.CollectionView;
  usersTooltip = 'Information on the last 200 users from selected country.';
 
  constructor(private dataService: DataService) {
    this.selectedCountryData = new wjCore.CollectionView(dataService.getCountryInfo('United States'), {
      pageSize: 25
    })
  }
 
  ngOnInit(): void {}
 
  ngOnChanges(changes: SimpleChanges): void {
    if(changes.selectedCountryName.currentValue && this.dataService.isValidCountry(changes.selectedCountryName.currentValue)) {
      this.selectedCountryData = new wjCore.CollectionView(this.dataService.getCountryInfo(changes.selectedCountryName.currentValue), {
        pageSize: 25
      });
    }
  }
}

We've got our @Input value, which is the country name that we emit from the FlexMap component, our CollectionView, and a tooltip that we use to let users know what the purpose of this card is for. We're also assigning a value to our CollectionView's pageSize property; this determines how many rows will be displayed per page, in this case, 25 rows of data. When run, the FlexGrid will look as such:

FlexGrid User

We'll now hop over to the issue-info component to code that. First, we'll create our markup inside of the issue-info.component.html file:

<div class="issues-container">
    <div class="issues-header">ISSUES REPORTED <i class="bi bi-question-circle" [wjTooltip]="issuesTooltip" [wjTooltipPosition]="6"></i></div>
    <wj-flex-grid #issueGrid [itemsSource]="selectedCountryData" [isReadOnly]="true" [selectionMode]="'Row'" [headersVisibility]="'Column'" [lazyRender]="false" (initialized)="initializedGrid(issueGrid)">
        <wj-flex-grid-column binding="country" header="Country" [width]="'*'"></wj-flex-grid-column>
        <wj-flex-grid-column binding="issue" header="Issue" [width]="'*'"></wj-flex-grid-column>
        <wj-flex-grid-column binding="status" header="Status" [width]="'*'">
            <ng-template wjFlexGridCellTemplate [cellType]="'Cell'" let-cell="cell">
                <span *ngIf="cell.item.status == 'High'" class="badge bg-danger">{{ cell.item.status }}</span>
                <span *ngIf="cell.item.status == 'Moderate'" class="badge bg-warning text-dark">{{ cell.item.status }}</span>
                <span *ngIf="cell.item.status == 'Low'" class="badge bg-info text-dark">{{ cell.item.status }}</span>
            </ng-template>
        </wj-flex-grid-column>
    </wj-flex-grid>
</div>

This is essentially identical to the markup for our user-info component: we create our FlexGrid control, create the columns using the wj-flex-grid-column tag, and use FlexGridCellTemplate to create a column with custom content.

Hopping over to the issue-info.component.css file, we'll add the following CSS:

.issues-container {
    padding: 1rem;
}
 
.issues-header {
    font-size: 1.05em;
    color: gray;
}
 
.wj-flexgrid {
    max-height: 480px;
    margin-top: 10px;
}

Like with our other component, we're just setting font size, some padding, and the size of the FlexGrid.

Finally, we'll go over to the issue-info.component.ts file to implement our TypeScript code:

export class IssueInfoComponent implements OnInit, OnChanges {
  @Input('selectedCountryName') selectedCountryName: string;
  selectedCountryData: wjCore.CollectionView;
  issuesTooltip = 'Issues reported by system';
 
  constructor(private dataService: DataService) {
    this.selectedCountryData = new wjCore.CollectionView(dataService.getIssueData('United States'));
  }
 
  ngOnInit(): void {
  }
 
  ngOnChanges(changes: SimpleChanges): void {
    if(changes.selectedCountryName.currentValue && this.dataService.isValidCountry(changes.selectedCountryName.currentValue)) {
      this.selectedCountryData = new wjCore.CollectionView(this.dataService.getIssueData(changes.selectedCountryName.currentValue));
    }
  }
 
  initializedGrid(grid: wjGrid.FlexGrid) {
    let toolTip = new wjCore.Tooltip();
    grid.formatItem.addHandler((s: wjGrid.FlexGrid, e: wjGrid.FormatItemEventArgs) => {
      if(e.panel == s.cells) {
        let item = s.rows[e.row].dataItem, binding = s.columns[e.col].binding, note = item.message;
        if(e.col == 1) {
          wjCore.toggleClass(e.cell, 'wj-has-notes', true);
          toolTip.setTooltip(e.cell, '<b>Error:</b><br/>' + note);
        }
      }
    });
    grid.updatingView.addHandler(() => {
      toolTip.dispose();
    });
  }
 
}

You'll notice that this code is identical to our user-info TypeScript code with one new piece added: the initializedGrid() method. We'll be incorporating tooltips into this grid, and we'll be using the initializedGrid() method. We'll assign a method to the grid's formatItem event, and inside of this event, we'll check to make sure that we're in the second column of the grid (since indexing an array starts at 0, this will be the column value of 1). Then, we'll assign a class to that cell, the wj-has-notes class we created in the first blog in this series, and some HTML to be displayed when a user hovers the cell.

When we run the code, we should see our FlexGrid rendered, including our tooltip notifier and our custom content:

FlexGrid Issue

And when a user hovers over one of the cells that contain a tooltip, the tooltip will be displayed:

FlexGrid Tooltip

Conclusion

And with that, our dashboard application is complete! If we run it, we should see the following in our browser:

Wijmo Live Map

Now, that was a fair amount of coding, so I'll do a once-over of everything that we did throughout this project:

  • Created a set of responsive cards that will hold our different controls
  • Implemented a component to display statistics for active sessions, load time, APDEX score, and bounce rate
  • Used FlexPie and FlexChart to show session info, load time/sessions by platform, and load time/sessions by browser
  • Created a set of gauges to display a list of top countries by session
  • Use FlexMap to create a choropleth map of the world, using a color scale to display countries with users that visit our site based on load time
  • Tied the FlexMap control to two FlexGrid controls, which display data on users as well as issues that users encountered when interacting with our site

With all of that completed, we now have a very clean dashboard that provides users that interact with it a fair amount of information. With the CSS that we wrote, the dashboard is also responsive and will be readable on whatever device the dashboard is accessed from.

Dashboards have become a crucial aspect of data analysis across many industries. They allow their users to quickly make high-level business decisions based on their information. Wijmo makes it easy for developers to implement swiftly different controls for a multitude of ways to view information, hook the controls up to your data source, and have a fully functioning dashboard ready for your company to take advantage of.

If you'd like to either run this application in your browser or download the source code for the application, you can do so here.

 

comments powered by Disqus