Showing posts sorted by relevance for query pixel. Sort by date Show all posts
Showing posts sorted by relevance for query pixel. Sort by date Show all posts

Friday, March 10, 2017

Google Pixel XL Phone Review, Part 1: The Long Wait

As the new owner of a Google Pixel XL Android phone, I'll be writing a series of posts to review my experiences with Google's new flagship handset. In this first post, I'll... be talking about the ordering experience, because I don't actually have the phone yet. Mostly I'll be complaining about the incredibly looooooong wait for the phone: I ordered it on February 13th, and am expecting to receive it on April 6th. That's a long 7 weeks of waiting. As I write this, I "only" have 4 weeks to go. Talk about agonizing. What I can do in first this post is discuss why I chose this phone (despite the long wait), and what I've learned about it. 
I won't go into the reasons for the delay, since I don't really know them. Google says it's due to high demand. Others speculate it's due to poor planning and management. Since this isn't Google's first phone, it is hard to explain.

Why the Pixel?


Why this phone? Well, my previous two phones have been the Moto X (first generation) and Moto X (second generation), which are also Android phones. I liked the Moto X a great deal, especially the "Moto actions" and other nice touches: I can just wave my hand above the phone to wake it up; I get battery-efficient notification icons; and the phone knows if I am looking at it and won't go to sleep. I also liked that I could design my own customized phone, choosing front (white), back (leather, engraved), and accent (silver). So why not just stay with Moto? Well, I was thrilled when Google bought Motorola Mobility; unfortunately, they've since sold it off to Lenovo. The current Moto phones all seem to have a camera bump, which is something of a deal-breaker for me: I want my phone to lie flat on the desk.

So, I looked around. For a variety of reasons I'm not a big fan of Samsung and LG given past experiences. I'm also quite weary of all the bloatware carriers tend to install on Android phones, often apps that can't be deleted. When Google announced the Pixel it made a lot of sense: no carrier bloatware, a finely-tuned Android experience, high-end hardware, and the best possible integration with Google services. Yes, it's pricey--but it seems like the best choice given what I value (pure Android experience, good hardware, backed by a company I have some respect for). And so, I placed my order and am now waiting (impatiently). In the meantime, I've read dozens of reviews by others so I have a pretty good idea of what to expect.

