RED-6830: License information with graphjs

This commit is contained in:
Adina Țeudan 2023-06-29 16:23:50 +03:00
parent 76b5755d5e
commit c8745f048d
40 changed files with 528 additions and 1417 deletions

View File

@ -63,7 +63,7 @@
"stylePreprocessorOptions": {
"includePaths": ["./apps/red-ui/src/assets/styles", "./libs/common-ui/src/assets/styles"]
},
"scripts": ["node_modules/@pdftron/webviewer/webviewer.min.js"],
"scripts": ["node_modules/@pdftron/webviewer/webviewer.min.js", "node_modules/chart.js/dist/chart.js"],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,

View File

@ -1,116 +0,0 @@
<ngx-charts-chart
(legendLabelActivate)="onActivate($event)"
(legendLabelClick)="onClick($event)"
(legendLabelDeactivate)="onDeactivate($event)"
[activeEntries]="activeEntries"
[animations]="animations"
[legendOptions]="legendOptions"
[showLegend]="legend"
[view]="[width + legendSpacing, height]"
xmlns:svg="http://www.w3.org/2000/svg"
>
<svg:g [attr.transform]="transform" class="bar-chart chart">
<svg:g
(dimensionsChanged)="updateXAxisHeight($event)"
*ngIf="xAxis"
[dims]="dims"
[labelText]="xAxisLabel"
[rotateTicks]="true"
[showLabel]="showXAxisLabel"
[tickFormatting]="xAxisTickFormatting"
[xScale]="xScale"
ngx-charts-x-axis
></svg:g>
<svg:g
(dimensionsChanged)="updateYAxisWidth($event)"
*ngIf="yAxis"
[dims]="dims"
[labelText]="yAxisLabel"
[showGridLines]="showGridLines"
[showLabel]="showYAxisLabel"
[tickFormatting]="yAxisTickFormatting"
[yOrient]="yOrientLeft"
[yScale]="yScale"
[valuesMaxLength]="getValuesMaxLength(1)"
red-ngx-charts-y-axis
></svg:g>
<svg:g
(dimensionsChanged)="updateYAxisWidth($event)"
*ngIf="yAxis"
[dims]="dims"
[labelText]="yAxisLabelRight"
[showGridLines]="showGridLines"
[showLabel]="showRightYAxisLabel"
[tickFormatting]="yRightAxisTickFormatting"
[yOrient]="yOrientRight"
[yScale]="yScaleLine"
[valuesMaxLength]="getValuesMaxLength(0)"
red-ngx-charts-y-axis
></svg:g>
<svg:g
(activate)="onActivate($event)"
(bandwidth)="updateLineWidth($event)"
(deactivate)="onDeactivate($event)"
[activeEntries]="activeEntries"
[animations]="animations"
[colors]="colors"
[dims]="dims"
[gradient]="gradient"
[noBarWhenZero]="noBarWhenZero"
[seriesLine]="lineChart"
[series]="results"
[tooltipDisabled]="true"
[xScale]="xScale"
[yScale]="yScale"
ngx-combo-charts-series-vertical
></svg:g>
</svg:g>
<svg:g [attr.transform]="transform" class="line-chart chart">
<svg:g>
<svg:g *ngFor="let series of lineChart; trackBy: trackBy">
<svg:g
[activeEntries]="activeEntries"
[animations]="animations"
[colors]="colorsLine"
[curve]="curve"
[data]="series"
[rangeFillOpacity]="rangeFillOpacity"
[scaleType]="scaleType"
[xScale]="xScaleLine"
[yScale]="yScaleLine"
ngx-charts-line-series
></svg:g>
</svg:g>
<svg:g
(hover)="updateHoveredVertical($event)"
*ngIf="!tooltipDisabled"
[colors]="colorsLine"
[dims]="dims"
[results]="combinedSeries"
[tooltipDisabled]="tooltipDisabled"
[xScale]="xScaleLine"
[xSet]="xSet"
[yScale]="yScaleLine"
ngx-charts-tooltip-area
></svg:g>
<svg:g *ngFor="let series of lineChart">
<svg:g
(activate)="onActivate($event)"
(deactivate)="onDeactivate($event)"
[activeEntries]="activeEntries"
[colors]="colorsLine"
[data]="series"
[scaleType]="scaleType"
[tooltipDisabled]="tooltipDisabled"
[visibleValue]="hoveredVertical"
[xScale]="xScaleLine"
[yScale]="yScaleLine"
ngx-charts-circle-series
></svg:g>
</svg:g>
</svg:g>
</svg:g>
</ngx-charts-chart>

View File

@ -1,103 +0,0 @@
.ngx-charts {
float: left;
overflow: visible;
.circle,
.bar,
.arc {
cursor: pointer;
}
.bar,
.cell,
.arc,
.card {
&.active,
&:hover {
opacity: 0.8;
transition: opacity 100ms ease-in-out;
}
&:focus {
outline: none;
}
&.hidden {
display: none;
}
}
g {
&:focus {
outline: none;
}
}
.line-series,
.line-series-range,
.area-series {
&.inactive {
transition: opacity 100ms ease-in-out;
opacity: 0.2;
}
}
.line-highlight {
display: none;
&.active {
display: block;
}
}
.area {
opacity: 0.6;
}
.circle {
&:hover {
cursor: pointer;
}
}
.label {
font-size: 12px;
font-weight: normal;
}
.tooltip-anchor {
fill: var(--iqser-text);
}
.gridline-path {
stroke: #ddd;
stroke-width: 1;
fill: none;
}
.grid-panel {
rect {
fill: none;
}
&.odd {
rect {
fill: rgba(0, 0, 0, 0.05);
}
}
}
fill: var(--iqser-text);
}
.chart-legend .legend-labels {
background: var(--iqser-alt-background) !important;
.legend-label .legend-label-text {
color: var(--iqser-text) !important;
&:hover {
color: rgba(var(--iqser-text-rgb), 0.8) !important;
}
}
}

View File

