5

I am new to Angular and i want to develop a funnel-graph. i like the funnel-graph-js library. i tried a lot but haven't succeed.

here is my funnel-graph-directive.ts

import { Directive, ElementRef } from '@angular/core';

// import * as graph from '../../../assets/js/funnel-graph.js';
import * as graph from 'funnel-graph-js/dist/js/funnel-graph.js';
var graph = new FunnelGraph({
  container: '.funnel',
  gradientDirection: 'horizontal',
  data: {
    labels: ['Impressions', 'Add To Cart', 'Buy'],
    subLabels: ['Direct', 'Social Media', 'Ads'],
    colors: [
      ['#FFB178', '#FF78B1', '#FF3C8E'],
      ['#A0BBFF', '#EC77FF'],
      ['#A0F9FF', '#7795FF']
    ],
    values: [
      [3500, 2500, 6500],
      [3300, 1400, 1000],
      [600, 200, 130]
    ]
  },
  displayPercent: true,
  direction: 'horizontal'
});

graph.draw();
@Directive({
  selector: '[appFunnelGraph]'
})
export class FunnelGraphDirective {
  style: any;
  constructor(el: ElementRef) {
    el.nativeElement.style.backgroundColor = 'yellow';
  }
}

I have added these lines in my angular.json

"styles": [
  "src/styles.scss",
  "./node_modules/funnel-graph-js/dist/css/main.css",
  "./node_modules/funnel-graph-js/dist/css/theme.css"
],
"scripts": [
  "./node_modules/funnel-graph-js/dist/js/funnel-graph.js"
]

Here is the error i am getting enter image description here

Zuber
  • 3,393
  • 1
  • 19
  • 34
  • 1
    In your HTML there should be an element with a "funnel" class. Are you sure this element exists in your code? – Drago96 Oct 01 '19 at 09:43
  • @Drago96 initially, i hadn't put the element with a `funnel` class but yes your comment was helpful. i made a `service` and that's how i solved my problem. – Zuber Oct 03 '19 at 05:51

2 Answers2

2

As long as you linked the javascript file in the html, it will work fine.

EDIT:

A better way to include an addition javascript file is to put it into the "scripts" section in the angular.json file. You can also add

declare const FunnelGraph: any

in order to compile without errors. This has been taken from an answer to a stackoverflow question and this guide. Remember to include the css files in that json too!

EDIT END

You get that error because the code tries to look for an HTML element with a class named "funnel", but cannot find it. Since this is a directive, it would be better if it was a little more generalized.

First of all, you should move your graph-generating code inside the constructor, since that's were the directive logic resides. To better generalize this directive, it would be best if you gave a unique id to that element and change the code accordingly. This is how I would do it:

HTML:

<div id="funnel-graph-1" appFunnelGraph></div>

JS:

import { Directive, ElementRef } from '@angular/core';

// It should be fine to just import this in the html with a script tag
// import * as graph from 'funnel-graph-js/dist/js/funnel-graph.js';

@Directive({
  selector: '[appFunnelGraph]'
})
export class FunnelGraphDirective {
  style: any;
  constructor(el: ElementRef) {
    el.nativeElement.style.backgroundColor = 'yellow';

    var graph = new FunnelGraph({
      // Generalize the container selector with the element id
      container: '#' + el.nativeElement.id,
      gradientDirection: 'horizontal',
      data: {
        labels: ['Impressions', 'Add To Cart', 'Buy'],
        subLabels: ['Direct', 'Social Media', 'Ads'],
        colors: [
          ['#FFB178', '#FF78B1', '#FF3C8E'],
          ['#A0BBFF', '#EC77FF'],
          ['#A0F9FF', '#7795FF']
        ],
        values: [
          [3500, 2500, 6500],
          [3300, 1400, 1000],
          [600, 200, 130]
        ]
      },
      displayPercent: true,
      direction: 'horizontal'
    });

    graph.draw();
  }
}
Drago96
  • 1,265
  • 10
  • 19
  • " this may generate a typescript error" ... That's why we should at least define an interface :) But all in all, that's my preferred way and more natural and less cumbersome than constructing a service. – ak.leimrey Oct 01 '19 at 15:27
  • yes you are right. `directive` approach is less cumbersome than `service`. thanks for answering. Upvote. – Zuber Oct 03 '19 at 07:51
  • @ak.leimrey You're right, I maybe went a little easy on that :) I updated the answer with some usefult tips found online (tested and working) – Drago96 Oct 03 '19 at 13:25