Having settled on a Pixel, I chose the larger XL model (5.5" screen) - Google's answer to the iPhone Plus. This is a bit of a departure for me, but I'm thinking the larger screen will be easier on the eyes. My wife got an iPhone Plus recently, and it took her about a week to get used to the larger device. I also went with the 128GB storage, because one of my few complaints about the Moto line was that I'm always running low on storage space. As for the color, my first choice would have been silver and after that blue; alas, almost everything was out of stock. When I finally could order something, it was the black. No matter, I'll be putting a skin on it.

The Ordering Experience

The ordering experience was simple enough. Since I'm not going through Verizon (poor coverage where I live, plus refusal to put up with any more carrier bloatware), and since the Pixel isn't available through any other carriers, that means I'm getting it directly from Google. The Google Store ordering experience is what you'd expect from Google: it's simple, and easy to use. Except...

Except that I couldn't actually order one. What's odd about the ordering experience is the backlog, and how that's being handled. When I first visited the Google Store in January, they were sold out, at least in the configurations I was interested in. And, you couldn't pre-order the phone. All you could do is get added to a waitlist (an email would come your way when a phone was available for order). After a few weeks, now in February, I still could not order the phone I wanted. Tired of this, I decided to cave on color, and ordered what was available: a black 128GB Pixel XL.

The Google Store shows you a Pixel and lets you change the base unit (Pixel or Pixel XL), color, and storage. As you change the configuration, you can see the phone from front, side, and back perspectives.


Google Store - Pixel

Here's the unit I ended up ordering: a Black Pixel XL 128GB.


Google Store - Pixel XL

Finally, my phone was ordered--but I was still in for a looong wait. A 7 week wait.

What I'll Like about the Pixel XL

👍 Free Unlimited Cloud Backup of Photos and Videos

The Pixel gives you unlimited storage of images and videos in Google Photos (cloud storage). Android users normally get this feature anyway, but not at full quality. For Pixel owners, they are archived at full quality.

👍 Great Battery Life


The battery life is reportedly very good. There's also fast charging: you're supposed to be able to get 7 hours of battery life from 15 minutes of charging.

👍 Speed

The Pixel's speed is reported as fantastic by reviewers - even after the honeymoon period. This is due to a fast processor plus a really well-tuned edition of Android. This is the whole point of the Pixel line: with Google in control of both the hardware and software, the phone really sings.

👍 Great Android Experience

Reviewers are also in agreement that the Android experience is first-rate. There are all sorts of improvements and refinements to be found, some important and some just to be more Apple-like. Given that the rest of my household is on Apple phones, being more Apple-like won't be a bad thing. 

  • New Pixel Launcher.
  • Round icons. 
  • Long-press to get application shortcuts. 
  • Swipe gestures, such as swiping up to get the app tray. Swipe down on the rear fingerprint sensor to see notifications, swipe back up to dismiss.
Gestures

👍 Camera

Google is advertising the best camera available for a smartphone. Reviews either agree or call it a close second to the iPhone 7. Either way, I'm sure to be happy with this camera.

What I Probably Won't Like About the Pixel XL

👎 Don't Get It Wet

One ding against the Pixel is it isn't waterproof. That's mostly a big deal just because its becoming a standard on high-end phones. For the high price, the Pixel should also be waterproof.

👎 Unique Features that No Longer Are

The features that are supposed to be unique to the Pixel are rapidly become available anywhere. You can install the Pixel launcher on other Android phones. As of this week, the Google Assistant is no longer exclusive to just the Pixel.

👎 Pixel 2 Rumors Already Circulating

Each week, more details are coming to light about the Pixel 2, which will apparently be released near the end of 2017. Meanwhile, I'm still waiting for my first-gen Pixel.

👎 Avoiding Damage

Although the Pixel is reportedly well-made, it also seems prone to picking up scratches, especially on the rear glass panel. I'll admit it, I'm one of those people who prefer to use their phone without a case. This of course has bitten me in the past: my first Moto X cracked when I dropped it just a month in; with my Moto X second gen I was ultra-careful and avoided any mishaps. 

Given the expense of the Pixel, I'm going to give in and get some protection. I've decided to go with a skin from ColorWare, which I recently ordered and will receive in a couple of weeks. It's Techno Blue, and looks like this. Of course, I'm told these skins can also get scratched so I'll still need to be careful.
Pixel XL skin by ColorWare

Well, I think that's all I can say for now. I'll share more when I actually get the thing. In the meantime, I'll be practicing how to be more patient. I hear it's a virtue.

Next: Pixel XL Phone Review, Part 2: Google Messes Up Fulfillment

Saturday, January 11, 2020

Review: Moto Z4

This is my review of the Moto Z4 phone from Motorola.


Buying Decision

About 3 months ago when my 2016 original Pixel XL phone starting losing its battery life, I initially compensated by bringing my phone charger to work. After a few weeks, though, even that wasn't sufficient to keep my phone charged all day and I knew it was time to replace it.

Whenever you have to replace a phone, the first thing that goes through your mind is how good or bad the experience with your last phone was. I had pretty mixed feelings about the Pixel: while it had worked fine for the last three years, it was a pricey phone at $1000—made all the worse when Google botched my fulfillment, charging me full-out for the phone even though I had arranged Financing.

Price

Price was going to be a key factor. Although I have always purchased top-of-the-line premium phones for myself, nowadays I'm on a campaign to reduce my living costs without heavily compromising lifestyle. I vowed to find a phone I could live with that cost no more than $500.

Like most tech professionals, I use my phone frequently and it's essential to have one you can count on. There are some very low cost phones out there, but I wasn't quite ready to move from top-of-the-line to the bottom and was hoping to find an affordable mid-range phone. I also wanted a new phone: while we've bought refurbished or last-year's-model phones for our children at times, an Android phone tends to only get updates for a few years, not perpetually.

Display Size

In addition to my cost objective, I had two others: display size and bloatware-free-Android. My older eyes need a larger-size phone I can see clearly, which is why my last phone was a Pixel XL. Of course, larger-size phones cost more which comes up against the lower-cost objective.

Stock Android

Avoiding Android bloatware is difficult, as any phone obtained through a carrier is sure to have altered and non-removable vendor apps including Contacts; so no carrier-sold phone for me. Even with an unlocked phone, many vendors may customize Android far more than you'd like: would you rather use Google Assistant or Samsung's Bixby? Although Samsung is the dominant phone manufacturer for Android with a ton of models for every budget, their non-stock Android is a deal-breaker. The two best phone brands for stock Android are Google (of course) and Motorola.

Design

Perhaps one final objective has to do with recent trends in phones that give me pause: the display notch and the camera bump. I don't have direct experience with either because I've avoided them. Taking away part of the display for a camera offends my design sensibilities, yet so many phones are doing it now. What if that notch hides a critical part of an app? Even it it doesn't, it seems to me it would be a constant irritant. The camera bump equally offends me: it suggests my phone won't lie flat which I'd hate.

Given my other objectives, I suspected I might have to give in on the notch or the bump. I decided to have an open mind and to handle any prospective phone in-person at a Best Buy before making a decision.

Pixel 3a XL vs. Moto Z4

With these parameters established, I soon narrowed the field to two possibilities: the Google Pixel 3a XL or the Motorola Moto Z4, both costing just under $500 for an unlocked phone.

The Pixel 3a XL is nearly half the price of the Pixel 3 XL, but what are you giving up? Both have the same award-winning rear camera, memory, design, and Android version. The 3a has a polycarbonite unibody instead of glass-and-metal, so it feels like a cheaper phone. It also has a lesser processor and a lesser front camera.

The Moto Z4 seemed to have a lot of what I was looking for. Thinking back, one of the best phones I ever owned was a Moto X, with an attractive design and stock Android. When Google bought Motorola, I was jubilant about the future; but after extracting what they wanted from the company and selling it off to Lenovo, I was less so. Still, perhaps another look was warranted. The Moto Z4 reviews mostly agreed the Z4 was a decent mid-range phone with the features I care about; but most of them also recommended passing on this phone because the Z4 didn't offer anything especially new or exciting. The Z4 has a lesser camera than the Pixel, but would likely give me an acceptable overall experience.

The Moto Z4 also supports Moto Mods, which are accessories that clip on as second backs to the phone. There are mods for 360 camera, video projection, photo printing, and longer battery life. But, most reviews describe Moto Mods as an idea that didn't catch on.

If you research the Moto Z4, some of the review headlines will certainly give you pause: The cheapest 5G phone you can buy but shouldn't; It’s Time to Move On; The Moto Z4 is tragically boring and behind the times. For my criteria, however, the Moto Z4 topped the list and it's what I picked.

Experience

I bought my black Moto Z4 from Best Buy and it came with the 360 camera Moto Mod included. I added a Metalllic Slate case from Tudia. I haven't really done anything with the Moto Mods, but I'm very happy with the phone.

Now that I'm a few months in I have no regrets about the Moto Z4: it does everything I want in a phone. I haven't minded the camera notch after all. The battery lasts a really long time and is typically at 80% when I get home from work.  It looks and feels great and is a pleasure to use. This is a phone I recommend.


Sunday, March 19, 2017

Google Pixel XL Phone Review, Part 2: Google Messes Up Fulfillment

This is Part 2 in a blog series about my new Google Pixel XL phone. In Part 1, I described the ordering process, which was fine except for the long 7-week wait for the phone. I fully expected the next post in this series to be about unpacking and configuring the phone, but unfortunately I need to first post about the fulfillment experience which has not been good.

Original Order

To recap my first post, I ordered my Pixel XL on 2/13/17 and was told to expect it around 4/04/17. I'd ordered with Google Financing over 2 years. I was accepted into Google Financing at the time I placed my order, and received confirming paperwork the following week. All set! All I had to do now was wait until April to get my phone.

Order confirmation, which clearly shows payment method Google Financing

A Problem With Your Order

On March 16, 4 weeks in on my 7-week wait, I received an email from Google Store saying that there was a problem with my order. There was no explanation of what the problem was, but the message did have a Fix It button.

Order on Hold Message

Eager to resolve any issue with getting my phone, I clicked Fix It. This took me to a Google Payments screen which wanted me to update my payment method to an updated credit or debit card. This was odd, because my original order had nothing to do with a credit or debit card: it was made through Google Financing. However, I do have my Google account on a debit card for miscellaneous purchases such as Google Music. After confirming that I was in fact dealing with a real Google web site, I cooperated in updating my Google account payment method to a current debit card and completed the form.

And I guess this was the wrong thing to do, as you'll see in the next section. I want to point out though that this interaction 1) Google provided no other recourse for "correcting the problem with my order" and 2) certainly did not did not indicate that any kind of charge was about to be made.

Unauthorized Debit of Full Price of Phone

At this point I was suspicious. Had I just changed the payment method for my phone order? I went back to call up the status of my original order. It still indicated that payment was through Google Financing, so I relaxed.

Order Check, Confirming that Google Financing was Still Method of Payment

But just a few seconds later, I received an alert from my bank that $968.67 had been debited from my bank account! Wait, what!?!

The unauthorized debit

This was unbelievable. Google had TRICKED ME into paying up front for a phone that had already been ordered and approved for financing--and they did it without telling me what was going to happen.

Your Order Has Shipped

On the next day, 3/17/17, I received an email that my order had shipped (unexpected, because original delivery expectations had been set for April). Delivery of the phone was now promised by 3/23/17. It was nice my phone was coming sooner than originally indicated... but I had been charged nearly $1000 up-front for a phone I had ordered with financing, and that wasn't nice at all.

Interactions with Google Support

Rather upset at this series of events, I proceeded to send a support message to Google. 

My complaint email

The support form promised a reply within 24 hours. 24 hours went by with no reply, but I did receive a reply the following day on 3/18/17.

First response from Google Support - didn't understand or address my issue

Unfortunately, it appears the support person did not even read the entire message. I was assured that my order was not held up any longer (still no explanation has ever been given about what the problem was) and would arrive soon. Nothing about the unauthorized charge or resolving it. I checked with my bank, and the charge had not been reversed.

I responded that I expected this unauthorized charge to be reversed, and wanted to pay for my phone the way it had been ordered, through Google Financing.

My first response, making it clear what action was needed

The Charge Came From Synchrony Bank

The next day (3/19/2017), I received another reply from Google. They said they had looked into the matter, and the bank charge actually came from their financing partner, Synchrony Bank. I would have to take the matter up with Synchrony, because Google had no ability to correct matters directly. They provided basic contact information for Synchrony.

Google Support's second response, saying the charge came from Synchrony

This was disappointing, to say the least. Now I would have to sort out a problem with Google's financing partner? When you are setting up a retail experience, it's fine to have partners but you should take responsibility for providing customers with a seamless experience. That includes managing customer service problems, even if they involve your partners. You wouldn't get this kind of treatment from Amazon: they are famous for stellar customer service.

Sigh. I proceed to the Synchrony bank Contact Us page at https://www.synchronyfinancial.com/contact-us.html, where I see Search or Select a Business. Many companies are listed here. Amazon is listed here. Walmart is listed here. Do you know who isn't listed here? Google.

Synchrony web site contact form - no option for Google

I select Other. Which only gave me an option to call by phone. Up till now, all my interactions has been through email so there would be a record of the communication, but that's not an option here. So, I pick up the phone.

For the next couple of hours, I am on the phone with Synchrony or Google as they in turn transfer me over to the other party.

  • Lavinia at Synchrony doesn't really seem to understand my problem. She assures me any problem must be a Google problem. She transfers me over to Google.


  • Jill at Google does try to help--she spends a lot of time on the phone with me, but ultimately concludes that it is Synchrony who must reverse the charge. I get transferred back to Synchrony.


  • Tina at Synchrony is of no help whatsoever. She doesn't seem to understand anything about the Google-Synchrony relationship or how to resolve issues.

Synchrony's support people are, well, completely useless. They are 100% clueless. Google's support people do seem to be making an effort, but appear to be powerless to actually correct the issue. All of them assure me they want to help me, but each time I end up getting sent back to the other company.

I have now had enough. Apparently no one is able to correct this, and it is extremely frustrating. I decide to accept the situation: my phone is coming, the logistics were screwy, the financing didn't really materialize. It's no longer worth my time. I don't really know who was at fault here but I know it wasn't me. I may just get my next phone from Apple the next time around.

Google Pixel Scorecard

So far, I have to grade my Google Pixel XL experience as follows:


Google gets an F for the order fulfillment experience. This is disappointing to say the least. I like Google. But this has been anything but pleasant. It shows a lot of immaturity when it comes to inventory logistics, reliable order fulfillment processes, and good customer service. Although I received much better support attention from Google than Synchrony, this is ultimately Google's failure because they own the product/process/partnership: it doesn't really matter if the problem was with Google or Synchrony: the responsibility lies with Google and they blew it.

Hopefully my experience with the actual phone (coming next week) will be much better. Stay tuned, I'll let you know.


Tuesday, December 27, 2016

Developing for Google Home: Getting Started

I recently blogged about the new Google Home I received for Christmas. In this blog, I'll talk about how developers can extend Google Home and teach it new conversational skills. Please note I am only 3 days in here, so this is just an introduction.

I'm a career software developer, so naturally I'm interested in extending Home further by developing new abilities for it. Amazon Echo is known to have an extensive developer and partner program with over 5,000 skills available; so what about Home? Google Home also has a developer program, which you can find out about at Actions on Google. Note, when you are developing for Home you are really developing for the Google Assistant, something that potentially can be used in additional ways (such as on Google Pixel phones).

There are several ways to develop conversation actions for Google Home and its Conversation API. One is with the Actions SDK, which includes a NodeJS client library and a web simulator. The other is API.AI, a web-based interface. API.AI is where I'm starting, and that's what I'm writing about today.


I've just started on learning, but I've already gone through the first tutorial and added a new conversation action that I can test in the web simulator or with my actual Google Home device. I can already tell this is going to be a great deal of fun.

Getting Started Sample: Silly Name Maker


Defining an Agent in API.AI

The API.AI site has a walk-through that gives you the steps to build a simple app named Silly Name Maker - and it's really simple - all it does is ask you for a number and a color, and constructs a silly name from that information. But, it does the job of getting you on your way. If you faithfully go to the API.AI site and enter the instructions, you will soon have an agent that looks like this:


Silly Name Maker tutorial sample in API.AI

and interacts like this:
User: Hey Google, Talk to Silly Name Maker.
Silly Name Maker: Hi! Welcome to Silly Name Maker!
Let's get started. What is your lucky number?
User: 22
Silly Name Maker: What is your favorite color?
User: Blue
Silly Name Maker: Alright, your silly name is Blue 22.
I hope you like it! See you next time.
Yes, it's not terribly useful but it does show you the ropes of defining an agent and running it. 

Intents

So what goes into making this agent? It consists of a number of intents. The first intent is named start_name_maker, and it's the first thing that happens after you invoke Silly Name Maker by asking Home to "Talk to Silly Name Maker". That's because it's tagged as a Welcome intent. All this intent does is welcome you to the agent, and prompt the user to select a number (and when the user does that, they'll be triggering the second intent). It's important for Google Home agents to clearly identify themselves to the user so they understand they're now interacting with an agent (app) and not Google.