@ -1,384 +0,0 @@
import { Component, EventEmitter, HostListener, Input, Output, ViewEncapsulation } from '@angular/core';
import { curveLinear } from 'd3-shape';
import { scaleBand, scaleLinear, scalePoint, scaleTime } from 'd3-scale';
import {
BaseChartComponent,
calculateViewDimensions,
Color,
ColorHelper,
LegendPosition,
Orientation,
ScaleType,
Series,
StringOrNumberOrDate,
ViewDimensions,
} from '@swimlane/ngx-charts';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'combo-chart-component',
templateUrl: './combo-chart.component.html',
styleUrls: ['./combo-chart.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class ComboChartComponent extends BaseChartComponent {
@Input() curve: any = curveLinear;
@Input() legend = false;
@Input() legendTitle = 'Legend';
@Input() legendPosition: LegendPosition = LegendPosition.Right;
@Input() xAxis;
@Input() yAxis;
@Input() showXAxisLabel;
@Input() showYAxisLabel;
@Input() showRightYAxisLabel;
@Input() xAxisLabel;
@Input() yAxisLabel;
@Input() yAxisLabelRight;
@Input() tooltipDisabled = false;
@Input() gradient: boolean;
@Input() showGridLines = true;
@Input() activeEntries: any[] = [];
@Input() schemeType: ScaleType;
@Input() yAxisTickFormatting: any;
@Input() yRightAxisTickFormatting: any;
@Input() roundDomains = false;
@Input() colorSchemeLine: Color;
@Input() autoScale;
@Input() lineChart: Series[];
@Input() yLeftAxisScaleFactor: any;
@Input() yRightAxisScaleFactor: any;
@Input() rangeFillOpacity: number;
@Input() animations = true;
@Input() noBarWhenZero = true;
@Output() activate = new EventEmitter<{ value; entries: unknown[] }>();
@Output() deactivate = new EventEmitter<{ value; entries: unknown[] }>();
dims: ViewDimensions;
xScale: any;
yScale: any;
xDomain: string[] | number[];
yDomain: string[] | number[];
transform: string;
colors: ColorHelper;
colorsLine: ColorHelper;
margin = [10, 20, 10, 20];
xAxisHeight = 0;
yAxisWidth = 0;
legendOptions: any;
scaleType: ScaleType = ScaleType.Linear;
xScaleLine;
yScaleLine;
xDomainLine;
yDomainLine;
seriesDomain;
combinedSeries: Series[];
xSet;
filteredDomain;
hoveredVertical;
yOrientLeft: Orientation = Orientation.Left;
yOrientRight: Orientation = Orientation.Right;
legendSpacing = 0;
bandwidth: number;
barPadding = 8;
@Input() xAxisTickFormatting: any;
trackBy(_index, item): string {
return item.name;
}
update(): void {
super.update();
this.dims = calculateViewDimensions({
width: this.width,
height: this.height,
margins: this.margin,
showXAxis: this.xAxis,
showYAxis: this.yAxis,
xAxisHeight: this.xAxisHeight,
yAxisWidth: this.yAxisWidth,
showXLabel: this.showXAxisLabel,
showYLabel: this.showYAxisLabel,
showLegend: this.legend,
legendType: this.schemeType,
legendPosition: this.legendPosition,
});
if (!this.yAxis) {
this.legendSpacing = 0;
} else if (this.showYAxisLabel && this.yAxis) {
this.legendSpacing = 150;
} else {
this.legendSpacing = 40;
}
this.xScale = this.getXScale();
this.yScale = this.getYScale();
// line chart
this.xDomainLine = this.getXDomainLine();
if (this.filteredDomain) {
this.xDomainLine = this.filteredDomain;
}
this.yDomainLine = this.getYDomainLine();
this.seriesDomain = this.getSeriesDomain();
this.scaleLines();
this.setColors();
this.legendOptions = this.getLegendOptions();
this.transform = `translate(${this.dims.xOffset} , ${this.margin[0]})`;
}
deactivateAll() {
this.activeEntries = [...this.activeEntries];
for (const entry of this.activeEntries) {
this.deactivate.emit({ value: entry, entries: [] });
}
this.activeEntries = [];
}
@HostListener('mouseleave')
hideCircles(): void {
this.hoveredVertical = null;
this.deactivateAll();
}
updateHoveredVertical(item): void {
this.hoveredVertical = item.value;
this.deactivateAll();
}
scaleLines() {
this.xScaleLine = this.getXScaleLine(this.xDomainLine, this.dims.width);
this.yScaleLine = this.getYScaleLine(this.yDomainLine, this.dims.height);
}
getSeriesDomain(): any[] {
this.combinedSeries = this.lineChart.slice(0);
this.combinedSeries.push({
name: this.yAxisLabel,
series: this.results,
});
return this.combinedSeries.map(d => d.name);
}
isDate(value): value is Date {
return value instanceof Date;
}
getScaleType(values): ScaleType {
let date = true;
let num = true;
for (const value of values) {
if (!this.isDate(value)) {
date = false;
}
if (typeof value !== 'number') {
num = false;
}
}
if (date) {
return ScaleType.Time;
}
if (num) {
return ScaleType.Linear;
}
return ScaleType.Ordinal;
}
getXDomainLine(): any[] {
let values: StringOrNumberOrDate[] = [];
for (const results of this.lineChart) {
for (const d of results.series) {
if (!values.includes(d.name)) {
values.push(d.name);
}
}
}
this.scaleType = this.getScaleType(values);
let domain = [];
if (this.scaleType === 'time') {
const min = Math.min(...(values as number[]));
const max = Math.max(...(values as number[]));
domain = [min, max];
} else if (this.scaleType === 'linear') {
values = values.map(v => Number(v));
const min = Math.min(...(values as number[]));
const max = Math.max(...(values as number[]));
domain = [min, max];
} else {
domain = values;
}
this.xSet = values;
return domain;
}
getYDomainLine(): any[] {
const domain: number[] = [];
for (const results of this.lineChart) {
for (const d of results.series) {
if (domain.indexOf(d.value) < 0) {
domain.push(d.value);
}
if (d.min !== undefined) {
if (domain.indexOf(d.min) < 0) {
domain.push(d.min);
}
}
if (d.max !== undefined) {
if (domain.indexOf(d.max) < 0) {
domain.push(d.max);
}
}
}
}
let min = Math.min(...domain);
const max = Math.max(...domain);
if (this.yRightAxisScaleFactor) {
const minMax = this.yRightAxisScaleFactor(min, max);
return [Math.min(0, minMax.min as number), minMax.max];
}
min = Math.min(0, min);
return [min, max];
}
getXScaleLine(domain, width: number): any {
let scale;
if (this.bandwidth === undefined) {
this.bandwidth = width - this.barPadding;
}
const offset = Math.floor((width + this.barPadding - (this.bandwidth + this.barPadding) * domain.length) / 2);
if (this.scaleType === 'time') {
scale = scaleTime().range([0, width]).domain(domain);
} else if (this.scaleType === 'linear') {
scale = scaleLinear().range([0, width]).domain(domain);
if (this.roundDomains) {
scale = scale.nice();
}
} else if (this.scaleType === 'ordinal') {
scale = scalePoint()
.range([offset + this.bandwidth / 2, width - offset - this.bandwidth / 2])
.domain(domain);
}
return scale;
}
getYScaleLine(domain, height): any {
const scale = scaleLinear().range([height, 0]).domain(domain);
return this.roundDomains ? scale.nice() : scale;
}
getXScale(): any {
this.xDomain = this.getXDomain();
const spacing = this.xDomain.length / (this.dims.width / this.barPadding + 1);
return scaleBand().range([0, this.dims.width]).paddingInner(spacing).domain(this.xDomain);
}
getYScale(): any {
this.yDomain = this.getYDomain();
const scale = scaleLinear().range([this.dims.height, 0]).domain(this.yDomain);
return this.roundDomains ? scale.nice() : scale;
}
getXDomain(): any[] {
return this.results.map(d => d.name);
}
getYDomain() {
const values: number[] = this.results.map(d => d.value);
const min = Math.min(0, ...values);
const max = Math.max(...values);
if (this.yLeftAxisScaleFactor) {
const minMax = this.yLeftAxisScaleFactor(min, max);
return [Math.min(0, minMax.min as number), minMax.max];
}
return [min, max];
}
onClick(data) {
this.select.emit(data);
}
setColors(): void {
let domain: number[] | string[];
if (this.schemeType === 'ordinal') {
domain = this.xDomain;
} else {
domain = this.yDomain;
}
this.colors = new ColorHelper(this.scheme, this.schemeType, domain, this.customColors);
this.colorsLine = new ColorHelper(this.colorSchemeLine, this.schemeType, domain, this.customColors);
}
getLegendOptions() {
const opts = {
scaleType: this.schemeType,
colors: undefined,
domain: [],
title: undefined,
position: this.legendPosition,
};
if (opts.scaleType === 'ordinal') {
opts.domain = this.seriesDomain;
opts.colors = this.colorsLine;
opts.title = this.legendTitle;
} else {
opts.domain = this.seriesDomain;
opts.colors = this.colors.scale;
}
return opts;
}
getValuesMaxLength(index: number): number {
const values = this.lineChart[index].series.map(s => s.value);
return Math.max(...values).toString().length;
}
updateLineWidth(width): void {
this.bandwidth = width;
this.scaleLines();
}
updateYAxisWidth({ width }: { width: number }): void {
this.yAxisWidth = width + 20;
this.update();
}
updateXAxisHeight({ height }): void {
this.xAxisHeight = height;
this.update();
}
onActivate(item) {
const idx = this.activeEntries.findIndex(d => d.name === item.name && d.value === item.value && d.series === item.series);
if (idx > -1) {
return;
}
this.activeEntries = [item, ...this.activeEntries];
this.activate.emit({ value: item, entries: this.activeEntries });
}
onDeactivate(item) {
const idx = this.activeEntries.findIndex(d => d.name === item.name && d.value === item.value && d.series === item.series);
this.activeEntries.splice(idx, 1);
this.activeEntries = [...this.activeEntries];
this.deactivate.emit({ value: item, entries: this.activeEntries });
}
}

View File