1

I ended up by creating service instead of using directive approach.

  • First i generated a service called dynamic-script-loader-service in my dashboard module.

dynamic-service-loader.service.service.ts

import { Injectable } from '@angular/core';

interface Scripts {
  name: string;
  src: string;
}

export const ScriptStore: Scripts[] = [
  { name: 'chartjs', src: 'https://unpkg.com/funnel-graph-js@1.3.9/dist/js/funnel-graph.min.js' },
];

declare var document: any;

@Injectable()
export class DynamicScriptLoaderServiceService {

  private scripts: any = {};

  constructor() {
    ScriptStore.forEach((script: any) => {
      this.scripts[script.name] = {
        loaded: false,
        src: script.src
      };
    });
  }

  load(...scripts: string[]) {
    const promises: any[] = [];
    scripts.forEach((script) => promises.push(this.loadScript(script)));
    return Promise.all(promises);
  }

  loadScript(name: string) {
    return new Promise((resolve, reject) => {
      if (!this.scripts[name].loaded) {
        //load script
        let script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = this.scripts[name].src;
        if (script.readyState) {  //IE
          script.onreadystatechange = () => {
            if (script.readyState === 'loaded' || script.readyState === 'complete') {
              script.onreadystatechange = null;
              this.scripts[name].loaded = true;
              resolve({ script: name, loaded: true, status: 'Loaded' });
            }
          };
        } else {  //Others
          script.onload = () => {
            this.scripts[name].loaded = true;
            resolve({ script: name, loaded: true, status: 'Loaded' });
          };
        }
        script.onerror = (error: any) => resolve({ script: name, loaded: false, status: 'Loaded' });
        document.getElementsByTagName('head')[0].appendChild(script);
      } else {
        resolve({ script: name, loaded: true, status: 'Already Loaded' });
      }
    });
  }

}

dashboard.component.ts

import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { DynamicScriptLoaderServiceService } from '../dynamic-script-loader-service.service';
import * as FunnelGraph from 'funnel-graph-js';

function dashboardFunnel() {
  const graph = new FunnelGraph({
    container: '.funnel',
    // gradientDirection: 'horizontal',
    data: {
      labels: ['Label 7', 'Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6'],
      colors: ['#00A8FF', '#00A8FF', '#00A8FF', '#00A8FF', '#00A8FF', '#00A8FF', '#00A8FF'],
      // color: '#00A8FF',
      values: [12000, 11000, 10000, 9000, 8000, 7000, 6000]
    },
    displayPercent: true,
    direction: 'horizontal',
  });

  graph.draw();
}

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: ['./dashboard.component.scss'],
  encapsulation: ViewEncapsulation.None
})
export class DashboardComponent implements OnInit {

  constructor(
    private dynamicScriptLoader: DynamicScriptLoaderServiceService
  ) {}


  ngOnInit() {
    this.loadScripts();
    dashboardFunnel();
  }

  private loadScripts() {
    // You can load multiple scripts by just providing the key as argument into load method of the service
    this.dynamicScriptLoader.load('chartjs', 'random-num').then(data => {
      // Script Loaded Successfully
    }).catch(error => console.log(error));
  }

}

added providers in my dashboard.module.ts

providers: [DynamicScriptLoaderServiceService],

added css in my angular.json

"styles": [
              "src/styles.scss",
              "./node_modules/funnel-graph-js/dist/css/main.css",
              "./node_modules/funnel-graph-js/dist/css/theme.css"
            ],

added div with class funnel in dashboard.component.html

<div class="funnel"></div>
Zuber
  • 3,393
  • 1
  • 19
  • 34