diff --git a/client/package.json b/client/package.json index 7e7cad96734364427fdcc7fe3022f1e64f022190..587d474ebd838b8a7878554cdc2c2c9b05da7944 100644 --- a/client/package.json +++ b/client/package.json @@ -26,6 +26,7 @@ "@ngrx/store": "12.1.0", "@ngrx/store-devtools": "12.1.0", "bootstrap": "4.6", + "d3": "^5.15.1", "keycloak-angular": "^8.2.0", "keycloak-js": "^14.0.0", "ngx-bootstrap": "^7.0.0-rc.1", @@ -39,6 +40,7 @@ "@angular-devkit/build-angular": "~12.0.4", "@angular/cli": "~12.0.4", "@angular/compiler-cli": "~12.0.4", + "@types/d3": "^5.7.2", "@types/jasmine": "~3.6.0", "@types/node": "^12.11.1", "jasmine-core": "~3.7.0", diff --git a/client/src/app/instance/detail/components/default/default-object.component.html b/client/src/app/instance/detail/components/default/default-object.component.html new file mode 100644 index 0000000000000000000000000000000000000000..bc6843ae7d94023d1cd8b39b9c9c57631d17822f --- /dev/null +++ b/client/src/app/instance/detail/components/default/default-object.component.html @@ -0,0 +1,11 @@ +<div class="row justify-content-center"> + <div class="col col-lg-10 col-xl-8 mt-4"> + <app-object-data + [datasetSelected]="datasetSelected" + [outputFamilyList]="outputFamilyList" + [outputCategoryList]="outputCategoryList" + [attributeList]="attributeList" + [object]="object"> + </app-object-data> + </div> +</div> diff --git a/client/src/app/instance/detail/components/default/default-object.component.ts b/client/src/app/instance/detail/components/default/default-object.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..058a6eda84682117c638f3b96313f04f6ae192bb --- /dev/null +++ b/client/src/app/instance/detail/components/default/default-object.component.ts @@ -0,0 +1,29 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; + +import { Attribute, OutputFamily, OutputCategory } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-default-object', + templateUrl: 'default-object.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * @class + * @classdesc Detail default object component. + */ +export class DefaultObjectComponent { + @Input() datasetSelected: string; + @Input() outputFamilyList: OutputFamily[]; + @Input() outputCategoryList: OutputCategory[]; + @Input() attributeList: Attribute[]; + @Input() object: any; +} diff --git a/client/src/app/instance/detail/components/default/index.ts b/client/src/app/instance/detail/components/default/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f36eed1181aaad1346a0f96e9b5375b1f68979b2 --- /dev/null +++ b/client/src/app/instance/detail/components/default/index.ts @@ -0,0 +1,5 @@ +import { DefaultObjectComponent } from './default-object.component'; + +export const defaultComponents = [ + DefaultObjectComponent +]; diff --git a/client/src/app/instance/detail/components/index.ts b/client/src/app/instance/detail/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e0d32c36208a417c4ec089814d619e25f5a69c6b --- /dev/null +++ b/client/src/app/instance/detail/components/index.ts @@ -0,0 +1,9 @@ +import { defaultComponents } from './default'; +import { spectraComponents } from './spectra'; +import { ObjectDataComponent } from './object-data.component'; + +export const dummiesComponents = [ + defaultComponents, + spectraComponents, + ObjectDataComponent +]; diff --git a/client/src/app/instance/detail/components/object-data.component.html b/client/src/app/instance/detail/components/object-data.component.html new file mode 100644 index 0000000000000000000000000000000000000000..8c469250b9ef97abbc24a6c81a407076bef0fa86 --- /dev/null +++ b/client/src/app/instance/detail/components/object-data.component.html @@ -0,0 +1,66 @@ +<div *ngIf="getAttributeBySearchFlag('RA') && getAttributeBySearchFlag('DEC')" class="row"> + <div class="col-12"> + <table class="table mb-1"> + <tr> + <th>Alpha</th> + <th>Delta</th> + <th class="text-center" rowspan="2"><img src="assets/cesam_anis80.png" alt="CeSAM logo" /></th> + </tr> + <tr> + <td>{{ object[getAttributeBySearchFlag('RA').label] }}</td> + <td>{{ object[getAttributeBySearchFlag('DEC').label] }}</td> + </tr> + </table> + <hr class="mt-0 mb-4"> + </div> +</div> + +<!-- Accordion families --> +<accordion [isAnimated]="true"> + <accordion-group *ngFor="let family of outputFamilyList" #ag [isOpen]="true" class="pl-2"> + <button class="btn btn-link btn-block clearfix pb-2" accordion-heading> + <span class="pull-left float-left text-primary"> + {{ family.label }} + + <span *ngIf="ag.isOpen"> + <span class="fas fa-chevron-up"></span> + </span> + <span *ngIf="!ag.isOpen"> + <span class="fas fa-chevron-down"></span> + </span> + </span> + </button> + + <!-- Accordion categories --> + <accordion [isAnimated]="true"> + <accordion-group *ngFor="let category of getCategoryByFamilySortedByDisplay(family.id)" #ag [isOpen]="true" class="pl-4"> + <button class="btn btn-link btn-block clearfix pb-2" accordion-heading> + <span class="pull-left float-left text-primary"> + {{ category.label }} + + <span *ngIf="ag.isOpen"> + <span class="fas fa-chevron-up"></span> + </span> + <span *ngIf="!ag.isOpen"> + <span class="fas fa-chevron-down"></span> + </span> + </span> + </button> + + <!-- Output list --> + <div *ngFor="let attribute of getAttributesVisibleByCategory(category.id)" class="row pb-2"> + <div class="col-5 font-weight-bold">{{ attribute.form_label }}</div> + <ng-container [ngSwitch]="attribute.renderer_detail"> + <div *ngSwitchCase="'download'" class="col"> + <a [href]="getDownloadHref(object[attribute.label])" role="button" class="btn btn-primary btn-sm"> + <span class="fas fa-download"></span> + {{ object[attribute.label] }} + </a> + </div> + <div *ngSwitchDefault class="col">{{ object[attribute.label] }}</div> + </ng-container > + </div> + </accordion-group> + </accordion> + </accordion-group> +</accordion> diff --git a/client/src/app/instance/detail/components/object-data.component.ts b/client/src/app/instance/detail/components/object-data.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d834874bede64081c6f7b45b6c5f4f0e5b9a1ad9 --- /dev/null +++ b/client/src/app/instance/detail/components/object-data.component.ts @@ -0,0 +1,83 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; + +import { Attribute, OutputCategory, OutputFamily } from 'src/app/metamodel/models'; +import { getHost } from 'src/app/shared/utils'; + +@Component({ + selector: 'app-object-data', + templateUrl: 'object-data.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * @class + * @classdesc Detail object data component. + */ +export class ObjectDataComponent { + @Input() datasetSelected: string; + @Input() outputFamilyList: OutputFamily[]; + @Input() outputCategoryList: OutputCategory[]; + @Input() attributeList: Attribute[]; + @Input() object: any; + + /** + * Returns category list sorted by display, for the given output family ID. + * + * @param {number} idFamily - The output family ID. + * + * @return Category[] + */ + getCategoryByFamilySortedByDisplay(idFamily: number): OutputCategory[] { + return this.outputCategoryList + .filter(category => category.id_output_family === idFamily) + //.sort(sortByDisplay); + } + + /** + * Returns attribute list sorted by detail display, for the given output category ID. + * + * @param {number} idCategory - The output category ID. + * + * @return Attribute[] + */ + getAttributesVisibleByCategory(idCategory: number): Attribute[] { + return this.attributeList + .filter(a => a.detail) + .filter(a => a.id_output_category === idCategory) + .sort((a, b) => a.display_detail - b.display_detail); + } + + /** + * Returns attribute list sorted by detail display. + * + * @return Attribute[] + */ + getAttributesVisible(): Attribute[] { + return this.attributeList + .filter(a => a.detail) + .sort((a, b) => a.display_detail - b.display_detail); + } + + /** + * Returns attribute for the given search flag. + * + * @param {string} searchFlag - The search flag. + * + * @return Attribute + */ + getAttributeBySearchFlag(searchFlag: string): Attribute { + return this.getAttributesVisible().find(attribute => attribute.search_flag === searchFlag); + } + + getDownloadHref(value: string) { + return getHost() + '/download-file/' + this.datasetSelected + '/' + value; + } +} diff --git a/client/src/app/instance/detail/components/spectra/graph/point.ts b/client/src/app/instance/detail/components/spectra/graph/point.ts new file mode 100644 index 0000000000000000000000000000000000000000..b45ba9d36139645b8cbb14a7bf558423d614a976 --- /dev/null +++ b/client/src/app/instance/detail/components/spectra/graph/point.ts @@ -0,0 +1,13 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export interface Point { + x: number, + y: number +} diff --git a/client/src/app/instance/detail/components/spectra/graph/rays.ts b/client/src/app/instance/detail/components/spectra/graph/rays.ts new file mode 100644 index 0000000000000000000000000000000000000000..8dbba2bc26073ec94082ec9804d58755aa6beb17 --- /dev/null +++ b/client/src/app/instance/detail/components/spectra/graph/rays.ts @@ -0,0 +1,108 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + + export const emissionLines = [ + {name: 'SII', wavelength: 10320}, + {name: 'HeI', wavelength: 7065.2}, + {name: 'SII', wavelength: 6732.68}, + {name: 'SII', wavelength: 6718.39}, + {name: 'NII', wavelength: 6585.27}, + {name: 'Hα', wavelength: 6564.61}, + {name: 'NII', wavelength: 6549.86}, + {name: 'HeI', wavelength: 5876.0}, + {name: 'OIII', wavelength: 5008.24}, + {name: 'OIII', wavelength: 4960.29}, + {name: 'Hβ', wavelength: 4862.70}, + {name: 'OIII', wavelength: 4364.44}, + {name: 'Hγ', wavelength: 4341.68}, + {name: 'Hδ', wavelength: 4102.89}, + {name: 'SII', wavelength: 4072.0}, + {name: 'Hksi', wavelength: 3890.15}, + {name: 'NeIII', wavelength: 3869.81}, + {name: 'H9', wavelength: 3836.47}, + {name: 'Hθ', wavelength: 3798.98}, + {name: 'H11', wavelength: 3771.70}, + {name: 'OII', wavelength: 3728.49}, + {name: 'NeV', wavelength: 3426.5}, + {name: 'NeV', wavelength: 3346.4}, + {name: 'HeII', wavelength: 3204.03}, + {name: 'FeII', wavelength: 2964.0}, + {name: 'MgII', wavelength: 2799.0}, + {name: 'NII', wavelength: 2142.0}, + {name: 'CIII', wavelength: 1908.73}, + {name: 'HeII', wavelength: 1640.42}, + {name: 'CIV', wavelength: 1549.0}, + {name: 'SiIV', wavelength: 1397.0}, + {name: 'CII', wavelength: 1334.53}, + {name: 'OI', wavelength: 1303.0}, + {name: 'NV', wavelength: 1240.0}, + {name: 'LyA', wavelength: 1215.67}, + {name: 'OVI', wavelength: 1033.0}, + {name: 'LyB', wavelength: 1025.72}, + {name: 'LyG', wavelength: 972.53} +]; + +export const absorptionLines = [ + {name: 'TiO', wavelength: 8863.0}, + {name: 'TiO', wavelength: 8430.0}, + {name: 'NaI', wavelength: 8197.05}, + {name: 'NaI', wavelength: 8185.50}, + {name: 'TiO', wavelength: 7590.0}, + {name: 'HeI', wavelength: 7065.2}, + {name: 'Hα', wavelength: 6564.61}, + {name: 'TiO', wavelength: 6159.0}, + {name: 'NaD', wavelength: 5892.5}, + {name: 'TiO', wavelength: 5603.0}, + {name: 'Ca,Fe', wavelength: 5269.0}, + {name: 'MgI', wavelength: 5174.12}, + {name: 'Hβ', wavelength: 4862.70}, + {name: 'Hγ', wavelength: 4341.68}, + {name: 'Gband', wavelength: 4304.4}, + {name: 'CN', wavelength: 4216.0}, + {name: 'Hδ', wavelength: 4102.89}, + {name: 'CaII_H', wavelength: 3969.59}, + {name: 'CaII_K', wavelength: 3934.78}, + {name: 'Hksi', wavelength: 3890.15}, + {name: 'H9', wavelength: 3836.47}, + {name: 'Hθ', wavelength: 3798.98}, + {name: 'H11', wavelength: 3771.70}, + {name: 'FeI', wavelength: 3581.0}, + {name: 'HeII', wavelength: 3204.03}, + {name: 'FeII', wavelength: 2964.0}, + {name: 'MgII', wavelength: 2796.35}, + {name: 'MgII', wavelength: 2803.53}, + {name: 'FeII', wavelength: 2626.45}, + {name: 'FeII', wavelength: 2600.17}, + {name: 'FeII', wavelength: 2586.65}, + {name: 'FeII', wavelength: 2382.76}, + {name: 'FeII', wavelength: 2374.46}, + {name: 'FeII', wavelength: 2344.21}, + {name: 'FeII', wavelength: 2260.78}, + {name: 'AlIII', wavelength: 1854.72}, + {name: 'AlII', wavelength: 1670.78}, + {name: 'HeII', wavelength: 1640.42}, + {name: 'FeII', wavelength: 1608.45}, + {name: 'CIV', wavelength: 1548.20}, + {name: 'CIV', wavelength: 1550.77}, + {name: 'SiII', wavelength: 1533.43}, + {name: 'SiII', wavelength: 1526.71}, + {name: 'SiIV', wavelength: 1402.77}, + {name: 'SiIV', wavelength: 1393.75}, + {name: 'CII', wavelength: 1334.53}, + {name: 'OI', wavelength: 1302.17}, + {name: 'OI', wavelength: 1304.86}, + {name: 'SiII', wavelength: 1260.42}, + {name: 'NV', wavelength: 1238.82}, + {name: 'NV', wavelength: 1242.80}, + {name: 'LyA', wavelength: 1215.67}, + {name: 'OVI', wavelength: 1037.62}, + {name: 'OVI', wavelength: 1031.93}, + {name: 'LyB', wavelength: 1025.72}, + {name: 'LyG', wavelength: 972.53} +]; diff --git a/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.html b/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.html new file mode 100644 index 0000000000000000000000000000000000000000..9a1c8a0aeab69b32d8cd98a5a5b96e4f54aa65d6 --- /dev/null +++ b/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.html @@ -0,0 +1,3 @@ +<div id="svg-container"> + <svg id="mygraph"></svg> +</div> diff --git a/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.scss b/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..5a58e12529fd174067354db0f9434c1a177a302a --- /dev/null +++ b/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.scss @@ -0,0 +1,119 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +.titles text { + fill: #5a5a5a; + font-size: 24px; +} + +.axis line { + fill: none; + stroke: black; + shape-rendering: crispEdges; +} + +.grid line { + fill: none; + stroke: lightgray; + shape-rendering: crispEdges; +} + +.ray line { + stroke-dasharray: 3, 3; + stroke-width: 1.5px; +} + +.emission line { + stroke: steelblue; +} + +.emission text { + fill: steelblue; +} + +.absorption line { + stroke: tomato; +} + +.absorption text { + fill: tomato; +} + +.spectra-line, .line { + fill: none; + stroke: black; + stroke-width: 1px; +} + +.spectra-area, .area { + fill: #D0D0D0; + opacity: 0.6; +} + +.overlay { + fill: none; + pointer-events: all; +} + +.big-circle-tootlip { + fill: #7ac29a; +} + +.little-circle-tooltip { + fill: rgb(16, 75, 42); + stroke: #fff; + stroke-width: 1.5px; +} + +.rect-tootlip { + fill: #fafafa; + stroke: #7ac29a; + opacity: 0.9; + stroke-width: 1; +} + +.text-tooltip { + font-size: 15px; + color: #333333; + fill: #333333; +} + +.text-y-value { + font-weight: bold; +} + +.emission-button circle { + stroke: steelblue; + stroke-width: 3px; + cursor: pointer; +} + +.button-off circle { + fill: transparent; +} + +.emission-button-on circle { + fill: steelblue; +} + +.absorption-button circle { + stroke: tomato; + stroke-width: 3px; + cursor: pointer; +} + +.absorption-button-on circle { + fill: tomato; +} + +.emission-button text, .absorption-button text { + font-size: 15px; + color: #333333; + fill: #333333; +} diff --git a/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.ts b/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4be29c7a18d65b933ad6285702f7d809a642adb --- /dev/null +++ b/client/src/app/instance/detail/components/spectra/graph/spectra-graph.component.ts @@ -0,0 +1,444 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, ChangeDetectionStrategy, ViewEncapsulation, OnInit, Input } from '@angular/core'; + +import * as d3 from 'd3'; + +import { emissionLines, absorptionLines } from './rays'; +import { SpectraType } from './spectra-type'; +import { Point } from './point'; + +@Component({ + selector: 'app-spectra-graph', + encapsulation: ViewEncapsulation.None, + templateUrl: 'spectra-graph.component.html', + styleUrls: ['spectra-graph.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SpectraGraphComponent implements OnInit { + @Input() z: number; + @Input() spectraCSV: string; + + svg: d3.Selection<d3.BaseType, unknown, HTMLElement, any>; + focus: d3.Selection<SVGGElement, unknown, HTMLElement, any>; + width: number; + height: number; + brushHeight: number; + margin = { top: 50, right: 10, bottom: 150, left: 100 }; + x: d3.ScaleLinear<number, number>; + xAxis: d3.Axis<number | { valueOf(): number }>; + y: d3.ScaleLinear<number, number>; + spectraLine: d3.Line<Point>; + spectraArea: d3.Area<Point>; + displayEmissionLines = true; + dispayAbsorptionLines = true; + + ngOnInit() { + this.width = document.getElementById("svg-container").offsetWidth * 1 - this.margin.left - this.margin.right; + this.height = 600 - this.margin.top - this.margin.bottom; + this.brushHeight = 50; + this.x = d3.scaleLinear().range([0, this.width]); + this.y = d3.scaleLinear().range([this.height, 0]); + + this.initSvg(); + this.addTitles(); + + const dataset = d3.csvParse<Point, SpectraType>(this.spectraCSV, (row) => { + return { + x: parseFloat(row.x), + y: parseFloat(row.Flux) + }; + }); + + this.setupDomain(dataset); + this.addGrid(); + this.addSpectraLine(dataset); + this.addSpectraArea(dataset); + this.addRays(); + this.addAxis(); + this.addRaysButtons(); + this.addBrush(dataset); + this.addTooltip(this.width, this.height, dataset, this.x, this.y); + } + + private initSvg(): void { + this.svg = d3.select("svg#mygraph") + .attr("width", this.width + this.margin.left + this.margin.right) + .attr("height", this.height + this.margin.top + this.margin.bottom); + + this.svg.append("defs").append("clipPath") + .attr("id", "clip") + .append("rect") + .attr("width", this.width) + .attr("height", this.height); + + this.focus = this.svg.append("g") + .attr("class", "focus") + .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")"); + } + + private addTitles(): void { + const titles = this.focus.append("g") + .attr("class", "titles"); + + titles.append("text") + .attr("x", (this.width / 2)) + .attr("y", this.height + this.margin.bottom - 20) + .attr("text-anchor", "middle") + .text("Wavelength [Ångströms]"); + + titles.append("text") + .attr("x", (this.margin.left * 0.75) * -1) + .attr("y", this.height / 2) + .attr("text-anchor", "middle") + .attr("transform", "rotate(-90," + ((this.margin.left * 0.75) * -1) + "," + this.height / 2 + ")") + .text("Flux Fλ (erg/sec/cm2/Å)"); + } + + private setupDomain(dataset: d3.DSVParsedArray<Point>): void { + const xMin = d3.min(dataset, (d) => d.x); + const xMax = d3.max(dataset, (d) => d.x); + this.x.domain([xMin, xMax]); + + const yMin = d3.min(dataset, (d) => d.y); + const yMax = d3.max(dataset, (d) => d.y) * 1.1; + this.y.domain([yMin, yMax]); + } + + private addGrid(): void { + const grid = this.focus.append("g") + .attr("class", "grid"); + + grid.append("g") + .attr("class", "grid-x") + .selectAll() + .data(this.x.ticks(10)) + .enter() + .append("line") + .attr("class", "grid-line-x") + .attr("x1", (d: number) => this.x(d)) + .attr("x2", (d: number) => this.x(d)) + .attr("y1", 0) + .attr("y2", this.height); + + grid.append("g") + .attr("class", "grid-y") + .selectAll() + .data(this.y.ticks(10)) + .enter() + .append("line") + .attr("class", "grid-line-y") + .attr("x1", 0) + .attr("x2", this.width) + .attr("y1", (d: number) => this.y(d)) + .attr("y2", (d: number) => this.y(d)); + } + + private addSpectraLine(dataset: d3.DSVParsedArray<Point>): void { + this.spectraLine = d3.line<Point>() + .x((d) => this.x(d.x)) + .y((d) => this.y(d.y)); + + this.focus.append("g") + .attr("clip-path", "url(#clip)") + .append("path") + .datum(dataset) + .attr("class", "spectra-line") + .attr("d", this.spectraLine); + } + + private addSpectraArea(dataset: d3.DSVParsedArray<Point>): void { + this.spectraArea = d3.area<Point>() + .x((d) => this.x(d.x)) + .y0(this.height) + .y1((d) => this.y(d.y)); + + this.focus.append("g") + .attr("clip-path", "url(#clip)") + .append("path") + .datum(dataset) + .attr("class", "spectra-area") + .attr("d", this.spectraArea); + } + + private addRays(): void { + const coef = 1 + this.z; + + const er = this.focus.append("g") + .style("display", "block") + .attr("class", "ray emission") + .attr("clip-path", "url(#clip)"); + + er.selectAll() + .data(emissionLines) + .enter() + .append("line") + .attr("x1", r => this.x(r.wavelength * coef)) + .attr("x2", r => this.x(r.wavelength * coef)) + .attr("y1", this.height) + .attr("y2", 0); + + er.selectAll() + .data(emissionLines) + .enter() + .append("text") + .attr("x", r => this.x(r.wavelength * coef) - 5) + .attr("y", this.height * 0.2) + .attr("transform", r => "rotate(-90, " + (this.x(r.wavelength * coef) - 5) + "," + this.height * 0.2 + ")") + .text(r => r.name); + + const ar = this.focus.append("g") + .style("display", "block") + .attr("class", "ray absorption") + .attr("clip-path", "url(#clip)"); + + ar.selectAll() + .data(absorptionLines) + .enter() + .append("line") + .attr("x1", r => this.x(r.wavelength * coef)) + .attr("x2", r => this.x(r.wavelength * coef)) + .attr("y1", this.height) + .attr("y2", 0); + + ar.selectAll() + .data(absorptionLines) + .enter() + .append("text") + .attr("x", r => this.x(r.wavelength * coef) - 5) + .attr("y", this.height * 0.8) + .attr("transform", r => "rotate(-90, " + (this.x(r.wavelength * coef) - 5) + "," + this.height * 0.8 + ")") + .text(r => r.name); + } + + private addAxis(): void { + this.xAxis = d3.axisBottom(this.x); + this.focus.append("g") + .attr("class", "axis axis-x") + .attr("transform", "translate(0," + this.height + ")") + .call(this.xAxis); + + this.focus.append("g") + .attr("class", "axis axis-y") + .call(d3.axisLeft(this.y).tickFormat(d3.format(".1e"))) + } + + private addRaysButtons(): void { + const gButtons = this.svg.append("g") + .attr("class", "rays-buttons") + .attr("transform", "translate(" + this.margin.left + "," + 25 + ")"); + + const emission = gButtons.append("g") + .attr("class", "emission-button emission-button-on"); + + const circleEmission = emission.append("circle") + .attr("cx", 20) + .attr("cy", 0) + .attr("r", 10); + + circleEmission.on("click", () => { + const e = this.focus.select(".emission"); + if (this.displayEmissionLines) { + e.style("display", "none"); + emission.attr("class", "emission-button button-off"); + } else { + e.style("display", "block"); + emission.attr("class", "emission-button emission-button-on"); + } + this.displayEmissionLines = !this.displayEmissionLines; + }); + + emission.append("text") + .attr("x", 35) + .attr("y", 5) + .text("Display emission lines") + + const absorption = gButtons.append("g") + .attr("class", "absorption-button absorption-button-on"); + + const circleAbsorption = absorption.append("circle") + .attr("cx", 215) + .attr("cy", 0) + .attr("r", 10); + + circleAbsorption.on("click", () => { + const a = this.focus.select(".absorption"); + if (this.dispayAbsorptionLines) { + a.style("display", "none"); + absorption.attr("class", "absorption-button button-off"); + } else { + a.style("display", "block"); + absorption.attr("class", "absorption-button absorption-button-on"); + } + this.dispayAbsorptionLines = !this.dispayAbsorptionLines; + }); + + absorption.append("text") + .attr("x", 230) + .attr("y", 5) + .text("Display absorption lines") + + } + + private addTooltip( + width: number, + height: number, + dataset: d3.DSVParsedArray<Point>, + x: d3.ScaleLinear<number, number>, + y: d3.ScaleLinear<number, number> + ): void { + const tooltip = this.focus.append("g") + .style("display", "none"); + + tooltip.append("circle") + .attr("class", "big-circle-tootlip") + .attr("r", 10); + + tooltip.append("circle") + .attr("class", "little-circle-tooltip") + .attr("r", 4); + + tooltip.append("polyline") + .attr("points","0,0 0,40 55,40 60,45 65,40 135,40 135,0 0,0") + .attr("class", "rect-tootlip") + .attr("transform", "translate(-60, -55)"); + + const xValue = tooltip.append("text") + .attr("class", "text-tooltip") + .attr("transform", "translate(-55, -40)") + .append("tspan") + .text("X : ") + .append("tspan"); + + const yValue = tooltip.append("text") + .attr("class", "text-tooltip") + .attr("transform", "translate(-55, -24)") + .append("tspan") + .text("Flux : ") + .append("tspan") + .attr("class", "text-y-value") + + const bisectX = d3.bisector((p: Point) => p.x).left; + this.focus.append("rect") + .attr("class", "overlay") + .attr("width", width) + .attr("height", height) + .on("mouseover", () => tooltip.style("display", null)) + .on("mouseout", () => tooltip.style("display", "none")) + .on("mousemove", (d, i, n) => { + const node = n[i]; + const mouse = d3.mouse(node); + const x0 = x.invert(mouse[0]); + const index = bisectX(dataset, x0); + const datum = dataset[index]; + tooltip.attr("transform", "translate(" + x(datum.x) + "," + y(datum.y) + ")"); + xValue.text(datum.x); + yValue.text(datum.y); + }); + } + + private addBrush(dataset: d3.DSVParsedArray<Point>): void { + const context = this.svg.append("g") + .attr("class", "context") + .attr("transform", "translate(" + this.margin.left + "," + 480 + ")"); + + const xBrush = d3.scaleLinear().range([0, this.width]); + const yBrush = d3.scaleLinear().range([this.brushHeight, 0]); + xBrush.domain(this.x.domain()); + yBrush.domain(this.y.domain()); + + const xBrushAxis = d3.axisBottom(xBrush); + + const lineBrush = d3.line<Point>() + .x((d) => xBrush(d.x)) + .y((d) => yBrush(d.y)); + + context.append("g") + .attr("class", "line") + .append("path") + .datum(dataset) + .attr("d", lineBrush); + + const areaBrush = d3.area<Point>() + .x((d) => xBrush(d.x)) + .y0(this.brushHeight) + .y1((d) => yBrush(d.y)); + + context.append("g") + .attr("class", "area") + .append("path") + .datum(dataset) + .attr("d", areaBrush); + + context.append("g") + .attr("class", "axis") + .attr("transform", "translate(0," + this.brushHeight + ")") + .call(xBrushAxis); + + const brush = d3.brushX() + .extent([[0, 0], [this.width, this.brushHeight]]) + .on("end", () => { + const selection = d3.event.selection || xBrush.range(); + this.x.domain(selection.map(xBrush.invert, xBrush)); + + // Update spectra graph + this.focus.select(".spectra-line") + .attr("d", this.spectraLine); + this.focus.select(".spectra-area") + .attr("d", this.spectraArea); + + // Update axis + this.focus.select(".axis-x").call(this.xAxis); + + // Update grid + this.focus.selectAll(".grid-line-x") + .remove(); + this.focus.select(".grid-x") + .selectAll() + .data(this.x.ticks()) + .enter() + .append("line") + .attr("class", "grid-line-x") + .attr("x1", (d: number) => this.x(d)) + .attr("x2", (d: number) => this.x(d)) + .attr("y1", 0) + .attr("y2", this.height); + + // Update rays + const coef = 1 + this.z; + this.focus.select(".emission") + .selectAll("line") + .data(emissionLines) + .attr("x1", r => this.x(r.wavelength * coef)) + .attr("x2", r => this.x(r.wavelength * coef)); + this.focus.select(".emission") + .selectAll("text") + .data(emissionLines) + .attr("x", r => this.x(r.wavelength * coef) - 5) + .attr("transform", r => "rotate(-90, " + (this.x(r.wavelength * coef) - 5) + "," + this.height * 0.2 + ")") + + this.focus.select(".absorption") + .selectAll("line") + .data(absorptionLines) + .attr("x1", r => this.x(r.wavelength * coef)) + .attr("x2", r => this.x(r.wavelength * coef)); + this.focus.select(".absorption") + .selectAll("text") + .data(absorptionLines) + .attr("x", r => this.x(r.wavelength * coef) - 5) + .attr("transform", r => "rotate(-90, " + (this.x(r.wavelength * coef) - 5) + "," + this.height * 0.8 + ")") + }); + + context.append("g") + .attr("class", "brush") + .call(brush) + .call(brush.move, this.x.range()); + } +} diff --git a/client/src/app/instance/detail/components/spectra/graph/spectra-type.ts b/client/src/app/instance/detail/components/spectra/graph/spectra-type.ts new file mode 100644 index 0000000000000000000000000000000000000000..180320a2cb8bfb3061fc46289f3d2ee574464c53 --- /dev/null +++ b/client/src/app/instance/detail/components/spectra/graph/spectra-type.ts @@ -0,0 +1,10 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export type SpectraType = "x" | "Flux"; diff --git a/client/src/app/instance/detail/components/spectra/index.ts b/client/src/app/instance/detail/components/spectra/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..77b5e03935e6f4321da07852a1d56a460bc70c3a --- /dev/null +++ b/client/src/app/instance/detail/components/spectra/index.ts @@ -0,0 +1,7 @@ +import { SpectraObjectComponent } from "./spectra-object.component"; +import { SpectraGraphComponent } from "./graph/spectra-graph.component"; + +export const spectraComponents = [ + SpectraObjectComponent, + SpectraGraphComponent +]; diff --git a/client/src/app/instance/detail/components/spectra/spectra-object.component.html b/client/src/app/instance/detail/components/spectra/spectra-object.component.html new file mode 100644 index 0000000000000000000000000000000000000000..a5c472ccf3df9c49290d85d91f3e7890bf6df7ea --- /dev/null +++ b/client/src/app/instance/detail/components/spectra/spectra-object.component.html @@ -0,0 +1,36 @@ +<div class="row"> + <div *ngIf="getAttributeSpectraGraph()" class="col col-md-8 col-sm-12"> + <div *ngIf="spectraIsLoading" id="div-spinner" class="text-center"> + <span class="fas fa-circle-notch fa-spin fa-3x"></span> + <span class="sr-only">Loading...</span> + </div> + <app-spectra-graph *ngIf="spectraIsLoaded" [z]="getZ()" [spectraCSV]="spectraCSV"></app-spectra-graph> + </div> + + <div [ngClass]="{'col-md-4 col-sm-12': getAttributeSpectraGraph()}" class="col mt-4"> + <div *ngIf="getSpectra()" class="jumbotron row mb-3 p-4"> + <div class="col-auto align-self-center"> + <p>Download:</p> + </div> + <div class="w-100 d-block d-xl-none"></div> + <div class="col"> + <div class="row justify-content-center"> + <div class="col-auto"> + <a [href]="getSpectra()" target="_blank" class="btn btn-lg btn-block dl-btn"> + <!-- <span [inlineSVG]="'assets/logo_spectra.svg'" title="Download SPECTRA archive"></span> --> + Download SPECTRA archive + </a> + </div> + </div> + </div> + </div> + + <app-object-data + [datasetSelected]="datasetSelected" + [outputFamilyList]="outputFamilyList" + [outputCategoryList]="outputCategoryList" + [attributeList]="attributeList" + [object]="object"> + </app-object-data> + </div> +</div> diff --git a/client/src/app/instance/detail/components/spectra/spectra-object.component.scss b/client/src/app/instance/detail/components/spectra/spectra-object.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..260a2036246abdb58c05276b1a262e50360ac3f3 --- /dev/null +++ b/client/src/app/instance/detail/components/spectra/spectra-object.component.scss @@ -0,0 +1,13 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +.dl-btn { + height: 80px; + display: inline-block; +} diff --git a/client/src/app/instance/detail/components/spectra/spectra-object.component.ts b/client/src/app/instance/detail/components/spectra/spectra-object.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..df397e7ea50f131d916162665963e277f9401229 --- /dev/null +++ b/client/src/app/instance/detail/components/spectra/spectra-object.component.ts @@ -0,0 +1,83 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, Input, ChangeDetectionStrategy, Output, EventEmitter, OnInit } from '@angular/core'; + +import { Attribute, OutputCategory, OutputFamily } from 'src/app/metamodel/models'; +import { getHost } from 'src/app/shared/utils'; + +@Component({ + selector: 'app-spectra-object', + templateUrl: 'spectra-object.component.html', + styleUrls: ['spectra-object.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * @class + * @classdesc Detail spectra object component. + * + * @implements OnInit + */ +export class SpectraObjectComponent implements OnInit { + @Input() datasetSelected: string; + @Input() outputFamilyList: OutputFamily[]; + @Input() outputCategoryList: OutputCategory[]; + @Input() attributeList: Attribute[]; + @Input() object: any; + @Input() spectraIsLoading: boolean; + @Input() spectraIsLoaded: boolean; + @Input() spectraCSV: string; + @Output() getSpectraCSV: EventEmitter<string> = new EventEmitter(); + + ngOnInit() { + const attributeSpectraGraph = this.getAttributeSpectraGraph(); + if (attributeSpectraGraph) { + Promise.resolve(null).then(() => this.getSpectraCSV.emit(this.object[attributeSpectraGraph.label])); + } + } + + /** + * Returns spectra file URL. + * + * @return string + */ + getSpectra(): string { + const spectraAttribute: Attribute = this.attributeList + .filter(a => a.detail) + .find(attribute => attribute.search_flag === 'SPECTRUM_1D'); + return getHost() + '/download-file/' + this.datasetSelected + '/' + this.object[spectraAttribute.label]; + } + + /** + * Returns detail rendered spectra graph attribute. + * + * @return Attribute + */ + getAttributeSpectraGraph(): Attribute { + return this.attributeList + .filter(a => a.detail) + .find(attribute => attribute.renderer_detail === 'spectra_graph'); + } + + /** + * Returns Z. + * + * @return number + */ + getZ(): number { + const attributeZ = this.attributeList + .filter(a => a.detail) + .find(attribute => attribute.search_flag === 'Z'); + if (attributeZ) { + return +this.object[attributeZ.label]; + } else { + return 0; + } + } +} diff --git a/client/src/app/instance/detail/containers/detail.component.html b/client/src/app/instance/detail/containers/detail.component.html new file mode 100644 index 0000000000000000000000000000000000000000..2899b8cf78fc9f670a81366d3efd8b049e074bc6 --- /dev/null +++ b/client/src/app/instance/detail/containers/detail.component.html @@ -0,0 +1,34 @@ +<div class="container-fluid"> + <div *ngIf="!(pristine | async)" class="row mt-2 mb-2 justify-content-between"> + <div class="col"> + <button (click)="goBackToResult()" class="btn btn-outline-secondary"> + <span class="fa fa-backward"></span> Back to search results + </button> + </div> + </div> + <app-spinner *ngIf="(attributeListIsLoading | async) + || (outputFamilyListIsLoading | async) + || (outputCategoryListIsLoading | async) + || (objectIsLoading | async)"> + </app-spinner> + <div *ngIf="(attributeListIsLoaded | async) && (outputFamilyListIsLoaded | async) && (outputCategoryListIsLoaded | async) && (objectIsLoaded | async)" [ngSwitch]="getObjectType(attributeList | async)"> + <app-spectra-object *ngSwitchCase="'spectra'" + [datasetSelected]="datasetSelected | async" + [outputFamilyList]="outputFamilyList | async" + [outputCategoryList]="outputCategoryList | async" + [attributeList]="attributeList | async" + [object]="object | async" + [spectraCSV]="spectraCSV | async" + [spectraIsLoading]="spectraIsLoading | async" + [spectraIsLoaded]="spectraIsLoaded | async" + (getSpectraCSV)="getSpectraCSV($event)"> + </app-spectra-object> + <app-default-object *ngSwitchDefault + [datasetSelected]="datasetSelected | async" + [outputFamilyList]="outputFamilyList | async" + [outputCategoryList]="outputCategoryList | async" + [attributeList]="attributeList | async" + [object]="object | async"> + </app-default-object> + </div> +</div> diff --git a/client/src/app/instance/detail/containers/detail.component.ts b/client/src/app/instance/detail/containers/detail.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..331290c257df23a6736d98e8f7f702e3ab2da687 --- /dev/null +++ b/client/src/app/instance/detail/containers/detail.component.ts @@ -0,0 +1,143 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Location } from '@angular/common'; + +import { Store } from '@ngrx/store'; +import { Observable, Subscription } from 'rxjs'; + +import { Attribute, OutputCategory, OutputFamily } from 'src/app/metamodel/models'; +import * as fromDetail from 'src/app/instance/store/reducers/detail.reducer'; +import * as detailActions from 'src/app/instance/store/actions/detail.actions'; +import * as detailSelector from 'src/app/instance/store/selectors/detail.selector'; +import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector'; +import * as searchSelector from 'src/app/instance/store/selectors/search.selector'; +import * as outputFamilyActions from 'src/app/metamodel/actions/output-family.actions'; +import * as outputFamilySelector from 'src/app/metamodel/selectors/output-family.selector'; +import * as outputCategoryActions from 'src/app/metamodel/actions/output-category.actions'; +import * as outputCategorySelector from 'src/app/metamodel/selectors/output-category.selector'; +import * as attributeActions from 'src/app/metamodel/actions/attribute.actions'; +import * as attributeSelector from 'src/app/metamodel/selectors/attribute.selector'; + +/** + * Interface for store state. + * + * @interface StoreState + */ +interface StoreState { + detail: fromDetail.State; +} + +@Component({ + selector: 'app-detail-page', + templateUrl: 'detail.component.html' +}) +/** + * @class + * @classdesc Detail container. + * + * @implements OnInit + * @implements OnDestroy + */ +export class DetailComponent implements OnInit, OnDestroy { + public datasetSelected: Observable<string>; + public pristine: Observable<boolean>; + public attributeList: Observable<Attribute[]>; + public attributeListIsLoading: Observable<boolean>; + public attributeListIsLoaded: Observable<boolean>; + public outputFamilyList: Observable<OutputFamily[]>; + public outputFamilyListIsLoading: Observable<boolean>; + public outputFamilyListIsLoaded: Observable<boolean>; + public outputCategoryList: Observable<OutputCategory[]>; + public outputCategoryListIsLoading: Observable<boolean>; + public outputCategoryListIsLoaded: Observable<boolean>; + public object: Observable<any>; + public objectIsLoading: Observable<boolean>; + public objectIsLoaded: Observable<boolean>; + public spectraCSV: Observable<string>; + public spectraIsLoading: Observable<boolean>; + public spectraIsLoaded: Observable<boolean>; + + private attributeListIsLoadedSubscription: Subscription; + + constructor(private location: Location, private store: Store<StoreState>) { + this.datasetSelected = store.select(datasetSelector.selectDatasetNameByRoute); + this.pristine = store.select(searchSelector.selectPristine); + this.attributeList = store.select(attributeSelector.selectAllAttributes); + this.attributeListIsLoading = store.select(attributeSelector.selectAttributeListIsLoading); + this.attributeListIsLoaded = store.select(attributeSelector.selectAttributeListIsLoaded); + this.outputFamilyList = store.select(outputFamilySelector.selectAllOutputFamilies); + this.outputFamilyListIsLoading = store.select(outputFamilySelector.selectOutputFamilyListIsLoading); + this.outputFamilyListIsLoaded = store.select(outputFamilySelector.selectOutputFamilyListIsLoaded); + this.outputCategoryList = store.select(outputCategorySelector.selectAllOutputCategories); + this.outputCategoryListIsLoading = store.select(outputCategorySelector.selectOutputCategoryListIsLoading); + this.outputCategoryListIsLoaded = store.select(outputCategorySelector.selectOutputCategoryListIsLoaded); + this.object = this.store.select(detailSelector.selectObject); + this.objectIsLoading = this.store.select(detailSelector.selectObjectIsLoading); + this.objectIsLoaded = this.store.select(detailSelector.selectObjectIsLoaded); + this.spectraCSV = this.store.select(detailSelector.selectSpectraCSV); + this.spectraIsLoading = this.store.select(detailSelector.selectObjectIsLoading); + this.spectraIsLoaded = this.store.select(detailSelector.selectSpectraIsLoaded); + } + + ngOnInit() { + // Create a micro task that is processed after the current synchronous code + // This micro task prevent the expression has changed after view init error + Promise.resolve(null).then(() => this.store.dispatch(attributeActions.loadAttributeList())); + Promise.resolve(null).then(() => this.store.dispatch(outputFamilyActions.loadOutputFamilyList())); + Promise.resolve(null).then(() => this.store.dispatch(outputCategoryActions.loadOutputCategoryList())); + this.attributeListIsLoadedSubscription = this.attributeListIsLoaded.subscribe(attributeListIsLoaded => { + if (attributeListIsLoaded) { + Promise.resolve(null).then(() => this.store.dispatch(detailActions.retrieveObject())); + } + }); + } + + /** + * Returns the object type. + * + * @param {Attribute[]} attributeList - The attribute list. + * + * @return string + */ + getObjectType(attributeList: Attribute[]): string { + const spectrumAttribute: Attribute = attributeList + .filter(a => a.detail) + .find(attribute => attribute.search_flag === 'SPECTRUM_1D'); + if (spectrumAttribute) { + return 'spectra'; + } + return 'default'; + } + + /** + * Gets back to result page. + */ + goBackToResult(): void { + this.location.back(); + } + + /** + * Dispatches action to retrieve spectra file. + * + * @param {string} spectraFile - The spectra file name. + */ + getSpectraCSV(spectraFile: string): void { + this.store.dispatch(detailActions.retrieveSpectra({ filename: spectraFile })); + } + + /** + * Resets detail information. + */ + ngOnDestroy() { + this.attributeListIsLoadedSubscription.unsubscribe(); + // this.store.dispatch(new detailActions.DestroyDetailAction()); + } +} diff --git a/client/src/app/instance/detail/detail-routing.module.ts b/client/src/app/instance/detail/detail-routing.module.ts index ee1308922ba3f0b402a1a1bee41c26a5c08dce7e..8f88b99bb56b2ad21b228b818173bdb623ed7f6d 100644 --- a/client/src/app/instance/detail/detail-routing.module.ts +++ b/client/src/app/instance/detail/detail-routing.module.ts @@ -10,10 +10,11 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -//import { DetailComponent } from './detail.component'; +import { DetailComponent } from './containers/detail.component'; const routes: Routes = [ - //{ path: '', component: DetailComponent } + { path: '', redirectTo: ':/dname/:id', pathMatch: 'full' }, + { path: ':dname/:id', component: DetailComponent } ]; @NgModule({ @@ -23,5 +24,5 @@ const routes: Routes = [ export class DetailRoutingModule { } export const routedComponents = [ - //DetailComponent + DetailComponent ]; diff --git a/client/src/app/instance/detail/detail.module.ts b/client/src/app/instance/detail/detail.module.ts index 7e1605ceeda56e6d572a37d69b6fb11fb8e8bc7d..fbaaabfa3e090c12c51338d99b4ec7ebd6f32bdc 100644 --- a/client/src/app/instance/detail/detail.module.ts +++ b/client/src/app/instance/detail/detail.module.ts @@ -11,12 +11,16 @@ import { NgModule } from '@angular/core'; import { SharedModule } from 'src/app/shared/shared.module'; import { DetailRoutingModule, routedComponents } from './detail-routing.module'; +import { dummiesComponents } from './components'; @NgModule({ imports: [ SharedModule, DetailRoutingModule ], - declarations: [routedComponents] + declarations: [ + routedComponents, + dummiesComponents + ] }) export class DetailModule { } diff --git a/client/src/app/instance/instance-routing.module.ts b/client/src/app/instance/instance-routing.module.ts index 37baa1075d4b111e51c541b0cad73beb09937cb2..f893d0721faffd7374ec7316e0030e4e5fbbb12c 100644 --- a/client/src/app/instance/instance-routing.module.ts +++ b/client/src/app/instance/instance-routing.module.ts @@ -18,7 +18,8 @@ const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', loadChildren: () => import('./home/home.module').then(m => m.HomeModule) }, { path: 'documentation', loadChildren: () => import('./documentation/documentation.module').then(m => m.DocumentationModule) }, - { path: 'search', loadChildren: () => import('./search/search.module').then(m => m.SearchModule) } + { path: 'search', loadChildren: () => import('./search/search.module').then(m => m.SearchModule) }, + { path: 'detail', loadChildren: () => import('./detail/detail.module').then(m => m.DetailModule) } ] } ]; diff --git a/client/src/app/instance/instance.reducer.ts b/client/src/app/instance/instance.reducer.ts index f4454161d4f8074368ddc3d583a050798ea7abf4..f660ab9ab6c3dd59f99ddf24e047eac5cd2b82a7 100644 --- a/client/src/app/instance/instance.reducer.ts +++ b/client/src/app/instance/instance.reducer.ts @@ -14,19 +14,22 @@ import * as metamodel from './store/reducers/metamodel.reducer'; import * as search from './store/reducers/search.reducer'; import * as samp from './store/reducers/samp.reducer'; import * as coneSearch from './store/reducers/cone-search.reducer'; +import * as detail from './store/reducers/detail.reducer'; export interface State { metamodel: metamodel.State, search: search.State, samp: samp.State, coneSearch: coneSearch.State + detail: detail.State } const reducers = { metamodel: metamodel.metamodelReducer, search: search.searchReducer, samp: samp.sampReducer, - coneSearch: coneSearch.coneSearchReducer + coneSearch: coneSearch.coneSearchReducer, + detail: detail.detailReducer }; export const instanceReducer = combineReducers(reducers); diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.html b/client/src/app/instance/search/components/result/datatable-tab.component.html index 66316d7af426d1a63ecfb6c0e2078d6b9d6d5605..26dd36d1be6a174d083b150ebb580194511e8e9c 100644 --- a/client/src/app/instance/search/components/result/datatable-tab.component.html +++ b/client/src/app/instance/search/components/result/datatable-tab.component.html @@ -10,6 +10,7 @@ </button> <app-datatable [dataset]="datasetList | datasetByName:datasetSelected" + [instanceSelected]="instanceSelected" [attributeList]="attributeList" [outputList]="outputList" [dataLength]="dataLength" diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.ts b/client/src/app/instance/search/components/result/datatable-tab.component.ts index c2d7dd1dd0ce224bd65ad333c910a84809bc0c1d..c24e1979d36d8710d5efe016bd9ba69a5e4c35d2 100644 --- a/client/src/app/instance/search/components/result/datatable-tab.component.ts +++ b/client/src/app/instance/search/components/result/datatable-tab.component.ts @@ -23,6 +23,7 @@ import { Pagination } from 'src/app/instance/store/models'; */ export class DatatableTabComponent { @Input() datasetSelected: string; + @Input() instanceSelected: string; @Input() datasetList: Dataset[]; @Input() attributeList: Attribute[]; @Input() outputList: number[]; diff --git a/client/src/app/instance/search/containers/result.component.html b/client/src/app/instance/search/containers/result.component.html index ab045ae224c68d9ae47765843967539f45be1aa7..9e4fec28e05e52c08e8baab91789b911b6bbf5f6 100644 --- a/client/src/app/instance/search/containers/result.component.html +++ b/client/src/app/instance/search/containers/result.component.html @@ -64,6 +64,7 @@ </app-cone-search-plot-tab> --> <app-datatable-tab [datasetSelected]="datasetSelected | async" + [instanceSelected]="instanceSelected | async" [datasetList]="datasetList | async" [attributeList]="attributeList | async" [outputList]="outputList | async" diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.html b/client/src/app/instance/shared-search/components/datatable/datatable.component.html index 8a373adc94235177078fdbe92c629b1bb642024b..881f79065975906242993d75e2239f945bd4ff4c 100644 --- a/client/src/app/instance/shared-search/components/datatable/datatable.component.html +++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.html @@ -45,6 +45,7 @@ <app-detail-renderer [value]="datum[attribute.label]" [datasetName]="dataset.name" + [instanceSelected]="instanceSelected" [config]="getRendererConfig(attribute)"> </app-detail-renderer> </div> diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts index 56d115ee220a55a674fc8b3a6343871b3c6c8178..698cf0403bf1ea003acb77f3b9da144bb38132db 100644 --- a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts +++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts @@ -26,6 +26,7 @@ import { Pagination, PaginationOrder } from 'src/app/instance/store/models'; */ export class DatatableComponent implements OnInit { @Input() dataset: Dataset; + @Input() instanceSelected: string; @Input() attributeList: Attribute[]; @Input() outputList: number[]; @Input() dataLength: number; diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.html b/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.html index 0b6336b1e1acb9cf348dc857b43bd2fedc62ddc2..37c9f3133e39d52496faf4585956836050038ccc 100644 --- a/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.html +++ b/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.html @@ -1,4 +1,4 @@ -<a routerLink="/detail/{{ datasetName }}/{{ value }}" [target]="config.blank == true ? '_blank' : '_self'" +<a routerLink="/instance/{{ instanceSelected }}/detail/{{ datasetName }}/{{ value }}" [target]="config.blank == true ? '_blank' : '_self'" [ngClass]="{'btn btn-outline-primary btn-sm' : config.display == 'text-button'}"> {{ value }} </a> diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.ts b/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.ts index 06411ef9ec5a95251f5cc49da809f0490eb95839..d6df4dfd8384e2931696c6807a5a0c97bfdeaefb 100644 --- a/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.ts +++ b/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.ts @@ -23,5 +23,6 @@ import { DetailRendererConfig } from 'src/app/metamodel/models/renderers/detail- export class DetailRendererComponent { @Input() value: string | number; @Input() datasetName: string; + @Input() instanceSelected: string; @Input() config: DetailRendererConfig; } diff --git a/client/src/app/instance/store/actions/detail.actions.ts b/client/src/app/instance/store/actions/detail.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..c88210bc8e4e24fb0ee165d34c38c07a90792322 --- /dev/null +++ b/client/src/app/instance/store/actions/detail.actions.ts @@ -0,0 +1,17 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createAction, props } from '@ngrx/store'; + +export const retrieveObject = createAction('[Detail] Retrieve Object'); +export const retrieveObjectSuccess = createAction('[Detail] Retrieve Object Success', props<{ object: any }>()); +export const retrieveObjectFail = createAction('[Detail] Retrieve Object Fail'); +export const retrieveSpectra = createAction('[Detail] Retrieve Spectra', props<{ filename: string }>()); +export const retrieveSpectraSuccess = createAction('[Detail] Retrieve Spectra Success', props<{ spectraCSV: string }>()); +export const retrieveSpectraFail = createAction('[Detail] Retrieve Spectra Fail'); diff --git a/client/src/app/instance/store/effects/detail.effects.ts b/client/src/app/instance/store/effects/detail.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..256412aea966b727115af6394a73795b181e22e7 --- /dev/null +++ b/client/src/app/instance/store/effects/detail.effects.ts @@ -0,0 +1,80 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; + +import { Actions, createEffect, ofType, concatLatestFrom } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { of } from 'rxjs'; +import { map, tap, mergeMap, catchError } from 'rxjs/operators'; +import { ToastrService } from 'ngx-toastr'; + +import { DetailService } from '../services/detail.service'; +import * as detailActions from '../actions/detail.actions'; +import * as detailSelector from '../selectors/detail.selector'; +import * as attributeSelector from 'src/app/metamodel/selectors/attribute.selector'; +import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector'; + +@Injectable() +export class DetailEffects { + retrieveObject$ = createEffect(() => + this.actions$.pipe( + ofType(detailActions.retrieveObject), + concatLatestFrom(() => [ + this.store.select(datasetSelector.selectDatasetNameByRoute), + this.store.select(attributeSelector.selectAllAttributes), + this.store.select(detailSelector.selectIdByRoute) + ]), + mergeMap(([action, datasetName, attributeList, id]) => this.detailService.retrieveObject( + datasetName, + attributeList.find(attribute => attribute.order_by).id, + id, + attributeList.filter(attribute => attribute.detail).map(attribute => attribute.id) + ).pipe( + map(object => detailActions.retrieveObjectSuccess({ object: object[0] })), + catchError(() => of(detailActions.retrieveObjectFail())) + )) + ) + ); + + retrieveObjectFail$ = createEffect(() => + this.actions$.pipe( + ofType(detailActions.retrieveObjectFail), + tap(() => this.toastr.error('Loading Failed!', 'Unable to load the object')) + ), { dispatch: false} + ); + + retrieveSpectra$ = createEffect(() => + this.actions$.pipe( + ofType(detailActions.retrieveSpectra), + concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)), + mergeMap(([action, datasetName]) => this.detailService.retrieveSpectra( + datasetName, + action.filename + ).pipe( + map(spectraCSV => detailActions.retrieveSpectraSuccess({ spectraCSV })), + catchError(() => of(detailActions.retrieveSpectraFail())) + )) + ) + ); + + retrieveSpectraFail$ = createEffect(() => + this.actions$.pipe( + ofType(detailActions.retrieveSpectraFail), + tap(() => this.toastr.error('Loading Failed!', 'Unable to load spectra')) + ), { dispatch: false} + ); + + constructor( + private actions$: Actions, + private detailService: DetailService, + private store: Store<{ }>, + private toastr: ToastrService + ) {} +} diff --git a/client/src/app/instance/store/effects/index.ts b/client/src/app/instance/store/effects/index.ts index 3d2ecd77ad3f9e6f15ba5a0e1938aac5679cb49d..29c6402a3c50c5a7138b9d239ae5723e6421e3ec 100644 --- a/client/src/app/instance/store/effects/index.ts +++ b/client/src/app/instance/store/effects/index.ts @@ -2,10 +2,12 @@ import { MetamodelEffects } from './metamodel.effects'; import { SampEffects } from './samp.effects'; import { SearchEffects } from './search.effects'; import { ConeSearchEffects } from './cone-search.effects'; +import { DetailEffects } from './detail.effects'; export const instanceEffects = [ MetamodelEffects, SampEffects, SearchEffects, - ConeSearchEffects + ConeSearchEffects, + DetailEffects ]; diff --git a/client/src/app/instance/store/reducers/detail.reducer.ts b/client/src/app/instance/store/reducers/detail.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..466fcfc1838c6e79d790cf87fafb944b96081189 --- /dev/null +++ b/client/src/app/instance/store/reducers/detail.reducer.ts @@ -0,0 +1,71 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createReducer, on } from '@ngrx/store'; + +import * as detailActions from '../actions/detail.actions'; + +export interface State { + object: any; + objectIsLoading: boolean; + objectIsLoaded: boolean; + spectraCSV: string; + spectraIsLoading: boolean; + spectraIsLoaded: boolean; +} + +export const initialState: State = { + object: null, + objectIsLoading: false, + objectIsLoaded: false, + spectraCSV: null, + spectraIsLoading: false, + spectraIsLoaded: false, +}; + +export const detailReducer = createReducer( + initialState, + on(detailActions.retrieveObject, state => ({ + ...state, + objectIsLoading: true, + objectIsLoaded: false + })), + on(detailActions.retrieveObjectSuccess, (state, { object }) => ({ + ...state, + objectIsLoading: false, + objectIsLoaded: true, + object + })), + on(detailActions.retrieveObjectFail, state => ({ + ...state, + objectIsLoading: false + })), + on(detailActions.retrieveSpectra, state => ({ + ...state, + spectraIsLoading: true, + spectraIsLoaded: false + })), + on(detailActions.retrieveSpectraSuccess, (state, { spectraCSV }) => ({ + ...state, + spectraIsLoading: false, + spectraIsLoaded: true, + spectraCSV + })), + on(detailActions.retrieveSpectraFail, state => ({ + ...state, + spectraIsLoading: false + })) +); + +export const selectObject = (state: State) => state.object; +export const selectObjectIsLoading = (state: State) => state.objectIsLoading; +export const selectObjectIsLoaded = (state: State) => state.objectIsLoaded; +export const selectSpectraCSV = (state: State) => state.spectraCSV; +export const selectSpectraIsLoading = (state: State) => state.spectraIsLoading; +export const selectSpectraIsLoaded = (state: State) => state.spectraIsLoaded; diff --git a/client/src/app/instance/store/selectors/detail.selector.ts b/client/src/app/instance/store/selectors/detail.selector.ts new file mode 100644 index 0000000000000000000000000000000000000000..2fe62d5722257342c255c5b4680e6883ecad19f5 --- /dev/null +++ b/client/src/app/instance/store/selectors/detail.selector.ts @@ -0,0 +1,53 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createSelector } from '@ngrx/store'; + +import * as reducer from '../../instance.reducer'; +import * as fromDetail from '../reducers/detail.reducer'; + +export const selectDetailState = createSelector( + reducer.getInstanceState, + (state: reducer.State) => state.detail +); + +export const selectObject = createSelector( + selectDetailState, + fromDetail.selectObject +); + +export const selectObjectIsLoading = createSelector( + selectDetailState, + fromDetail.selectObjectIsLoading +); + +export const selectObjectIsLoaded = createSelector( + selectDetailState, + fromDetail.selectObjectIsLoaded +); + +export const selectSpectraCSV = createSelector( + selectDetailState, + fromDetail.selectSpectraCSV +); + +export const selectSpectraIsLoading = createSelector( + selectDetailState, + fromDetail.selectSpectraIsLoading +); + +export const selectSpectraIsLoaded = createSelector( + selectDetailState, + fromDetail.selectSpectraIsLoaded +); + +export const selectIdByRoute = createSelector( + reducer.selectRouterState, + router => router.state.params.id +); diff --git a/client/src/app/instance/store/selectors/search.selector.ts b/client/src/app/instance/store/selectors/search.selector.ts index 094c36733e1f0aed698df8503b56aaf91977b642..44ebe388b9cde19e8bbfe3eff35a5bb9e7b68ecd 100644 --- a/client/src/app/instance/store/selectors/search.selector.ts +++ b/client/src/app/instance/store/selectors/search.selector.ts @@ -14,88 +14,88 @@ import * as reducer from '../../instance.reducer'; import * as fromSearch from '../reducers/search.reducer'; import * as coneSearchSelector from './cone-search.selector'; -export const selectInstanceState = createSelector( +export const selectSearchState = createSelector( reducer.getInstanceState, (state: reducer.State) => state.search ); export const selectPristine = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectPristine ); export const selectCurrentDataset = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectCurrentDataset ); export const selectCurrentStep = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectCurrentStep ); export const selectCriteriaStepChecked = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectCriteriaStepChecked ); export const selectOutputStepChecked = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectOutputStepChecked ); export const selectResultStepChecked = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectResultStepChecked ); export const selectIsConeSearchAdded = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectIsConeSearchAdded ); export const selectCriteriaList = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectCriteriaList ); export const selectOutputList = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectOutputList ); export const selectDataLengthIsLoading = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectDataLengthIsLoading ); export const selectDataLengthIsLoaded = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectDataLengthIsLoaded ); export const selectDataLength = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectDataLength ); export const selectDataIsLoading = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectDataIsLoading ); export const selectDataIsLoaded = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectDataIsLoaded ); export const selectData = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectData ); export const selectSelectedData = createSelector( - selectInstanceState, + selectSearchState, fromSearch.selectSelectedData ); diff --git a/client/src/app/instance/store/services/detail.service.ts b/client/src/app/instance/store/services/detail.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..e1b5db35066ac3cdcf46e331755d201835014ab4 --- /dev/null +++ b/client/src/app/instance/store/services/detail.service.ts @@ -0,0 +1,53 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { environment } from 'src/environments/environment'; + +@Injectable() +/** + * @class + * @classdesc Detail service. + */ +export class DetailService { + private API_PATH: string = environment.apiUrl + '/search/'; + private SERVICES_PATH: string = environment.servicesUrl; + + constructor(private http: HttpClient) { } + + /** + * Retrieves object details for the given parameters. + * + * @param {string} dname - The dataset name. + * @param {number} criterionId - The criterion ID. + * @param {string} objectSelected - The selected object ID. + * @param {number[]} outputList - The output list. + * + * @return Observable<any[]> + */ + retrieveObject(dname: string, criterionId: number, objectSelected: string, outputList: number[]): Observable<any[]> { + const query = dname + '?c=' + criterionId + '::eq::' + objectSelected + '&a=' + outputList.join(';'); + return this.http.get<any[]>(this.API_PATH + query); + } + + /** + * Retrieves object details for the given parameters. + * + * @param {string} spectraFile - The spectra file name. + * + * @return Observable<string> + */ + retrieveSpectra(dname: string, spectraFile: string): Observable<string> { + return this.http.get(this.SERVICES_PATH + '/spectra-to-csv/' + dname + '?filename=' + spectraFile, { responseType: 'text' }); + } +} diff --git a/client/src/app/instance/store/services/index.ts b/client/src/app/instance/store/services/index.ts index 4707c60a165f5e0b28a886c148854579537703d5..cabc079690c1bfea8a44fddb9baa7123796937b2 100644 --- a/client/src/app/instance/store/services/index.ts +++ b/client/src/app/instance/store/services/index.ts @@ -1,9 +1,11 @@ import { SearchService } from './search.service'; import { SampService } from './samp.service'; import { ConeSearchService } from './cone-search.service'; +import { DetailService } from './detail.service'; export const instanceServices = [ SearchService, SampService, - ConeSearchService + ConeSearchService, + DetailService ]; diff --git a/client/yarn.lock b/client/yarn.lock index 2a005f9358ac87f05bedd31cb41ab6f7bf727db2..374022b1be318ba6228961cb9dc4dcfcd78a8d14 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1344,6 +1344,222 @@ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.10.tgz#61cc8469849e5bcdd0c7044122265c39cec10cf4" integrity sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ== +"@types/d3-array@^1": + version "1.2.9" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.9.tgz#c7dc78992cd8ca5c850243a265fd257ea56df1fa" + integrity sha512-E/7RgPr2ylT5dWG0CswMi9NpFcjIEDqLcUSBgNHe/EMahfqYaTx4zhcggG3khqoEB/leY4Vl6nTSbwLUPjXceA== + +"@types/d3-axis@^1": + version "1.0.16" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-1.0.16.tgz#93d7a28795c2f8b0e2fd550fcc4d29b7f174e693" + integrity sha512-p7085weOmo4W+DzlRRVC/7OI/jugaKbVa6WMQGCQscaMylcbuaVEGk7abJLNyGVFLeCBNrHTdDiqRGnzvL0nXQ== + dependencies: + "@types/d3-selection" "^1" + +"@types/d3-brush@^1": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-1.1.5.tgz#c7cfb58decbfd53ad3e47f0376345e3640a68186" + integrity sha512-4zGkBafJf5zCsBtLtvDj/pNMo5X9+Ii/1hUz0GvQ+wEwelUBm2AbIDAzJnp2hLDFF307o0fhxmmocHclhXC+tw== + dependencies: + "@types/d3-selection" "^1" + +"@types/d3-chord@^1": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-1.0.11.tgz#5760765db1b1a4b936c0d9355a821dde9dd25da2" + integrity sha512-0DdfJ//bxyW3G9Nefwq/LDgazSKNN8NU0lBT3Cza6uVuInC2awMNsAcv1oKyRFLn9z7kXClH5XjwpveZjuz2eg== + +"@types/d3-collection@*": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-collection/-/d3-collection-1.0.10.tgz#bca161e336156968f267c077f7f2bfa8ff224e58" + integrity sha512-54Fdv8u5JbuXymtmXm2SYzi1x/Svt+jfWBU5junkhrCewL92VjqtCBDn97coBRVwVFmYNnVTNDyV8gQyPYfm+A== + +"@types/d3-color@^1": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.4.2.tgz#944f281d04a0f06e134ea96adbb68303515b2784" + integrity sha512-fYtiVLBYy7VQX+Kx7wU/uOIkGQn8aAEY8oWMoyja3N4dLd8Yf6XgSIR/4yWvMuveNOH5VShnqCgRqqh/UNanBA== + +"@types/d3-contour@^1": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-1.3.3.tgz#44529d498bbc1db78b195d75e1c9bb889edd647a" + integrity sha512-LxwmGIfVJIc1cKs7ZFRQ1FbtXpfH7QTXYRdMIJsFP71uCMdF6jJ0XZakYDX6Hn4yZkLf+7V8FgD34yCcok+5Ww== + dependencies: + "@types/d3-array" "^1" + "@types/geojson" "*" + +"@types/d3-dispatch@^1": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-1.0.9.tgz#c5a180f1e251de853b399cfbfbb6dd7f8bf842ae" + integrity sha512-zJ44YgjqALmyps+II7b1mZLhrtfV/FOxw9owT87mrweGWcg+WK5oiJX2M3SYJ0XUAExBduarysfgbR11YxzojQ== + +"@types/d3-drag@^1": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-1.2.5.tgz#0b1b852cb41577075aa625ae6149379ea6c34dfd" + integrity sha512-7NeTnfolst1Js3Vs7myctBkmJWu6DMI3k597AaHUX98saHjHWJ6vouT83UrpE+xfbSceHV+8A0JgxuwgqgmqWw== + dependencies: + "@types/d3-selection" "^1" + +"@types/d3-dsv@^1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-1.2.1.tgz#1524fee9f19d689c2f76aa0e24e230762bf96994" + integrity sha512-LLmJmjiqp/fTNEdij5bIwUJ6P6TVNk5hKM9/uk5RPO2YNgEu9XvKO0dJ7Iqd3psEdmZN1m7gB1bOsjr4HmO2BA== + +"@types/d3-ease@^1": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-1.0.10.tgz#09910e8558439b6038a7ed620650e510394ffa6d" + integrity sha512-fMFTCzd8DOwruE9zlu2O8ci5ct+U5jkGcDS+cH+HCidnJlDs0MZ+TuSVCFtEzh4E5MasItwy+HvgoFtxPHa5Cw== + +"@types/d3-fetch@^1": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-1.2.2.tgz#b93bfe248b8b761af82f4dac57959c989f67da3e" + integrity sha512-rtFs92GugtV/NpiJQd0WsmGLcg52tIL0uF0bKbbJg231pR9JEb6HT4AUwrtuLq3lOeKdLBhsjV14qb0pMmd0Aw== + dependencies: + "@types/d3-dsv" "^1" + +"@types/d3-force@^1": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-1.2.4.tgz#6e274c72288c2db08fbdb8f5b87b9aa83e55a9e8" + integrity sha512-fkorLTKvt6AQbFBQwn4aq7h9rJ4c7ZVcPMGB8X6eFFveAyMZcv7t7m6wgF4Eg93rkPgPORU7sAho1QSHNcZu6w== + +"@types/d3-format@^1": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.4.2.tgz#ea17bf559b71d9afd569ae9bfe4c544dab863baa" + integrity sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ== + +"@types/d3-geo@^1": + version "1.12.3" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-1.12.3.tgz#512ebe735cb1cdf5f87ad59608416e2e9e868c5a" + integrity sha512-yZbPb7/5DyL/pXkeOmZ7L5ySpuGr4H48t1cuALjnJy5sXQqmSSAYBiwa6Ya/XpWKX2rJqGDDubmh3nOaopOpeA== + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@^1": + version "1.1.8" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz#50657f420d565a06c0b950a4b82eee0a369f2dea" + integrity sha512-AbStKxNyWiMDQPGDguG2Kuhlq1Sv539pZSxYbx4UZeYkutpPwXCcgyiRrlV4YH64nIOsKx7XVnOMy9O7rJsXkg== + +"@types/d3-interpolate@^1": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-1.4.2.tgz#88902a205f682773a517612299a44699285eed7b" + integrity sha512-ylycts6llFf8yAEs1tXzx2loxxzDZHseuhPokrqKprTQSTcD3JbJI1omZP1rphsELZO3Q+of3ff0ZS7+O6yVzg== + dependencies: + "@types/d3-color" "^1" + +"@types/d3-path@^1": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c" + integrity sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ== + +"@types/d3-polygon@^1": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-1.0.8.tgz#127ee83fccda5bf57384011da90f31367fea1530" + integrity sha512-1TOJPXCBJC9V3+K3tGbTqD/CsqLyv/YkTXAcwdsZzxqw5cvpdnCuDl42M4Dvi8XzMxZNCT9pL4ibrK2n4VmAcw== + +"@types/d3-quadtree@^1": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-1.0.9.tgz#c7c3b795b5af06e5b043d1d34e754a434b3bae59" + integrity sha512-5E0OJJn2QVavITFEc1AQlI8gLcIoDZcTKOD3feKFckQVmFV4CXhqRFt83tYNVNIN4ZzRkjlAMavJa1ldMhf5rA== + +"@types/d3-random@^1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-1.1.3.tgz#8f7fdc23f92d1561e0694eb49567e8ab50537a19" + integrity sha512-XXR+ZbFCoOd4peXSMYJzwk0/elP37WWAzS/DG+90eilzVbUSsgKhBcWqylGWe+lA2ubgr7afWAOBaBxRgMUrBQ== + +"@types/d3-scale-chromatic@^1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-1.5.1.tgz#e2b7c3401e5c13809f831911eb820e444f4fc67a" + integrity sha512-7FtJYrmXTEWLykShjYhoGuDNR/Bda0+tstZMkFj4RRxUEryv16AGh3be21tqg84B6KfEwiZyEpBcTyPyU+GWjg== + +"@types/d3-scale@^2": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-2.2.6.tgz#28540b4dfc99d978970e873e4138a6bea2ea6ab8" + integrity sha512-CHu34T5bGrJOeuhGxyiz9Xvaa9PlsIaQoOqjDg7zqeGj2x0rwPhGquiy03unigvcMxmvY0hEaAouT0LOFTLpIw== + dependencies: + "@types/d3-time" "^1" + +"@types/d3-selection@^1": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.4.3.tgz#36928bbe64eb8e0bbcbaa01fb05c21ff6c71fa93" + integrity sha512-GjKQWVZO6Sa96HiKO6R93VBE8DUW+DDkFpIMf9vpY5S78qZTlRRSNUsHr/afDpF7TvLDV7VxrUFOWW7vdIlYkA== + +"@types/d3-shape@^1": + version "1.3.8" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.8.tgz#c3c15ec7436b4ce24e38de517586850f1fea8e89" + integrity sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg== + dependencies: + "@types/d3-path" "^1" + +"@types/d3-time-format@^2": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-2.3.1.tgz#87a30e4513b9d1d53b920327a361f87255bf3372" + integrity sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA== + +"@types/d3-time@^1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.1.1.tgz#6cf3a4242c3bbac00440dfb8ba7884f16bedfcbf" + integrity sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw== + +"@types/d3-timer@^1": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-1.0.10.tgz#329c51c2c931f44ed0acff78b8c84571acf0ed21" + integrity sha512-ZnAbquVqy+4ZjdW0cY6URp+qF/AzTVNda2jYyOzpR2cPT35FTXl78s15Bomph9+ckOiI1TtkljnWkwbIGAb6rg== + +"@types/d3-transition@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.3.2.tgz#ed59beca7b4d679cfa52f88a6a50e5bbeb7e0a3c" + integrity sha512-J+a3SuF/E7wXbOSN19p8ZieQSFIm5hU2Egqtndbc54LXaAEOpLfDx4sBu/PKAKzHOdgKK1wkMhINKqNh4aoZAg== + dependencies: + "@types/d3-selection" "^1" + +"@types/d3-voronoi@*": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@types/d3-voronoi/-/d3-voronoi-1.1.9.tgz#7bbc210818a3a5c5e0bafb051420df206617c9e5" + integrity sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ== + +"@types/d3-zoom@^1": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-1.8.3.tgz#00237900c6fdc2bb4fe82679ee4d74eb8fbe7b3c" + integrity sha512-3kHkL6sPiDdbfGhzlp5gIHyu3kULhtnHTTAl3UBZVtWB1PzcLL8vdmz5mTx7plLiUqOA2Y+yT2GKjt/TdA2p7Q== + dependencies: + "@types/d3-interpolate" "^1" + "@types/d3-selection" "^1" + +"@types/d3@^5.7.2": + version "5.16.4" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-5.16.4.tgz#a7dc24a3dc1c19922eee72ba16144fd5bcea987a" + integrity sha512-2u0O9iP1MubFiQ+AhR1id4Egs+07BLtvRATG6IL2Gs9+KzdrfaxCKNq5hxEyw1kxwsqB/lCgr108XuHcKtb/5w== + dependencies: + "@types/d3-array" "^1" + "@types/d3-axis" "^1" + "@types/d3-brush" "^1" + "@types/d3-chord" "^1" + "@types/d3-collection" "*" + "@types/d3-color" "^1" + "@types/d3-contour" "^1" + "@types/d3-dispatch" "^1" + "@types/d3-drag" "^1" + "@types/d3-dsv" "^1" + "@types/d3-ease" "^1" + "@types/d3-fetch" "^1" + "@types/d3-force" "^1" + "@types/d3-format" "^1" + "@types/d3-geo" "^1" + "@types/d3-hierarchy" "^1" + "@types/d3-interpolate" "^1" + "@types/d3-path" "^1" + "@types/d3-polygon" "^1" + "@types/d3-quadtree" "^1" + "@types/d3-random" "^1" + "@types/d3-scale" "^2" + "@types/d3-scale-chromatic" "^1" + "@types/d3-selection" "^1" + "@types/d3-shape" "^1" + "@types/d3-time" "^1" + "@types/d3-time-format" "^2" + "@types/d3-timer" "^1" + "@types/d3-transition" "^1" + "@types/d3-voronoi" "*" + "@types/d3-zoom" "^1" + "@types/eslint-scope@^3.7.0": version "3.7.0" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.0.tgz#4792816e31119ebd506902a482caec4951fabd86" @@ -1370,6 +1586,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.47.tgz#d7a51db20f0650efec24cd04994f523d93172ed4" integrity sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg== +"@types/geojson@*": + version "7946.0.8" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca" + integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA== + "@types/glob@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" @@ -2402,7 +2623,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.20.0: +commander@2, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -2780,6 +3001,254 @@ custom-event@~1.0.0: resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU= +d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" + integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== + +d3-axis@1: + version "1.0.12" + resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9" + integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ== + +d3-brush@1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.1.6.tgz#b0a22c7372cabec128bdddf9bddc058592f89e9b" + integrity sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA== + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + +d3-chord@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.6.tgz#309157e3f2db2c752f0280fedd35f2067ccbb15f" + integrity sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA== + dependencies: + d3-array "1" + d3-path "1" + +d3-collection@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" + integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A== + +d3-color@1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" + integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== + +d3-contour@1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3" + integrity sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg== + dependencies: + d3-array "^1.1.1" + +d3-dispatch@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58" + integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA== + +d3-drag@1: + version "1.2.5" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.5.tgz#2537f451acd39d31406677b7dc77c82f7d988f70" + integrity sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w== + dependencies: + d3-dispatch "1" + d3-selection "1" + +d3-dsv@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.2.0.tgz#9d5f75c3a5f8abd611f74d3f5847b0d4338b885c" + integrity sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g== + dependencies: + commander "2" + iconv-lite "0.4" + rw "1" + +d3-ease@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2" + integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ== + +d3-fetch@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.2.0.tgz#15ce2ecfc41b092b1db50abd2c552c2316cf7fc7" + integrity sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA== + dependencies: + d3-dsv "1" + +d3-force@1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.2.1.tgz#fd29a5d1ff181c9e7f0669e4bd72bdb0e914ec0b" + integrity sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg== + dependencies: + d3-collection "1" + d3-dispatch "1" + d3-quadtree "1" + d3-timer "1" + +d3-format@1: + version "1.4.5" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" + integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== + +d3-geo@1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.12.1.tgz#7fc2ab7414b72e59fbcbd603e80d9adc029b035f" + integrity sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg== + dependencies: + d3-array "1" + +d3-hierarchy@1: + version "1.1.9" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83" + integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ== + +d3-interpolate@1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" + integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== + dependencies: + d3-color "1" + +d3-path@1: + version "1.0.9" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" + integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== + +d3-polygon@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.6.tgz#0bf8cb8180a6dc107f518ddf7975e12abbfbd38e" + integrity sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ== + +d3-quadtree@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz#ca8b84df7bb53763fe3c2f24bd435137f4e53135" + integrity sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA== + +d3-random@1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.2.tgz#2833be7c124360bf9e2d3fd4f33847cfe6cab291" + integrity sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ== + +d3-scale-chromatic@1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98" + integrity sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg== + dependencies: + d3-color "1" + d3-interpolate "1" + +d3-scale@2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f" + integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw== + dependencies: + d3-array "^1.2.0" + d3-collection "1" + d3-format "1" + d3-interpolate "1" + d3-time "1" + d3-time-format "2" + +d3-selection@1, d3-selection@^1.1.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c" + integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg== + +d3-shape@1: + version "1.3.7" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" + integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== + dependencies: + d3-path "1" + +d3-time-format@2: + version "2.3.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850" + integrity sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ== + dependencies: + d3-time "1" + +d3-time@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" + integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== + +d3-timer@1: + version "1.0.10" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" + integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== + +d3-transition@1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398" + integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA== + dependencies: + d3-color "1" + d3-dispatch "1" + d3-ease "1" + d3-interpolate "1" + d3-selection "^1.1.0" + d3-timer "1" + +d3-voronoi@1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297" + integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg== + +d3-zoom@1: + version "1.8.3" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.8.3.tgz#b6a3dbe738c7763121cd05b8a7795ffe17f4fc0a" + integrity sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ== + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + +d3@^5.15.1: + version "5.16.0" + resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877" + integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw== + dependencies: + d3-array "1" + d3-axis "1" + d3-brush "1" + d3-chord "1" + d3-collection "1" + d3-color "1" + d3-contour "1" + d3-dispatch "1" + d3-drag "1" + d3-dsv "1" + d3-ease "1" + d3-fetch "1" + d3-force "1" + d3-format "1" + d3-geo "1" + d3-hierarchy "1" + d3-interpolate "1" + d3-path "1" + d3-polygon "1" + d3-quadtree "1" + d3-random "1" + d3-scale "2" + d3-scale-chromatic "1" + d3-selection "1" + d3-shape "1" + d3-time "1" + d3-time-format "2" + d3-timer "1" + d3-transition "1" + d3-voronoi "1" + d3-zoom "1" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -3851,7 +4320,7 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: +iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -6618,6 +7087,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rw@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= + rxjs@6.6.7, rxjs@^6.6.6, rxjs@~6.6.0: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"