@ -1,200 +0,0 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { animate, style, transition, trigger } from '@angular/animations';
import { Bar, BarOrientation, formatLabel, PlacementTypes, StyleTypes } from '@swimlane/ngx-charts';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'g[ngx-combo-charts-series-vertical]',
template: `
<svg:g
xmlns:svg="http://www.w3.org/2000/svg"
ngx-charts-bar
*ngFor="let bar of bars; trackBy: trackBy"
[@animationState]="'active'"
[width]="bar.width"
[height]="bar.height"
[x]="bar.x"
[y]="bar.y"
[fill]="bar.color"
[stops]="bar.gradientStops"
[data]="bar.data"
[orientation]="orientations.Vertical"
[roundEdges]="bar.roundEdges"
[gradient]="gradient"
[isActive]="isActive(bar.data)"
[animations]="animations"
[noBarWhenZero]="noBarWhenZero"
(activate)="activate.emit($event)"
(deactivate)="deactivate.emit($event)"
ngx-tooltip
[tooltipDisabled]="tooltipDisabled"
[tooltipPlacement]="tooltipPlacements.Top"
[tooltipType]="tooltipTypes.tooltip"
[tooltipTitle]="bar.tooltipText"
></svg:g>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('animationState', [
transition('* => void', [
style({
opacity: 1,
transform: '*',
}),
animate(500, style({ opacity: 0, transform: 'scale(0)' })),
]),
]),
],
})
export class ComboSeriesVerticalComponent implements OnChanges {
@Input() dims;
@Input() type = 'standard';
@Input() series;
@Input() seriesLine;
@Input() xScale;
@Input() yScale;
@Input() colors;
@Input() tooltipDisabled = false;
@Input() gradient: boolean;
@Input() activeEntries: any[];
@Input() seriesName: string;
@Input() animations = true;
@Input() noBarWhenZero = true;
@Output() activate = new EventEmitter();
@Output() deactivate = new EventEmitter();
@Output() bandwidth = new EventEmitter();
bars: Bar[];
x: any;
y: any;
readonly tooltipTypes = StyleTypes;
readonly tooltipPlacements = PlacementTypes;
readonly orientations = BarOrientation;
ngOnChanges(): void {
this.update();
}
update(): void {
let width;
if (this.series.length) {
width = this.xScale.bandwidth();
this.bandwidth.emit(width);
}
let d0 = 0;
let total;
if (this.type === 'normalized') {
total = this.series.map(d => d.value).reduce((sum: number, d: number) => sum + d, 0);
}
this.bars = this.series.map((d, index) => {
let value: number = d.value;
const label = d.name;
const formattedLabel = formatLabel(label);
const roundEdges = this.type === 'standard';
const bar: Bar = {
value,
label,
roundEdges,
data: d,
width,
formattedLabel,
height: 0,
x: 0,
y: 0,
ariaLabel: label,
tooltipText: label,
color: undefined,
gradientStops: undefined,
};
let offset0 = d0;
let offset1 = offset0 + value;
if (this.type === 'standard') {
bar.height = Math.abs(this.yScale(value) - this.yScale(0));
bar.x = this.xScale(label);
if (value < 0) {
bar.y = this.yScale(0);
} else {
bar.y = this.yScale(value);
}
} else if (this.type === 'stacked') {
d0 += value;
bar.height = this.yScale(offset0) - this.yScale(offset1);
bar.x = 0;
bar.y = this.yScale(offset1);
// bar.offset0 = offset0;
// bar.offset1 = offset1;
} else if (this.type === 'normalized') {
d0 += value;
if (total > 0) {
offset0 = (offset0 * 100) / total;
offset1 = (offset1 * 100) / total;
} else {
offset0 = 0;
offset1 = 0;
}
bar.height = this.yScale(offset0) - this.yScale(offset1);
bar.x = 0;
bar.y = this.yScale(offset1);
// bar.offset0 = offset0;
// bar.offset1 = offset1;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
value = (offset1 - offset0).toFixed(2) + '%';
}
if (this.colors.scaleType === 'ordinal') {
bar.color = this.colors.getColor(label);
} else {
if (this.type === 'standard') {
bar.color = this.colors.getColor(value);
bar.gradientStops = this.colors.getLinearGradientStops(value);
} else {
bar.color = this.colors.getColor(offset1);
bar.gradientStops = this.colors.getLinearGradientStops(offset1, offset0);
}
}
let tooltipLabel = formattedLabel;
if (this.seriesName) {
tooltipLabel = `${this.seriesName}${formattedLabel}`;
}
this.getSeriesTooltips(this.seriesLine, index);
const lineValue: string = this.seriesLine[0].series[index].value;
bar.tooltipText = `
<span class="tooltip-label">${tooltipLabel}</span>
<span class="tooltip-val">
Y1 - ${value.toLocaleString()} Y2 - ${lineValue.toLocaleString()}%
</span>
`;
return bar;
});
}
getSeriesTooltips(seriesLine, index) {
return seriesLine.map(d => d.series[index]);
}
isActive(entry): boolean {
if (!this.activeEntries) {
return false;
}
const item = this.activeEntries.find(d => entry.name === d.name && entry.series === d.series);
return item !== undefined;
}
trackBy(_index: number, bar: Bar): string {
return bar.label;
}
}

View File

@ -1,3 +0,0 @@
export * from './combo-chart.component';
export * from './combo-series-vertical.component';
export * from './y-axis.component';

View File

@ -1,103 +0,0 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild } from '@angular/core';
import { Orientation, ViewDimensions, YAxisTicksComponent } from '@swimlane/ngx-charts';
@Component({
selector: 'g[red-ngx-charts-y-axis]',
template: `
<svg:g xmlns:svg="http://www.w3.org/2000/svg" [attr.class]="yAxisClassName" [attr.transform]="transform">
<svg:g
ngx-charts-y-axis-ticks
*ngIf="yScale"
[trimTicks]="trimTicks"
[maxTickLength]="maxTickLength"
[tickFormatting]="tickFormatting"
[tickArguments]="tickArguments"
[tickValues]="ticks"
[tickStroke]="tickStroke"
[scale]="yScale"
[orient]="yOrient"
[showGridLines]="showGridLines"
[gridLineWidth]="dims.width"
[referenceLines]="referenceLines"
[showRefLines]="showRefLines"
[showRefLabels]="showRefLabels"
[height]="dims.height"
(dimensionsChanged)="emitTicksWidth($event)"
/>
<svg:g
ngx-charts-axis-label
*ngIf="showLabel"
[label]="labelText"
[offset]="labelOffset"
[orient]="yOrient"
[height]="dims.height"
[width]="dims.width"
></svg:g>
</svg:g>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class YAxisComponent implements OnChanges {
@Input() yScale;
@Input() dims: ViewDimensions;
@Input() trimTicks: boolean;
@Input() maxTickLength: number;
@Input() tickFormatting;
@Input() ticks: any[];
@Input() showGridLines = false;
@Input() showLabel: boolean;
@Input() labelText: string;
@Input() yAxisTickCount: any;
@Input() yOrient: Orientation = Orientation.Left;
@Input() referenceLines;
@Input() showRefLines: boolean;
@Input() showRefLabels: boolean;
@Input() yAxisOffset = 0;
@Input() valuesMaxLength = 0;
@Output() dimensionsChanged = new EventEmitter();
yAxisClassName = 'y axis';
tickArguments: number[];
offset: number;
transform: string;
labelOffset = 15;
fill = 'none';
stroke = '#CCC';
tickStroke = '#CCC';
strokeWidth = 1;
padding = 5;
@ViewChild(YAxisTicksComponent) ticksComponent: YAxisTicksComponent;
ngOnChanges(changes: SimpleChanges): void {
this.update();
}
update(): void {
this.offset = -(this.yAxisOffset + this.padding);
if (this.yOrient === Orientation.Right) {
this.labelOffset = 65 + (this.valuesMaxLength + this.valuesMaxLength / 4) * 5;
this.transform = `translate(${this.offset + this.dims.width} , 0)`;
} else {
this.transform = `translate(${this.offset} , 0)`;
}
if (this.yAxisTickCount !== undefined) {
this.tickArguments = [this.yAxisTickCount];
}
}
emitTicksWidth({ width }): void {
if (width !== this.labelOffset && this.yOrient === Orientation.Right) {
this.labelOffset = width + this.labelOffset + 300;
setTimeout(() => {
this.dimensionsChanged.emit({ width });
}, 0);
} else if (width !== this.labelOffset) {
this.labelOffset = width + (this.valuesMaxLength + this.valuesMaxLength / 4) * 5;
setTimeout(() => {
this.dimensionsChanged.emit({ width });
}, 0);
}
}
}

View File

@ -0,0 +1,3 @@
<div class="canvas-container">
<canvas [data]="chartData" [options]="chartOptions" [type]="'line'" baseChart></canvas>
</div>

View File

@ -0,0 +1,8 @@
:host {
margin: 0 auto;
}
.canvas-container {
position: relative;
width: 1000px;
}

View File