start_name_maker intent

The second intent is called make_name, and it gets triggered when the user says a number or says "My lucky number is number". The intent can recognize several patterns of input, any of which will trigger it. Note how easy it is in API.AI to define user input - you simply provide samples of what someone would say, and then you can mark parts of that as parameter values. Parameter values have types; for example, @sys.color is a color and @sys.number is a number. 

Some of those "user says" patterns may provide parameter data - in this case, both patterns define the parameter $number. The intent has another parameter, named $color. Since $color is marked as a required parameter, and none of the defined "user says" patterns provide $color, Home will automatically prompt for it when this intent is triggered. That's why the example dialog above asks "What is your favorite color?" The make_name intent incorporates the parameters in its response: "Alright, your silly name is $color $number. I hope you really like it! See you next time!". Although it's not done in this case, it's possible to list multiple text responses for an intent (if more than one is present, one will be selected at random for the response).

name_maker intent

Now let's revisit the earlier sample dialog, with an understanding of what is happening behind the scenes:
User: Hey Google, Talk to Silly Name Maker.
[Silly Name Maker agent is launched. start_make_name intent runs,
and provides its response]
Silly Name Maker: Hi! Welcome to Silly Name Maker!
Let's get started. What is your lucky number?
[start_make_name intent ends]
User: 22
[make_name intent matches user input of number, and executes]
[$number parameter is from the user input]
[$color parameter value has not been provided yet, 
so API.AI prompts for it automatically]
Silly Name Maker: What is your favorite color?
User: Blue
[make_name intent provides its response]
Silly Name Maker: Alright, your silly name is Blue 22.
I hope you like it! See you next time.
[make_name intent ends]

Testing and Deploying your Agent

You can run the agent in a couple of ways. There's a web simulator on the right-hand side of the API.AI site you can interact with: just click the microphone icon and say something. You'll see how that was interpreted and what the response is. Click the Play link to have the response spoken to you.

Test Console built into API.API

Even better, you can test this on your own Google Home device. If you're logged into API.AI with the same Google account that your Google Home device uses, you can deploy it and go over and talk to the Home. Click Integrations on the left-side menu. On the list of One-click Integrations, click Actions on Google.

Integrations

On the Actions on Google dialog that appears, review the options (you can for example change the voice Home will use), then click Authorize at bottom right.

Actions on Google dialog with Authorize option

Now there is a Preview option available at bottom right. Click it. Preview is a temporary deployment (to do a permanent deployment, you'll need to first formally register your agent).

Actions on Google dialog with Preview option

Once you click Preview, you'll soon see confirmation that your agent has been published. Now you can walk over to your Home device and try it out. Begin with "Hey Google, Talk to Silly Name Maker."

Preview published confirmation

In my next post, I'll develop my first solo agent for Google Home.

Sunday, October 8, 2017

An AngularJS Dashboard, Part 3: KPI Tile, Responsive Layout, and Back End

NOTE: for best results, view the http: version of this page (else you won't get syntax highlighting).

This is Part 3 in a series on creating a dashboard in AngularJS. I'm blogging as I progressively create the dashboard, refining my Angular experience along the way.

Previously in Part 2, we developed our first four tiles. We implemented the chart tiles using a service, and demonstrated that we could switch out the service to use different chart providers.

Here in Part 3, we are going to add a new tile for KPIs; start work on a responsive tile rendering algorithm; and create a back-end data service for our dashboard. Our objectives are:

  • Add a new tile type, a KPI tile for visualizing a Key Performance Indicator
  • Render tiles more intelligently, taking available screen size into account
  • Begin a back-end service for the dashboard that loads tile layout from a database
Here's what we'll be ending up with today:

KPI Tile

Our first work item for today will be adding a new tile type. A Key Performance Indicator (KPI) is a measurable value that serves as a key metric in evaluting the state of an organization or its activities (such as revenue, expenses, projects, employee performance, customer satisfaction, productivity, fundraising, etc.).

Visually, our KPI tile will show a bar segmented into three equal sections, colored red, yellow and green. The KPI will plot a value on this bar. The KPI will be a horizontal bar, except in the case of the 1x2 size tile which will be vertical.

Here's what a tile definition for a KPI tile looks like.
{
    title: 'Monthly Revenue',
    type: 'kpi',
    width: 1, height: 1,
    color: 'purple',
    dataSource: 'inline',
    columns: ['$0', '$10K', '$20K', '$30K'],
    label: '$15,000',
    value: 60
},
Key properties in the tile definition are these:
  • type: 'kpi'
  • label: a display version of the value. For example, $15,000 or "60 / 100".
  • value: the value to plot on the bar, as a percentage (0 to 100).
  • columns: an array of four label strings. columns[0] will appear at the start of the red area; [1] at the start of the yellow area; [2] at the start of the green area; and [3] at the end of the green area.

Template Changes

Recall that in our template, inside the ng-repeat loop, is markup code for each tile type, made conditional through the use of ng-if. We'll be adding markup for a kpi tile. There are several variations of this markup based on tile dimensions, so the ng-if checks not only for a tile type of 'kpi' but also specific tile dimensions.
<!-- KPI tile -->
<div ng-if="tile.type=='kpi' && tile.height==1"
        style="text-align: left; position: relative; left: 0; right: 0; margin: 0 auto; overflow-x: hidden; overflow-y: hidden;  height: 200px; padding: 16px">
    <div style="display: block; margin-top: 35px; font-size: 10px;">
        <div style="display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[0]}}</div><div style="display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[1]}}</div><div style="display: inline-block; text-align: left; width: auto; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[2]}}</div><div style="display inline; text-align: right; float: right; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[3]}}</div>
    </div>
    <div style="display: inline-block; width: 100%; height: 15%;  background-color: rgb(45,151,34); border: 1px solid white">
        <div style="background-color: rgb(210,54,34); display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0"></div><div style="background-color: rgb(252,209,22); display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0"></div>
    </div>
    <div style="font-size: 24px; text-align: center">{{tile.label}}</div>
    <div style="position: absolute; top: 75px; left: 17px; right: 17px; bottom: 0"><div style="background-color: black; height: 5%; width: {{tile.value}}%; float: left"></div></div>
</div>
<div ng-if="tile.type=='kpi' && tile.height==2 && tile.width==2"
        style="text-align: left; position: relative; left: 0; right: 0; margin: 0 auto; overflow-x: hidden; overflow-y: hidden;  height: 200px; padding: 16px">
    <div style="display: block; margin-top: 135px; font-size: 10px;">
        <div style="display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[0]}}</div><div style="display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[1]}}</div><div style="display: inline-block; text-align: left; width: auto; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[2]}}</div><div style="display inline; text-align: right; float: right; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[3]}}</div>
    </div>
    <div style="display: inline-block; width: 100%; height: 15%;  background-color: rgb(45,151,34); border: 1px solid white">
        <div style="background-color: rgb(210,54,34); display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0"></div><div style="background-color: rgb(252,209,22); display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0"></div>
    </div>
    <div style="font-size: 24px; text-align: center">{{tile.label}}</div>
    <div style="position: absolute; top: 177px; left: 17px; right: 17px; bottom: 0"><div style="background-color: black; height: 10%; width: {{tile.value}}%; float: left"></div></div>
</div>
<!-- KPI 1x2 (vertical) -->
<div ng-if="tile.type=='kpi' && tile.height==2 && tile.width==1"
        style="text-align: left; position: relative; left: 0; right: 0; margin: 0 auto; overflow-x: hidden; overflow-y: hidden;  height: 416px; padding: 16px">
    <div style="display: block; margin-left: 40px; width: 50%; height: 80%; background-color: rgb(210,54,34); border: 1px solid white">
        <div style="background-color: rgb(45,151,34); display: block; width: 100%; height: 33%; border: none; padding: 0; margin: 0"></div><div style="background-color: rgb(252,209,22); display: block; width: 100%; height: 33%; border: none; padding: 0; margin: 0"></div>
    </div><div style="font-size: 24px; text-align: center">{{tile.label}}</div>
    <div style="position: absolute; margin-top: 17px; margin-bottom: 98px; top:0; left: 90px; right: 17px;  height:333px; width: 20px"><div style="position: absolute; bottom: 0; height: {{tile.value}}%; background-color: black; width: 20px"></div></div>
    <div style="position: absolute; margin-top: 17px; top: 0; left: 10px; font-size: 12px">{{tile.columns[3]}}</div>
    <div style="position: absolute; margin-top: 17px; top: 100px; left: 10px; font-size: 12px">{{tile.columns[2]}}</div>
    <div style="position: absolute; margin-top: 17px; top: 215px; left: 10px; font-size: 12px">{{tile.columns[1]}}</div>
    <div style="position: absolute; margin-top: 17px; top: 318px; left: 10px; font-size: 12px">{{tile.columns[0]}}</div>
