File

libs/d3/src/lib/export-image-button/export-image-button.component.ts

Metadata

selector n52-export-image-button
styleUrls ./export-image-button.component.scss
templateUrl ./export-image-button.component.html

Index

Properties
Methods
Inputs

Constructor

constructor(servicesConnector: HelgolandServicesConnector, applicationRef: ApplicationRef, injector: Injector, componentFactoryResolver: ComponentFactoryResolver, timeSrvc: Time, graphHelper: D3GraphHelperService)
Parameters :
Name Type Optional
servicesConnector HelgolandServicesConnector No
applicationRef ApplicationRef No
injector Injector No
componentFactoryResolver ComponentFactoryResolver No
timeSrvc Time No
graphHelper D3GraphHelperService No

Inputs

datasetIds
Type : string[]

List of datasetIds, similiar to the timeseries component

datasetOptions
Type : Map<string | DatasetOptions>

Map of datasetOptions, similiar to the timeseries component

exportType
Type : "png" | "svg"
Default value : 'png'

Filetype for the export, currently png and svg are possible, default is 'png'

fileName
Default value : 'export'

Filename for the exported file, default is 'export'

height
Default value : 300

Height (as number) in px for the diagram extent, default is 300

showFirstLastDate
Type : boolean

Option to show first and last date at the bottom edges of the exported picture

showLegend
Default value : false

Option to show a simple legend in th exported picture

timespan
Type : Timespan

Timespan, similiar to the timeseries component

title
Type : string

Optional title in the picture of the exported file

width
Default value : 600

Width (as number) in px for the diagram extent, default is 600

Methods

Private addFirstLastDate
addFirstLastDate(element: SVGSVGElement)
Parameters :
Name Type Optional
element SVGSVGElement No
Returns : void
Private addLegend
addLegend(element: SVGSVGElement)
Parameters :
Name Type Optional
element SVGSVGElement No
Returns : Observable<void>
Private addTitle
addTitle(element: SVGSVGElement)
Parameters :
Name Type Optional
element SVGSVGElement No
Returns : void
Private appendComponentToBody
appendComponentToBody(component: any)
Parameters :
Name Type Optional
component any No
Returns : any
Private createDiagramElem
createDiagramElem()
Returns : void
Private createPngImageDownload
createPngImageDownload(element: SVGSVGElement)
Parameters :
Name Type Optional
element SVGSVGElement No
Returns : void
Private createSvgDownload
createSvgDownload(element: SVGSVGElement)
Parameters :
Name Type Optional
element SVGSVGElement No
Returns : void
Private diagramAdjustments
diagramAdjustments(svgElem: SVGSVGElement)
Parameters :
Name Type Optional
svgElem SVGSVGElement No
Returns : Observable<void>
Public exportImage
exportImage()
Returns : void
Private moveDown
moveDown(graph: d3.Selection, sizeToMove: number)
Parameters :
Name Type Optional
graph d3.Selection<SVGGraphicsElement | any | null | undefined> No
sizeToMove number No
Returns : void
Private prepareSelector
prepareSelector(selector: string)
Parameters :
Name Type Optional
selector string No
Returns : string
Private removeComponentFromBody
removeComponentFromBody(componentRef: ComponentRef)
Parameters :
Name Type Optional
componentRef ComponentRef<any> No
Returns : void

Properties