@ -0,0 +1,72 @@
import { Component, Input, OnChanges } from '@angular/core';
import { ChartConfiguration, ChartDataset } from 'chart.js';
@Component({
selector: 'redaction-chart',
templateUrl: './chart.component.html',
styleUrls: ['./chart.component.scss'],
})
export class ChartComponent implements OnChanges {
@Input({ required: true }) datasets: ChartDataset[];
@Input({ required: true }) labels: string[];
@Input() ticksCallback?: (value: number) => string;
@Input() secondaryAxis = false;
@Input() yAxisLabel?: string;
@Input() yAxisLabelRight?: string;
chartData: ChartConfiguration['data'];
chartOptions: ChartConfiguration<'line'>['options'] = {};
constructor() {
this.#setChartOptions();
}
ngOnChanges() {
this.chartData = {
labels: this.labels,
datasets: this.datasets,
};
this.#setChartOptions();
}
#setChartOptions(): void {
this.chartOptions = {
scales: {
y: {
stacked: true,
ticks: { callback: this.ticksCallback ? (value: number) => this.ticksCallback(value) : undefined },
title: {
display: !!this.yAxisLabel,
text: this.yAxisLabel,
padding: { bottom: 20 },
},
},
y1: {
display: this.secondaryAxis,
position: 'right',
title: {
display: !!this.yAxisLabelRight,
text: this.yAxisLabelRight,
padding: { bottom: 20 },
},
},
},
plugins: {
legend: { position: 'right' },
tooltip: {
callbacks: {
label: this.ticksCallback ? item => `${item.dataset.label}: ${this.ticksCallback(item.parsed.y)}` : undefined,
},
},
},
layout: {
padding: {
top: 50,
bottom: 50,
},
},
datasets: { bar: { barPercentage: 0.7 } },
aspectRatio: 2.5,
};
}
}

View File

@ -0,0 +1,47 @@
<div class="section-title all-caps-label" translate="license-info-screen.storage-details"></div>
<div class="row">
<div translate="license-info-screen.storage.active-documents"></div>
<div>{{ licenseService.currentLicenseReport.activeFilesUploadedBytes | size }}</div>
</div>
<div class="row">
<div translate="license-info-screen.storage.archived-documents"></div>
<div>{{ licenseService.currentLicenseReport.archivedFilesUploadedBytes | size }}</div>
</div>
<div class="row">
<div translate="license-info-screen.storage.trash-documents"></div>
<div>{{ licenseService.currentLicenseReport.trashFilesUploadedBytes | size }}</div>
</div>
<div class="row">
<div translate="license-info-screen.storage.all-documents"></div>
<div>
{{ licenseService.currentLicenseReport.totalFilesUploadedBytes | size }}
<ng-container *ngIf="uploadedBytesCapacityPercentage !== -1">
({{ uploadedBytesCapacityPercentage | number : '1.0-2' }}%)
</ng-container>
</div>
</div>
<div class="donut-chart-wrapper pl-20">
<redaction-donut-chart
*ngIf="uploadedBytesCapacityPercentage !== -1"
[config]="donutChartConfig"
[direction]="'row'"
[radius]="80"
[strokeWidth]="15"
[subtitles]="['license-info-screen.storage.storage-capacity' | translate]"
[totalType]="'sum'"
[valueFormatter]="size"
></redaction-donut-chart>
</div>
<div class="row">
<redaction-chart
*ngIf="data$ | async as data"
[datasets]="data.datasets"
[labels]="data.labels"
[ticksCallback]="formatSize"
></redaction-chart>
</div>

View File

@ -0,0 +1,38 @@
:host {
display: contents;
}
.donut-chart-wrapper {
grid-row: 11 / span 4;
grid-column: 3;
width: fit-content;
}
.row {
display: contents;
> div {
padding: 8px 20px;
&:first-of-type {
font-weight: 600;
}
}
&:hover {
> div {
background-color: var(--iqser-alt-background);
}
}
}
.section-title {
grid-column: span 3;
padding: 20px 20px 8px;
margin-bottom: 8px;
border-bottom: 1px solid var(--iqser-separator);
}
redaction-chart {
grid-column: span 3;
}

View File

@ -0,0 +1,91 @@
import { Component } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { size } from '@iqser/common-ui';
import { LicenseService } from '@services/license.service';
import { map, tap } from 'rxjs/operators';
import type { DonutChartConfig, ILicenseReport } from '@red/domain';
import { ChartDataset } from 'chart.js';
import { ChartBlue, ChartGreen, ChartGrey, ChartRed } from '../../utils/constants';
import { getLabelsFromMonthlyData, getLineConfig } from '../../utils/functions';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
selector: 'red-license-storage',
templateUrl: './license-storage.component.html',
styleUrls: ['./license-storage.component.scss'],
})
export class LicenseStorageComponent {
readonly formatSize = size;
uploadedBytesCapacityPercentage = -1;
donutChartConfig: DonutChartConfig[] = [];
readonly data$ = this.licenseService.licenseData$.pipe(
map(() => this.licenseService.currentLicenseReport),
tap(license => {
this.uploadedBytesCapacityPercentage = this.#getUploadedBytesCapacityPercentage(license);
this.donutChartConfig = this.#getDonutChartConfig(license);
}),
map(license => ({
datasets: this.#getDatasets(license),
labels: getLabelsFromMonthlyData(license.monthlyData),
})),
);
readonly size = size;
constructor(readonly licenseService: LicenseService, private readonly _translateService: TranslateService) {}
#getUploadedBytesCapacityPercentage(license: ILicenseReport): number {
return this.licenseService.uploadedBytesCapacity
? (license.totalFilesUploadedBytes / this.licenseService.uploadedBytesCapacity) * 100
: -1;
}
#getDonutChartConfig(license: ILicenseReport): DonutChartConfig[] {
return [
{
value: license.activeFilesUploadedBytes,
color: ChartGreen,
label: this._translateService.instant(_('license-info-screen.storage.active-documents')),
},
{
value: license.archivedFilesUploadedBytes,
color: ChartBlue,
label: this._translateService.instant(_('license-info-screen.storage.archived-documents')),
},
{
value: license.trashFilesUploadedBytes,
color: ChartRed,
label: this._translateService.instant(_('license-info-screen.storage.trash-documents')),
},
{
value: this.licenseService.uploadedBytesCapacity - license.totalFilesUploadedBytes,
color: ChartGrey,
label: this._translateService.instant(_('license-info-screen.storage.unused')),
},
];
}
#getDatasets(license: ILicenseReport): ChartDataset[] {
const monthlyData = license.monthlyData;
return [
{
data: monthlyData.flatMap(d => d.activeFilesUploadedBytes),
label: this._translateService.instant('license-info-screen.storage.active-documents'),
...getLineConfig(ChartGreen, 'origin'),
stack: 'storage',
},
{
data: monthlyData.flatMap(d => d.archivedFilesUploadedBytes),
label: this._translateService.instant('license-info-screen.storage.archived-documents'),
...getLineConfig(ChartBlue, '-1'),
stack: 'storage',
},
{
data: monthlyData.flatMap(d => d.trashFilesUploadedBytes),
label: this._translateService.instant('license-info-screen.storage.trash-documents'),
...getLineConfig(ChartRed, '-1'),
stack: 'storage',
},
];
}
}

View File

@ -0,0 +1,41 @@
<div class="section-title all-caps-label" translate="license-info-screen.usage-details"></div>
<div class="row">
<div translate="license-info-screen.current-analyzed"></div>
<div>
{{ licenseService.analyzedPagesInCurrentLicensingPeriod }}
({{ analysisPercentageOfLicense$ | async | number : '1.0-2' }}%)
</div>
</div>
<div class="row">
<div translate="license-info-screen.ocr-analyzed-pages"></div>
<div>{{ licenseService.currentLicenseReport.numberOfOcrPages }}</div>
</div>
<div *ngIf="!!licenseService.unlicensedPages" class="row">
<div translate="license-info-screen.unlicensed-analyzed"></div>
<div>{{ licenseService.unlicensedPages }}</div>
</div>
<div class="row">
<div [innerHTML]="'license-info-screen.total-analyzed' | translate"></div>
<div>{{ licenseService.allLicensesReport.numberOfAnalyzedPages }}</div>
</div>
<div class="row">
<div [innerHTML]="'license-info-screen.total-ocr-analyzed' | translate"></div>
<div>{{ licenseService.allLicensesReport.numberOfOcrPages }}</div>
</div>
<div class="row">
<redaction-chart
*ngIf="data$ | async as data"
[datasets]="data.datasets"
[labels]="data.labels"
[secondaryAxis]="true"
[yAxisLabelRight]="'license-info-screen.chart.total-pages' | translate"
[yAxisLabel]="'license-info-screen.chart.pages-per-month' | translate"
></redaction-chart>
</div>