</div>

Seeing the KPI Tile in Action

When we run the dashboard, here's what the new KPI tile looks like.

1x1 KPI Tile

Our tile is showing the KPI with the tile properties we defined, in a clear visual presentation. Below are a few other examples of the KPI tile in other sizes.

2x1 KPI Tile

The 1x2 KPI shows a vertical bar rather than a horizontal bar.

1x2 KPI Tile

Now that we've added a new tile type, we're going to update our tile definitions. Below is our new list of tile definitions and how it displays.
self.tiles = [
            {
                title: 'Site Traffic',
                type: 'counter',
                width: 1, height: 1,
                color: 'red',
                dataSource: 'inline',
                value: '1365',
                label: 'Site Visits'
            },
            {
                title: 'Orders',
                type: 'counter',
                width: 1, height: 1,
                color: 'yellow',
                dataSource: 'inline',
                value: '100',
                label: 'Orders'

            },
            {
                title: 'Customer Satisfaction',
                type: 'kpi',
                width: 2, height: 1,
                color: 'gray',
                dataSource: 'inline',
                columns: ['Poor', 'Adequate', '', 'Excellent'],
                label: '90 / 100',
                value: 90
            },
            {
                title: 'Monthly Revenue',
                type: 'kpi',
                width: 2, height: 1,
                color: 'gray',
                dataSource: 'inline',
                columns: ['$0', '$10K', '$20K', '$30K'],
                label: '$15,000',
                value: 50
            },
            {
                title: 'Time Management',
                type: 'donut',
                width: 2, height: 1,
                color: 'green',
                dataSource: 'inline',
                columns: ['Work', 'Eat', 'Commute', 'Watch TV', 'Sleep', "Exercise"],
                value: [11, 2, 2, 2, 7, 4]
            },
            {
                title: 'Employees',
                type: 'table',
                width: 1, height: 2,
                color: 'orange',
                dataSource: 'inline',
                columns: [['Name', 'string'], ['Salary', 'number'], ['FTE', 'boolean']],
                value: [
                            ['Mike', 10000, true],
                            ['Jim', 8000, false],
                            ['Alice', 12500, true],
                            ['Bob', 7000, true],
                            ['Cheryl', 7000, true],
                            ['Don', 7000, true],
                            ['Edith', 7000, true],
                            ['Frank', 7000, true],
                            ['Gary', 7000, true],
                            ['Hazel', 7000, true],
                            ['Ira', 7000, true],
                            ['Jacob', 7000, true],
                            ['Loretta', 7000, true],
                            ['Mary', 7000, true],
                            ['Neil', 7000, true],
                            ['Obadiah', 7000, true],
                            ['Peter', 7000, true],
                            ['Quincy', 7000, true]
                ]
            },
            {
                title: 'Charity Fundraising',
                type: 'kpi',
                width: 1, height: 2,
                color: 'purple',
                dataSource: 'inline',
                columns: ['$0', '$5K', '$10K', '$15K'],
                label: '$7,500',
                value: 50
            },
            {
                title: 'Precious Metals',
                type: 'column',
                width: 2, height: 2,
                color: 'blue',
                dataSource: 'inline',
                columns: ['Copper', 'Silver', 'Gold', 'Platinum'],
                value: [8.94, 10.49, 19.30, 21.45]
            }
        ];
Revised Tile Definitions Including KPI Tiles

Here's what our revised layout now looks like when run:

Revised Dashboard with KPI Tiles

Making Our Tile Layout Responsive

Previously, our algorithm for displaying tiles was pretty simple. Each tile was 200 pixels wide plus a 16px right margin, and if we kept displaying them one after the other, they'd eventually wrap to the next line--but would still look nice and aligned. Except that wasn't the whole story. We are supporting tiles that can be 1- or 2-unit wide and 1- or 2-unit high. That can leave odd gaps in the display, depending on the variation and order of tiles combined with the size of a user's screen area. 

To improve on this, we're going to move away from the original method of rendering and instead use an HTML table (HTML tables are much-maligned these days by designers, but they still have their place and this is a good use of a table). We'll be able to take advantage of the colspan and rowspan attributes in our table for positioning our extra-wide and extra-high tiles. 

Our goal for today is to do the following:
  • Be aware of windows resizing, and dynamically compute the number of tile units across we can fit.
  • Render our tiles in an HTML table
This will be an improvement, but it doesn't yet get us "smart fill", where tiles are moved to plug any space gaps. We will address that in a future post.

Adding a New Directive, window-size

You may recall that in Part 2 we had a need to execute some JavaScript after the HTML template was rendered by Angular in order to draw our charts. We added an Angular directive named "after-render". We have a similar need now to compute the number of tiles we can fit across whenever our window is resized, and then to re-render the HTML template. To do this, we will add a new directive named window-size. And, we'll be removing the after-render directive because it is now superfluous now that we have window-resize.

Below is our app.module.js, with after-render removed and window-size added. Window-size records the change in screen height and width, and computes tile-units-across, both initially and whenever the browser window is resized. It also calls a new function in the dashboard controller named computeLayout. We'll study this and other controller changes in just a minute.
var dashboardApp = null;
var dashboardApp = null;

(function () {
    'use strict';

    // Define the 'dashboardApp' module
    dashboardApp = angular.module('dashboardApp', [
      'dashboard'
    ]);

    angular.module('dashboardApp').directive('windowSize', function ($window) {
        return function (scope, element) {
            var w = angular.element($window);
            scope.getWindowDimensions = function () {
                var tilesAcross = 1;
                var width = w.width()-100;
                if (width > 0) {
                    tilesAcross = Math.floor(width / 216);
                }
                return {
                    'h': w.height(),
                    'w': w.width(),
                    'tilesAcross': tilesAcross,
                    'tablecolumns': tilesAcross,
                    'tablerows': 3
                };
            };
            scope.$watch(scope.getWindowDimensions, function (newValue, oldValue) {
                scope.windowHeight = newValue.h;
                scope.windowWidth = newValue.w;
                scope.tilesAcross = newValue.tilesAcross;
                scope.tablecolumns = newValue.tilesAcross;
                scope.tablerows = 3;
                scope.style = function () {
                    return {
                        'height': (newValue.h - 100) + 'px',
                        'width': (newValue.w - 100) + 'px',
                        'tilesAcross': newValue.tilesAcross,
                        'tablecolumns': tilesAcross,
                        'tablerows': 3
                    };
                };
                var ctrl = angular.element('#dashboard').scope().$$childHead.$ctrl;
                ctrl.computeLayout();
                deferCreateCharts();
            }, true);

            w.bind('resize', function () {
                scope.$apply();
            });
        }
    })

    // Define the dashboard module
    angular.module('dashboard', [
      'dashboard'
    ]);

})();
app.module.js with after-render directive removed and window-size added


Controller Changes

Our controller, dashboard.component.js, needs a number of changes to support our smarter layout. Up until now, our template has used ng-repeat to loop through the controller's Tiles array. Now that we've added smarter layout, we need a version of the tile layout that has been generated for the user's current screen dimensions. We're going to make these changes to our controller:
  • Add a new property named layoutView. We will be changing the template and other code to use layoutView as the reference for tile layout instead of controller.tiles. Controller.tiles remains the master layout, but we'll derive an updated layoutView each time there has been a change in window size.
  • Add a new function named computeLayout. This function is called by window-size each time the window size has changed. it will re-compute the layoutView, and Angular will re-render the HTML template with the revised layout.
Here are the additions to the controller:
// Register 'dashboard' component, along with its associated controller and template

angular.module('dashboard').component('dashboard', {
    templateUrl: '/components/dashboard/dashboard.template.html',
    controller: [
        '$scope', '$window', '$http', 'ChartService', function DashboardController($scope, $window, $http, ChartService) {
            var self = this;
            var ctrl = angular.element('#dashboard').scope().$$childHead.$ctrl;

            self.chartProvider = ChartService.chartProvider();

            self.tablecolumns = function () {
                return $scope.tablecolumns;
            }

            self.tablerows = function () {
                return $scope.tablerows;
            }

            self.title = 'Sample Dashboard';
            self.tiles = [
                {
                    title: 'Site Traffic',
                    type: 'counter',
                    width: 1, height: 1,
                    color: 'red',
                    value: '1365',
                    label: 'Site Visits'
                },
                ...
            ];

            self.layoutView = [];           // set by self.computeLayout

            ...

            // Compute layout based on current screen dimensions (self.tablecolumns()) and generate updated tile layout
            // Inputs: self.tablecolumns() ........... number of tiles across.
            // Ouptuts: self.layoutView .............. updated array of tile rows, each element an array of tiles for a row [[ tile1, ... tileN ], ... [ tileX, ... tileZ ] ]

            self.computeLayout = function () {
                var numcols = self.tablecolumns();
                if (numcols < 1) numcols = 1;
                var newlayout = [];

                var index = 0;
                var row = [];

                var tileid = 1;

                var tile = null;
                for (var t = 0; t < self.tiles.length; t++) {
                    tile = self.tiles[t];
                    if (index + tile.width > numcols) { // no more room in row. commit row and start new one.
                        newlayout.push(row);
                        row = [];
                        index = 0;
                    }
                    tile.id = tileid.toString();
                    tileid = tileid + 1;
                    row.push(tile);
                    index = index + tile.width;
                }
                if (row.length > 0) {   // commit final row if not empty.
                    newlayout.push(row);
                }

                $scope.tablerows = newlayout.length;

                self.layoutView = newlayout;
            };

        }]
});
Additions to dashboard.component.js controller

