Browsing Tag: Responsive

    web design

    Create Responsive Image Effects With CSS Gradients And aspect-ratio — Smashing Magazine

    02/23/2021

    About The Author

    Stephanie Eckles is a front-end focused SWE at Microsoft. She’s also the author of ModernCSS.dev which provides modern solutions to old CSS problems as in-depth …
    More about
    Stephanie

    A classic problem in CSS is maintaining the aspect ratio of images across related components, such as cards. The newly supported aspect-ratio property in combination with object-fit provides a remedy to this headache of the past! Let’s learn to use these properties, in addition to creating a responsive gradient image effect for extra flair.

    To prepare for our future image effects, we’re going to set up a card component that has a large image at the top followed by a headline and description. The common problem with this setup is that we may not always have perfect control over what the image is, and more importantly to our layout, what its dimensions are. And while this can be resolved by cropping ahead of time, we can still encounter issues due to responsively sized containers. A consequence is uneven positions of the card content which really stands out when you present a row of cards.

    Another previous solution besides cropping may have been to swap from an inline img to a blank div that only existed to present the image via background-image. I’ve implemented this solution many times myself in the past. One advantage this has is using an older trick for aspect ratio which uses a zero-height element and sets a padding-bottom value. Setting a padding value as a percent results in a final computed value that is relative to the element’s width. You may have also used this idea to maintain a 16:9 ratio for video embeds, in which case the padding value is found with the formula: 9/16 = 0.5625 * 100% = 56.26%. But we’re going to explore two modern CSS properties that don’t involve extra math, give us more flexibility, and also allow keeping the semantics provided by using a real img instead of an empty div.

    First, let’s define the HTML semantics, including use of an unordered list as the cards’ container:

    <ul class="card-wrapper">
      <li class="card">
        <img src="http://www.smashingmagazine.com/" alt="http://www.smashingmagazine.com/">
        <h3>A Super Wonderful Headline</h3>
        <p>Lorem ipsum sit dolor amit</p>
      </li>
      <!-- additional cards -->
    </ul>
    

    Next, we’ll create a minimal set of baseline styles for the .card component. We’ll set some basic visual styles for the card itself, a quick update to the expected h3 headline, then essential styles to begin to style the card image.

    .card {
      background-color: #fff;
      border-radius: 0.5rem;
      box-shadow: 0.05rem 0.1rem 0.3rem -0.03rem rgba(0, 0, 0, 0.45);
      padding-bottom: 1rem;
    }
    
    .card > :last-child {
      margin-bottom: 0;
    }
    
    .card h3 {
      margin-top: 1rem;
      font-size: 1.25rem;
    }
    
    img {
      border-radius: 0.5rem 0.5rem 0 0;
      width: 100%;
    }
    
    img ~ * {
      margin-left: 1rem;
      margin-right: 1rem;
    }
    

    The last rule uses the general sibling combinator to add a horizontal margin to any element that follows the img since we want the image itself to be flush with the sides of the card.

    And our progress so far leads us to the following card appearance:

    One card with the baseline styles previously described applied and including an image from Unsplash of a dessert on a small plate next to a hot beverage in a mug
    One card with the baseline styles previously described applied and including an image from Unsplash of a dessert on a small plate next to a hot beverage in a mug. (Large preview)

    Finally, we’ll create the .card-wrapper styles for a quick responsive layout using CSS grid. This will also remove the default list styles.

    .card-wrapper {
      list-style: none;
      padding: 0;
      margin: 0;
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(30ch, 1fr));
      grid-gap: 1.5rem;
    }
    

    Note: If this grid technique is unfamiliar to you, review the explanation in my tutorial about modern solutions for the 12-column grid.

    With this applied and with all cards containing an image with a valid source path, our .card-wrapper styles give us the following layout:

    Three cards are shown in a row due to the card wrapper layout styles applied. Each card has a unique image that has different natural aspect ratios, with the last card having a vertically oriented image that is more than twice the height of the other card images
    Three cards are shown in a row due to the card wrapper layout styles applied. Each card has a unique image that has different natural aspect ratios, with the last card having a vertically oriented image that is more than twice the height of the other card images. (Large preview)

    As demonstrated in the preview image, these baseline styles aren’t enough to properly contain the images given their varying natural dimensions. We’re in need of a method to constrain these images uniformly and consistently.

    Enable Uniform Image Sizes with object-fit

    As noted earlier, you may previously have made an update in this scenario to change the images to be added via background-image instead and used background-size: cover to handle nicely resizing the image. Or you may have tried to enforce cropping ahead of time (still a worthy goal since any image size reduction will improve performance!).

    Now, we have the property object-fit available which enables an img tag to act as the container for the image. And, it comes with a cover value as well that results in a similar effect as the background image solution, but with the bonus of retaining the semantics of an inline image. Let’s apply it and see how it works.

    We do need to pair it with a height dimension for extra guidance on how we want the image container to behave (recall we had already added width: 100%). And we’re going to use the max() function to select either 10rem or 30vh depending on which is larger in a given context, which prevents the image height from shrinking too much on smaller viewports or when the user has set a large zoom.

    img {
      /* ...existing styles */
      object-fit: cover;
      height: max(10rem, 30vh);
    }
    

    Bonus Accessibility Tip: You should always test your layouts with 200% and 400% zoom on desktop. While there isn’t currently a zoom media query, functions like max() can help resolve layout issues. Another context this technique is useful is spacing between elements.

    With this update, we’ve definitely improved things, and the visual result is as if we’d use the older background image technique:

    The three-card images now appear to have a uniform height and the image contents are centered within the image as if it was a container
    The three-card images now appear to have a uniform height and the image contents are centered within the image as if it was a container. (Large preview)

    Responsively Consistent Image Sizing With aspect-ratio

    When using object-fit by itself, one downside is that we still need to set some dimension hints.

    An upcoming property (currently available in Chromium browsers) called aspect-ratio will enhance our ability to consistently size images.

    Using this property, we can define a ratio to resize the image instead of setting explicit dimensions. We’ll continue to use it in combination with object-fit to ensure these dimensions only affect the image as a container, otherwise, the image could appear distorted.

    Here is our full updated image rule:

    img {
      border-radius: 0.5rem 0.5rem 0 0;
      width: 100%;
      object-fit: cover;
      aspect-ratio: 4/3;
    }
    

    We’re going to start with an image ratio of 43 for our card context, but you could choose any ratio. For example, 11 for a square, or 169 for standard video embeds.

    Here are the updated cards, although it will probably be difficult to notice the visual difference in this particular instance since the aspect ratio happens to closely match the appearance we achieved by setting the height for object-fit alone.

    The three-card images have identical width and height dimensions, which are slightly different than the previous object-fit solution
    The three-card images have identical width and height dimensions, which are slightly different than the previous object-fit solution. (Large preview)

    Setting an aspect-ratio results in the ratio being maintained as the elements grow or shrink, whereas when only setting object-fit and height the image ratio will constantly be in flux as the container dimensions change.

    Adding Responsive Effects With CSS Gradients And Functions

    OK, now that we know how to setup consistently sized images, let’s have some fun with them by adding a gradient effect!

    Our goal with this effect is to make it appear as though the image is fading into the card content. You may be tempted to wrap the image in its own container to add the gradient, but thanks to the work we’ve already done on the image sizing, we can work out how to safely do it on the main .card.

    The first step is to define a gradient. We’re going to use a CSS custom property to add in the gradient colors to enable easily swapping the gradient effect, starting with a blue to pink. The last color in the gradient will always be white to maintain the transition into the card content background and create the “feathered” edge.

    .card {
      --card-gradient: #5E9AD9, #E271AD;
    
      background-image: linear-gradient(
        var(--card-gradient),
        white max(9.5rem, 27vh)
      );
      /* ...existing styles */
    }
    

    But wait — is that a CSS max() function? In a gradient? Yes, it’s possible, and it’s the magic that makes this gradient effective responsively!

    However, if I were to add a screenshot, we wouldn’t actually see the gradient having any effect on the image yet. For that, we need to bring in the mix-blend-mode property, and in this scenario we’ll use the overlay value:

    img {
      /* ...existing styles */
      mix-blend-mode: overlay;
    }
    

    The mix-blend-mode property is similar to applying the layer blending styles available in photo manipulation software like Photoshop. And the overlay value will have the effect of allowing the medium tones in the image to blend with the gradient behind it, leading to the following result:

    Each card image has a gradient blending effect that starts with a light blue at the top, that blends to a reddish pink, and then ends by feathering into a white prior to the rest of the card text content
    Each card image has a gradient blending effect that starts with a light blue at the top, that blends to a reddish pink, and then ends by feathering into a white prior to the rest of the card text content. (Large preview)

    Now, at this point, we are relying on the aspect-ratio value alone to resize the image. And if we resize the container and cause the card layout to reflow, the changing image height causes inconsistencies in where the gradient fades to white.

    So we’ll add a max-height property as well that also uses the max() function and contains values slightly greater than the ones in the gradient. The resulting behavior is that the gradient will (almost always) correctly line up with the bottom of the image.

    img {
      /* ...existing styles */
      max-height: max(10rem, 30vh);
    }
    

    It’s important to note that adding a max-height alters the aspect-ratio behavior. Instead of always using the exact ratio, it will be used only when there’s enough allotted space given the new extra constraint of the max-height.

    However, aspect-ratio will still continue to ensure the images resize consistently as was the benefit over only object-fit. Try commenting out aspect-ratio in the final CodePen demo to see the difference it’s making across container sizes.

    Since our original goal was to enable consistently responsive image dimensions, we’ve still hit the mark. For your own use case, you may need to fiddle with the ratio and height values to achieve your desired effect.

    Alternate: mix-blend-mode And Adding A Filter

    Using overlay as the mix-blend-mode value was the best choice for the fade-to-white effect we were looking for, but let’s try an alternate option for a more dramatic effect.

    We’re going to update our solution to add a CSS custom property for the mix-blend-mode value and also update the color values for the gradient:

    .card {
      --card-gradient: tomato, orange;
      --card-blend-mode: multiply;
    }
    
    img {
      /* ...existing styles */
      mix-blend-mode: var(--card-blend-mode);
    }
    

    The multiply value has a darkening effect on mid-tones, but keeps white and black as is, resulting in the following appearance:

    Each card image has a strong orange tint from the new gradient that starts goes from a red-orange to pure orange. White areas are still white and black areas are still black
    Each card image has a strong orange tint from the new gradient that starts goes from a red-orange to pure orange. White areas are still white and black areas are still black. (Large preview)

    While we’ve lost the fade and now have a hard edge on the bottom of the image, the white part of our gradient is still important to ensure that the gradient ends prior to the card content.

    One additional modification we can add is the use of filter and, in particular, use the grayscale() function to remove the image colors and therefore have the gradient be the only source of image coloring.

    img {
      /* ...existing styles */
      filter: grayscale(100);
    }
    

    Using the value of grayscale(100) results in complete removal of the image’s natural colors and transforming it into black and white. Here’s the update for comparison with the previous screenshot of its effect when using our orange gradient with multiply:

    Now each card image still has the orange gradient but all other color is removed and replaced by shades of gray
    Now each card image still has the orange gradient but all other color is removed and replaced by shades of gray. (Large preview)

    Use aspect-ratio As A Progressive Enhancement

    As previously mentioned, currently aspect-ratio is only supported in the latest version of Chromium browsers (Chrome and Edge). However, all browsers support object-fit and that along with our height constraints results in a less-ideal but still acceptable result, seen here for Safari:

    The card image height is capped, but each card has a slightly different realized height
    The card image height is capped, but each card has a slightly different realized height. (Large preview)

    Without aspect-ratio functioning, the result here is that ultimately the image height is capped but the natural dimensions of each image still lead to some variance between card image heights. You may want to instead change to adding a max-height or make use of the max() function again to help make a max-height more responsive across varying card sizes.

    Extending The Gradient Effects

    Since we defined the gradient color stops as a CSS custom property, we have ready access to change them under different contexts. For example, we might change the gradient to more strongly feature one of the colors if the card is hovered or has one of its children in focus.

    First, we’ll update each card h3 to contain a link, such as:

    <h3><a href="http://www.smashingmagazine.com/">A Super Wonderful Headline</a></h3>
    

    Then, we can use one of our newest available selectors — :focus-within — to alter the card gradient when the link is in focus. For extra coverage of possible interactions, we’ll couple this with :hover. And, we’ll reuse our max() idea to assign a single color to take over coverage of the image portion of the card. The downside to this particular effect is that gradient stops and color changes aren’t reliably animateable — but they will be soon thanks to CSS Houdini.

    To update the color and add the new color stop, we just need to re-assign the value of --card-gradient within this new rule:

    .card:focus-within,
    .card:hover {
      --card-gradient: #24a9d5 max(8.5rem, 20vh);
    }
    

    Our max() values are less than the original in use for white to maintain the feathered edge. If we used the same values, it would meet the white and create a clearly straightedge separation.

    In creating this demo, I originally tried an effect that used transform with scale for a zoom-in effect. But I discovered that due to mix-blend-mode being applied, the browser would not consistently repaint the image which caused an unpleasant flickering. There will always be trade-offs in requesting the browser perform CSS-only effects and animations, and while it’s very cool what we can do, it’s always best to check the performance impact of your effects.

    Have Fun Experimenting!

    Modern CSS has given us some awesome tools for updating our web design toolkits, with aspect-ratio being the latest addition. So go forth, and experiment with object-fit, aspect-ratio, and adding functions like max() into your gradients for some fun responsive effects! Just be sure to double-check things cross-browser (for now!) and across varying viewports and container sizes.

    Here is the CodePen including the features and effects we reviewed today:

    See the Pen [Responsive Image Effects with CSS Gradients and aspect-ratio](https://codepen.io/smashingmag/pen/WNoERXo) by Stephanie Eckles.

    See the Pen Responsive Image Effects with CSS Gradients and aspect-ratio by Stephanie Eckles.

    Looking for more? Make sure you check out our CSS Guide here on Smashing →

    Smashing Editorial
    (vf, il)

    Source link

    web design

    Create A Responsive Dashboard With Angular Material And ng2-Charts — Smashing Magazine

    07/27/2020

    About The Author

    Zara Cooper is a software developer and technical writer who enjoys sharing what she learns as a developer with others. When she’s got time to spare, she enjoys …
    More about
    Zara

    Schematics in Angular 9 are code generators that can create components and patterns in projects using predetermined templates and layouts. In this article, Zara Cooper explains how to take advantage of schematics in Angular Material and ng2-charts to substantially reduce the time and work that goes into building a dashboard.

    Creating a dashboard from scratch is often pretty complicated. You have to create tools to collect data on items of interest. Once collected this data has to be presented in an easy to understand and meaningful way to your users. It involves intricate planning of what data to include and how to display it effectively. Once you have a plan, implementing the design is a massive task especially since it involves building multiple components.

    With Angular Material and ng2-charts, you can take advantage of schematics to cut down the effort and time you may spend building a dashboard. Angular Material ships with a number of schematics that you could use to generate a dashboard. Similarly, ng2-charts provides schematics for generating multiple chart components. In this article, I’ll illustrate how to use both ng2-charts and Angular Material to set up a dashboard fairly quickly.

    An Example

    To illustrate how to build a dashboard, we’ll take the example of an online store selling leather goods like bags, wallets, key holders, and so on. The store owner would like to track information such as where customers come from to their online store, how their products sell, how traffic sources relate to sales, among other things.

    We’ll build a dashboard to display this information and help the store owner analyze it. The dashboard will contain four small summary cards, four different kinds of charts, and a table listing most recent orders made. The four summary cards will display information such as total revenue from sales, average order value, the total number of orders, and number of returning customers. The charts will display the number of units sold for each product, sales by traffic source, online store sessions over time, and sales for the week.

    Prerequisites

    To follow along, you’ll need to have Angular CLI installed. If you do not have it installed, you can find out how to get it at cli.angular.io. If you’re not starting from a pre-existing Angular project, you need to generate one by running ng new <your project name>. For instance, to create an admin panel for the aforementioned store, we’ll run:

    ng new store-admin-panel

    Your project also needs to have routes configured for it. If you’re starting from a new app, select yes when prompted on whether to add an Angular Routing module during your project setup above.

    Add Angular Material And Ng2-Charts To Your Project

    Angular Material ships with various schematics for generating a variety of useful components like address books, trees, tables, navigation, and so on. To add Angular Material to your project, run:

    ng add @angular/material

    Pick a theme from the options provided in subsequent prompts. Next, you’ll be prompted to choose whether to add Angular Material typography styles and browser animations. You do not need these and could just respond no.

    Next, you’ll need to install ng2-charts. ng2-charts requires charts.js as a dependency. To install ng2-charts run:

    npm install ng2-charts --save

    Then install charts.js:

    npm install chart.js --save

    To access the charts, add the ChartsModule to the AppModule’s imports.

    import { ChartsModule } from 'ng2-charts';
    
    @NgModule({
      imports: [
       …
       ChartsModule,
       …
    ]
    })

    Lastly, install ng2-charts schematics as a dev dependency because they do not ship with ng2-charts by default.

    npm install --save-dev ng2-charts-schematics

    Generating A Navigation Component

    First off, we’ll need to add a navigation component to help users maneuver through the app comfortably. The navigation should contain links to the dashboard and other pages that will be part of the admin panel. Angular material provides a schematic that generates a navigation component. We’ll name this component nav. Adding a side nav to the application is accomplished by running:

    ng generate @angular/material:navigation nav

    To link other routes in the navigation, use the routerLink directive and change the page name in the toolbar depending on what route a user is on.

    // nav.component.ts
    ...
    menuItems = ['dashboard', ’sales', 'orders', 'customers', 'products'];
    <!--nav.component.html-->
    ...
    <mat-nav-list>
         <a *ngFor="let item of menuItems" mat-list-item [routerLink]="'/'+item"> {{item | titlecase}} </a>
    ...

    To see this component, add it to app.component.html.

    <!--app.component.html-->
    <app-nav></app-nav>

    This is what the NavComponent looks like.

    Navigation component
    Navigation component (Large preview)

    Since the nav will be displayed alongside other components, adding a router-outlet to it would help switch between the other different components. In the nav.component.html template, just after the closing </mat-toolbar>, replace the <!-- Add Content Here --> comment with <router-outlet></router-outlet>.

    <!--nav.component.html-->
    <mat-sidenav-container>
       ...
       <mat-sidenav-content>
         <mat-toolbar>
           ...
         </mat-toolbar>
         <router-outlet></router-outlet>
       </mat-sidenav-content>
     </mat-sidenav-container>

    In the screenshots that follow in this article, this nav component will be omitted to better highlight the dashboard we’ll be generating for the sake of the tutorial. If you’re following along while building this dashboard, the nav will still appear as pictured above in your browser with the dashboard within it.

    Generate The Dashboard

    The most important part of the dashboard is its layout. It needs to hold all the components mentioned earlier and be responsive when displayed on different devices. To generate the dashboard layout, you’ll need to run the @angular/material:dashboard schematic. It will generate a responsive dashboard component. Pass the preferred name for your dashboard to the schematic. In this instance, let’s name it dash.

    ng generate @angular/material:dashboard dash

    To view the newly generated dashboard within the nav component, add a route for it to the router.

    // app-routing.module.ts
    import { DashComponent } from './dash/dash.component';
    
    const routes: Routes = [{ path: 'dashboard', component: DashComponent }];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
    })

    Once done, to see the results, run npm start and head on over to localhost:4200/dashboard. You should see this:

    generated dashboard component
    Generated dashboard component (Large preview)

    The schematic generates four cards in the template and displays them in a responsive grid. The Angular Material CDK uses the Layout package to style this responsive card grid. The BreakpointObserver utility of the Layout package assesses media queries and makes UI changes based on them. There are various breakpoints available but within the generated component, only two categories are catered for. The Breakpoints.Handset and other queries that do not match it. The Layout package specifies 14 breakpoint states that you can use to customize the responsiveness of your dashboard.

    // dashboard.component.js
    ...
    cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe(
        map(({ matches }) => {
          if (matches) {
            ...
          }
          ...
        })
      );

    Going back to the dashboard, since four summary cards, four charts, and a table will be on the dashboard, we need nine cards in total. Breakpoints.Handset and Breakpoints.Tablet matches will display in a one-column grid where:

    • The four summary cards will span one row.
    • The charts will span two rows.
    • The table will span four rows.

    Non-Breakpoints.Handset and non-Breakpoints.Tablet matches will display in four columns where:

    • The four summary cards will span one row and one column.
    • The charts will span two rows and two columns.
    • The table will span four rows and four columns.

    It should look something like the screenshot below in non-Breakpoints.Handset and non-Breakpoints.Tablet matches. On Breakpoints.Handset and Breakpoints.Tablet matches, everything will just display in one column.

    dashboard component with additional cards
    Dashboard component with additional cards (Large preview)

    Create A Card Component

    In the dashboard component, all the cards are generated through iteration. To prevent repetition, when adding all the new components, we’ll create a reusable card component. The card component will accept a title as input and use ng-content to dynamically add the rest of the content. To create the card component, run:

    ng g c card -m app --style css

    From the dashboard component template, we’ll just take the markup enclosed within the <mat-card> tag and place it In the card template:

    <!--card.component.html-->
    <mat-card class="dashboard-card">
       <mat-card-header>
           <mat-card-title>
               {{title}}
               <button mat-icon-button class="more-button" [matMenuTriggerFor]="menu" aria-label="Toggle menu">
                   <mat-icon>more_vert</mat-icon>
               </button>
               <mat-menu #menu="matMenu" xPosition="before">
                   <button mat-menu-item>Expand</button>
                   <button mat-menu-item>Remove</button>
               </mat-menu>
           </mat-card-title>
       </mat-card-header>
       <mat-card-content class="dashboard-card-content">
           <ng-content></ng-content>
       </mat-card-content>
    </mat-card>

    To add the title as input to the card:

    // card.component.ts
    import { Component, Input } from '@angular/core';
    
    ...
    
    export class CardComponent{
     @Input() title: string;
    
    
    ...
    }

    To style the card:

    /*card.component.css*/
    .more-button {
       position: absolute;
       top: 5px;
       right: 10px;
    }
    
    .dashboard-card {
       position: absolute;
       top: 15px;
       left: 15px;
       right: 15px;
       bottom: 15px;
    }
    
    
    .dashboard-card-content {
       text-align: center;
       flex-grow: 1;
       display: flex;
       flex-direction: column;
       align-items: center;
       max-height: 100%;
       justify-content: center;
       align-items: stretch;
    }
    
    mat-card {
       display: flex;
       flex-direction: column;
    }

    Adding Cards To The Dashboard

    Since the dashboard elements will be added individually and not through iteration, the dashboard component needs to be modified to account for this. In dashboard.component.ts, remove the cards property and replace it with a cardLayout property instead. The cardLayout variable will define the number of columns for the material grid list and how many rows and columns each of the dashboard cards will span. Breakpoints.Handset and Breakpoints.Tablet query matches will display in 1 column and those that do not match will display in 4 columns.

    // dashboard.component.js
    ...
    
    
    cardLayout = this.breakpointObserver.observe(Breakpoints.Handset).pipe(
       map(({ matches }) => {
         if (matches) {
           return {
             columns: 1,
             miniCard: { cols: 1, rows: 1 },
             chart: { cols: 1, rows: 2 },
             table: { cols: 1, rows: 4 },
           };
         }
    
        return {
           columns: 4,
           miniCard: { cols: 1, rows: 1 },
           chart: { cols: 2, rows: 2 },
           table: { cols: 4, rows: 4 },
         };
       })
     );
    
    ...

    In the dash.component.html template, replace the colspan and rowspan values of mat-grid-tile elements and the cols property of the mat-grid-list element.

    <!--dash.component.html-->
    <div class="grid-container">
     <h1 class="mat-h1">Dashboard</h1>
     <mat-grid-list cols="{{ ( cardLayout | async )?.columns }}" rowHeight="200px">
       <!--Mini Cards-->
       <mat-grid-tile *ngFor="let i of [1, 2, 3, 4]" [colspan]="( cardLayout | async )?.miniCard.cols"
         [rowspan]="( cardLayout | async )?.miniCard.rows">
         <app-card title="Card {{i}}"><div>Mini Card Content Here</div></app-card>
       </mat-grid-tile>
       <!--Charts-->
       <mat-grid-tile *ngFor="let i of [5, 6, 7, 8]" [colspan]="( cardLayout | async )?.chart.cols"
         [rowspan]="( cardLayout | async )?.chart.rows">
         <app-card title="Card {{i}}"><div>Chart Content Here</div></app-card>
       </mat-grid-tile>
       <!--Table-->
       <mat-grid-tile [colspan]="( cardLayout | async )?.table.cols" [rowspan]="( cardLayout | async )?.table.rows">
         <app-card title="Card 9"><div>Table Content Here</div></app-card>
       </mat-grid-tile>
     </mat-grid-list>
    </div>

    The dashboard will end up looking exactly like the most recent screenshot linked above.

    Generating The Charts

    The four charts that we need for the dashboard are:

    • A radar chart of products by unit sold.
    • A pie chart of sales by traffic source.
    • A bar chart of online store sessions.
    • A line chart of sales across the year.

    Similar to creating the dashboard, generating chart components involves running a schematic. Using the ng2-charts schematics, generate the four different charts. We’ll place them in a folder called charts. Run ng generate ng2-charts-schematics:<chart type> <chart name>.

    ng generate ng2-charts-schematics:radar charts/product-sales-chart
    ng generate ng2-charts-schematics:pie charts/sales-traffic-chart
    ng generate ng2-charts-schematics:line charts/annual-sales-chart 
    ng generate ng2-charts-schematics:bar charts/store-sessions-chart

    After running these commands, all four chart components are generated and are populated with sample data ready for display. Depending on what data you’d like to show, pick charts that most suit your data visualization needs. For each of the charts generated above, add the chartContainer class to the divs that enclose the canvas element in the chart templates.

    <div class="chartContainer">
      <canvas baseChart width="400" height="400">
        ...

    Next, add this styling to styles.css so that they could be accessible to all the chart components.

    /*styles.css*/
    ...
    
    .chartContainer canvas {
        max-height: 250px;
        width: auto;
    }
    
    .chartContainer{
        height: 100%;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    }

    Adding Data To The Charts

    The generated chart components come with sample data already plugged in. If you have pre-existing services that provide your own data, you can add this data from them to the chart components. The charts take labels for the x-axis, data or data sets, a chart type, colors, a legend as well as other customization options. To provide the data and labels to the charts, create a service that will fetch data from a source of your choice and return it in a form that the charts accept. For instance, the AnnualSalesChartComponent receives its dataset and labels from the SalesService’s getSalesByMonth method which returns an array of sales for each month for the current year. You can find this service here and data it returns here. Inject the service as a private property to the AnnualSalesChartComponent constructor. Call the method that returns the required chart data from the service within the ngOnInit lifecycle hook.

    // annual-sales-chart.component.ts
    import { SalesService } from ’src/app/sales/sales.service';
    
    
    ...
    
    
    export class AnnualSalesChartComponent implements OnInit {
      public salesChartData: ChartDataSets[] = [
       { data: [], label: 'Total Sales' },
     ];
    
     public salesChartLabels: Label[] = [];
      ...
    
      constructor(private salesService: SalesService) { }
      ngOnInit() {
       this.salesService.getSalesByMonth().subscribe({
         next: salesItems => {
           salesItems.forEach(li => {
             this.salesChartData[0].data.push(li.revenue);
             this.salesChartLabels.push(li.month);
           });
         },
        ...
       });
     }
    }

    Adding Charts To The Dashboard

    The next step involves adding the charts to the dashboard, in dash.component.html. Here’s what that looks like:

    <!--dash.component.html-->
    ...
    
    <!--Charts-->
    <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
       <app-card title="Monthly Revenue">
           <app-annual-sale-chart></app-annual-sale-chart>
       </app-card>
    </mat-grid-tile>
    <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
       <app-card title="Product Sales">
           <app-product-sales-chart></app-product-sales-chart>
       </app-card>
    </mat-grid-tile>
    <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
       <app-card title="Sales by Traffic Source">
           <app-sales-traffic-chart></app-sales-traffic-chart>
       </app-card>
    </mat-grid-tile>
    <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
       <app-card title="Online Store Sessions by Traffic Source">
           <app-store-sessions-chart></app-store-sessions-chart>
       </app-card>
    </mat-grid-tile>
    
    ...

    This is what the resultant responsive dashboard looks like.

    dashboard with charts
    Dashboard with charts (Large preview)

    Generating A Table

    We’ll add an orders table to give the shop owner an overview of the most recent orders placed and their status. To generate the orders table component, run the schematic:

    ng generate @angular/material:table orders-table

    This will generate a table component that will look like this.

    table generated by Angular Material schematic
    Table generated by Angular Material schematic (Large preview)

    Tables with many columns may be difficult to make responsive for handset and tablet views. When adding the table to a card, make it horizontally scrollable so that all the data can be viewed properly and is not obstructed. You can do this by adding the styling below to your table component:

    <!--table.component.html-->
    <div class="mat-elevation-z8 small-table">
     <table mat-table class="full-width-table" matSort aria-label="Elements">
       ...
    /*table.component.css*/
    
    ...
    
    .small-table{
      overflow-x: scroll !important;
    }

    To add the table to the dash component:

    <!-- dashboard.component.html>
    ...
      <mat-grid-tile [colspan]="( cardLayout | async )?.table.cols" [rowspan]="( cardLayout | async )?.table.rows">
         <app-card title="Latest Orders">
           <app-orders-table></app-orders-table>
         </app-card>
      </mat-grid-tile>
    ...

    Adding Data To The Table

    Like with charts, you can add data to the table in the ngOnInit method from a service. Additionally, you will need to modify your table’s generated data source to consume data from the service. To start off, inject the service in the table’s class constructor. Let’s take the example of a table listing the latest orders for this dashboard. To get data for the table, let’s inject the OrderService in the OrdersTableComponent constructor, change the MatTable type assertion of the table view child, and amend the list of displayed columns to reflect an order interface. If you’re interested in the data being added to the table, you can find it here. The last thing involves getting the total length of the data items available to be used to set the total in the table’s <mat-paginator>.

    // orders-table.component.ts
    import { OrderService } from '../orders.service';
    import { Order } from '../order';
    ...
    export class OrdersTableComponent implements AfterViewInit, OnInit {
    ...
     @ViewChild(MatTable) table: MatTable;
     dataLength: number;
     
     displayedColumns = [
       "id",
       "date",
       "name",
       "status",
       "orderTotal",
       "paymentMode",
     ];
    ...
    constructor(private orderService: OrderService){}
    
    ngOnInit() {
     this.datasource = new OrdersTableDataSource(this.orderService);
     this.orderService.getOrderCount().subscribe({
       next: orderCount => {
        this.dataLength = orderCount;
       },
       ...
     });
    }
    ...
    }

    Next, we’ll need to modify the OrdersTableDataSource class to accept the OrderService as a parameter in its constructor. We’ll have to modify its connect and destroy methods as well. The connect method connects the data source to the table and updates the table when new data items are emitted from the stream it returns, in this case, an orders array observable. The dataMutations constant combines the first data load, pagination, and sorting events into one stream for the table to consume. Pagination and sorting are handled by the OrderService server-side. So we need to pass the offset and page size from paginator and the active sort field and sort direction of the sort property to the getOrders method of the OrderService. The disconnect method should be used to close any connections made and release resources held up in the connect method.

    // orders-table.datasource.ts
    ...
    export class OrdersTableDataSource extends DataSource<Order> {
     paginator: MatPaginator;
     sort: MatSort;
    
     constructor(private orderService: OrderService) {
       super();
     }
    
     connect(): Observable<Order[]> {
       const dataMutations = [
         of('Initial load'),
         this.paginator.page,
         this.sort.sortChange
       ];
    
       return merge(...dataMutations).pipe(mergeMap(() => {
         return this.orderService.getOrders(
           this.paginator.pageIndex * this.paginator.pageSize,
           this.paginator.pageSize,
           this.sort.active,
           this.sort.direction
         );
       }));
     }
    
     disconnect() {} 
    }

    In the orders table template, insert the new columns and bind the length property of <mat-paginator> to the dataLength property. For the status column, use a <mat-chip> element for better visualization of the order status. To have access to <mat-chip>, add the MatChipsModule as an import to AppModule.

    <!-- orders-table.component.html -->
    <div class="mat-elevation-z8">
     <table mat-table class="full-width-table" matSort aria-label="Elements">
       <!-- Id Column -->
       <ng-container matColumnDef="id">
         <th mat-header-cell *matHeaderCellDef mat-sort-header>Id</th>
         <td mat-cell *matCellDef="let row">{{row.id}}</td>
       </ng-container>
    
       <!-- Date Column -->
       <ng-container matColumnDef="date">
         <th mat-header-cell *matHeaderCellDef mat-sort-header>Date</th>
         <td mat-cell *matCellDef="let row">{{row.date | date }}</td>
       </ng-container>
    
       <!-- Name Column -->
       <ng-container matColumnDef="name">
         <th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
         <td mat-cell *matCellDef="let row">{{row.name}}</td>
       </ng-container>
    
       <!-- Order Total Column -->
       <ng-container matColumnDef="orderTotal">
         <th mat-header-cell *matHeaderCellDef mat-sort-header>Order Total</th>
         <td mat-cell *matCellDef="let row">{{row.orderTotal | currency}}</td>
       </ng-container>
    
       <!-- Payment Mode Column -->
       <ng-container matColumnDef="paymentMode">
         <th mat-header-cell *matHeaderCellDef mat-sort-header>Payment Mode</th>
         <td mat-cell *matCellDef="let row">{{row.paymentMode}}</td>
       </ng-container>
    
       <!-- Status Column -->
       <ng-container matColumnDef="status">
         <th mat-header-cell *matHeaderCellDef mat-sort-header>Status</th>
         <td mat-cell *matCellDef="let row">
           <mat-chip-list>
             <mat-chip color="{{ row.status == 'delivered' ? 'primary' : ( row.status == ’shipped' ? 'accent' : 'warn' ) }}" selected>
               {{row.status}}
             </mat-chip>
           </mat-chip-list>
         </td>
       </ng-container>
    
       <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
       <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
     </table>
    
     <mat-paginator #paginator [length]="dataLength" [pageIndex]="0" [pageSize]="5" [pageSizeOptions]="[5, 10, 15, 20]">
     </mat-paginator>
    </div>

    Once data has been added to the table, this is what the dashboard will look like:

    dashboard with charts and table
    Dashboard with charts and table (Large preview)

    Creating A Mini Card Component

    All that’s left to complete the dashboard is to populate the four small cards that sit at the top. Having smaller summary cards as part of the dashboard makes it easy to highlight brief pieces of information that do not need whole charts or tables. In this example, the four mini cards will display total sales, average order value, the total number of orders, and the number of returning customers that visited the store for the day. This is just an example. These mini cards cannot be generated like with the navigation, dashboard layout, charts, and the table. They have no schematics. Below we’ll briefly go through how to create them. Although we’re going to add data specific to the example, you can add whatever you want to them or decide to do away with them altogether. To start off, generate the mini-card component, run:

    ng g c mini-card -m app --style css

    You can find the template for the component linked here and its styling here. This component has eight input properties that you can find out how to add here. To get data to the mini card components, inject the service that provides data to them in the DashComponent constructor. Assign data received from the service to a property of the DashComponent. In this instance, we’ll get data from the StoreSummaryService and assign it to the miniCardData property. Here’s how:

    // dash.component.ts
    export class DashComponent implements OnInit{
    ...
     miniCardData: StoreSummary[];
    
     constructor(private breakpointObserver: BreakpointObserver, private summaryService: StoreSummaryService) {}
    
     ngOnInit() {
       this.summaryService.getStoreSummary().subscribe({
         next: summaryData => {
           this.miniCardData = summaryData;
         }
       });
     } 
    }

    To add the mini-cards to the dash component and have them populated with data from the service:

    <!--dash.component.html-->
    ...
    
    <!--Mini Cards-->
       <mat-grid-tile *ngFor="let mc of miniCardData" [colspan]="( cardLayout | async )?.miniCard.cols"
         [rowspan]="( cardLayout | async )?.miniCard.rows">
         <app-mini-card Create A Responsive Dashboard With Angular Material And ng2-Charts — Smashing Magazine="mc.title" [textValue]="mc.textValue" [value]="mc.value" [color]="mc.color" [percentValue]="mc.percentValue"></app-mini-card>
       </mat-grid-tile>
    
    ...

    The screenshot below is what the dashboard will look like with the mini cards populated.

    dashboard with charts, tables, and mini-cards
    Dashboard with charts, tables, and mini-cards. (Large preview)

    Putting All Together

    In the end, the dashboard component template should contain:

    <!-- dashboard.component.html -->
    <div class="grid-container">
     <h1 class="mat-h1">Dashboard</h1>
     <mat-grid-list cols="{{ ( cardLayout | async )?.columns }}" rowHeight="200px">
       <!--Mini Cards-->
       <mat-grid-tile *ngFor="let mc of miniCardData" [colspan]="( cardLayout | async )?.miniCard.cols"
         [rowspan]="( cardLayout | async )?.miniCard.rows">
         <app-mini-card [icon]="mc.icon" Create A Responsive Dashboard With Angular Material And ng2-Charts — Smashing Magazine="mc.title" [value]="mc.value" [color]="mc.color" [isIncrease]="mc.isIncrease" duration="since last month" [percentValue]="mc.percentValue" [isCurrency]="mc. isCurrency"></app-mini-card>
       </mat-grid-tile>
       <!--Charts-->
       <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
         <app-card title="Monthly Revenue">
           <app-annual-sale-chart></app-annual-sale-chart>
         </app-card>
       </mat-grid-tile>
       <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
         <app-card title="Product Sales">
           <app-product-sales-chart></app-product-sales-chart>
         </app-card>
       </mat-grid-tile>
       <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
         <app-card title="Sales by Traffic Source">
           <app-sales-traffic-chart></app-sales-traffic-chart>
         </app-card>
       </mat-grid-tile>
       <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
         <app-card title="Online Store Sessions by Traffic Source">
           <app-store-sessions-chart></app-store-sessions-chart>
         </app-card>
       </mat-grid-tile>
       <!--Table-->
       <mat-grid-tile [colspan]="( cardLayout | async )?.table.cols" [rowspan]="( cardLayout | async )?.table.rows">
         <app-card title="Latest Orders">
           <app-orders-table></app-orders-table>
         </app-card>
       </mat-grid-tile>
     </mat-grid-list>
    </div>

    Here’s what the resultant dashboard contains.

    completed dashboard
    Completed dashboard (Large preview)

    Conclusion

    Creating dashboards involves a fair amount of work and planning. A way to make building them faster is to use the various schematics provided by Angular Material and ng2-charts. With these schematics, running a command will generate a wholly complete component and can result in having a dashboard up and running fairly quickly. This leaves you a lot more time to focus on creating data services and adding them to your dashboard components.

    If you want to learn more about some of the schematics provided by Angular Material, visit material.angular.io, and for those provided by ng2-charts, visit their site linked here.

    Smashing Editorial
    (ra, yk, il)

    Source link

    web design

    Create A Responsive Dashboard With Angular Material And ng2-Charts — Smashing Magazine

    07/27/2020

    About The Author

    Zara Cooper is a software developer and technical writer who enjoys sharing what she learns as a developer with others. When she’s got time to spare, she enjoys …
    More about
    Zara

    Schematics in Angular 9 are code generators that can create components and patterns in projects using predetermined templates and layouts. In this article, Zara Cooper explains how to take advantage of schematics in Angular Material and ng2-charts to substantially reduce the time and work that goes into building a dashboard.

    Creating a dashboard from scratch is often pretty complicated. You have to create tools to collect data on items of interest. Once collected this data has to be presented in an easy to understand and meaningful way to your users. It involves intricate planning of what data to include and how to display it effectively. Once you have a plan, implementing the design is a massive task especially since it involves building multiple components.

    With Angular Material and ng2-charts, you can take advantage of schematics to cut down the effort and time you may spend building a dashboard. Angular Material ships with a number of schematics that you could use to generate a dashboard. Similarly, ng2-charts provides schematics for generating multiple chart components. In this article, I’ll illustrate how to use both ng2-charts and Angular Material to set up a dashboard fairly quickly.

    An Example

    To illustrate how to build a dashboard, we’ll take the example of an online store selling leather goods like bags, wallets, key holders, and so on. The store owner would like to track information such as where customers come from to their online store, how their products sell, how traffic sources relate to sales, among other things.

    We’ll build a dashboard to display this information and help the store owner analyze it. The dashboard will contain four small summary cards, four different kinds of charts, and a table listing most recent orders made. The four summary cards will display information such as total revenue from sales, average order value, the total number of orders, and number of returning customers. The charts will display the number of units sold for each product, sales by traffic source, online store sessions over time, and sales for the week.

    Prerequisites

    To follow along, you’ll need to have Angular CLI installed. If you do not have it installed, you can find out how to get it at cli.angular.io. If you’re not starting from a pre-existing Angular project, you need to generate one by running ng new <your project name>. For instance, to create an admin panel for the aforementioned store, we’ll run:

    ng new store-admin-panel

    Your project also needs to have routes configured for it. If you’re starting from a new app, select yes when prompted on whether to add an Angular Routing module during your project setup above.

    Add Angular Material And Ng2-Charts To Your Project

    Angular Material ships with various schematics for generating a variety of useful components like address books, trees, tables, navigation, and so on. To add Angular Material to your project, run:

    ng add @angular/material

    Pick a theme from the options provided in subsequent prompts. Next, you’ll be prompted to choose whether to add Angular Material typography styles and browser animations. You do not need these and could just respond no.

    Next, you’ll need to install ng2-charts. ng2-charts requires charts.js as a dependency. To install ng2-charts run:

    npm install ng2-charts --save

    Then install charts.js:

    npm install chart.js --save

    To access the charts, add the ChartsModule to the AppModule’s imports.

    import { ChartsModule } from 'ng2-charts';
    
    @NgModule({
      imports: [
       …
       ChartsModule,
       …
    ]
    })

    Lastly, install ng2-charts schematics as a dev dependency because they do not ship with ng2-charts by default.

    npm install --save-dev ng2-charts-schematics

    Generating A Navigation Component

    First off, we’ll need to add a navigation component to help users maneuver through the app comfortably. The navigation should contain links to the dashboard and other pages that will be part of the admin panel. Angular material provides a schematic that generates a navigation component. We’ll name this component nav. Adding a side nav to the application is accomplished by running:

    ng generate @angular/material:navigation nav

    To link other routes in the navigation, use the routerLink directive and change the page name in the toolbar depending on what route a user is on.

    // nav.component.ts
    ...
    menuItems = ['dashboard', ’sales', 'orders', 'customers', 'products'];
    <!--nav.component.html-->
    ...
    <mat-nav-list>
         <a *ngFor="let item of menuItems" mat-list-item [routerLink]="'/'+item"> {{item | titlecase}} </a>
    ...

    To see this component, add it to app.component.html.

    <!--app.component.html-->
    <app-nav></app-nav>

    This is what the NavComponent looks like.

    Navigation component
    Navigation component (Large preview)

    Since the nav will be displayed alongside other components, adding a router-outlet to it would help switch between the other different components. In the nav.component.html template, just after the closing </mat-toolbar>, replace the <!-- Add Content Here --> comment with <router-outlet></router-outlet>.

    <!--nav.component.html-->
    <mat-sidenav-container>
       ...
       <mat-sidenav-content>
         <mat-toolbar>
           ...
         </mat-toolbar>
         <router-outlet></router-outlet>
       </mat-sidenav-content>
     </mat-sidenav-container>

    In the screenshots that follow in this article, this nav component will be omitted to better highlight the dashboard we’ll be generating for the sake of the tutorial. If you’re following along while building this dashboard, the nav will still appear as pictured above in your browser with the dashboard within it.

    Generate The Dashboard

    The most important part of the dashboard is its layout. It needs to hold all the components mentioned earlier and be responsive when displayed on different devices. To generate the dashboard layout, you’ll need to run the @angular/material:dashboard schematic. It will generate a responsive dashboard component. Pass the preferred name for your dashboard to the schematic. In this instance, let’s name it dash.

    ng generate @angular/material:dashboard dash

    To view the newly generated dashboard within the nav component, add a route for it to the router.

    // app-routing.module.ts
    import { DashComponent } from './dash/dash.component';
    
    const routes: Routes = [{ path: 'dashboard', component: DashComponent }];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
    })

    Once done, to see the results, run npm start and head on over to localhost:4200/dashboard. You should see this:

    generated dashboard component
    Generated dashboard component (Large preview)

    The schematic generates four cards in the template and displays them in a responsive grid. The Angular Material CDK uses the Layout package to style this responsive card grid. The BreakpointObserver utility of the Layout package assesses media queries and makes UI changes based on them. There are various breakpoints available but within the generated component, only two categories are catered for. The Breakpoints.Handset and other queries that do not match it. The Layout package specifies 14 breakpoint states that you can use to customize the responsiveness of your dashboard.

    // dashboard.component.js
    ...
    cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe(
        map(({ matches }) => {
          if (matches) {
            ...
          }
          ...
        })
      );

    Going back to the dashboard, since four summary cards, four charts, and a table will be on the dashboard, we need nine cards in total. Breakpoints.Handset and Breakpoints.Tablet matches will display in a one-column grid where:

    • The four summary cards will span one row.
    • The charts will span two rows.
    • The table will span four rows.

    Non-Breakpoints.Handset and non-Breakpoints.Tablet matches will display in four columns where:

    • The four summary cards will span one row and one column.
    • The charts will span two rows and two columns.
    • The table will span four rows and four columns.

    It should look something like the screenshot below in non-Breakpoints.Handset and non-Breakpoints.Tablet matches. On Breakpoints.Handset and Breakpoints.Tablet matches, everything will just display in one column.

    dashboard component with additional cards
    Dashboard component with additional cards (Large preview)

    Create A Card Component

    In the dashboard component, all the cards are generated through iteration. To prevent repetition, when adding all the new components, we’ll create a reusable card component. The card component will accept a title as input and use ng-content to dynamically add the rest of the content. To create the card component, run:

    ng g c card -m app --style css

    From the dashboard component template, we’ll just take the markup enclosed within the <mat-card> tag and place it In the card template:

    <!--card.component.html-->
    <mat-card class="dashboard-card">
       <mat-card-header>
           <mat-card-title>
               {{title}}
               <button mat-icon-button class="more-button" [matMenuTriggerFor]="menu" aria-label="Toggle menu">
                   <mat-icon>more_vert</mat-icon>
               </button>
               <mat-menu #menu="matMenu" xPosition="before">
                   <button mat-menu-item>Expand</button>
                   <button mat-menu-item>Remove</button>
               </mat-menu>
           </mat-card-title>
       </mat-card-header>
       <mat-card-content class="dashboard-card-content">
           <ng-content></ng-content>
       </mat-card-content>
    </mat-card>

    To add the title as input to the card:

    // card.component.ts
    import { Component, Input } from '@angular/core';
    
    ...
    
    export class CardComponent{
     @Input() title: string;
    
    
    ...
    }

    To style the card:

    /*card.component.css*/
    .more-button {
       position: absolute;
       top: 5px;
       right: 10px;
    }
    
    .dashboard-card {
       position: absolute;
       top: 15px;
       left: 15px;
       right: 15px;
       bottom: 15px;
    }
    
    
    .dashboard-card-content {
       text-align: center;
       flex-grow: 1;
       display: flex;
       flex-direction: column;
       align-items: center;
       max-height: 100%;
       justify-content: center;
       align-items: stretch;
    }
    
    mat-card {
       display: flex;
       flex-direction: column;
    }

    Adding Cards To The Dashboard

    Since the dashboard elements will be added individually and not through iteration, the dashboard component needs to be modified to account for this. In dashboard.component.ts, remove the cards property and replace it with a cardLayout property instead. The cardLayout variable will define the number of columns for the material grid list and how many rows and columns each of the dashboard cards will span. Breakpoints.Handset and Breakpoints.Tablet query matches will display in 1 column and those that do not match will display in 4 columns.

    // dashboard.component.js
    ...
    
    
    cardLayout = this.breakpointObserver.observe(Breakpoints.Handset).pipe(
       map(({ matches }) => {
         if (matches) {
           return {
             columns: 1,
             miniCard: { cols: 1, rows: 1 },
             chart: { cols: 1, rows: 2 },
             table: { cols: 1, rows: 4 },
           };
         }
    
        return {
           columns: 4,
           miniCard: { cols: 1, rows: 1 },
           chart: { cols: 2, rows: 2 },
           table: { cols: 4, rows: 4 },
         };
       })
     );
    
    ...

    In the dash.component.html template, replace the colspan and rowspan values of mat-grid-tile elements and the cols property of the mat-grid-list element.

    <!--dash.component.html-->
    <div class="grid-container">
     <h1 class="mat-h1">Dashboard</h1>
     <mat-grid-list cols="{{ ( cardLayout | async )?.columns }}" rowHeight="200px">
       <!--Mini Cards-->
       <mat-grid-tile *ngFor="let i of [1, 2, 3, 4]" [colspan]="( cardLayout | async )?.miniCard.cols"
         [rowspan]="( cardLayout | async )?.miniCard.rows">
         <app-card title="Card {{i}}"><div>Mini Card Content Here</div></app-card>
       </mat-grid-tile>
       <!--Charts-->
       <mat-grid-tile *ngFor="let i of [5, 6, 7, 8]" [colspan]="( cardLayout | async )?.chart.cols"
         [rowspan]="( cardLayout | async )?.chart.rows">
         <app-card title="Card {{i}}"><div>Chart Content Here</div></app-card>
       </mat-grid-tile>
       <!--Table-->
       <mat-grid-tile [colspan]="( cardLayout | async )?.table.cols" [rowspan]="( cardLayout | async )?.table.rows">
         <app-card title="Card 9"><div>Table Content Here</div></app-card>
       </mat-grid-tile>
     </mat-grid-list>
    </div>

    The dashboard will end up looking exactly like the most recent screenshot linked above.

    Generating The Charts

    The four charts that we need for the dashboard are:

    • A radar chart of products by unit sold.
    • A pie chart of sales by traffic source.
    • A bar chart of online store sessions.
    • A line chart of sales across the year.

    Similar to creating the dashboard, generating chart components involves running a schematic. Using the ng2-charts schematics, generate the four different charts. We’ll place them in a folder called charts. Run ng generate ng2-charts-schematics:<chart type> <chart name>.

    ng generate ng2-charts-schematics:radar charts/product-sales-chart
    ng generate ng2-charts-schematics:pie charts/sales-traffic-chart
    ng generate ng2-charts-schematics:line charts/annual-sales-chart 
    ng generate ng2-charts-schematics:bar charts/store-sessions-chart

    After running these commands, all four chart components are generated and are populated with sample data ready for display. Depending on what data you’d like to show, pick charts that most suit your data visualization needs. For each of the charts generated above, add the chartContainer class to the divs that enclose the canvas element in the chart templates.

    <div class="chartContainer">
      <canvas baseChart width="400" height="400">
        ...

    Next, add this styling to styles.css so that they could be accessible to all the chart components.

    /*styles.css*/
    ...
    
    .chartContainer canvas {
        max-height: 250px;
        width: auto;
    }
    
    .chartContainer{
        height: 100%;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    }

    Adding Data To The Charts

    The generated chart components come with sample data already plugged in. If you have pre-existing services that provide your own data, you can add this data from them to the chart components. The charts take labels for the x-axis, data or data sets, a chart type, colors, a legend as well as other customization options. To provide the data and labels to the charts, create a service that will fetch data from a source of your choice and return it in a form that the charts accept. For instance, the AnnualSalesChartComponent receives its dataset and labels from the SalesService’s getSalesByMonth method which returns an array of sales for each month for the current year. You can find this service here and data it returns here. Inject the service as a private property to the AnnualSalesChartComponent constructor. Call the method that returns the required chart data from the service within the ngOnInit lifecycle hook.

    // annual-sales-chart.component.ts
    import { SalesService } from ’src/app/sales/sales.service';
    
    
    ...
    
    
    export class AnnualSalesChartComponent implements OnInit {
      public salesChartData: ChartDataSets[] = [
       { data: [], label: 'Total Sales' },
     ];
    
     public salesChartLabels: Label[] = [];
      ...
    
      constructor(private salesService: SalesService) { }
      ngOnInit() {
       this.salesService.getSalesByMonth().subscribe({
         next: salesItems => {
           salesItems.forEach(li => {
             this.salesChartData[0].data.push(li.revenue);
             this.salesChartLabels.push(li.month);
           });
         },
        ...
       });
     }
    }

    Adding Charts To The Dashboard

    The next step involves adding the charts to the dashboard, in dash.component.html. Here’s what that looks like:

    <!--dash.component.html-->
    ...
    
    <!--Charts-->
    <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
       <app-card title="Monthly Revenue">
           <app-annual-sale-chart></app-annual-sale-chart>
       </app-card>
    </mat-grid-tile>
    <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
       <app-card title="Product Sales">
           <app-product-sales-chart></app-product-sales-chart>
       </app-card>
    </mat-grid-tile>
    <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
       <app-card title="Sales by Traffic Source">
           <app-sales-traffic-chart></app-sales-traffic-chart>
       </app-card>
    </mat-grid-tile>
    <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
       <app-card title="Online Store Sessions by Traffic Source">
           <app-store-sessions-chart></app-store-sessions-chart>
       </app-card>
    </mat-grid-tile>
    
    ...

    This is what the resultant responsive dashboard looks like.

    dashboard with charts
    Dashboard with charts (Large preview)

    Generating A Table

    We’ll add an orders table to give the shop owner an overview of the most recent orders placed and their status. To generate the orders table component, run the schematic:

    ng generate @angular/material:table orders-table

    This will generate a table component that will look like this.

    table generated by Angular Material schematic
    Table generated by Angular Material schematic (Large preview)

    Tables with many columns may be difficult to make responsive for handset and tablet views. When adding the table to a card, make it horizontally scrollable so that all the data can be viewed properly and is not obstructed. You can do this by adding the styling below to your table component:

    <!--table.component.html-->
    <div class="mat-elevation-z8 small-table">
     <table mat-table class="full-width-table" matSort aria-label="Elements">
       ...
    /*table.component.css*/
    
    ...
    
    .small-table{
      overflow-x: scroll !important;
    }

    To add the table to the dash component:

    <!-- dashboard.component.html>
    ...
      <mat-grid-tile [colspan]="( cardLayout | async )?.table.cols" [rowspan]="( cardLayout | async )?.table.rows">
         <app-card title="Latest Orders">
           <app-orders-table></app-orders-table>
         </app-card>
      </mat-grid-tile>
    ...

    Adding Data To The Table

    Like with charts, you can add data to the table in the ngOnInit method from a service. Additionally, you will need to modify your table’s generated data source to consume data from the service. To start off, inject the service in the table’s class constructor. Let’s take the example of a table listing the latest orders for this dashboard. To get data for the table, let’s inject the OrderService in the OrdersTableComponent constructor, change the MatTable type assertion of the table view child, and amend the list of displayed columns to reflect an order interface. If you’re interested in the data being added to the table, you can find it here. The last thing involves getting the total length of the data items available to be used to set the total in the table’s <mat-paginator>.

    // orders-table.component.ts
    import { OrderService } from '../orders.service';
    import { Order } from '../order';
    ...
    export class OrdersTableComponent implements AfterViewInit, OnInit {
    ...
     @ViewChild(MatTable) table: MatTable;
     dataLength: number;
     
     displayedColumns = [
       "id",
       "date",
       "name",
       "status",
       "orderTotal",
       "paymentMode",
     ];
    ...
    constructor(private orderService: OrderService){}
    
    ngOnInit() {
     this.datasource = new OrdersTableDataSource(this.orderService);
     this.orderService.getOrderCount().subscribe({
       next: orderCount => {
        this.dataLength = orderCount;
       },
       ...
     });
    }
    ...
    }

    Next, we’ll need to modify the OrdersTableDataSource class to accept the OrderService as a parameter in its constructor. We’ll have to modify its connect and destroy methods as well. The connect method connects the data source to the table and updates the table when new data items are emitted from the stream it returns, in this case, an orders array observable. The dataMutations constant combines the first data load, pagination, and sorting events into one stream for the table to consume. Pagination and sorting are handled by the OrderService server-side. So we need to pass the offset and page size from paginator and the active sort field and sort direction of the sort property to the getOrders method of the OrderService. The disconnect method should be used to close any connections made and release resources held up in the connect method.

    // orders-table.datasource.ts
    ...
    export class OrdersTableDataSource extends DataSource<Order> {
     paginator: MatPaginator;
     sort: MatSort;
    
     constructor(private orderService: OrderService) {
       super();
     }
    
     connect(): Observable<Order[]> {
       const dataMutations = [
         of('Initial load'),
         this.paginator.page,
         this.sort.sortChange
       ];
    
       return merge(...dataMutations).pipe(mergeMap(() => {
         return this.orderService.getOrders(
           this.paginator.pageIndex * this.paginator.pageSize,
           this.paginator.pageSize,
           this.sort.active,
           this.sort.direction
         );
       }));
     }
    
     disconnect() {} 
    }

    In the orders table template, insert the new columns and bind the length property of <mat-paginator> to the dataLength property. For the status column, use a <mat-chip> element for better visualization of the order status. To have access to <mat-chip>, add the MatChipsModule as an import to AppModule.

    <!-- orders-table.component.html -->
    <div class="mat-elevation-z8">
     <table mat-table class="full-width-table" matSort aria-label="Elements">
       <!-- Id Column -->
       <ng-container matColumnDef="id">
         <th mat-header-cell *matHeaderCellDef mat-sort-header>Id</th>
         <td mat-cell *matCellDef="let row">{{row.id}}</td>
       </ng-container>
    
       <!-- Date Column -->
       <ng-container matColumnDef="date">
         <th mat-header-cell *matHeaderCellDef mat-sort-header>Date</th>
         <td mat-cell *matCellDef="let row">{{row.date | date }}</td>
       </ng-container>
    
       <!-- Name Column -->
       <ng-container matColumnDef="name">
         <th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
         <td mat-cell *matCellDef="let row">{{row.name}}</td>
       </ng-container>
    
       <!-- Order Total Column -->
       <ng-container matColumnDef="orderTotal">
         <th mat-header-cell *matHeaderCellDef mat-sort-header>Order Total</th>
         <td mat-cell *matCellDef="let row">{{row.orderTotal | currency}}</td>
       </ng-container>
    
       <!-- Payment Mode Column -->
       <ng-container matColumnDef="paymentMode">
         <th mat-header-cell *matHeaderCellDef mat-sort-header>Payment Mode</th>
         <td mat-cell *matCellDef="let row">{{row.paymentMode}}</td>
       </ng-container>
    
       <!-- Status Column -->
       <ng-container matColumnDef="status">
         <th mat-header-cell *matHeaderCellDef mat-sort-header>Status</th>
         <td mat-cell *matCellDef="let row">
           <mat-chip-list>
             <mat-chip color="{{ row.status == 'delivered' ? 'primary' : ( row.status == ’shipped' ? 'accent' : 'warn' ) }}" selected>
               {{row.status}}
             </mat-chip>
           </mat-chip-list>
         </td>
       </ng-container>
    
       <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
       <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
     </table>
    
     <mat-paginator #paginator [length]="dataLength" [pageIndex]="0" [pageSize]="5" [pageSizeOptions]="[5, 10, 15, 20]">
     </mat-paginator>
    </div>

    Once data has been added to the table, this is what the dashboard will look like:

    dashboard with charts and table
    Dashboard with charts and table (Large preview)

    Creating A Mini Card Component

    All that’s left to complete the dashboard is to populate the four small cards that sit at the top. Having smaller summary cards as part of the dashboard makes it easy to highlight brief pieces of information that do not need whole charts or tables. In this example, the four mini cards will display total sales, average order value, the total number of orders, and the number of returning customers that visited the store for the day. This is just an example. These mini cards cannot be generated like with the navigation, dashboard layout, charts, and the table. They have no schematics. Below we’ll briefly go through how to create them. Although we’re going to add data specific to the example, you can add whatever you want to them or decide to do away with them altogether. To start off, generate the mini-card component, run:

    ng g c mini-card -m app --style css

    You can find the template for the component linked here and its styling here. This component has eight input properties that you can find out how to add here. To get data to the mini card components, inject the service that provides data to them in the DashComponent constructor. Assign data received from the service to a property of the DashComponent. In this instance, we’ll get data from the StoreSummaryService and assign it to the miniCardData property. Here’s how:

    // dash.component.ts
    export class DashComponent implements OnInit{
    ...
     miniCardData: StoreSummary[];
    
     constructor(private breakpointObserver: BreakpointObserver, private summaryService: StoreSummaryService) {}
    
     ngOnInit() {
       this.summaryService.getStoreSummary().subscribe({
         next: summaryData => {
           this.miniCardData = summaryData;
         }
       });
     } 
    }

    To add the mini-cards to the dash component and have them populated with data from the service:

    <!--dash.component.html-->
    ...
    
    <!--Mini Cards-->
       <mat-grid-tile *ngFor="let mc of miniCardData" [colspan]="( cardLayout | async )?.miniCard.cols"
         [rowspan]="( cardLayout | async )?.miniCard.rows">
         <app-mini-card Create A Responsive Dashboard With Angular Material And ng2-Charts — Smashing Magazine="mc.title" [textValue]="mc.textValue" [value]="mc.value" [color]="mc.color" [percentValue]="mc.percentValue"></app-mini-card>
       </mat-grid-tile>
    
    ...

    The screenshot below is what the dashboard will look like with the mini cards populated.

    dashboard with charts, tables, and mini-cards
    Dashboard with charts, tables, and mini-cards. (Large preview)

    Putting All Together

    In the end, the dashboard component template should contain:

    <!-- dashboard.component.html -->
    <div class="grid-container">
     <h1 class="mat-h1">Dashboard</h1>
     <mat-grid-list cols="{{ ( cardLayout | async )?.columns }}" rowHeight="200px">
       <!--Mini Cards-->
       <mat-grid-tile *ngFor="let mc of miniCardData" [colspan]="( cardLayout | async )?.miniCard.cols"
         [rowspan]="( cardLayout | async )?.miniCard.rows">
         <app-mini-card [icon]="mc.icon" Create A Responsive Dashboard With Angular Material And ng2-Charts — Smashing Magazine="mc.title" [value]="mc.value" [color]="mc.color" [isIncrease]="mc.isIncrease" duration="since last month" [percentValue]="mc.percentValue" [isCurrency]="mc. isCurrency"></app-mini-card>
       </mat-grid-tile>
       <!--Charts-->
       <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
         <app-card title="Monthly Revenue">
           <app-annual-sale-chart></app-annual-sale-chart>
         </app-card>
       </mat-grid-tile>
       <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
         <app-card title="Product Sales">
           <app-product-sales-chart></app-product-sales-chart>
         </app-card>
       </mat-grid-tile>
       <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
         <app-card title="Sales by Traffic Source">
           <app-sales-traffic-chart></app-sales-traffic-chart>
         </app-card>
       </mat-grid-tile>
       <mat-grid-tile [colspan]="( cardLayout | async )?.chart.cols" [rowspan]="( cardLayout | async )?.chart.rows">
         <app-card title="Online Store Sessions by Traffic Source">
           <app-store-sessions-chart></app-store-sessions-chart>
         </app-card>
       </mat-grid-tile>
       <!--Table-->
       <mat-grid-tile [colspan]="( cardLayout | async )?.table.cols" [rowspan]="( cardLayout | async )?.table.rows">
         <app-card title="Latest Orders">
           <app-orders-table></app-orders-table>
         </app-card>
       </mat-grid-tile>
     </mat-grid-list>
    </div>

    Here’s what the resultant dashboard contains.

    completed dashboard
    Completed dashboard (Large preview)

    Conclusion

    Creating dashboards involves a fair amount of work and planning. A way to make building them faster is to use the various schematics provided by Angular Material and ng2-charts. With these schematics, running a command will generate a wholly complete component and can result in having a dashboard up and running fairly quickly. This leaves you a lot more time to focus on creating data services and adding them to your dashboard components.

    If you want to learn more about some of the schematics provided by Angular Material, visit material.angular.io, and for those provided by ng2-charts, visit their site linked here.

    Smashing Editorial
    (ra, yk, il)

    Source link