View File

@ -0,0 +1,36 @@
:host {
display: contents;
}
.row {
display: contents;
> div {
padding: 8px 20px;
&:first-of-type {
font-weight: 600;
}
&:nth-child(2) {
grid-column: span 2;
}
}
&:hover {
> div {
background-color: var(--iqser-alt-background);
}
}
}
.section-title {
grid-column: span 3;
padding: 20px 20px 8px;
margin-bottom: 8px;
border-bottom: 1px solid var(--iqser-separator);
}
redaction-chart {
grid-column: span 3;
}

View File

@ -0,0 +1,63 @@
import { Component } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { LicenseService } from '@services/license.service';
import { map } from 'rxjs/operators';
import type { ILicenseReport } from '@red/domain';
import { ChartDataset } from 'chart.js';
import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants';
import { getLabelsFromMonthlyData, getLineConfig } from '../../utils/functions';
@Component({
selector: 'red-license-usage',
templateUrl: './license-usage.component.html',
styleUrls: ['./license-usage.component.scss'],
})
export class LicenseUsageComponent {
readonly analysisPercentageOfLicense$ = this.licenseService.selectedLicense$.pipe(map(() => this.getAnalysisPercentageOfLicense()));
readonly data$ = this.licenseService.selectedLicense$.pipe(
map(() => this.licenseService.currentLicenseReport),
map(license => ({
datasets: this.#getDatasets(license),
labels: getLabelsFromMonthlyData(license.monthlyData),
})),
);
constructor(readonly licenseService: LicenseService, private readonly _translateService: TranslateService) {}
getAnalysisPercentageOfLicense() {
const totalLicensedNumberOfPages = this.licenseService.totalLicensedNumberOfPages;
const numberOfAnalyzedPages = this.licenseService.analyzedPagesInCurrentLicensingPeriod;
return totalLicensedNumberOfPages > 0 ? (numberOfAnalyzedPages / totalLicensedNumberOfPages) * 100 : 100;
}
#getDatasets(license: ILicenseReport): ChartDataset[] {
const monthlyData = license.monthlyData;
return [
{
data: monthlyData.flatMap(d => d.numberOfAnalyzedPages),
label: 'Pages per Month',
type: 'bar',
backgroundColor: ChartBlue,
order: 2,
},
{
data: monthlyData.flatMap(() => 200000),
label: 'Total Pages',
...getLineConfig(ChartRed, false),
yAxisID: 'y1',
order: 1,
},
{
data: monthlyData.map(
(month, monthIndex) =>
month.numberOfAnalyzedPages +
monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + curr.numberOfAnalyzedPages, 0),
),
label: 'Cumulative Pages',
yAxisID: 'y1',
order: 1,
...getLineConfig(ChartGreen, false),
},
];
}
}

View File

@ -1,18 +0,0 @@
<combo-chart-component
*ngIf="lineChartSeries$ | async as lineChartSeries"
[animations]="true"
[colorSchemeLine]="lineChartScheme"
[legendTitle]="'license-info-screen.chart.legend' | translate"
[legend]="true"
[lineChart]="lineChartSeries"
[results]="barChart"
[scheme]="comboBarScheme"
[showGridLines]="true"
[showRightYAxisLabel]="true"
[showYAxisLabel]="true"
[view]="[1000, 300]"
[xAxis]="true"
[yAxisLabelRight]="'license-info-screen.chart.total-pages' | translate"
[yAxisLabel]="'license-info-screen.chart.pages-per-month' | translate"
[yAxis]="true"
></combo-chart-component>

View File

@ -1,142 +0,0 @@
import { Component } from '@angular/core';
import { ComboBarScheme, LICENSE_STORAGE_KEY, LineChartScheme } from '../utils/constants';
import dayjs from 'dayjs';
import { IDateRange, ILicense, ILicenseReport } from '@red/domain';
import { LicenseService } from '@services/license.service';
import { switchMap, tap } from 'rxjs/operators';
import { List, LoadingService } from '@iqser/common-ui';
import { isCurrentMonth, toDate, verboseDate } from '../utils/functions';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { Series } from '@swimlane/ngx-charts';
@Component({
selector: 'redaction-license-chart',
templateUrl: './license-chart.component.html',
})
export class LicenseChartComponent {
readonly lineChartScheme = LineChartScheme;
readonly comboBarScheme = ComboBarScheme;
readonly lineChartSeries$ = this.#licenseChartSeries$;
barChart = [];
get #licenseChartSeries$(): Observable<Series[]> {
return this._licenseService.selectedLicense$.pipe(
tap(() => this._loadingService.start()),
switchMap(license => this.#getLicenseData(license)),
tap(() => this._loadingService.stop()),
);
}
constructor(
private readonly _translateService: TranslateService,
private readonly _licenseService: LicenseService,
private readonly _loadingService: LoadingService,
) {}
async #getLicenseData(license: ILicense): Promise<Series[]> {
const startDate = dayjs(license.validFrom);
const endDate = dayjs(license.validUntil);
const startDay: number = startDate.date();
const startMonth: number = startDate.month();
const startYear: number = startDate.year();
const dateRanges = [];
for (let dt = startDate; dt <= endDate; dt = dt.add(1, 'month')) {
const end = dt.add(1, 'month');
dateRanges.push({ startMonth: dt.month(), startYear: dt.year(), endMonth: end.month(), endYear: end.year() });
}
if (dateRanges.length > 0) {
dateRanges[0].startDay = startDay;
}
const reports = await this.#getReports(dateRanges, license.id);
return this.#mapRangesToReports(startMonth, startYear, dateRanges, reports);
}
#mapRangesToReports(month: number, year: number, dateRanges: List<IDateRange>, reports: List<ILicenseReport>): Series[] {
return [
{
name: this._translateService.instant('license-info-screen.chart.total-pages'),
series: this.#totalLicensedPagesSeries(dateRanges),
},
{
name: this._translateService.instant('license-info-screen.chart.cumulative'),
series: this.#setBar(month, year, reports),
},
];
}
#setBar(month: number, year: number, reports: List<ILicenseReport>) {
let cumulativePages = 0;
const cumulativePagesSeries = [];
this.barChart = [];
const monthNames = dayjs.monthsShort();
for (const report of reports) {
cumulativePages += report.numberOfAnalyzedPages;
const name = `${monthNames[month]} ${year}`;
this.barChart.push({
name,
value: report.numberOfAnalyzedPages,
});
cumulativePagesSeries.push({
name,
value: cumulativePages,
});
month++;
if (month === 12) {
month = 0;
year++;
}
}
if (cumulativePages !== this._licenseService.currentLicenseInfo.numberOfAnalyzedPages) {
this._licenseService.wipeStoredReportsAndReloadSelectedLicenseData();
}
return cumulativePagesSeries;
}
#getReports(dateRanges: List<IDateRange>, id: string) {
const reports = dateRanges.map(range => {
const startMonth = range.startMonth + 1;
const endMonth = range.endMonth + 1;
const key = `${id}-${startMonth}.${range.startYear}-${endMonth}.${range.endYear}`;
const existingReport = this._licenseService.storedReports[key];
if (existingReport) {
return existingReport;
}
const startDate = toDate(startMonth, range.startYear, range.startDay);
const endDate = toDate(endMonth, range.endYear);
const requestedReport = this._licenseService.getReport({ startDate, endDate });
return requestedReport.then(report => this.#storeReportIfNotCurrentMonth(range, report, key));
});
return Promise.all(reports);
}
#storeReportIfNotCurrentMonth(dateRange: IDateRange, report: ILicenseReport, key: string) {
if (!isCurrentMonth(dateRange.startMonth + 1, dateRange.startYear)) {
this._licenseService.storedReports[key] = report;
localStorage.setItem(LICENSE_STORAGE_KEY, JSON.stringify(this._licenseService.storedReports));
}
return report;
}
#totalLicensedPagesSeries(dateRanges: List<IDateRange>) {
return dateRanges.map(dateRange => ({
name: verboseDate(dateRange),
value: this._licenseService.totalLicensedNumberOfPages,
}));
}
}