Let's talk about computeLayout(). It iterates through the master tile layout, self.tiles, constructing an array of row items, where each row item is an array of tiles. If a tile won't fit in the current row, the row is added to the array and a new row is started. Thus, we end up with a layout that will fit on the user's current screen width. When complete, this array of arrays becomes the new value for self.layoutView and the template is re-rendered. On a 1920-pixel wide browser window, layoutView has this value:
[
    [ // row 1
        {
            title: 'Site Traffic',
            type: 'counter',
            width: 1, height: 1,
            color: 'red',
            value: '1365',
            label: 'Site Visits'
        },
        {
            title: 'Orders',
            type: 'counter',
            width: 1, height: 1,
            color: 'yellow',
            value: '100',
            label: 'Orders'

        },
        {
            title: 'Customer Satisfaction',
            type: 'kpi',
            width: 2, height: 1,
            color: 'gray',
            dataSource: 'inline',
            columns: ['Poor', 'Adequate', '', 'Excellent'],
            label: '90 / 100',
            value: 90
        },
        {
            title: 'Monthly Revenue',
            type: 'kpi',
            width: 2, height: 1,
            color: 'gray',
            dataSource: 'inline',
            columns: ['$0', '$10K', '$20K', '$30K'],
            label: '$15,000',
            value: 50
        }
    ],
    [ // row 2
        {
            title: 'Time Management',
            type: 'donut',
            width: 2, height: 1,
            color: 'purple',
            columns: ['Work', 'Eat', 'Commute', 'Watch TV', 'Sleep', "Exercise"],
            value: [11, 2, 2, 2, 7, 4]
        },
        {
            title: 'Employees',
            type: 'table',
            width: 1, height: 2,
            color: 'orange',
            columns: [['Name', 'string'], ['Salary', 'number'], ['FTE', 'boolean']],
            value: [
                        ['Mike', 10000, true],
                        ['Jim', 8000, false],
                        ['Alice', 12500, true],
                        ['Bob', 7000, true],
                        ['Cheryl', 7000, true],
                        ['Don', 7000, true],
                        ['Edith', 7000, true],
                        ['Frank', 7000, true],
                        ['Gary', 7000, true],
                        ['Hazel', 7000, true],
                        ['Ira', 7000, true],
                        ['Jacob', 7000, true],
                        ['Loretta', 7000, true],
                        ['Mary', 7000, true],
                        ['Neil', 7000, true],
                        ['Obadiah', 7000, true],
                        ['Peter', 7000, true],
                        ['Quincy', 7000, true]
            ]
        },
        {
            title: 'Charity Fundraising',
            type: 'kpi',
            width: 1, height: 2,
            color: 'purple',
            dataSource: 'inline',
            columns: ['$0', '$5K', '$10K', '$15K'],
            label: '$7,500',
            value: 50
        },
        {
            title: 'Precious Metals',
            width: 2, height: 2,
            color: 'blue',
            type: 'column',
            columns: ['Copper', 'Silver', 'Gold', 'Platinum'],
            value: [8.94, 10.49, 19.30, 21.45]
        }
    ]
]
layoutView array

Template Changes

Our template needs a number of changes:
  • Reference the new window-size directive (line 1), and remove after-render
  • Replacement of prior outer markup and ng-angular with new markup to generate a table (lines 6-8). 
  • Use of ng-repeat in tr and td elements that reference controller's layoutView, which replaces the previous div with ng-repeat that looped through controller's tiles array
The  inner markup that renders each tile (lines 9-72) doesn't need any changes, except that we're referencing tile.id instead of $index to get tile ids.
<div class="dashboard-controller" window-size>
    <div>
        <div>{{$ctrl.title}} (chart provider: {{$ctrl.chartProvider}})</div>
        <span after-render="deferCreateCharts"></span>
        <div class="dashboard-panel">
            <table>
                <tr ng-repeat="row in $ctrl.layoutView track by $index">
                    <td ng-repeat="tile in row" colspan="{{tile.width}}">
                        <div id="tile-{{tile.id}}"
                             class="tile" ng-class="tile.classes"
                             style="overflow: hidden"
                             draggable="true" ondragstart="tile_dragstart(event);"
                             ondrop="tile_drop(event);" ondragover="tile_dragover(event);">
                            <div class="hovermenu"><i class="fa fa-ellipsis-h" aria-hidden="true"></i></div>
                            <div style="overflow: hidden; white-space: nowrap">{{tile.title}}</div>
                            <div style="position: relative; height: 100%">
                                <!-- COUNTER tile -->
                                <div ng-if="tile.type=='counter' && tile.height==1"
                                     style="text-align: center; position: absolute; left: 0; right: 0; margin: 0 auto; top: 25px">
                                    <div style="font-size: 72px">{{tile.value}}</div>
                                    <div style="font-size: 24px">{{tile.label}}</div>
                                </div>
                                <div ng-if="tile.type=='counter' && tile.height==2"
                                     style="text-align: center; position: absolute; left: 0; right: 0; margin: 0 auto; top: 135px">
                                    <div style="font-size: 72px">{{tile.value}}</div>
                                    <div style="font-size: 24px">{{tile.label}}</div>
                                </div>
                                <!-- KPI tile -->
                                <div ng-if="tile.type=='kpi' && tile.height==1"
                                     style="text-align: left; position: relative; left: 0; right: 0; margin: 0 auto; overflow-x: hidden; overflow-y: hidden;  height: 200px; padding: 16px">
                                    <div style="display: block; margin-top: 35px; font-size: 10px;">
                                        <div style="display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[0]}}</div><div style="display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[1]}}</div><div style="display: inline-block; text-align: left; width: auto; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[2]}}</div><div style="display inline; text-align: right; float: right; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[3]}}</div>
                                    </div>
                                    <div style="display: inline-block; width: 100%; height: 15%;  background-color: rgb(45,151,34); border: 1px solid white">
                                        <div style="background-color: rgb(210,54,34); display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0"></div><div style="background-color: rgb(252,209,22); display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0"></div>
                                    </div>
                                    <div style="font-size: 24px; text-align: center">{{tile.label}}</div>
                                    <div style="position: absolute; top: 75px; left: 17px; right: 17px; bottom: 0"><div style="background-color: black; height: 5%; width: {{tile.value}}%; float: left"></div></div>
                                </div>
                                <div ng-if="tile.type=='kpi' && tile.height==2 && tile.width==2"
                                     style="text-align: left; position: relative; left: 0; right: 0; margin: 0 auto; overflow-x: hidden; overflow-y: hidden;  height: 200px; padding: 16px">
                                    <div style="display: block; margin-top: 135px; font-size: 10px;">
                                        <div style="display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[0]}}</div><div style="display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[1]}}</div><div style="display: inline-block; text-align: left; width: auto; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[2]}}</div><div style="display inline; text-align: right; float: right; height: 100%; border: none; padding: 0; margin: 0">{{tile.columns[3]}}</div>
                                    </div>
                                    <div style="display: inline-block; width: 100%; height: 15%;  background-color: rgb(45,151,34); border: 1px solid white">
                                        <div style="background-color: rgb(210,54,34); display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0"></div><div style="background-color: rgb(252,209,22); display: inline-block; width: 33%; height: 100%; border: none; padding: 0; margin: 0"></div>
                                    </div>
                                    <div style="font-size: 24px; text-align: center">{{tile.label}}</div>
                                    <div style="position: absolute; top: 177px; left: 17px; right: 17px; bottom: 0"><div style="background-color: black; height: 10%; width: {{tile.value}}%; float: left"></div></div>
                                </div>
                                <!-- KPI 1x2 (vertical) -->
                                <div ng-if="tile.type=='kpi' && tile.height==2 && tile.width==1"
                                     style="text-align: left; position: relative; left: 0; right: 0; margin: 0 auto; overflow-x: hidden; overflow-y: hidden;  height: 416px; padding: 16px">
                                    <div style="display: block; margin-left: 40px; width: 50%; height: 80%; background-color: rgb(210,54,34); border: 1px solid white">
                                        <div style="background-color: rgb(45,151,34); display: block; width: 100%; height: 33%; border: none; padding: 0; margin: 0"></div><div style="background-color: rgb(252,209,22); display: block; width: 100%; height: 33%; border: none; padding: 0; margin: 0"></div>
                                    </div><div style="font-size: 24px; text-align: center">{{tile.label}}</div>
                                    <div style="position: absolute; margin-top: 17px; margin-bottom: 98px; top:0; left: 90px; right: 17px;  height:333px; width: 20px"><div style="position: absolute; bottom: 0; height: {{tile.value}}%; background-color: black; width: 20px"></div></div>
                                    <div style="position: absolute; margin-top: 17px; top: 0; left: 10px; font-size: 12px">{{tile.columns[3]}}</div>
                                    <div style="position: absolute; margin-top: 17px; top: 100px; left: 10px; font-size: 12px">{{tile.columns[2]}}</div>
                                    <div style="position: absolute; margin-top: 17px; top: 215px; left: 10px; font-size: 12px">{{tile.columns[1]}}</div>
                                    <div style="position: absolute; margin-top: 17px; top: 318px; left: 10px; font-size: 12px">{{tile.columns[0]}}</div>
                                </div>
                                <div ng-if="tile.type=='table'"
                                     style="text-align: left !important; padding: 16px; height: 100%">
                                    <div style="height: 100%; text-align: left !important">
                                        <table style="padding-bottom: 52px;">
                                            <tr>
                                                <th ng-repeat="col in tile.columns">{{col[0]}}</th>
                                            </tr>
                                            <tr ng-repeat="row in tile.value">
                                                <td ng-repeat="cell in row">
                                                    <div ng-if="tile.columns[$index][1]=='number'" class="td-right">{{cell}}</div>
                                                    <div ng-if="tile.columns[$index][1]!='number'">{{cell}}</div>
                                                </td>
                                            </tr>
                                        </table>
                                    </div>
                                </div>
                                <!-- TABLE tile -->
                                <div ng-if="tile.type=='table'"
                                     style="text-align: left !important; padding: 16px; height: 100%">
                                    <div style="height: 100%; text-align: left !important">
                                        <table style="padding-bottom: 52px;">
                                            <tr>
                                                <th ng-repeat="col in tile.columns">{{col[0]}}</th>
                                            </tr>
                                            <tr ng-repeat="row in tile.value">
                                                <td ng-repeat="cell in row">
                                                    <div ng-if="tile.columns[$index][1]=='number'" class="td-right">{{cell}}</div>
                                                    <div ng-if="tile.columns[$index][1]!='number'">{{cell}}</div>
                                                </td>
                                            </tr>
                                        </table>
                                    </div>
                                </div>
                                <!-- Google DONUT tile -->
                                <div ng-if="$ctrl.chartProvider=='Google' && tile.type=='donut'"
                                     style="text-align: center; padding: 16px; height: 100%">
                                    <div class="donutchart" id="tile-donutchart-{{tile.id}}" style="height: 100%"></div>
                                </div>
                                <!-- Google COLUMN tile -->
                                <div ng-if="$ctrl.chartProvider=='Google' && tile.type=='column'"
                                     style="text-align: center; padding: 16px; height: 100%">
                                    <div class="columnchart" id="tile-columnchart-{{tile.id}}" style="height: 100%"></div>
                                </div>
                                <!-- Chart.js DONUT tile -->
                                <div ng-if="$ctrl.chartProvider=='ChartJS' && tile.type=='donut'"
                                     style="text-align: center; padding: 16px; height: 100%">
                                    <canvas class="chartjs-donutchart" id="tile-canvas-{{tile.id}}" style="margin: 0 auto; width: 90%; max-width: 90%; height: auto; max-height: 80%"></canvas>
                                </div>
                                <!-- Chart.js COLUMN tile -->
                                <div ng-if="$ctrl.chartProvider=='ChartJS' && tile.type=='column'"
                                     style="text-align: center; padding: 16px; height: 100%">
                                    <canvas class="chartjs-columnchart" id="tile-canvas-{{tile.id}}" style="margin: 0 auto; width: 90%; max-width: 90%; height: auto; max-height: 80%" ng-if="tile.width==1 && tile.height==1"></canvas>
                                    <canvas class="chartjs-columnchart" id="tile-canvas-{{tile.id1}}" style="margin: 0 auto; width: 90%; max-width: 90%; height: auto; max-height: 75%" ng-if="tile.width==2 && tile.height==1"></canvas>
                                    <canvas class="chartjs-columnchart" id="tile-canvas-{{tile.id}}" style="margin: 0 auto; width: 90%; max-width: 90%; height: auto; max-height: 85%" ng-if="tile.height==2"></canvas>
                                </div>
                                <!-- PLACEHOLDER tile -->
                                <div ng-if="tile.type=='placeholder'"
                                     style="position: absolute; top: 0">
                                </div>
                            </div>
                        </div> <!-- end tile -->
                    </td>
                </tr>
            </table>
        </div>
    </div>

    <script lang="javascript">
        // Handler for start of tile drag.
        function tile_dragstart(ev) {
            ev.dataTransfer.dropEffect = "move";
            var sourceId = getParentTileId(ev.target);
            if (sourceId != null) {
                ev.dataTransfer.setData("text/plain", sourceId);
            }
        }

        // Handler for tile dragover.
        function tile_dragover(ev) {
            ev.preventDefault();
            ev.dataTransfer.dropEffect = "move";
        }

        var controller = null;

        // Handler for tile drop.
        function tile_drop(ev, tiles) {
            ev.preventDefault();
            var destId = getParentTileId(ev.target);
            var sourceTileIndex = parseInt(ev.dataTransfer.getData("text").substring(5)) - 1;
            var destTileIndex = parseInt(destId.substring(5)) - 1;
            var ctrl = angular.element('#dashboard').scope().$$childHead.$ctrl;
            controller = ctrl;
            ctrl.dragTile(sourceTileIndex, destTileIndex);
        }

        // Given a drag element (which may or may not have an Id, and which may or may not be in a tile), return the tile Id that contains it. Or null, if not in a tile.
        function getParentTileId(element) {
            var ctrl = angular.element('#dashboard').scope().$$childHead.$ctrl;
            for (var t = 1; t <= ctrl.tiles.length; t++) {
                if (document.getElementById('tile-' + t.toString()).contains(element)) {
                    return 'tile-' + t.toString();
                } // end if
            } // next t
            return null;
        }
    </script>