Private internalHeight
Type : number
Private internalWidth
Type : number
Public loading
Type : boolean
import {
  ApplicationRef,
  Component,
  ComponentFactoryResolver,
  ComponentRef,
  EmbeddedViewRef,
  Injector,
  Input,
} from '@angular/core';
import { DatasetOptions, DatasetType, HelgolandServicesConnector, Time, Timespan } from '@helgoland/core';
import * as d3 from 'd3';
import { forkJoin, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';

import { D3TimeseriesGraphComponent } from '../d3-timeseries-graph/d3-timeseries-graph.component';
import { D3GraphHelperService } from '../helper/d3-graph-helper.service';

const wrapperClassName = 'export-diagram-wrapper';

@Component({
  selector: 'n52-export-image-button',
  templateUrl: './export-image-button.component.html',
  styleUrls: ['./export-image-button.component.scss']
})
export class ExportImageButtonComponent {

  /**
   * List of datasetIds, similiar to the timeseries component
   */
  @Input() datasetIds: string[];

  /**
   * Map of datasetOptions, similiar to the timeseries component
   */
  @Input() datasetOptions: Map<string, DatasetOptions>;

  /**
   * Timespan, similiar to the timeseries component
   */
  @Input() timespan: Timespan;

  /**
   * Height (as number) in px for the diagram extent, default is 300
   */
  @Input() height = 300;

  /**
   * Width (as number) in px for the diagram extent, default is 600
   */
  @Input() width = 600;

  /**
   * Filename for the exported file, default is 'export'
   */
  @Input() fileName = 'export';

  /**
   * Filetype for the export, currently png and svg are possible, default is 'png'
   */
  @Input() exportType: 'png' | 'svg' = 'png';

  /**
   * Optional title in the picture of the exported file
   */
  @Input() title: string;

  /**
   * Option to show a simple legend in th exported picture
   */
  @Input() showLegend = false;

  /**
   * Option to show first and last date at the bottom edges of the exported picture
   */
  @Input() showFirstLastDate: boolean;

  public loading: boolean;

  private internalHeight: number;
  private internalWidth: number;

  constructor(
    private servicesConnector: HelgolandServicesConnector,
    private applicationRef: ApplicationRef,
    private injector: Injector,
    private componentFactoryResolver: ComponentFactoryResolver,
    private timeSrvc: Time,
    private graphHelper: D3GraphHelperService
  ) { }

  public exportImage() {
    this.createDiagramElem();
  }

  private createDiagramElem() {
    this.loading = true;

    this.internalHeight = this.height;
    this.internalWidth = this.width;

    const comp = this.appendComponentToBody(D3TimeseriesGraphComponent) as ComponentRef<D3TimeseriesGraphComponent>;

    comp.instance.datasetIds = this.datasetIds;
    comp.instance.datasetOptions = this.datasetOptions;
    comp.instance.setTimespan(this.timespan);
    comp.instance.presenterOptions = {
      showTimeLabel: false,
      grid: true
    };

    comp.instance.onContentLoading.subscribe(loadFinished => {
      if (loadFinished) {
        setTimeout(() => {
          const temp = this.prepareSelector(`.${wrapperClassName} n52-d3-timeseries-graph`);
          const svgElem = document.querySelector<SVGSVGElement>(temp);
          if (svgElem) {
            this.diagramAdjustments(svgElem).subscribe(() => {
              switch (this.exportType) {
                case 'svg':
                  this.createSvgDownload(svgElem);
                  break;
                case 'png':
                default:
                  this.createPngImageDownload(svgElem);
                  break;
              }
              this.removeComponentFromBody(comp);
              this.loading = false;
            });
          }
        }, 1000);
      }
    });
  }

  private diagramAdjustments(svgElem: SVGSVGElement): Observable<void> {
    // adjust y axis fill out
    svgElem.querySelectorAll<SVGSVGElement>('.y.axisDiv').forEach(el => el.style.fill = 'none');

    // adjust grid lines
    d3.selectAll('.d3 .grid .tick line').style('stroke', '#d3d3d3');

    this.addTitle(svgElem);

    this.addFirstLastDate(svgElem);

    return this.addLegend(svgElem);
  }

  private addFirstLastDate(element: SVGSVGElement) {
    if (this.showFirstLastDate) {
      this.internalHeight += 20;
      const selection = d3.select(element);
      const backgroundRect: d3.Selection<SVGGraphicsElement, {}, HTMLElement, any> = selection.select('.graph-background');

      const firstDate = selection.append<SVGGraphicsElement>('svg:text').text(new Date(this.timespan.from).toLocaleDateString());
      const firstDateWidth = firstDate.node().getBBox().width;
      firstDate.attr('x', (this.internalWidth - backgroundRect.node().getBBox().width - (firstDateWidth / 2))).attr('y', (this.internalHeight));

      const lastDate = selection.append<SVGGraphicsElement>('svg:text').text(new Date(this.timespan.to).toLocaleDateString());
      const lastDateWidth = lastDate.node().getBBox().width;
      lastDate.attr('x', (this.internalWidth - lastDateWidth)).attr('y', (this.internalHeight));
    }
  }

  private addLegend(element: SVGSVGElement): Observable<void> {
    if (this.showLegend) {
      const obs: Observable<{ label: d3.Selection<SVGGraphicsElement, unknown, null, undefined>, xPos: number }>[] = [];
      const selection = d3.select(element);
      this.datasetOptions.forEach((option, k) => {
        obs.push(
          this.servicesConnector.getDataset(k, { type: DatasetType.Timeseries }).pipe(map(ts => {
            if (this.timeSrvc.overlaps(this.timespan, ts.firstValue.timestamp, ts.lastValue.timestamp)) {
              const label = selection.append<SVGSVGElement>('g').attr('class', 'legend-entry');
              this.graphHelper.drawDatasetSign(label, option, -10, -5, false);
              label.append<SVGGraphicsElement>('svg:text').text(ts.label);
              this.internalHeight += 25;
              return {
                label,
                xPos: this.internalHeight - 10
              };
            } else {
              return {
                label: null,
                xPos: 0
              };
            }
          }))
        );
      });
      return forkJoin(obs).pipe(map(elem => {
        const maxWidth = Math.max(...elem.map(e => e.label ? e.label.node().getBBox().width : 0));
        elem.forEach(e => {
          if (e.label) {
            e.label.attr('transform', `translate(${(this.internalWidth - maxWidth) / 2},${e.xPos})`);
          }
        });
      }));
    } else {
      return of(null);
    }
  }

  private addTitle(element: SVGSVGElement) {
    if (this.title) {
      const addedHeight = 20;

      this.internalHeight += addedHeight;

      const selection = d3.select(element);

      const graph = selection.select<SVGGraphicsElement>('g');

      this.moveDown(graph, addedHeight);

      const titleElem = selection.append<SVGGraphicsElement>('svg:text').text(this.title);
      const titleWidth = titleElem.node().getBBox().width;
      titleElem.attr('x', (this.internalWidth - titleWidth) / 2).attr('y', '15');
    }
  }

  private moveDown(graph: d3.Selection<SVGGraphicsElement, any, null, undefined>, sizeToMove: number) {
    const matrix = graph.style('transform');
    const matrixArray = matrix.substring(7, matrix.length - 1).split(',').map(e => parseInt(e, 10));
    if (matrixArray.length === 6) {
      matrixArray[5] += sizeToMove;
    }
    graph.attr('transform', `matrix(${matrixArray.join(',')})`);
  }

  private createPngImageDownload(element: SVGSVGElement) {
    console.log(`Generate PNG file with width: ${this.internalWidth} and height: ${this.internalHeight}`);
    const svgString = new XMLSerializer().serializeToString(element);
    const canvas = document.createElement('canvas');
    canvas.width = this.internalWidth;
    canvas.height = this.internalHeight;
    const ctx = canvas.getContext('2d');
    const image = new Image();
    const svg = new Blob([svgString], { type: 'image/svg+xml;base64;' });
    const url = window.URL.createObjectURL(svg);
    image.onload = () => {
      ctx.drawImage(image, 0, 0);
      if (window.navigator && window.navigator.msSaveOrOpenBlob) {
        window.navigator.msSaveOrOpenBlob(svg, `${this.fileName}.png`);
      } else {
        const png = canvas.toDataURL('image/png');
        const a = document.createElement('a');
        const downloadAttrSupport = typeof a.download !== 'undefined';
        if (downloadAttrSupport) {
          a.download = `${this.fileName}.png`;
          a.href = png;
          a.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
        }
        window.URL.revokeObjectURL(png);
      }
    };
    image.src = url;
  }

  private createSvgDownload(element: SVGSVGElement) {
    console.log(`Generate SVG file with width: ${this.internalWidth} and height: ${this.internalHeight}`);
    const serializer = new XMLSerializer();
    let source = serializer.serializeToString(element);
    if (!source.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)) {
      source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
    }
    if (!source.match(/^<svg[^>]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)) {
      source = source.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
    }
    source = '<?xml version="1.0" standalone="no"?>\r\n' + source;
    const svgBlob = new Blob([source], { type: 'image/svg+xml;charset=utf-8' });
    const svgUrl = URL.createObjectURL(svgBlob);
    const downloadLink = document.createElement('a');
    downloadLink.href = svgUrl;
    downloadLink.download = `${this.fileName}.svg`;
    document.body.appendChild(downloadLink);
    downloadLink.click();
    document.body.removeChild(downloadLink);
  }

  private prepareSelector(selector: string): string {
    if (selector.endsWith(' svg')) {
      return selector;
    }
    return `${selector} svg`;
  }

  private appendComponentToBody(component: any) {
    // create component ref
    const componentRef = this.componentFactoryResolver.resolveComponentFactory(component)
      .create(this.injector);

    // attach component to the appRef.
    this.applicationRef.attachView(componentRef.hostView);

    // get DOM element from component
    const domElem = (componentRef.hostView as EmbeddedViewRef<any>)
      .rootNodes[0] as HTMLElement;

    // create wrapper to set position and size
    const wrapper = document.createElement('div');
    wrapper.style.position = 'absolute';
    wrapper.style.top = `${-this.internalHeight * 2}px`;
    wrapper.className = wrapperClassName;
    wrapper.style.height = `${this.internalHeight}px`;
    wrapper.style.width = `${this.internalWidth}px`;
    wrapper.appendChild(domElem);

    document.body.appendChild(wrapper);

    return componentRef;
  }

  private removeComponentFromBody(componentRef: ComponentRef<any>) {
    this.applicationRef.detachView(componentRef.hostView);
    document.querySelector(`.${wrapperClassName}`).remove();
    componentRef.destroy();
  }

}
<button (click)="exportImage()">
    <span>Export</span>
    <span *ngIf="loading"> - loading...</span>
</button>

./export-image-button.component.scss

Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""