View File

@ -61,65 +61,8 @@
<div>{{ licenseService.totalLicensedNumberOfPages }}</div>
</div>
<!---------- STORAGE DETAILS -->
<div class="section-title all-caps-label" translate="license-info-screen.storage-details"></div>
<div class="row">
<div translate="license-info-screen.storage.active-documents"></div>
<div>{{ licenseService.currentLicenseInfo.activeFilesUploadedBytes | size }}</div>
</div>
<div class="row">
<div translate="license-info-screen.storage.archived-documents"></div>
<div>{{ licenseService.currentLicenseInfo.archivedFilesUploadedBytes | size }}</div>
</div>
<div class="row">
<div translate="license-info-screen.storage.trash-documents"></div>
<div>{{ licenseService.currentLicenseInfo.trashFilesUploadedBytes | size }}</div>
</div>
<div class="row">
<div translate="license-info-screen.storage.all-documents"></div>
<div>{{ licenseService.currentLicenseInfo.totalFilesUploadedBytes | size }}</div>
</div>
<!---------- USAGE DETAILS -->
<div class="section-title all-caps-label" translate="license-info-screen.usage-details"></div>
<div class="row">
<div translate="license-info-screen.current-analyzed"></div>
<div>
{{ licenseService.analyzedPagesInCurrentLicensingPeriod }}
({{ analysisPercentageOfLicense$ | async | number : '1.0-2' }}%)
</div>
</div>
<div class="row">
<div translate="license-info-screen.ocr-analyzed-pages"></div>
<div>{{ licenseService.currentLicenseInfo.numberOfOcrPages }}</div>
</div>
<div *ngIf="!!licenseService.unlicensedPages" class="row">
<div translate="license-info-screen.unlicensed-analyzed"></div>
<div>{{ licenseService.unlicensedPages }}</div>
</div>
<div class="row">
<div [innerHTML]="'license-info-screen.total-analyzed' | translate"></div>
<div>{{ licenseService.allLicensesInfo.numberOfAnalyzedPages }}</div>
</div>
<div class="row">
<div [innerHTML]="'license-info-screen.total-ocr-analyzed' | translate"></div>
<div>{{ licenseService.allLicensesInfo.numberOfOcrPages }}</div>
</div>
<div class="row">
<redaction-license-chart></redaction-license-chart>
</div>
<red-license-storage></red-license-storage>
<red-license-usage></red-license-usage>
</div>
</div>
</div>

View File

@ -11,7 +11,7 @@
.grid-container {
width: calc(100% - 40px);
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-columns: 1fr 2fr 2fr;
margin: 20px;
.row {
@ -23,6 +23,10 @@
&:first-of-type {
font-weight: 600;
}
&:nth-child(2) {
grid-column: span 2;
}
}
&:hover {
@ -33,15 +37,14 @@
}
.section-title {
grid-column: span 2;
grid-column: span 3;
padding: 20px 20px 8px;
margin-bottom: 8px;
border-bottom: 1px solid var(--iqser-separator);
}
}
redaction-license-chart {
margin: 50px 0;
grid-column: span 2;
redaction-chart {
grid-column: span 3;
}
}
}

View File