</div>

Seeing The New Layout in Action

Now we're ready to try things out. For starters, we'll run the project in a full-screen browser window on my 1920-wide Dell laptop.

The tiles are rendering now in a table, and the dashboard has concluded it can fit 6 tile units across.

Dashboard on 1920-wide laptop

Now, we'll reduce our window size, to two-thirds of the width. The dashboard re-renders, with a revised layout to fit the new screen width. Now, there is only room for four tiles across.


Let's reduce it even more, to the width of a smartphone.


This last example shows why we will need to go further and do "smart fill." At a future time, we'll be adding the smarts to move tiles higher up in the layout to fill gaps. Today, though, this is as far as we'l go on revising the layout and rendering.

Creating a Back End

Up until now, we've been cheating a bit. Our tile definitions and their data are just inline in the dashboard controller. This isn't very "real world". In the real world, the layout for a dashboard would vary from one customer to another. We'd want users to be able to modify their layout and have it remembered. And, we'd expect the data for these tiles to come from a real data source, such as an application database.

To create a back end, we'll be adding another service, the DataService. We saw in Part 2 that injecting a chart service was a very useful pattern, because it allowed us to have multiple implementations of the service. We are going to do exactly the same thing with the DataService, with two implementations in mind:

  • The first will be a "demo" data service, with the same inline tile definitions and data that have been in our poject all along, but now separated into a service. 
  • The second version of the data service will access ASP.NET MVC controller actions, which in turn will draw data from a SQL Server database.

Controller Updates for New Data Service

