Need to show users of your Angular app a trendline? If so, then you've come to the right place.

I'll show you how to make it happen with ngx-echarts.

And when you're done, you'll have a beautiful chart that looks like this:

Pretty neat, huh?

Follow this article if you want to produce something akin to that.

If you don't want to spend a lot of time reading words, though, you're welcome to spend a lot of time reading code instead by going straight to the source on GitHub.

Otherwise, let's get started.

A Not-So-New Beginning

This guide assumes you've already got ngx-echarts set up and working with your Angular app. If not, feel free to check out my guide on getting up and running with ngx-echarts.

Then come on back. I'll save your seat at the table.

What You're Doing

Before I get into the how, let me first explain the what.

In this guide, you're going to show a CRM app user the revenue contribution that he or she will make to the company during the next 60 days.

You'll do that by gathering all the info about the user's deals that are expected to close within the next couple of months. Then, you'll also get the expected date of each deal closure.

Armed with that data, you'll use ngx-echarts to show how much revenue the user will generate for the company during that time period.

You'll do that with a line graph that's destined to go up because the value of each deal adds to the summarized values of the previous deals. 

In the graph above, for example, it looks like the user will generate around $1.6 million over the next 60 days with each dot on the graph reprsenting one or more deals that close on the date specified on the x-axis.

Now it's time to learn how to do that.

With Options

You'll do it with options.

Remember, when you use ngx-echarts with Angular, all the heavy lifting happens in the options object. Then, you just include the following HTML in your template:

<div echarts [options]="options"></div>

See how easy that is? 

So all you need to do is populate that options object in the related component class.

Here's what the code in that class looks like:

const maxDays: number = 60;

@Component({
  selector: 'app-revenue-contribution',
  templateUrl: './revenue-contribution.component.html',
  styleUrls: ['./revenue-contribution.component.css']
})
export class RevenueContributionComponent implements OnInit {

  options: any;
  pageTitle: string = 'Revenue Contribution';
  map: Map<string, number> = new Map<string, number>();
  pageSubtitle: string = 'Next ' + maxDays + ' Days';

  constructor(private dealService: DealService, private dateService: DateService) { }

  ngOnInit(): void {
    this.loadDeals();
  }

  private getDealCriteria(): DealCriteria {
    let criteria: DealCriteria = new DealCriteria();
    criteria.userId = "6014081e221e1b534a8aa432";
    criteria.minDate = Date.now();
    criteria.maxDate = this.dateService.getDaysForwardAsNumber(maxDays);
    criteria.orderType = 'ASC';

    return criteria;
  }

  private loadDeals() {
    let criteria: DealCriteria = this.getDealCriteria();

    //console.log("Deal criteria is ", criteria);

    this.dealService.fetchDealsByCriteria(criteria).subscribe(
      (deals: Deal[]) => this.handleDealsResponse(deals),
      (err: Error) => this.handleError(err)
    );
  }

  private handleDealsResponse(deals: Deal[]) {
    deals.forEach(deal => {
      this.addDeal(deal);
    });

    //console.log("chartData is ", this.map);

    this.setOptions();
  }

  private addDeal(deal: Deal) {
    let date: string = this.dateService.getShortDateDisplay(deal.expectedClosureDate);
    let value: number = this.dealService.getAmount(deal);

    if (this.map.get(date)) {
      let currentVal: number = this.map.get(date);
      let newVal: number = currentVal + value;

      this.map.set(date, newVal);
    } else {
      let lastValue: number = Array.from(this.map.values()).pop();
      if (!lastValue) lastValue = 0;

      this.map.set(date, value + lastValue);
    }
  }

