user3642173
user3642173

Reputation: 1255

Aggregating stream of data in Angular2

I have a stream of data coming in as an observable stream from Firebase. i.e.

items: FirebaseListObservable<BucketlistItem[]> = [{
  room: 'kitchen', 
  price: 200
}, {
  room: 'kitchen', 
  price: 400
}, {
  room: 'bedroom', 
  price: 400
}]

I want to turn this into an aggregated list such adding up the price for similar rooms.

[{
  room: 'kitchen', 
  price: 600
}, {
  room: 'bedroom', 
  price: 400
}]

I have achieved that with by subscribing to the items observable and aggregating it on the fly to shows it in a barchart. However, this doesn't seem like the optimal solutions especially as the items array is often changed in the view. Is there a clever way to use observables for a more robust solution?

import {Component, Input, OnInit, Output, EventEmitter} from '@angular/core';
import {BucketlistItem} from "../../bucketlist/bucketlist-item";
import {FirebaseListObservable} from "angularfire2";

@Component({
  selector: 'app-inspiration-header',
  templateUrl: './inspiration-header.component.html',
  styleUrls: ['./inspiration-header.component.css']
})
export class InspirationHeaderComponent implements OnInit, Input {
  @Input() items: FirebaseListObservable<BucketlistItem[]>;

  @Input() budget: string;
  @Output() onBudget = new EventEmitter<string>();
  public percentOfBudget: number;
  public totalCost: number;

  public barChartLabels: string[] = [];

  public barChartData: any =
    [{data: []}]
  ;

  constructor() { }

  ngOnInit() {
    this.items
      .subscribe(items => {
        const aggregate = {};
        let totalCost = 0;
        for (const item of items) {
          if (item.bucketed) {
            totalCost += Number(item.price);
            if (item.room in aggregate) {
              aggregate[item.room] += Number(item.price);
            } else {
              aggregate[item.room] = Number(item.price);
            }
          }
        }
        this.setPercentageOfBudget(totalCost);
        this.totalCost =  totalCost;
        for (const room in aggregate) {
          this.barChartLabels.push(room);
          this.barChartData[0].data.push(aggregate[room]);
        }
        this.barChartLabels = Object.keys(aggregate);
      });
  }

  private setPercentageOfBudget(totalCost: number) {
    this.percentOfBudget = Math.round(totalCost / Number(this.budget) * 100);
  }

}

Upvotes: 0

Views: 752

Answers (1)

snorkpete
snorkpete

Reputation: 14574

Based on what you're describing, you probably want to use the scan operator.

The scan operator will emit whenever its source observable emits a value. But, the callback to the scan operator will receive both the current value emitted from the source observable, plus an accumulated value from the past submissions (similar in concept to how Array.reduce works).

It sounds complicated, but what this means in practical terms is that each time your source observable emits a value, your scan operator will combine that new value with an 'old value' and then emit that new combined value.

You still have to write the logic to combine the new value with the old accumulated value, but I think the resulting observable will get you closer to what you want.

// don't forget to import your operator
import 'rxjs/add/operator/scan';
.....


  @Input() items: FirebaseListObservable<BucketlistItem[]>;
  combinedItems: Observable<BucketlistItem[]>;

  ngOnInit() {
    this.combinedItems = items.scan((accumulated, current) => {
      return this.combine(accumulated, current);
    });
  }

  combine(total: BucketListItem[], newItem: BucketListItem[]) {
    // implement your similar logic of combining 
    // [{room: 'kitchen', price: 200}]  <-- accumulated 
    // with [{room: 'kitchen', price: 200}, {room: 'kitchen', price: 400}]  <--- current
    // to give your final output
  }

There's no getting around implementing the combination logic (although you can probably make creative use of Array.reduce to simplify your code).

Upvotes: 2

Related Questions