@ -1,11 +1,10 @@
import { Component } from '@angular/core';
import { ConfigService } from '@services/config.service';
import { TranslateService } from '@ngx-translate/core';
import { ButtonConfig, getCurrentUser, IconButtonTypes, IqserPermissionsService, LoadingService } from '@iqser/common-ui';
import { ButtonConfig, getCurrentUser, IconButtonTypes, IqserPermissionsService } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { RouterHistoryService } from '@services/router-history.service';
import { LicenseService } from '@services/license.service';
import { map } from 'rxjs/operators';
import { Roles } from '@users/roles';
import type { User } from '@red/domain';
@ -27,24 +26,13 @@ export class LicenseScreenComponent {
},
];
readonly analysisPercentageOfLicense$ = this.licenseService.selectedLicense$.pipe(map(() => this.getAnalysisPercentageOfLicense()));
constructor(
readonly configService: ConfigService,
readonly licenseService: LicenseService,
readonly permissionsService: IqserPermissionsService,
private readonly _loadingService: LoadingService,
readonly routerHistoryService: RouterHistoryService,
private readonly _translateService: TranslateService,
) {
_loadingService.start();
}
getAnalysisPercentageOfLicense() {
const totalLicensedNumberOfPages = this.licenseService.totalLicensedNumberOfPages;
const numberOfAnalyzedPages = this.licenseService.analyzedPagesInCurrentLicensingPeriod;
return totalLicensedNumberOfPages > 0 ? (numberOfAnalyzedPages / totalLicensedNumberOfPages) * 100 : 100;
}
) {}
sendMail(): void {
const licenseCustomer = this.licenseService.selectedLicense.licensedTo;
@ -54,7 +42,7 @@ export class LicenseScreenComponent {
const lineBreak = '%0D%0A';
const body = [
this._translateService.instant('license-info-screen.email.body.analyzed', {
pages: this.licenseService.currentLicenseInfo.numberOfAnalyzedPages,
pages: this.licenseService.currentLicenseReport.numberOfAnalyzedPages,
}),
this._translateService.instant('license-info-screen.email.body.licensed', {
pages: this.licenseService.totalLicensedNumberOfPages,

View File

@ -1,16 +1,18 @@
import { inject, NgModule } from '@angular/core';
import { LicenseScreenComponent } from './license-screen/license-screen.component';
import { LicenseSelectComponent } from './license-select/license-select.component';
import { LicenseChartComponent } from './license-chart/license-chart.component';
import { LicenseSelectComponent } from './components/license-select/license-select.component';
import { RouterModule, Routes } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { MatSelectModule } from '@angular/material/select';
import { IqserHelpModeModule, IqserListingModule, SizePipe } from '@iqser/common-ui';
import { NgxChartsModule } from '@swimlane/ngx-charts';
import { ComboChartComponent, ComboSeriesVerticalComponent, YAxisComponent } from './combo-chart';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { LicenseService } from '@services/license.service';
import { ChartComponent } from './components/chart/chart.component';
import { NgChartsModule } from 'ng2-charts';
import { LicenseStorageComponent } from './components/license-storage/license-storage.component';
import { LicenseUsageComponent } from './components/license-usage/license-usage.component';
import { DonutChartComponent } from '@shared/components/donut-chart/donut-chart.component';
const routes: Routes = [
{
@ -23,24 +25,18 @@ const routes: Routes = [
];
@NgModule({
declarations: [
LicenseScreenComponent,
LicenseSelectComponent,
LicenseChartComponent,
ComboChartComponent,
ComboSeriesVerticalComponent,
YAxisComponent,
],
declarations: [LicenseScreenComponent, LicenseSelectComponent, ChartComponent, LicenseStorageComponent, LicenseUsageComponent],
imports: [
RouterModule.forChild(routes),
CommonModule,
TranslateModule,
MatSelectModule,
FormsModule,
NgxChartsModule,
IqserListingModule,
IqserHelpModeModule,
SizePipe,
NgChartsModule,
DonutChartComponent,
],
})
export class LicenseModule {}

View File

@ -1,17 +1,4 @@
import { Color, ScaleType } from '@swimlane/ngx-charts';
export const ComboBarScheme: Color = {
name: 'Combo bar scheme',
selectable: true,
group: ScaleType.Ordinal,
domain: ['#0389ec'],
};
export const LineChartScheme: Color = {
name: 'Line chart scheme',
selectable: true,
group: ScaleType.Ordinal,
domain: ['#dd4d50', '#5ce594', '#0389ec'],
};
export const LICENSE_STORAGE_KEY = 'redaction-license-reports';
export const ChartRed = '#dd4d50';
export const ChartGreen = '#5ce594';
export const ChartBlue = '#0389ec';
export const ChartGrey = '#ccced3'; // grey-5

View File

@ -1,18 +1,32 @@
import dayjs from 'dayjs';
import { IDateRange } from '@red/domain';
export function toDate(month: number, year: number, day: number = 1) {
return dayjs(`${day}-${month}-${year}`, 'D-M-YYYY').toDate();
}
export function isCurrentMonth(month: number, year: number) {
const now = dayjs();
const currentMonth = now.month() + 1;
const currentYear = now.year();
return month === currentMonth && year === currentYear;
}
import dayjs, { Dayjs } from 'dayjs';
import { FillTarget } from 'chart.js';
import { hexToRgba } from '@utils/functions';
import { ILicenseData } from '@red/domain';
import { ComplexFillTarget } from 'chart.js/dist/types';
const monthNames = dayjs.monthsShort();
export const verboseDate = (range: IDateRange) => `${monthNames[range.startMonth]} ${range.startYear}`;
export const verboseDate = (date: Dayjs) => `${monthNames[date.month()]} ${date.year()}`;
export const getLineConfig: (
color: string,
target: FillTarget,
) => {
type: 'line';
borderColor: string;
backgroundColor: string;
pointBackgroundColor: string;
fill: ComplexFillTarget;
} = (color, target) => ({
type: 'line',
borderColor: hexToRgba(color, 1),
backgroundColor: hexToRgba(color, 1),
pointBackgroundColor: hexToRgba(color, 1),
fill: {
target: target ?? '-1',
above: hexToRgba(color, 0.3),
below: hexToRgba(color, 0.3),
},
});
export const getLabelsFromMonthlyData = (monthlyData: ILicenseData[]) => monthlyData.map(data => verboseDate(dayjs(data.startDate)));

View File

@ -18,7 +18,7 @@
</svg>
<div [style]="'height: ' + size + 'px; width: ' + size + 'px; padding: ' + (strokeWidth + 5) + 'px;'" class="text-container">
<div class="heading-xl">{{ displayedDataTotal }}</div>
<div class="heading-xl">{{ getFormattedValue(displayedDataTotal) }}</div>
<div *ngIf="subtitles.length === 1" class="mt-5">{{ subtitles[0] }}</div>
<mat-select

View File

@ -23,6 +23,7 @@ export class DonutChartComponent implements OnChanges, OnInit {
@Input() counterText: string;
@Input() filterKey;
@Input() helpModeKey;
@Input() valueFormatter?: (value: number) => string;
@Output() readonly subtitleChanged = new EventEmitter<number>();
@ -32,10 +33,6 @@ export class DonutChartComponent implements OnChanges, OnInit {
size = 0;
filters$: Observable<INestedFilter[]>;
constructor(@Optional() readonly filterService: FilterService) {
// TODO: move this component to a separate module, split into smaller components, improve filters
}
get circumference(): number {
return 2 * Math.PI * this.radius;
}
@ -48,6 +45,10 @@ export class DonutChartComponent implements OnChanges, OnInit {
return this.totalType === 'sum' ? this.dataTotal : this.config.length;
}
constructor(@Optional() readonly filterService: FilterService) {
// TODO: move this component to a separate module, split into smaller components, improve filters
}
ngOnInit() {
const filterModels$ = this.filterService?.getFilterModels$(this.filterKey).pipe(
map(filters => filters ?? []),
@ -71,6 +72,10 @@ export class DonutChartComponent implements OnChanges, OnInit {
);
}
getFormattedValue(value: number): string {
return this.valueFormatter ? this.valueFormatter(value) : value.toString();
}
calculateChartData() {
let angleOffset = -90;
this.chartData = this.config.map(dataVal => {
@ -101,8 +106,8 @@ export class DonutChartComponent implements OnChanges, OnInit {
return this.totalType === 'simpleLabel'
? `${label}`
: this.totalType === 'sum'
? `${value} ${label}`
: `${label} (${value} ${this.counterText})`;
? `${this.getFormattedValue(value)} ${label}`
: `${label} (${this.getFormattedValue(value)} ${this.counterText})`;
}
selectValue(key: string): void {

View File

@ -1,17 +1,11 @@
import { Injectable } from '@angular/core';
import { GenericService, QueryParam } from '@iqser/common-ui';
import { GenericService } from '@iqser/common-ui';
import { ILicense, ILicenseReport, ILicenseReportRequest, ILicenses } from '@red/domain';
import { BehaviorSubject, firstValueFrom, Observable, of } from 'rxjs';
import { catchError, filter, tap } from 'rxjs/operators';
import { LICENSE_STORAGE_KEY } from '../modules/admin/screens/license/utils/constants';
import dayjs from 'dayjs';
import { NGXLogger } from 'ngx-logger';
export function getStoredReports() {
const rawStoredReports = localStorage.getItem(LICENSE_STORAGE_KEY);
return JSON.parse(rawStoredReports ?? '{}') as Record<string, ILicenseReport>;
}
const defaultOnError: ILicenses = {
activeLicense: 'err',
licenses: [
@ -45,15 +39,15 @@ const defaultOnError: ILicenses = {
providedIn: 'root',
})
export class LicenseService extends GenericService<ILicenseReport> {
storedReports = getStoredReports();
readonly licenseData$: Observable<ILicenses>;
readonly selectedLicense$: Observable<ILicense>;
activeLicenseId: string;
totalLicensedNumberOfPages = 0;
currentLicenseInfo: ILicenseReport = {};
allLicensesInfo: ILicenseReport = {};
currentLicenseReport: ILicenseReport = {};
allLicensesReport: ILicenseReport = {};
unlicensedPages = 0;
analyzedPagesInCurrentLicensingPeriod = 0;
uploadedBytesCapacity = 0;
protected readonly _defaultModelPath = 'report';
readonly #licenseData$ = new BehaviorSubject<ILicenses | undefined>(undefined);
readonly #selectedLicense$ = new BehaviorSubject<ILicense | undefined>(undefined);
@ -81,15 +75,13 @@ export class LicenseService extends GenericService<ILicenseReport> {
this.licenseData$ = this.#licenseData$.pipe(
filter(licenses => !!licenses),
tap(data => (this.activeLicenseId = data.activeLicense)),
tap(() => {
const uploadedBytesCapacity = this.activeLicense.features.find(f => f.name === 'uploadedBytesCapacity')?.value;
this.uploadedBytesCapacity = uploadedBytesCapacity ? parseInt(uploadedBytesCapacity, 10) : 0;
}),
);
}
wipeStoredReportsAndReloadSelectedLicenseData() {
this._logger.info('[LICENSE] Wiping stored reports and reloading license data');
this.storedReports = {};
this.setSelectedLicense(this.selectedLicense);
}
async loadLicenseData(license: ILicense = this.selectedLicense) {
this.totalLicensedNumberOfPages = this.getTotalLicensedNumberOfPages(license);
@ -109,14 +101,14 @@ export class LicenseService extends GenericService<ILicenseReport> {
const configs = [currentLicenseConfig, allLicensesConfig];
const reports = configs.map(config => this.getReport(config));
[this.currentLicenseInfo, this.allLicensesInfo] = await Promise.all(reports);
[this.currentLicenseReport, this.allLicensesReport] = await Promise.all(reports);
if (this.currentLicenseInfo.numberOfAnalyzedPages > this.totalLicensedNumberOfPages) {
this.unlicensedPages = this.currentLicenseInfo.numberOfAnalyzedPages - this.totalLicensedNumberOfPages;
if (this.currentLicenseReport.numberOfAnalyzedPages > this.totalLicensedNumberOfPages) {
this.unlicensedPages = this.currentLicenseReport.numberOfAnalyzedPages - this.totalLicensedNumberOfPages;
} else {
this.unlicensedPages = 0;
}
this.analyzedPagesInCurrentLicensingPeriod = this.currentLicenseInfo.numberOfAnalyzedPages;
this.analyzedPagesInCurrentLicensingPeriod = this.currentLicenseReport.numberOfAnalyzedPages;
}
getTotalLicensedNumberOfPages(license: ILicense) {
@ -128,17 +120,8 @@ export class LicenseService extends GenericService<ILicenseReport> {
this.setSelectedLicense(this.activeLicense);
}
getReport(body: ILicenseReportRequest, limit?: number, offset?: number) {
const queryParams: QueryParam[] = [];
if (limit) {
queryParams.push({ key: 'limit', value: limit });
}
if (offset) {
queryParams.push({ key: 'offset', value: offset });
}
return firstValueFrom(this._post(body, `${this._defaultModelPath}/license`, queryParams));
getReport(body: ILicenseReportRequest) {
return firstValueFrom(this._post(body, `${this._defaultModelPath}/license`));
}
async loadLicenses() {

View File

@ -14,6 +14,11 @@ export function hexToRgb(hex: string) {
: null;
}
export function hexToRgba(hex: string, alpha: number) {
const rgb = hexToRgb(hex);
return rgb ? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})` : null;
}
export function getFirstRelevantTextPart(text: string, direction: 'FORWARD' | 'BACKWARD') {
let spaceCount = 0;
let accumulator = '';

View File

@ -1681,7 +1681,9 @@
"active-documents": "",
"all-documents": "",
"archived-documents": "",
"trash-documents": ""
"storage-capacity": "",
"trash-documents": "",
"unused": ""
},
"total-analyzed": "Seit {date} insgesamt analysierte Seiten",
"total-ocr-analyzed": "",

View File

@ -1679,9 +1679,11 @@
"storage-details": "Storage Details",
"storage": {
"active-documents": "Active Documents",
"all-documents": "All Documents",
"all-documents": "Total Storage Used",
"archived-documents": "Archived Documents",
"trash-documents": "Documents in Trash"
"storage-capacity": "Storage Capacity",
"trash-documents": "Documents in Trash",
"unused": "Unused Storage"
},
"total-analyzed": "Total Analyzed Pages",
"total-ocr-analyzed": "Total OCR Processed Pages",

View File

@ -1681,7 +1681,9 @@
"active-documents": "",
"all-documents": "",
"archived-documents": "",
"trash-documents": ""
"storage-capacity": "",
"trash-documents": "",
"unused": ""
},
"total-analyzed": "Seit {date} insgesamt analysierte Seiten",
"total-ocr-analyzed": "",

View File

@ -1679,9 +1679,11 @@
"storage-details": "Storage Details",
"storage": {
"active-documents": "Active Documents",
"all-documents": "All Documents",
"all-documents": "Total Storage Used",
"archived-documents": "Archived Documents",
"trash-documents": "Documents in Trash"
"storage-capacity": "Storage Capacity",
"trash-documents": "Documents in Trash",
"unused": "Unused Storage"
},
"total-analyzed": "Total Analyzed Pages Since {date}",
"total-ocr-analyzed": "Total OCR Processed Pages Since {date}",

@ -1 +1 @@
Subproject commit aa4516286eeeaab7e69adadb4f58193e8f2b64ed
Subproject commit 2d6ee6655c26debca43f3bf248f8521e793117f4

View File

@ -1,8 +1,4 @@
import { List } from '@iqser/common-ui';
export interface ILicenseReportRequest {
dossierIds?: List;
endDate?: Date | string;
requestId?: string;
startDate?: Date | string;
}

View File

@ -1,4 +1,4 @@
interface ILicenseData {
export interface ILicenseData {
activeFilesUploadedBytes?: number;
archivedFilesUploadedBytes?: number;
totalFilesUploadedBytes?: number;
@ -16,6 +16,5 @@ export interface ILicenseReport extends ILicenseData {
numberOfDossiers?: number;
numberOfOcrFiles?: number;
offset?: number;
requestId?: string;
monthlyData?: ILicenseData[];
}

View File

@ -37,8 +37,9 @@
"@messageformat/core": "^3.1.0",
"@ngx-translate/core": "15.0.0",
"@ngx-translate/http-loader": "8.0.0",
"@nx/angular": "16.3.2",
"@pdftron/webviewer": "10.1.1",
"@swimlane/ngx-charts": "20.4.1",
"chart.js": "^4.3.0",
"dayjs": "^1.11.5",
"file-saver": "^2.0.5",
"jwt-decode": "^3.1.2",
@ -46,6 +47,7 @@
"keycloak-js": "21.1.1",
"lodash-es": "^4.17.21",
"monaco-editor": "0.39.0",
"ng2-charts": "^4.1.1",
"ngx-color-picker": "^14.0.0",
"ngx-logger": "^5.0.11",
"ngx-toastr": "17.0.2",
@ -57,8 +59,7 @@
"scroll-into-view-if-needed": "^3.0.6",
"streamsaver": "^2.0.5",
"tslib": "2.5.3",
"zone.js": "0.13.1",
"@nx/angular": "16.3.2"
"zone.js": "0.13.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "16.1.0",

189
yarn.lock
View File

@ -2233,6 +2233,11 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
"@kurkle/color@^0.3.0":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f"
integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==
"@leichtgewicht/ip-codec@^2.0.1":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
@ -3568,26 +3573,6 @@
dependencies:
"@sinonjs/commons" "^3.0.0"
"@swimlane/ngx-charts@20.4.1":
version "20.4.1"
resolved "https://registry.yarnpkg.com/@swimlane/ngx-charts/-/ngx-charts-20.4.1.tgz#42f3d63c1326cfe347d62d1f626840d6c1511276"
integrity sha512-DyTQe0fcqLDoLEZca45gkdjxP8iLH7kh4pCkr+TCFIkmgEdfQ5DpavNBOOVO0qd5J5uV/tbtSnkYWSx8JkbFpg==
dependencies:
d3-array "^3.1.1"
d3-brush "^3.0.0"
d3-color "^3.1.0"
d3-ease "^3.0.1"
d3-format "^3.1.0"
d3-hierarchy "^3.1.0"
d3-interpolate "^3.0.1"
d3-scale "^4.0.2"
d3-selection "^3.0.0"
d3-shape "^3.2.0"
d3-time-format "^3.0.0"
d3-transition "^3.0.1"
rfdc "^1.3.0"
tslib "^2.0.0"
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@ -5051,6 +5036,13 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
chart.js@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.3.0.tgz#ac363030ab3fec572850d2d872956f32a46326a1"
integrity sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g==
dependencies:
"@kurkle/color" "^0.3.0"
chokidar@3.5.3, "chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.5.1, chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
@ -5625,143 +5617,6 @@ cuint@^0.2.2:
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
integrity sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==
d3-array@2:
version "2.12.1"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81"
integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==
dependencies:
internmap "^1.0.0"
"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.1:
version "3.2.4"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
dependencies:
internmap "1 - 2"
d3-brush@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
dependencies:
d3-dispatch "1 - 3"
d3-drag "2 - 3"
d3-interpolate "1 - 3"
d3-selection "3"
d3-transition "3"
"d3-color@1 - 3", d3-color@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
"d3-dispatch@1 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
"d3-drag@2 - 3":
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
dependencies:
d3-dispatch "1 - 3"
d3-selection "3"
"d3-ease@1 - 3", d3-ease@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
"d3-format@1 - 3", d3-format@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
d3-hierarchy@^3.1.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
dependencies:
d3-color "1 - 3"
d3-path@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
d3-scale@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
dependencies:
d3-array "2.10.0 - 3"
d3-format "1 - 3"
d3-interpolate "1.2.0 - 3"
d3-time "2.1.1 - 3"
d3-time-format "2 - 4"
d3-selection@3, d3-selection@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
d3-shape@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
dependencies:
d3-path "^3.1.0"
"d3-time-format@2 - 4":
version "4.1.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
dependencies:
d3-time "1 - 3"
d3-time-format@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6"
integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==
dependencies:
d3-time "1 - 2"
"d3-time@1 - 2":
version "2.1.1"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682"
integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==
dependencies:
d3-array "2"
"d3-time@1 - 3", "d3-time@2.1.1 - 3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
dependencies:
d3-array "2 - 3"
"d3-timer@1 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
d3-transition@3, d3-transition@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
dependencies:
d3-color "1 - 3"
d3-dispatch "1 - 3"
d3-ease "1 - 3"
d3-interpolate "1 - 3"
d3-timer "1 - 3"
data-urls@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143"
@ -7516,16 +7371,6 @@ internal-slot@^1.0.4:
has "^1.0.3"
side-channel "^1.0.4"
"internmap@1 - 2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
internmap@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95"
integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==
ip@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
@ -8571,7 +8416,7 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash-es@^4.17.21:
lodash-es@^4.17.15, lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
@ -9052,6 +8897,14 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
ng2-charts@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ng2-charts/-/ng2-charts-4.1.1.tgz#699bee539030b093caf54ab6111c6dea9e8e7ed3"
integrity sha512-iHwXDbmX86lfeH8VRcsaW2tJATsuAZo4kvvC/Yk2l35zOHjevja1qBvO6BAibiDazi9r9aS6ZRJOqWPsz1pP2w==
dependencies:
lodash-es "^4.17.15"
tslib "^2.3.0"
ngx-color-picker@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/ngx-color-picker/-/ngx-color-picker-14.0.0.tgz#4587f517ac5683a705d4e55cd0939afa91faa853"