First off, we want to tell Angular the our dashboard controller now requires a data service.
angular.module('dashboard').component('dashboard', {
    templateUrl: '/components/dashboard/dashboard.template.html',
    controller: [
        '$scope', '$window', '$http', 'ChartService', 'DataService', function DashboardController($scope, $window, $http, ChartService, DataService) {
            var self = this;
            var ctrl = angular.element('#dashboard').scope().$$childHead.$ctrl;

            self.chartProvider = ChartService.chartProvider();
            self.dataProvider = DataService.dataProvider();
Injecting Data Service in dashboard.component.js

Here's a diagram illustrating the actors in our project now--highlighting the newly-added data service.


Data Service injected into Controller

As with the chart controller, the first function we'll add to the data service is a simple one returning the name of the data provider in case we want to display it.

Previousy, the tile definitions were inline in the dashoard controller. We're replacing that code with a call to the data service to get the tile layout:
            self.title = 'Sample Dashboard';
            self.tiles = DataService.getTileLayout();
            self.layoutView = [];           // set by self.computeLayout
Retrieving tile layout from data service

Demo DataService

Now we'll create the demo data service. We're adding a new code module in our project's dashboard folder named demo.data.service.js. In it we have two functions:
  • dataProvider : this function returns the name of the data service ("demo")
  • getTileLayout : this function returns the tiles array that the controller needs.
'use strict';
'use strict';

// DemoDataService : DataService, contains functions for access tile layouts and data for tiles (demo version, all data is inline).
//
// dataProvider ............. returns the name of the data provider ("demo")
// getTileLayout ............ returns the master tile layout for the dashboard

dashboardApp.service('DataService', function ($http) {

    this.dataProvider = function () {
        return 'Demo';
    }

    // -------------------- getTileLayout : Returns the master layout for tiles (returns tiles array) ------------------

    this.getTileLayout = function (dashboardName) {
        var tiles = [
                    {
                        title: 'Site Traffic',
                        type: 'counter',
                        width: 1, height: 1,
                        color: 'red',
                        dataSource: 'inline',
                        value: '1365',
                        label: 'Site Visits'
                    },
                    {
                        title: 'Orders',
                        type: 'counter',
                        width: 1, height: 1,
                        color: 'yellow',
                        dataSource: 'inline',
                        value: '100',
                        label: 'Orders'

                    },
                    {
                        title: 'Customer Satisfaction',
                        type: 'kpi',
                        width: 2, height: 1,
                        color: 'gray',
                        dataSource: 'inline',
                        columns: ['Poor', 'Adequate', '', 'Excellent'],
                        label: '90 / 100',
                        value: 90
                    },
                    {
                        title: 'Monthly Revenue',
                        type: 'kpi',
                        width: 2, height: 1,
                        color: 'gray',
                        dataSource: 'inline',
                        columns: ['$0', '$10K', '$20K', '$30K'],
                        label: '$15,000',
                        value: 50
                    },
                    {
                        title: 'Time Management',
                        type: 'donut',
                        width: 2, height: 1,
                        color: 'green',
                        dataSource: 'inline',
                        columns: ['Work', 'Eat', 'Commute', 'Watch TV', 'Sleep', "Exercise"],
                        value: [11, 2, 2, 2, 7, 4]
                    },
                    {
                        title: 'Employees',
                        type: 'table',
                        width: 1, height: 2,
                        color: 'orange',
                        dataSource: 'inline',
                        columns: [['Name', 'string'], ['Salary', 'number'], ['FTE', 'boolean']],
                        value: [
                                  ['Mike', 10000, true],
                                  ['Jim', 8000, false],
                                  ['Alice', 12500, true],
                                  ['Bob', 7000, true],
                                  ['Cheryl', 7000, true],
                                  ['Don', 7000, true],
                                  ['Edith', 7000, true],
                                  ['Frank', 7000, true],
                                  ['Gary', 7000, true],
                                  ['Hazel', 7000, true],
                                  ['Ira', 7000, true],
                                  ['Jacob', 7000, true],
                                  ['Loretta', 7000, true],
                                  ['Mary', 7000, true],
                                  ['Neil', 7000, true],
                                  ['Obadiah', 7000, true],
                                  ['Peter', 7000, true],
                                  ['Quincy', 7000, true]
                        ]
                    },
                    {
                        title: 'Charity Fundraising',
                        type: 'kpi',
                        width: 1, height: 2,
                        color: 'purple',
                        dataSource: 'inline',
                        columns: ['$0', '$5K', '$10K', '$15K'],
                        label: '$7,500',
                        value: 50
                    },
                    {
                        title: 'Precious Metals',
                        type: 'column',
                        width: 2, height: 2,
                        color: 'blue',
                        dataSource: 'inline',
                        columns: ['Copper', 'Silver', 'Gold', 'Platinum'],
                        value: [8.94, 10.49, 19.30, 21.45]
                    }
                ];
        return tiles;
    }

});
demo.data.service.js

getTileLayout() returns the same tiles array we've always had, but with one difference. We've added a dataSource property to each tile. For the demo data service, this is just set to 'inline', signifying that the tile already have their data. Later on when we create the alternate edition of the data service that talks to a database, the dataSource property will identify the name of the query that will be used to pull tile data.

In index.html, our web page, we now must include our data service.
        <!-- Google Visualization chart service -->
        <script src="/components/dashboard/google.chart.service.js"></script>
        <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>

        <!-- Chart.js chart service -->
        <!--<script src="/components/dashboard/chartjs.chart.service.js"></script>
        <script src="/Scripts/Chart.bundle.js" type="text/javascript"></script>-->

        <!-- Demo data service -->
        <script src="/components/dashboard/demo.data.service.js"></script>

        <script src="/components/dashboard/dashboard.component.js"></script>

    </body>
</html>

Trying Out the Demo Data Service

Now we can try things out. We clear our cache and launch the project, and soon we see a familiar sight:
Dashboard now using demo data service

Our dashboard doesn't look any different, but we've made some important internal changes. The tile layout and tile data now comes from a data service.

A Real Back-End

The data service we just created, DemoDataService, still has tile definitions and tile data inline in the JavaScript. Now that we have the shape of the service defined, it's time to create a real data service that talks to a web server back end and retrieves data from a database.

We need to give our app a database, so we'll create one. If you download the sample project, you'll find that it's configured to use the demo data service (demo.data.service.js) and no database is required. However, the readme file explains how to run a script (dashboard_database.sql) to create a SQL Server database and how to change the project to use the SQL Server data service (sql.data.service.js).

Creating the Dashboard Database

Our database is mimicking an application database. We will ultimately depend on our database for (1) dashboard configuration (definitions of tiles and dashboard layout,) and (2) application data to query to set values for each tile. Today, we are going to tackle the first category, dashboard configuration. We will address the second category, querying application data, in our next post. This means we'll be hitting the database to find out which tiles to show, but we'll still have hard-coded data values for right now.

To track database configuration, we'll create these tables:

  • A DashboardLayout table to represent the current dashboard tile layout. This will include not only a default layout, but saved layouts for individual users. If an entry can't be found for the user's username, we'll select the record named 'default'.
  • A DashboardLayoutTile table to link the DashboardLayout record to individual tiles and an order of layout. If a dashboard had 5 tiles, it would have 5 DashboardLayoutTile records.
  • A DashboardLayoutTileProperty table to hold each property name and value for a tile.
The relationship between these tables is as follows.
  • The back end will locate a user's dashboard record in the DashboardLayout table (if a customized record is not found for a user, then 'default' is used to get the default layout).
  • The sequence of tiles is found by reading records from the subordinate DashboardLayoutTile table. This gives us the order of tiles.
  • The tile detail (properties) are loaded from DashboardLayoutTileProperty for each tile.

Relataionship of Dashboard Configuration Tables

Here's how our tables look loaded with some configuration data. We've defined five tiles in our default dashboard (one of each type).

Sample Dashboard Configuration Data

With all of this in place, we can now implement our SQL Server data service.

SQL Server Data Service

As with the Demo Data Service, the SQL Server Data Service has the same functions (dataProvider and getTileLayout). To get the tile layout, the service needs to talk to the back end MVC action /Dashboard/Index which will query the dashboard database tables and return a Json tiles array.
'use strict';

// SqlDataService : DataService, contains functions for access tile layouts and data for tiles (SQL Server version, all data comes from database).
//
// dataProvider ............. returns the name of the data provider ("demo")
// getTileLayout ............ returns the master tile layout for the dashboard

var dbg = null;

dashboardApp.service('DataService', function ($http) {

    this.dataProvider = function () {
        return 'SQL Server';
    }

    // -------------------- getDashboard :  getTileLayout : Returns the master layout for tiles (returns tiles array) ------------------

    this.getTileLayout = function () {   // TODO: add dashboard-name as param

        var request = $http({
            method: "GET",
            url: "/Dashboard/",
        });

        return (request.then(handleSuccess, handleError));
    }

    function handleSuccess(response) {

        var tile = null;
        var tiles = [];

        var name = '';
        var value = '';

        // create tile object with properties

        for (var t = 0; t < response.data.Tiles.length; t++) {
            tile = {};
            for (var p = 0; p < response.data.Tiles[t].Properties.length; p++) {
                name = response.data.Tiles[t].Properties[p].PropertyName;
                value = response.data.Tiles[t].Properties[p].PropertyValue;
                switch (name) {
                    case 'columns': // column values beginning with [ are evaluated into arrays.
                        tile[name] = eval(value);
                        break
                    case 'width':       // treat these property value as integers
                    case 'height':
                        tile[name] = parseInt(value);
                        break;
                    case 'value':
                        if (value.substring(0, 1) == '[') {
                            tile[name] = eval(value);
                        }
                        else {
                            tile[name] = value;
                        }
                        break;
                    default:    // treat all other property values as strings
                        tile[name] = value;
                        break;
                }
            }
            tiles.push(tile);   // add tile to tiles array
        }

        return tiles;
    }

    function handleError(response) {
        console.log('sql.data.service.js : handleError 01');
        alert('error');
    }

});

sql.server.data.service

Note that we do some post-processing after getting the Json tiles array back from the MVC action. If you recall out previous inline tiles structure, proeprties like columns and value sometimes contain arrays. Since we're just storing the value property for a tile in the database as a string, values like '[ 1, 2, 3, 4, 5 ]' need to converted from a string to a JavaScript array. We do this with the JavaScript eval function (lines 45 and 53).


Revised Dashboard Controller

Although we've taken pains to implement identical functions in our second data service, it does have some behavioral differences that are going to force us to revise our Angular dashboard controller code. The DataService's getTileLayout function makes an Ajax call to the back end, and then returns the Json tile layout with a JavaScript promise. This means the controller's self.tiles = DataService.getTileLayout(); statement won't work correctly. Instead, we need to assign the function result in a promise (lines 136-161 below). And, since the rest of the initialization code in the controller isn't ready to execute until we have the tiles array set, we'll move all the rest of that code into the promise as well.
//'use strict'; // removed because of complaints about dragTile function - in strict mode code, functions can only be declared at top level or inside a block
// Register 'dashboard' component, along with its associated controller and template

angular.module('dashboard').component('dashboard', {
    templateUrl: '/components/dashboard/dashboard.template.html',
    //templateUrl: '/Scripts/HRMS/Dash/dashboard/dashboard.template.html',
    controller: [
        '$scope', '$window', '$http', 'ChartService', 'DataService', function DashboardController($scope, $window, $http, ChartService, DataService) {
            var self = this;
            var ctrl = angular.element('#dashboard').scope().$$childHead.$ctrl;

            self.chartProvider = ChartService.chartProvider();
            self.dataProvider = DataService.dataProvider();

            self.tablecolumns = function () {
                return $scope.tablecolumns;
            }

            self.tablerows = function () {
                return $scope.tablerows;
            }

            self.title = 'Sample Dashboard';

            // Complete a drag-and-drop operation.

            self.dragTile = function (sourceTileIndex, destTileIndex) {

                try {
                    var sourceTile = self.tiles[sourceTileIndex];

                    self.tiles.splice(sourceTileIndex, 1); // remove source tile from tile list

                    if (sourceTileIndex < destTileIndex) {
                        destTileIndex--;
                    }

                    self.tiles.splice(destTileIndex, 0, sourceTile); // insert dest tile

                    // Regenerate tile ids.
                    var tile;
                    if (self.tiles != null) {
                        for (var t = 0; t < self.tiles.length; t++) {
                            tile = self.tiles[t];
                            tile.id = 'tile-' + (t + 1).toString(); // tile id (tile_1, tile_2, ...)
                        }
                    }

                    var scope = angular.element('#dashboard').scope();
                    scope.$apply(this);
                } catch (e) {
                    console.log('dragTile: EXCEPTION '); // + e.toString());
                }
            };

            // Draw charts (tile types: donut, column, table) - execute JavaScript to invoke chart service.

            self.createCharts = function () {

                if (self.chartProvider == 'ChartJS') {
                    $('.chartjs-donutchart').each(function () {
                        self.drawDonutChart(this.id);
                    });

                    $('.chartjs-columnchart').each(function () {
                        self.drawColumnChart(this.id);
                    });
                }

                if (self.chartProvider == 'Google') {
                    $('.donutchart').each(function () {
                        self.drawDonutChart(this.id);
                    });

                    $('.columnchart').each(function () {
                        self.drawColumnChart(this.id);
                    });
                }
            }

            // Draw a donut chart

            self.drawDonutChart = function (elementId) {
                ChartService.drawDonutChart(self, elementId);
            }

            // Draw a column chart

            self.drawColumnChart = function (elementId) {
                ChartService.drawColumnChart(self, elementId);
            }

            // Compute layout based on current screen dimensions (self.tablecolumns()) and generate updated tile layout
            // Inputs: self.tablecolumns() ........... number of tiles across.
            // Ouptuts: self.layoutView .............. updated array of tile rows, each element an array of tiles for a row [[ tile1, ... tileN ], ... [ tileX, ... tileZ ] ]

            self.computeLayout = function () {
                if (self.tiles == null) return;

                var numcols = self.tablecolumns();
                if (numcols < 1) numcols = 1;

                var newlayout = [];

                var index = 0;
                var row = [];

                var tileid = 1;

                var tile = null;
                for (var t = 0; t < self.tiles.length; t++) {
                    tile = self.tiles[t];
                    if (index + tile.width > numcols) { // no more room in row. commit row and start new one.
                        newlayout.push(row);
                        row = [];
                        index = 0;
                    }
                    tile.id = tileid.toString();
                    tileid = tileid + 1;
                    row.push(tile);
                    index = index + tile.width;
                }
                if (row.length > 0) {   // commit final row if not empty.
                    newlayout.push(row);
                }

                $scope.tablerows = newlayout.length;

                self.layoutView = newlayout;
            };

            self.layoutView = [];           // set by self.computeLayout

            // Load tile definitions and perform remaining initilization.

            DataService.getTileLayout().then(function (response) {
                self.tiles = response;

                // Generate tile properties id, sizeClass, bgClass, and classes.
                var tile;
                if (self.tiles != null) {
                    for (var t = 0; t < self.tiles.length; t++) {
                        tile = self.tiles[t];
                        tile.id = 'tile-' + (t + 1).toString(); // tile id (tile_1, tile_2, ...)
                        tile.bgClass = "tile-" + tile.color; // tile background color class (tile-red, tile-yellow, ...)
                        tile.sizeClass = "tile_" + tile.width.toString() + "_" + tile.height.toString(); // tile size class (tile_1_1, tile_1_2, tile_2_1, tile_2_2)
                        tile.classes = tile.sizeClass + ' ' + tile.bgClass; // full list of classes to customize tile appearance
                        if (tile.color == 'white' || tile.color == 'yellow' || tile.color == 'gray') {
                            tile.textColor = 'black';
                        }
                        else {
                            tile.textColor = 'white';
                        }
                    }
                }

                self.computeLayout();
                deferCreateCharts();
            });
        }]
});

Revised Dashboard Controller Code with Promise for getTileLayout

MVC Controller

To retreive our dashboard layout from the database, we need a back-end service. We've added an MVC controller to the ASP.NET project named DashboardController. It's Index() function reads dashboard layout from the database and returns it as a Json result. Our JavaScript SQL Server Data Service calls this MVC action in its getTileLayout() function.
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data.SqlClient;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace Dashboard.controllers
{
    public class DashboardController : Controller
    {
        // /Dashboard .... return dashboard layout as JSON

        public JsonResult Index()
        {
            int dashboardId = -1;

            Dashboard dashboard = new Dashboard()
            {
                DashboardName = "Home",
                Username = "default",   // TODO: check for username, if not found fall back to default
                Tiles = new List<Tile>()
            };
            
            try
            {
                using (SqlConnection conn = new SqlConnection(System.Configuration.ConfigurationManager.AppSettings["Database"]))
                {
                    conn.Open();

                    // Load the default home dashboard.

                    String query = "SELECT DashboardId FROM DashboardLayout WHERE DashboardName='Home' AND Username='default'";

                    using (SqlCommand cmd = new SqlCommand(query, conn))
                    {
                        cmd.CommandType = System.Data.CommandType.Text;
                        using (SqlDataReader reader = cmd.ExecuteReader())
                        {
                            if (reader.Read())
                            {
                                dashboardId = Convert.ToInt32(reader["DashboardId"]);
                            }
                        }
                    }

                    if (dashboardId != -1) // If found a dashboard...
                    {
                        // Load dashboard layout

                        query = @"SELECT prop.* FROM DashboardLayoutTile tile
                                  INNER JOIN DashboardLayoutTileProperty prop  
                                  ON tile.DashboardId=prop.DashboardId
                                  WHERE tile.DashboardId=@DashboardId
                                  ORDER BY prop.Sequence,prop.PropertyName";

                        Tile tile = null;
                        int lastSeq = 0;
                        int seq = 0;
                        using (SqlCommand cmd = new SqlCommand(query, conn))
                        {
                            cmd.CommandType = System.Data.CommandType.Text;
                            cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                            using (SqlDataReader reader = cmd.ExecuteReader())
                            {
                                while(reader.Read())
                                {
                                    seq = Convert.ToInt32(reader["Sequence"]);
                                    if (seq != lastSeq) // starting a new sequence (tile)
                                    {
                                        lastSeq = seq;
                                        if (tile != null)
                                        {
                                            dashboard.Tiles.Add(tile);
                                        }
                                        tile = new Tile()
                                        {
                                            Sequence = seq,
                                            Properties = new List<TileProperty>()
                                        };
                                    }

                                    tile.Properties.Add(new TileProperty(Convert.ToString(reader["PropertyName"]), Convert.ToString(reader["PropertyValue"])));
                                } // end while have tile property
                                dashboard.Tiles.Add(tile); // add final tile to tiles collection
                            }
                        }
                    }

                    conn.Close();
                }
            }
            catch(Exception ex)
            {
                Console.WriteLine("EXCEPTION: " + ex.Message);
            }

            return Json(dashboard, JsonRequestBehavior.AllowGet);
        }
    }

    #region Dashboard Layout Objects

    public class Dashboard
    {
        public String DashboardName { get; set; }
        public String Username { get; set; }
        public List<Tile> Tiles { get; set; }

        public Dashboard()
        {
            DashboardName = "Home";
            Tiles = new List<Tile>();
        }

        public Dashboard(String name)
        {
            DashboardName = name;
            Tiles = new List<Tile>();
        }
    }

    public class Tile
    {
        public int Sequence { get; set; }
        public List<TileProperty> Properties { get; set; }
    }

    public class TileProperty
    {
        public String PropertyName { get; set; }
        public String PropertyValue { get; set; }

        public TileProperty() { }

        public TileProperty(String name, String value = null) 
        {
            PropertyName = name;
            PropertyValue = value;
        }
    }

    #endregion
}
DashboardController

Index.html Changes

We need to modify our web page, index.html, to load the SQL Server Data Service instead of the Demo Data Service.
        ...
        <!-- Demo data service -->
        <!--<script src="/components/dashboard/demo.data.service.js"></script>-->

        <!-- SQL Server data service -->
        <script src="/components/dashboard/sql.data.service.js"></script>

        <script src="/components/dashboard/dashboard.component.js"></script>
    </body>
</html>
Index.html changes to allow Switching between Data Services

Seeing the Dashboard Run with DB Back-end for Configuration

Now we're ready to run our project with the dashboard configuration coming from our database. When we run it, we get the following dashboard rendered. Previously, changing our dashboard meant changing out our tile structure in JavaScript code. Now, we can make changes to the database to change the dashboard.

Dashboard Rendered from Back-End and DB Dashboard Configuration

Our back end is off to a good start, but it isn't finished yet. Next time, we'll add querying the database to supply tile data.

Summary

Today in Part 3 we accomplished a great deal:

  • We implemented a new tile type, a KPI tile.
  • We changed our too-simple tile rendering scheme to detect window resizing, compute the number of tile units across that will fit, and render tiles in an HTML table.
  • We created the notion of a data service, injected into the dashboard controller.
  • We implemented a simple Demo Data Service, with inline JavaScript tile definitions.
  • We added a database and defined tables for dashboard layout.
  • We implemented a SQL Server Data Service, that retrieves dashboard layout from a back end.
  • We implemented a back-end Dashboard MVC controller which accesses our database. 

Download Source
Dashboard_03.zip
https://drive.google.com/file/d/0B4734rvcufGGaDVFdnpNeW44X1U/view?usp=sharing

Next: An AngularJS Dashboard, Part 4: Bar/Pie Charts and Back-end Data Queries