  private setOptions() {
    this.options = {
      grid: {
        //prevents cutoff of y-axis labels
        left: '15%'
      },
      xAxis: {
        type: 'category',
        data: Array.from(this.map.keys())
      },
      yAxis: {
        type: 'value',
        axisLabel: {
          formatter: function (value) {
            if (value < 1000000) return '$' + (value / 1000) + 'K';
            else return '$' + (value / 1000000) + 'M';
          }
        }
      },
      series: [{
        data: Array.from(this.map.values()),
        type: 'line',
        smooth: true
      }]
    };
  }

  private handleError(err: Error) {
    console.error(err);
  }
}

I'll unpack that beast method-by-method.

The loadDeals() method retrieves deals based on the criteria defined in getDealCriteria().

That getDealCriteria() method instantiates a DealCriteria object and sets the following criteria using its properties:

  • The user ID (currently hardcoded for simplicity)
  • The minimum date to search for deals (the current point in time)
  • The maximum date to search for deals (60 days out)
  • The sort order of the returned deals (ascending by date)

The deals, by the way, get returned as an array of Deal objects.

But alas, since the retrieval happens asyncronously, the code subscribes to the Observable returned by DealService and then extracts the array from that Observable.

That's when execution gets handed off to handleDealsResponse().

The handleDealsResponse() method iterates over each Deal object returned by the downstream service and invokes addDeal().

That addDeal() method extracts two important pieces of info from the Deal object: 

  • The date of the deal
  • The amount (in dollars) of the deal value

It's going to store that info in a Map object with the date (in string format) as the key and the value as the... value.

But... it's a possible that more than one deal can close on the same date. In that case the code will simply add the values of the deals that close on the same date together and store that sum in the Map object.

That's the point of that if check you see in addDeal().

Another but... the value for each date is cumulative. That means the code takes the previous date's value and adds it to the current date's value. Hence this code:

let lastValue: number = Array.from(this.map.values()).pop();

That's how it gets the previous date's value.

Once addDeal() has processed each Deal object, the Map is ready to for use in the chart.

So let's take a closer look at setOptions():

private setOptions() {
  this.options = {
    grid: {
      //prevents cutoff of y-axis labels
      left: '15%'
    },
    xAxis: {
      type: 'category',
      data: Array.from(this.map.keys())
    },
    yAxis: {
      type: 'value',
      axisLabel: {
        formatter: function (value) {
          if (value < 1000000) return '$' + (value / 1000) + 'K';
          else return '$' + (value / 1000000) + 'M';
        }
      }
    },
    series: [{
      data: Array.from(this.map.values()),
      type: 'line',
      smooth: true
    }]
  };
}

Remember: options is a complex object with lots of properties that you may or may not use. The code above doesn't even scratch the surface of what you can accomplish.

But it does need some explanation.

For starters, the grid property shifts display of the grid to the left by 15% (the default is 10%). That prevents the labels on the y-axis from getting cut off.

But you might not need to do that if your graph takes up the whole screen.

Next, the xAxis object sets details about the x-axis.

The code sets the type property to 'category' because it's treating dates like strings here. 

The data on the xAxis gets set from the keys in the Map object I talked about above. Remember: each key in the Map is a date in string format.

That Array.from() business you see above just takes all the keys in the Map (in insertion order) and translates them to an array.

The yAxis object sets several properties relevant to the y-axis.

The type there is set to 'value' because that's where the numbers go.

That complex axisLabel setting creates an abbreviated y-axis label while also plopping a dollar sign in front of it. So the output looks like "$600K" instead of "600000".

The series object defines a few properties relevant to the data that gets graphed.

The data property there grabs all the values from the Map object as an array. Those are the numbers that ultimately get plotted on the graph.

The type property defines the chart type. It's 'line' because this is a line chart.

Finally, that smooth boolean set to true instructs ECharts to output a curvy line instead of a jagged line.

And that's it. No need to see the finished product because you've already seen it above.

Wrapping It Up

There you have it. You now know how to produce a line graph using ngx-echarts.

So take what you've learned here and apply it as early and as often as you like.

Just make sure that you have fun!

Photo by Andrea Piacquadio from Pexels