Code Monkey home page Code Monkey logo

angular-progress-http's Introduction

angular-progress-http

A thin wrapper around Angular 2+ Http service that adds ability to work with upload/download progress

npm

NPM Version NPM Downloads

build info

Build Status Coverage Status

Usage

Import HttpModule and ProgressHttpModule

import { NgModule } from "@angular/core";
import { HttpModule } from "@angular/http";
import { ProgressHttpModule } from "angular-progress-http";

@NgModule({
    imports: [
        HttpModule,
        ProgressHttpModule
    ]
})
export class AppModule {}

Inject ProgressHttp into your component and you are ready to go. See API description below for available methods.

import {Component} from "@angular/core";
import { ProgressHttp } from "angular-progress-http";

@Component({})
export class AppComponent {
    constructor(private http: ProgressHttp) {
        const form = new FormData();
        form.append("data", "someValue or file");

        this.http
            .withUploadProgressListener(progress => { console.log(`Uploading ${progress.percentage}%`); })
            .withDownloadProgressListener(progress => { console.log(`Downloading ${progress.percentage}%`); })
            .post("/fileUpload", form)
            .subscribe((response) => {
                console.log(response)
            })
    }
}

Supported Angular versions

  • Both Angular 4 and 5 are supported by the latest version
  • Angular 2 is supported in v0.5.1. Use command npm install [email protected] to get it

Releases

For release notes please see CHANGELOG.md

API description

ProgressHttp service extends Http service provided by Angular/Http which means that you get all of the Http methods including

request(url: string | Request, options?: RequestOptionsArgs): Observable<Response>;
get(url: string, options?: RequestOptionsArgs): Observable<Response>;
post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response>;

and others.

In addition it provides two methods for handling progress:

withDownloadProgressListener(listener: (progress: Progress) => void): HttpWithDownloadProgressListener;
withUploadProgressListener(listener: (progress: Progress) => void): HttpWithUploadProgressListener;

They both take callback as argument and return new instances of the service.

The interfaces returned from methods are described below:

interface HttpWithDownloadProgressListener extends Http {
    withUploadProgressListener(listener: (progress: Progress) => void): Http;
}

interface HttpWithUploadProgressListener extends Http {
    withDownloadProgressListener(listener: (progress: Progress) => void): Http;
}

Their purpose is to make libary easier to use and add compile-time checks for method calls

progressHttp //can use http api or call withUploadProgressListener or withDownloadProgressListener
    .withUploadProgressListener(progress => {}) //can use http api or call withDownloadProgressListener
    .withDownloadProgressListener(progress => {}) //here and on lines below can only use http api
    .post("/fileUpload", form)
    .subscribe((response) => {})

This restriction is used to make sure that there are now repeating calls to add progress listeners that will overwrite previously assigned handlers and may confuse developer

Calls to both methods are immutable (return new instances and do not change the internal state of the service), so you may do next things

let http1 = this.progressHttp.withUploadProgressListener(progress => { console.log("Uploading 1") });
let http2 = this.progressHttp.withUploadProgressListener(progress => { console.log("Uploading 2") });
let http3 = http1.withDownloadProgressListener(progress => { console.log("Downloading 1") });

In the code above http1 and http2 will have different upload listeners. http3 will have same upload listener as http1 and a download listener

This behavior may be useful when uploading multiple files simultaneously e.g.

this.files.forEach(f => {
    const form = new FormData();
    form.append("file", f.file);

    this.progressHttp
        .withUploadProgressListener(progress => { f.percentage = progress.percentage; })
        .post("/fileUpload", form)
        .subscribe((r) => {
            f.uploaded = true;
        })
});

Progress interface

Both upload and download progress listeners accept single argument that implements Progress interface

interface Progress {
    event: ProgressEvent, //event emitted by XHR
    lengthComputable: boolean, //if false percentage and total are undefined
    percentage?: number, //percentage of work finished
    loaded: number, //amount of data loaded in bytes
    total?: number // amount of data total in bytes
}

