Responsive Angular Dashboards Part 3: Show GeoData with Map Controls
Read the articles in this series:
How to Build a Responsive Dashboard Application in Angular
Responsive Angular Dashboards Part 2: How to Add Datagrids & Charts
Responsive Angular Dashboards Part 3: Show GeoData with Map Controls
In the previous article, we built out the information cards 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 implement our JavaScript map component, FlexMap, which will display visual information for users per country, and FlexGrids, which show additional information when users click different countries on the map.
Ready to Create Custom Dashboards? Download Wijmo Today!
FlexMap
While many of the other controls 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 with associated data. If a user clicks on a country, the FlexGrids (which we will implement later in this article) will update and 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 minimal amount of markup for a control as complex as an interactive map. While this markup creates our FlexMap control, there are actually three separate components used in this map: FlexMap, GeoMapLayer, and ColorScale.
- FlexMap creates the window containing the map and the tooltips that will be displayed when users hover over different sections of the map.
- GeoMapLayer uses GeoJSON to draw the map and initialize the control.
- ColorScale is used to bind countries that will be colored and 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. This file contains a lot of data we won't show in the article, but you can download the file and the application to view this data.
Next, let's prepare our data.service.ts file with the necessary data and methods our component will need with this code:
countryData = [
{
Country: 'United States',
AverageResponseTime: '1.2000',
PageViews: '21.9k',
IssuesReported: '72',
},
{
Country: 'Canada',
AverageResponseTime: '1.4250',
PageViews: '12.7k',
IssuesReported: '35',
},
{
Country: 'Mexico',
AverageResponseTime: '1.5500',
PageViews: '4.2k',
IssuesReported: '24',
},
{
Country: 'Brazil',
AverageResponseTime: '2.6505',
PageViews: '1.3k',
IssuesReported: '7',
},
{
Country: 'Peru',
AverageResponseTime: '3.2400',
PageViews: '0.8k',
IssuesReported: '2',
},
{
Country: 'United Kingdom',
AverageResponseTime: '1.7500',
PageViews: '7.9k',
IssuesReported: '29',
},
{
Country: 'France',
AverageResponseTime: '1.9000',
PageViews: '3.4k',
IssuesReported: '19',
},
{
Country: 'Germany',
AverageResponseTime: '2.1000',
PageViews: '5.6k',
IssuesReported: '15',
},
{
Country: 'Spain',
AverageResponseTime: '2.2500',
PageViews: '2.3k',
IssuesReported: '9',
},
{
Country: 'Italy',
AverageResponseTime: '2.3500',
PageViews: '1.9k',
IssuesReported: '6',
},
{
Country: 'Netherlands',
AverageResponseTime: '1.9250',
PageViews: '0.9k',
IssuesReported: '4',
},
{
Country: 'Finland',
AverageResponseTime: '2.0150',
PageViews: '1.1k',
IssuesReported: '7',
},
{
Country: 'Denmark',
AverageResponseTime: '3.5025',
PageViews: '1.8k',
IssuesReported: '9',
},
{
Country: 'Norway',
AverageResponseTime: '2.7500',
PageViews: '2.1k',
IssuesReported: '14',
},
{
Country: 'Poland',
AverageResponseTime: '3.4000',
PageViews: '0.3k',
IssuesReported: '3',
},
{
Country: 'Russia',
AverageResponseTime: '2.2250',
PageViews: '5.9k',
IssuesReported: '11',
},
{
Country: 'Ukraine',
AverageResponseTime: '3.2500',
PageViews: '3.1k',
IssuesReported: '8',
},
{
Country: 'China',
AverageResponseTime: '2.7000',
PageViews: '11.3k',
IssuesReported: '18',
},
{
Country: 'Japan',
AverageResponseTime: '2.3000',
PageViews: '13.8k',
IssuesReported: '17',
},
{
Country: 'Australia',
AverageResponseTime: '3.1000',
PageViews: '2.4k',
IssuesReported: '7',
},
];
getCountryData() {
return this.countryData;
}
isValidCountry(countryName: string): boolean {
for (var i = 0; i < this.countryData.length; i++) {
if (this.countryData[i].Country == countryName) {
return true;
}
}
return false;
}
Let's be sure to update our app.component.ts with the following necessary code:
selectedCountryName: string;
countryName(e: any) {
this.selectedCountryName = e;
}
Now that we've written the markup, we'll move over to the map-info.component.ts file to write the TypeScript that will allow users to interact with the FlexMap and break it down section by section.
import {
Component,
OnInit,
Output,
ViewChild,
EventEmitter,
} from '@angular/core';
import { Palettes } from '@mescius/wijmo.chart';
import { FlexMap } from '@mescius/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 to 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 ViewChild, EventEmitter, and Output to pull information from the markup and emit the data to other components—in this case, the FlexGrids, which we will implement later.
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 are used to color countries
- selectedColor is used to set the color value of a country that the user clicks on
- selectedCountry is used to retrieve the string value of the country that the user clicks on
- selectedID is used to retrieve 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 be 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 review in this file is 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:
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));
});
}
Inside 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 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: dataMap, pageViewMap, and issuesReportedMap. These three maps will be used to color the map and build out the tooltips that will be displayed when a user hovers over a country.
Next, we'll set up the method that will be 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(this.hitTestInfo._item !== undefined) {
this.emitCountryName(this.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 will be 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 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 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 retrieve data associated with that country. The getCountryTooltip() method is used to build out HTML that will be 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 these tooltips, we're displaying the country name as well as the average response time, the number of page views, and the number of issues reported per country.
The completed version of our TypeScript file is shown below:
import {
Component,
OnInit,
Output,
ViewChild,
EventEmitter,
} from '@angular/core';
import { Palettes } from '@mescius/wijmo.chart';
import { FlexMap } from '@mescius/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 (this.hitTestInfo._item !== undefined) {
this.emitCountryName(this.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 our app.component.html file:
<div class="livemap map">
<app-map-info (countryName)="countryName($event)"></app-map-info>
</div>
And when we run our application, we'll see the following:
If a user hovers over a country, they'll see a tooltip with the associated data:
And that's how we implement a FlexMap control! Next, we'll review the process of implementing dashboard cards that contain our FlexGrids.
Ready to Create Custom Dashboards? Download Wijmo Today!
FlexGrid
The final thing to do is implement the two components 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 who 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 will use 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 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 depend on the value within the cell, so we'll use *ngIf to screen the cell value and then select the appropriate markup 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 uses. 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, a width and margin on the FlexGrid control, and a width on our CollectionViewNavigator when the screen size is smaller than 800px.
The last thing 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 the user-info.component.ts file:
import {
Component,
Input,
OnInit,
OnChanges,
SimpleChanges,
} from '@angular/core';
import { DataService } from '../data.service';
import * as wjCore from '@mescius/wijmo';
@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 also need to add some code to our data.service.ts file:
getCountryInfo(country: string) {
var data = [],
platforms = ['Desktop', 'Mobile', 'Tablet', 'Other'],
browsers = ['Chrome', 'Firefox', 'Edge', 'Safari', 'Other'];
for (var i = 0; i < 200; i++) {
data.push({
country: country,
sessionDuration:
Math.round(Math.random() * 7) +
'm ' +
Math.round(Math.random() * 60) +
's',
ipAddress:
Math.round(Math.random() * (999 - 1) + 1) +
'.' +
Math.round(Math.random() * (999 - 1) + 1) +
'.' +
Math.round(Math.random() * (999 - 1) + 1) +
'.' +
Math.round(Math.random() * (999 - 1) + 1),
platform: platforms[Math.round(Math.random() * 3)],
browser: browsers[Math.round(Math.random() * 4)],
});
}
return data;
}
Note: Some users may see index signature errors. A quick fix for this is to update your tsconfig.json file as shown below:
"noPropertyAccessFromIndexSignature": false
With our component put together, we need to put it into the app.component.html file:
<div class="livemap user-info">
<app-user-info></app-user-info>
</div>
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. 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 running the application, the FlexGrid will appear as shown below:
We'll now hop over to the issue-info component to code that. First, we'll create our markup inside 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;
}
As with our other component, we're just setting font size, some padding, and the size of the FlexGrid.
Now we need to update the data.service.ts file with this code:
reportedIssues = [
{ country: 'United States', issuesReported: 72 },
{ country: 'Canada', issuesReported: 35 },
{ country: 'Mexico', issuesReported: 24 },
{ country: 'Brazil', issuesReported: 7 },
{ country: 'Peru', issuesReported: 2 },
{ country: 'United Kingdom', issuesReported: 29 },
{ country: 'France', issuesReported: 19 },
{ country: 'Germany', issuesReported: 15 },
{ country: 'Spain', issuesReported: 9 },
{ country: 'Italy', issuesReported: 6 },
{ country: 'Netherlands', issuesReported: 4 },
{ country: 'Finland', issuesReported: 7 },
{ country: 'Denmark', issuesReported: 9 },
{ country: 'Norway', issuesReported: 14 },
{ country: 'Poland', issuesReported: 3 },
{ country: 'Russia', issuesReported: 11 },
{ country: 'Ukraine', issuesReported: 8 },
{ country: 'China', issuesReported: 18 },
{ country: 'Japan', issuesReported: 17 },
{ country: 'Australia', issuesReported: 7 },
];
getIssueData(country: string) {
var issues = [
{
issue: '500 Internal Server Error',
status: 'High',
message: 'General purpose error: potential server overload',
},
{
issue: '400 Bad Request',
status: 'High',
message: 'Browser error: corrupted request',
},
{
issue: '408 Request Time-Out',
status: 'High',
message: 'Slow response time: check server request',
},
{
issue: '403 Forbidden',
status: 'Moderate',
message:
'Refused access: user attempted to access forbidden directory',
},
{
issue: '501 Not Implemented',
status: 'Moderate',
message: 'Request refused: unsupported browser feature',
},
{
issue: '401 Unauthorised',
status: 'Low',
message: 'Login failed: user does not have access',
},
{
issue: '404 Not Found',
status: 'Low',
message: 'Page not returned: check status of requested page',
},
],
data = [];
for (var i = 0; i < this.reportedIssues.length; i++) {
if (this.reportedIssues[i].country == country) {
for (var j = 0; j < this.reportedIssues[i].issuesReported; j++) {
var selector = Math.round(Math.random() * 6);
data.push({
country: country,
issue: issues[selector].issue,
status: issues[selector].status,
message: issues[selector].message,
});
}
break;
}
}
return data;
}
Finally, we'll go over to the issue-info.component.ts file to implement our TypeScript code:
import {
Component,
OnInit,
Input,
OnChanges,
SimpleChanges,
} from '@angular/core';
import * as wjCore from '@mescius/wijmo';
import * as wjGrid from '@mescius/wijmo.grid';
import { DataService } from '../data.service';
@Component({
selector: 'app-issue-info',
templateUrl: './issue-info.component.html',
styleUrls: ['./issue-info.component.css'],
})
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 using the initializedGrid() method. We'll assign a method to the grid's formatItem event, and inside this event, we'll check to ensure 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.
Next, all we need to do is drop the component inside our app.component.html file one last time:
<div class="livemap issue-info">
<app-issue-info></app-issue-info>
</div>
When we run the code, we should see our FlexGrid rendered, including our tooltip notifier and our custom content:
When a user hovers over one of the cells that contains a tooltip, the tooltip will be displayed:
Conclusion
With that, our dashboard application is complete! If we run it, we should see the following in our browser:
Now, that was a fair amount of coding, so we'll review everything that we performed throughout this project. We:
- 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 the top countries by session
- Used FlexMap to create a choropleth map of the world, using a color scale to display countries with users who 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 who interact with it a fair amount of information. With the CSS that we wrote, the dashboard will also be responsive and will be readable on whatever device from which the dashboard is accessed.
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 swiftly implement different controls to view information in a multitude of ways, hook the controls up to your data source, and have a fully functioning dashboard ready for your company to take advantage of.
You can run this application in your browser or download the source code for the application to try it yourself.
Ready to Create Custom Dashboards? Download Wijmo Today!