How it works internally

The library tries to rely on Angular code as much as possible instead of reinventing the wheel.

It extends BrowserXhr class with logic that adds event listeners to XMLHttpRequest and executes progress listeners. Other parts that are responsible for http calls (Http, XhrConnection, XhrBackend) are used as is, which means that angular-progress-http will automatically receive fixes and new features from newer versions of angular/http

Using custom HTTP implementations

If you want to use custom Http service with progress you need to follow certain steps. Let's review them on example of ng2-adal library - a library for accessing APIs restricted by Azure AD.

  1. create factory class that will implement HttpFactory interface
interface HttpFactory {
    create<T extends Http>(backend: ConnectionBackend, requestOptions: RequestOptions): T;
}

This interface contains single method to create instances of class derived from Http. The create method accepts ConnectionBackend and default RequestOptions which are always required for Http to make creation of factory easier.

Let's examine AuthHttp (Http implementation from ng2-adal) constructor to understand what dependencies it has:

constructor(http: Http, adalService: AdalService);

As you can see, it needs an instance of http service and adalService to work properly. With this knowledge we can now create the factory class.

The factory for ng2-adal is quite simple and will look next way:

import { Injectable } from "@angular/core";
import { ConnectionBackend, RequestOptions } from "@angular/http";
import { AuthHttp, AdalService } from "ng2-adal/core";
import { HttpFactory, AngularHttpFactory } from "angular-progress-http";

@Injectable()
export class AuthHttpFactory implements HttpFactory {
  constructor(
    private adalService: AdalService,
    private angularHttpFactory: AngularHttpFactory
  ) {}

  public create(backend: ConnectionBackend, requestOptions: RequestOptions) {
    const http = this.angularHttpFactory.create(backend, requestOptions);
    return new AuthHttp(http, this.adalService);
  }
}
  1. Register created factory as a provider in your application
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { ProgressHttpModule, HTTP_FACTORY } from 'angular-progress-http';
import { AuthHttpModule } from "ng2-adal/core";
import { AuthHttpFactory } from "./ng2-adal.http.factory.service";

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpModule,
    ProgressHttpModule,
    AuthHttpModule
  ],
  providers: [
    { provide: HTTP_FACTORY, useClass: AuthHttpFactory }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

That's it. Now each time when you will call methods of ProgressHttp it will use your custom http implementation internally and add progress listeners to it.

Building from sources

  1. Clone the repository to the local PC
  2. Run
npm install
npm run build
  1. The built library can be found in "build" folder

Running tests

  1. Clone the repository to the local PC
  2. Run
npm install
npm test

Running examples

There are two example projects at the moment

  • basic example of upload progress functionality (examples/upload-download)
  • an example that uses custom http implementation (examples/custom-http)
  1. Make sure that you built library from sources as described above
  2. Navigate to selected example folder
  3. Run
npm install
npm start
  1. Open browser on http://localhost:3000
  2. Choose some files (big size of the files will let you see the progress bar) and click upload
  3. Use throttling in Chrome dev tools to slow down network if progress jumps from 0 to 100 immediately

Сontribution

Feel free to ask questions and post bugs/ideas in the issues, as well as send pull requests.

License

angular-progress-http's People

Contributors

darkxahtep avatar josephliccini avatar sebastianstehle avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

angular-progress-http's Issues

Breaking changes in typescript 2.4.1

I'm getting the following error using the latest typescript - 2.4.1. Reverting to 2.3.2 fixed the issue

ERROR in C:/git/admin-veronica-ng/node_modules/angular-progress-http/lib/AngularHttpFactory.d.ts (3,22): Class 'AngularHttpFactory' incorrectly implements interface 'HttpFactory'.
  Types of property 'create' are incompatible.
    Type '(backend: ConnectionBackend, requestOptions: RequestOptions) => Http' is not assignable to type '<T extends Http>(backend: ConnectionBackend, requestOptions: RequestOptions)
 => T'.
      Type 'Http' is not assignable to type 'T'.

Issue loading with SystemJs

Hi, I have issue trying to load it with SystemJs. Here is excerpt from my SystemJs config:

paths: {
'npm:': 'node_modules/'
},
map: {
'angular-progress-http': 'npm:angular-progress-http'
},
packages: {
'angular-progress-http': {
main: 'index.js',
defaultExtension: 'js'
}
}

I'm getting following error in browser:

Error: Unable to dynamically transpile ES module A loader plugin needs to be configured via SystemJS.config({ transpiler: 'transpiler-module' }). Instantiating http://localhost:8090/node_modules/angular-progress-http/index.js

Do you have example working with SystemJs loader?

Problem lengthComputable = false

Hi,

I have a problem width the response on request.

I receive lengthComputable = false in response but in my php file i set a content length :
header("Content-Length: ".$size);

and when i look on postman the result of my request i see :
Content-Length →16102153

Can you help me ?

Getting an error while running the build

While running npm run build, getting below error,
Could you please help me to understand the issue?

[email protected] copy-ts C:\Siva\Devlopment\angular-progress-http
copyup 'src/**/*' build

[email protected] transform-package C:\Siva\Devlopment\angular-prog
ess-http
node scripts/transformPackageJson.js

fs.js:558
return binding.open(pathModule._makeLong(path), stringToFlags(flags), mode);
^

Error: ENOENT: no such file or directory, open 'C:\Siva\Devlopment\angular-prog
ess-http\build\package.json'
at Object.fs.openSync (fs.js:558:18)
at Object.fs.writeFileSync (fs.js:1223:33)
at Object. (C:\Siva\Devlopment\angular-progress-http\scripts\tra
sformPackageJson.js:11:4)
at Module._compile (module.js:571:32)
at Object.Module._extensions..js (module.js:580:10)
at Module.load (module.js:488:32)
at tryModuleLoad (module.js:447:12)
at Function.Module._load (module.js:439:3)

Angular 5 compatibility

Any plans for Angular 5 compatibility? We'll be forking this locally in our project. Would be happy to contribute if needed.

[Question] - How can I send the progress Event?

I have an upload service which I access from a component. How can I send the upload progress from my service to my component?

From all the examples it looks like I cannot do this but have to have my service in my component which defeats the purpose of having a service.

upload.service.ts:

uploadSingleFileToServer(userFile) {
    return this.http
      .withUploadProgressListener(progress => { console.log(`Uploading ${progress.percentage}%`); }) // Inserted for testing
      .post(this.url + '/upload', userFile)
      .map(res => res.json());
  }

app.component.ts:

onFileUploadSubmit(form: NgForm) {
    const formData = new FormData();
    formData.append('userFile', this.userFile);
    formData.append('UserID', this.currentUID);
    this.fileUploadSubscription = this.uploadService
      .uploadSingleFileToServer(formData)
      .subscribe(data => {
        if (data.success) {
          this._notificationService.success('File Upload', data.msg);
          this.modalRef.hide();
          this.mongoImages = [];
          this.getFiles();
        } else {
          this._notificationService.error('File Upload', data.msg);
          this.modalRef.hide();
        }
      });
  }

As you can imagine, I want the progress in my component so that I can make use of it in the UI for the user.

Upload progress not accurate

Hi thanks for awesome work,
The download works as sweet
But for the upload, it shows the progress of upload but only after showing 100% percentage uploaded, it is hitting API and then API takes more time to send response back to client which makes as non-realistic progress

I am using Angular 5, .Net core API

Can you throw some light?

Setup code coverage

Use Istanbul and TypeScript source maps for code coverage and coveralls.io as the coverage reporting tool

node module should be pre-compiled

It is common when developing with Typescript + NPM to ignore node_modules folder when compiling. By requiring your module to be compiled, we are forced to either enumerate all modules in the tsconfig that shouldn't be compiled or compile all of our node_modules, neither of which are particularly friendly. It would be preferable to include an index.js either instead of index.ts or additionally in a dist/ folder and refer to it from package.json.

How to use angular-progress-http in service

This is a question rather than an issue,but i already have all my http requests in services, how can i migrate to angular-progress-http while keeping it in service ?

Thanks

Cannot use custom Http (with Angular 4)

I cannot seems to use a custom Http implementation. Maybe I do something wrong or maybe this is because I use Angular 4.

I get this error :

TypeError: this.http.request is not a function

in the request() method of ProgressHttp.

Here is my Http factory (derived from angular2-adal). The console.log do display a correct HttpAuth object as expected :

import { Injectable } from '@angular/core';
import { ConnectionBackend, RequestOptions } from '@angular/http';
import { HttpFactory, AngularHttpFactory } from 'angular-progress-http';
import { AuthService } from './auth.service';
import { HttpAuth } from './http-auth.service';

@Injectable()
export class HttpAuthFactory implements HttpFactory {
    constructor(
        private authService: AuthService,
        private angularHttpFactory: AngularHttpFactory
    ) {}

    public create(backend: ConnectionBackend, requestOptions: RequestOptions) {
        const http = this.angularHttpFactory.create(backend, requestOptions);
        let httpAuth = new HttpAuth(http, this.authService);
        console.log(httpAuth);

        return httpAuth ;
    }
}

My Http implementation (derived from angular2-adal) :

import { Injectable } from '@angular/core';
import { Http, Response, Headers, RequestOptionsArgs, RequestOptions, RequestMethod, URLSearchParams } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { AuthService } from './auth.service';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

@Injectable()
export class HttpAuth {

    constructor (private http: Http, private authService: AuthService) {}

    get(url: string, options?: RequestOptionsArgs): Observable<any> {
        let options1 = new RequestOptions({ method: RequestMethod.Get });
        options1 = options1.merge(options);
        return this.sendRequest(url, options1);
    }

    post(url: string, body: any, options?: RequestOptionsArgs): Observable<any> {
        let options1 = new RequestOptions({ method: RequestMethod.Post, body: body });
        options1 = options1.merge(options);
        return this.sendRequest(url, options1);
    }

    delete(url: string, options?: RequestOptionsArgs): Observable<any> {
        let options1 = new RequestOptions({ method: RequestMethod.Delete });
        options1 = options1.merge(options);
        return this.sendRequest(url, options1);
    }

    patch(url: string, body: any, options?: RequestOptionsArgs): Observable<any> {
        let options1 = new RequestOptions({ method: RequestMethod.Patch, body: body });
        options1 = options1.merge(options);
        return this.sendRequest(url, options1);
    }

    put(url: string, body: any, options?: RequestOptionsArgs): Observable<any> {
        let options1 = new RequestOptions({ method: RequestMethod.Put, body: body });
        options1 = options1.merge(options);
        return this.sendRequest(url, options1);
    }

    head(url: string, options?: RequestOptionsArgs): Observable<any> {
        let options1 = new RequestOptions({ method: RequestMethod.Put });
        options1 = options1.merge(options);
        return this.sendRequest(url, options1);
    }

    private sendRequest(url: string, options: RequestOptionsArgs): Observable<string> {
        //make a copy
        let options1 = new RequestOptions();
        options1.method = options.method;
        options1 = options1.merge(options);

        if (!options1.headers) {
            options1.headers = new Headers();
        }

        options1.headers.set('Authorization', 'Bearer ' + this.authService.token);

        return this.http.request(url, options).map(this.extractData).catch(this.handleError);
    }

    private extractData(res: Response) {
        if (res.status < 200 || res.status >= 300) {
            throw new Error('Bad response status: ' + res.status);
        }

        var body = {};
        //if there is some content, parse it
        if (res.status != 204) {
            body = res.json();
        }

        return body || {};
    }

    private handleError(error: any) {
        // In a real world app, we might send the error to remote logging infrastructure
        let errMsg = error.message || 'Server error';
        console.error(JSON.stringify(error)); // log to console instead

        return Observable.throw(error);
    }
}

In my module:

@NgModule({
    bootstrap: [ AppComponent ],
    declarations: [
        AppComponent,
        VersionFormComponent,
        LoginComponent
    ],
    imports: [ // import Angular's modules
        BrowserModule,
        FormsModule,
        HttpModule,
        RouterModule.forRoot(ROUTES, { useHash: true, preloadingStrategy: PreloadAllModules }),
        Select2Module,
        NgbModule.forRoot(),
        ProgressHttpModule
    ],
    providers: [ // expose our Services and Providers into Angular's dependency injection
        ENV_PROVIDERS,
        APP_PROVIDERS,
        DataService,
        AuthService,
        { provide: HTTP_FACTORY, useClass: HttpAuthFactory }
    ]
})

If I remove the last provider entry (HTTP_FACTORY), it works again but of I course it doesn't use my custom implementation

Add download progress example

Use output throttling on node.js side if possible to make progress more illustrative and avoid using big files for it

browser inconsistency when using --prod

Everything works when using ng serve, but when I use ng serve --prod there is inconsistent behaviour among different browsers. I do not get any errors or warning in the compiler or console.

Also I could not get the example project working out of the box because I got the following error:

ERROR in Error encountered resolving symbol values statically. Could not resolve angular-progress-http relative to C:/Users/chris/OneDrive/DEVtalk/Projects/Angular/angular-progress/examples/upload-download/src/app/app.module.ts., resolving symbol AppModule in C:/Users/chris/OneDrive/DEVtalk/Projects/Angular/angular-progress/examples/upload-download/src/app/app.module.ts, resolving symbol AppModule in C:/Users/chris/OneDrive/DEVtalk/Projects/Angular/angular-progress/examples/upload-download/src/app/app.module.ts

ERROR in ./src/main.ts
Module not found: Error: Can't resolve './$$_gendir/app/app.module.ngfactory' in 'C:\Users\chris\OneDrive\DEVtalk\Projects\Angular\angular-progress\examples\upload-download\src'
 @ ./src/main.ts 4:0-74
 @ multi webpack-dev-server/client?http://localhost:4200 ./src/main.ts

My angular versions:

@angular/cli: 1.0.0
node: 6.9.5
os: win32 x64
@angular/animations: 4.2.5
@angular/common: 4.2.5
@angular/compiler: 4.2.5
@angular/core: 4.2.5
@angular/forms: 4.2.5
@angular/http: 4.2.5
@angular/material: 2.0.0-beta.7
@angular/platform-browser: 4.2.5
@angular/platform-browser-dynamic: 4.2.5
@angular/router: 4.2.5
@angular/service-worker: 1.0.0-beta.16
@angular/cli: 1.0.0
@angular/compiler-cli: 4.2.5

My code:

return this.http
      .withUploadProgressListener(progress => {
        console.log(progress.percentage);
        this.requestProgress.emit(progress)
      })
      .post(url, body)
      .map(response => response.json());

Browser behaviour:
IE => OK, in console I get an incremental percentage
Edge => NOK, in console I don't get an incremental percentage, only 100 at the end of the upload.
Chrome => NOK, I don't get anything in console
Firefox => NOK, I don't get anything in console

Error: No provider for ProgressHttp

Hi,

I am getting this error after ProgressHTTP to my project:

ERROR Error: Uncaught (in promise): Error: No provider for ProgressHttp!
Error: No provider for ProgressHttp!
at injectionError (reflective_errors.ts:71)
at noProviderError (reflective_errors.ts:105)
at ReflectiveInjector_.throwOrNull (reflective_injector.ts:500)
at ReflectiveInjector
.getByKeyDefault (reflective_injector.ts:543)
at ReflectiveInjector
.getByKey (reflective_injector.ts:404)
at ReflectiveInjector
.get (reflective_injector.ts:349)
at CaPPModuleInjector.get (module.ngfactory.js:320)
at CaPPModuleInjector.get (module.ngfactory.js:325)
at CaPPModuleInjector.getInternal (module.ngfactory.js:1113)
at CaPPModuleInjector.NgModuleInjector.get (ng_module_factory.ts:141)
at injectionError (reflective_errors.ts:71)
at noProviderError (reflective_errors.ts:105)
at ReflectiveInjector_.throwOrNull (reflective_injector.ts:500)
at ReflectiveInjector
.getByKeyDefault (reflective_injector.ts:543)
at ReflectiveInjector
.getByKey (reflective_injector.ts:404)
at ReflectiveInjector
.get (reflective_injector.ts:349)
at CaPPModuleInjector.get (module.ngfactory.js:320)
at CaPPModuleInjector.get (module.ngfactory.js:325)
at CaPPModuleInjector.getInternal (module.ngfactory.js:1113)
at CaPPModuleInjector.NgModuleInjector.get (ng_module_factory.ts:141)
at resolvePromise (zone.js:468)
at resolvePromise (zone.js:453)
at zone.js:502
at ZoneDelegate.invokeTask (zone.js:265)
at Object.onInvokeTask (ng_zone.ts:254)
at ZoneDelegate.invokeTask (zone.js:264)
at Zone.runTask (zone.js:154)
at drainMicroTaskQueue (zone.js:401)
at XMLHttpRequest.ZoneTask.invoke (zone.js:339)

Progress percentage undefined

Hi !
I try this lib but I don't have percentage on my download, it's always undefined.

here my simple code (download file works)

public getInstallerNew = (): Observable<Response> => { this.headers.set('Authorization', 'Basic ' + getToken(this._router)); return this._httpProgress.withDownloadProgressListener(progress => { console.log('Downloading ' + progress.percentage + '%'); }).get(this.url + "download/app", {responseType: ResponseContentType.Blob, headers: this.headers}); };

console write "Downloading undefined%" about 200 times

Replace deprecated OpaqueToken with InjectionToken<?>

OpaqueToken is deprecated as of angular/core 4.0.0:

/*
 * @deprecated since v4.0.0 because it does not support type information, use `InjectionToken<?>`
 * instead.
 */
export declare class OpaqueToken {
    protected _desc: string;
    constructor(_desc: string);
    toString(): string;
}

It should be replaced by typed InjectionToken<?>:

/**
 * Creates a token that can be used in a DI Provider.
 *
 * Use an `InjectionToken` whenever the type you are injecting is not reified (does not have a
 * runtime representation) such as when injecting an interface, callable type, array or
 * parametrized type.
 *
 * `InjectionToken` is parameterized on `T` which is the type of object which will be returned by
 * the `Injector`. This provides additional level of type safety.
 *
 * ```
 * interface MyInterface {...}
 * var myInterface = injector.get(new InjectionToken<MyInterface>('SomeToken'));
 * // myInterface is inferred to be MyInterface.
 * ```
 *
 * ### Example
 *
 * {@example core/di/ts/injector_spec.ts region='InjectionToken'}
 *
 * @stable
 */
export declare class InjectionToken<T> extends OpaqueToken {
    private _differentiate_from_OpaqueToken_structurally;
    constructor(desc: string);
    toString(): string;
}

Doc: https://angular.io/api/core/InjectionToken

Discussion: https://stackoverflow.com/questions/43419050/angular-2-opaquetoken-vs-angular-4-injectiontoken

Create 1-2 end-to-end tests

Create 1 or 2 tests (upload or upload & download) that will cover whole request flow from Angular component to server. Use xhr-mock if there will be no ability to use server in tests

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.