@nova-ui/charts
Version:
Nova Charts is a library created to provide potential consumers with solutions for various data visualizations that conform with the Nova Design Language. It's designed to solve common patterns identified by UX designers, but also be very flexible so that
1,095 lines (1,082 loc) • 608 kB
JavaScript
import * as i1$1 from '@angular/cdk/overlay';
import { OverlayModule } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import * as i0 from '@angular/core';
import { Input, ChangeDetectionStrategy, Component, Directive, InjectionToken, ViewChildren, EventEmitter, ViewChild, Output, forwardRef, Injectable, Inject, ViewEncapsulation, HostListener, HostBinding, Optional, Host, NgModule } from '@angular/core';
import { Subject, BehaviorSubject, of } from 'rxjs';
import { trigger, transition, style, animate } from '@angular/animations';
import * as i1 from '@angular/common';
import isUndefined from 'lodash/isUndefined';
import { arc, curveLinear, line, pie, area } from 'd3-shape';
import defaultsDeep from 'lodash/defaultsDeep';
import { extent, forceCollide, forceSimulation, select, min as min$1, max as max$1, scaleBand, bisect, axisLeft, axisRight, axisBottom, axisTop, timeMinute, timeDay, timeMonth, timeYear } from 'd3';
import each from 'lodash/each';
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import isNil from 'lodash/isNil';
import { takeUntil, filter as filter$1, map, switchMap } from 'rxjs/operators';
import pickBy from 'lodash/pickBy';
import values from 'lodash/values';
import * as i1$2 from '@nova-ui/bits';
import { PopoverComponent, UtilService, NuiCommonModule, NuiIconModule, NuiPopoverModule } from '@nova-ui/bits';
import isFunction from 'lodash/isFunction';
import 'd3-selection-multi';
import debounce from 'lodash/debounce';
import findKey from 'lodash/findKey';
import isEmpty from 'lodash/isEmpty';
import cloneDeep from 'lodash/cloneDeep';
import sortBy from 'lodash/sortBy';
import unionWith from 'lodash/unionWith';
import { rgb } from 'd3-color';
import isArray from 'lodash/isArray';
import startsWith from 'lodash/startsWith';
import trim from 'lodash/trim';
import trimEnd from 'lodash/trimEnd';
import trimStart from 'lodash/trimStart';
import { min, max, bisect as bisect$1 } from 'd3-array';
import { local, select as select$1, event, mouse } from 'd3-selection';
import flatten from 'lodash/flatten';
import uniq from 'lodash/uniq';
import identity from 'lodash/identity';
import filter from 'lodash/filter';
import includes from 'lodash/includes';
import keyBy from 'lodash/keyBy';
import clone from 'lodash/clone';
import toString from 'lodash/toString';
import { scaleLinear, scalePoint, scaleTime, scaleBand as scaleBand$1 } from 'd3-scale';
import indexOf from 'lodash/indexOf';
import find from 'lodash/find';
import orderBy from 'lodash/orderBy';
import unionBy from 'lodash/unionBy';
import moment, { duration } from 'moment/moment';
import { brushX } from 'd3-brush';
import findIndex from 'lodash/findIndex';
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
class ChartTooltipComponent {
template;
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ChartTooltipComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: ChartTooltipComponent, selector: "nui-chart-tooltip", inputs: { template: "template" }, ngImport: i0, template: "<div>\n <div @tooltip class=\"nui-chart-tooltip px-2 py-1 nui-text-small\">\n <ng-container *ngTemplateOutlet=\"template\"></ng-container>\n </div>\n</div>\n", styles: [":host{display:block;pointer-events:none}.nui-chart-tooltip{background-color:var(--nui-color-bg-inverse,#111);color:var(--nui-color-text-inverse,#fff);line-height:normal}\n"], dependencies: [{ kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], animations: [
trigger("tooltip", [
transition(":enter", [
style({ opacity: 0 }),
animate(300, style({ opacity: 1 })),
]),
transition(":leave", [animate(300, style({ opacity: 0 }))]),
]),
], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ChartTooltipComponent, decorators: [{
type: Component,
args: [{ selector: "nui-chart-tooltip", changeDetection: ChangeDetectionStrategy.OnPush, animations: [
trigger("tooltip", [
transition(":enter", [
style({ opacity: 0 }),
animate(300, style({ opacity: 1 })),
]),
transition(":leave", [animate(300, style({ opacity: 0 }))]),
]),
], template: "<div>\n <div @tooltip class=\"nui-chart-tooltip px-2 py-1 nui-text-small\">\n <ng-container *ngTemplateOutlet=\"template\"></ng-container>\n </div>\n</div>\n", styles: [":host{display:block;pointer-events:none}.nui-chart-tooltip{background-color:var(--nui-color-bg-inverse,#111);color:var(--nui-color-text-inverse,#fff);line-height:normal}\n"] }]
}], propDecorators: { template: [{
type: Input
}] } });
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
/** @ignore */
class ChartTooltipDirective {
overlay;
overlayPositionBuilder;
scrollStrategyOptions;
elementRef;
template;
openRemoteControl;
closeRemoteControl;
positions;
overlayRef;
openSubscription;
closeSubscription;
positionStrategy;
constructor(overlay, overlayPositionBuilder, scrollStrategyOptions, elementRef) {
this.overlay = overlay;
this.overlayPositionBuilder = overlayPositionBuilder;
this.scrollStrategyOptions = scrollStrategyOptions;
this.elementRef = elementRef;
}
ngOnInit() {
this.positionStrategy = this.overlayPositionBuilder
.flexibleConnectedTo(this.elementRef)
.withPositions(this.positions)
.withFlexibleDimensions(false)
.withPush(true);
this.overlayRef = this.overlay.create({
panelClass: "nui-chart-tooltip-pane",
positionStrategy: this.positionStrategy,
scrollStrategy: this.scrollStrategyOptions.close(),
});
if (this.openRemoteControl) {
this.openSubscription = this.openRemoteControl.subscribe(() => {
this.show();
});
}
if (this.closeRemoteControl) {
this.closeSubscription = this.closeRemoteControl.subscribe(() => {
this.hide();
});
}
}
show() {
if (this.overlayRef.hasAttached()) {
this.positionStrategy.apply();
return;
}
const tooltipRef = this.overlayRef.attach(new ComponentPortal(ChartTooltipComponent));
tooltipRef.instance.template = this.template;
}
hide() {
this.overlayRef.detach();
}
ngOnDestroy() {
this.hide();
if (this.openSubscription) {
this.openSubscription.unsubscribe();
}
if (this.closeSubscription) {
this.closeSubscription.unsubscribe();
}
}
getOverlayElement() {
return this.overlayRef.overlayElement;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ChartTooltipDirective, deps: [{ token: i1$1.Overlay }, { token: i1$1.OverlayPositionBuilder }, { token: i1$1.ScrollStrategyOptions }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: ChartTooltipDirective, selector: "[nuiChartTooltip]", inputs: { template: "template", openRemoteControl: "openRemoteControl", closeRemoteControl: "closeRemoteControl", positions: "positions" }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ChartTooltipDirective, decorators: [{
type: Directive,
args: [{ selector: "[nuiChartTooltip]" }]
}], ctorParameters: () => [{ type: i1$1.Overlay }, { type: i1$1.OverlayPositionBuilder }, { type: i1$1.ScrollStrategyOptions }, { type: i0.ElementRef }], propDecorators: { template: [{
type: Input
}], openRemoteControl: [{
type: Input
}], closeRemoteControl: [{
type: Input
}], positions: [{
type: Input
}] } });
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
var RenderState;
(function (RenderState) {
RenderState["hidden"] = "hidden";
RenderState["deemphasized"] = "deemphasized";
RenderState["emphasized"] = "emphasized";
RenderState["default"] = "default";
})(RenderState || (RenderState = {}));
var RenderLayerName;
(function (RenderLayerName) {
RenderLayerName["background"] = "background";
RenderLayerName["data"] = "data";
RenderLayerName["unclippedData"] = "unclipped-data";
RenderLayerName["foreground"] = "foreground";
})(RenderLayerName || (RenderLayerName = {}));
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
const MOUSE_ACTIVE_EVENT = "mouse_active";
const INTERACTION_VALUES_ACTIVE_EVENT = "interaction_values_active";
const INTERACTION_VALUES_EVENT = "interaction_values";
const INTERACTION_COORDINATES_EVENT = "interaction_coordinates";
const HIGHLIGHT_DATA_POINT_EVENT = "highlight_data_point";
const SELECT_DATA_POINT_EVENT = "select_data_point";
const HIGHLIGHT_SERIES_EVENT = "highlight_series";
const INTERACTION_SERIES_EVENT = "interaction_series";
const INTERACTION_DATA_POINTS_EVENT = "interaction_data_points";
const INTERACTION_DATA_POINT_EVENT = "interaction_data_point";
const DESTROY_EVENT = "destroy";
const SET_DOMAIN_EVENT = "set_domain";
const REFRESH_EVENT = "refresh";
const CHART_VIEW_STATUS_EVENT = "chart_view_status";
const SERIES_STATE_CHANGE_EVENT = "series_state_change";
const AXES_STYLE_CHANGE_EVENT = "axes_style_change";
/** @ignore */
const CHART_COMPONENT = new InjectionToken("chart_component");
/** @ignore */
const STANDARD_RENDER_LAYERS = {
[RenderLayerName.background]: {
name: RenderLayerName.background,
order: 0,
clipped: true,
},
[RenderLayerName.data]: {
name: RenderLayerName.data,
order: 50,
clipped: true,
},
[RenderLayerName.unclippedData]: {
name: RenderLayerName.unclippedData,
// order is one greater than the data layer to ensure the unclipped data layer appears just after the data layer in the DOM
order: 51,
clipped: false,
},
[RenderLayerName.foreground]: {
name: RenderLayerName.foreground,
order: 1000,
clipped: false,
},
};
/** @ignore */
const DATA_POINT_NOT_FOUND = -1;
/** @ignore */
const DATA_POINT_INTERACTION_RESET = -2;
/** Use this class to prevent DOM elements from triggering mouse-interactive-area events */
const IGNORE_INTERACTION_CLASS = "ignore-interaction";
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
/** Domain for series with empty or null data */
const EMPTY_CONTINUOUS_DOMAIN = [0, 0];
/** A reasonable non-data-driven domain for charts */
const NORMALIZED_DOMAIN = [0, 1];
/** Type guard for the domain calculator with ticks */
function isDomainWithTicksCalculator(obj) {
return !!obj.domainWithTicks;
}
function isBandScale(scale) {
return typeof scale.bandwidth === "function";
}
function hasInnerScale(scale) {
return typeof scale.innerScale !== "undefined";
}
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
/**
* The abstract base class for chart renderers with some limited default functionality
*/
// For why the "dynamic" decorator is used see https://github.com/ng-packagr/ng-packagr/issues/641
// @dynamic
class Renderer {
config;
static DEFAULT_CONFIG = {
stateStyles: {
[RenderState.default]: {
opacity: 1,
},
[RenderState.hidden]: {
opacity: 0,
},
[RenderState.deemphasized]: {
opacity: 0.1,
},
[RenderState.emphasized]: {
opacity: 1,
},
},
};
constructor(config = {}) {
this.config = config;
// setting default values to the properties that were not set by user
this.config = defaultsDeep(this.config, Renderer.DEFAULT_CONFIG);
}
interaction = {};
/**
* Based on provided values, return the nearest data point that the given coordinates represent. This is used for mouse hover behavior
*
* @param {IDataSeries} series series from which to determine the index corresponding to the specified values
* @param {{ [axis: string]: any }} values the values from which a data point index can be determined
* @param {Scales} scales the scales to be used in the index calculation
*
* @returns {number} negative value means that index is not found
*/
getDataPointIndex(series, values, scales) {
return DATA_POINT_NOT_FOUND;
}
/**
* Highlight the data point corresponding to the specified data point index
*
* @param {IRenderSeries} renderSeries The series on which to render the data point highlight
* @param {number} dataPointIndex index of the highlighted point within the data series (pass -1 to remove the highlight marker)
* @param {Subject<IRendererEventPayload>} rendererSubject A subject to optionally invoke for emitting events regarding a data point
*/
highlightDataPoint(renderSeries, dataPointIndex, rendererSubject) { }
/**
* Get the style attributes for the specified state that we need to apply to a series container
*
* @param {RenderState} state the state for which to retrieve container styles
*
* @returns {ValueMap<any, any>} the container styles for the specified state
*/
getContainerStateStyles = (state) => {
if (!this.config.stateStyles) {
throw new Error("stateStyles property is not defined");
}
return this.config.stateStyles[state || RenderState.default];
};
/**
* Set the RenderState of the target data series
*
* @param {IRenderContainers} renderContainers the render containers of the series
* @param {RenderState} state The new state for the target series
*/
setSeriesState(renderContainers, state) { }
/**
* Set the RenderState of the target data point
*
* @param {D3Selection} target the target data point
* @param {RenderState} state The new state for the target data point
*/
setDataPointState(target, state) { }
/**
* Calculate domain for data filtered by given filterScales
*
* @param dataSeries
* @param filterScales
* @param scaleKey
* @param scale
* @returns array of datapoints from <code>dataSeries</code> filtered by domains of given <code>filterScale</code>s
*/
getDomainOfFilteredData(dataSeries, filterScales, scaleKey, scale) {
let filteredData = dataSeries.data;
for (const fixedScaleKey of Object.keys(filterScales)) {
const filterScale = filterScales[fixedScaleKey];
if (!filterScale.isDomainFixed || !filterScale.isContinuous()) {
continue;
}
filteredData = this.filterDataByDomain(filteredData, dataSeries, fixedScaleKey, filterScale.domain());
}
return this.getDomain(filteredData, dataSeries, scaleKey, scale);
}
/**
* Calculate the domain using the data of a series
*
* @param {any[]} data source data, can be filtered
* @param dataSeries related data series
* @param {string} scaleName name of the scale for which domain calculation is needed
* @param scale
*
* @returns {[any, any]} min and max values as an array
*/
getDomain(data, dataSeries, scaleName, scale) {
if (!data || data.length === 0) {
return EMPTY_CONTINUOUS_DOMAIN;
}
return extent(data, (datum, index, arr) => dataSeries.accessors.data?.[scaleName]?.(datum, index, Array.from(arr), dataSeries));
}
/**
* Filters given dataset by domain of provided scale
*
* @param data
* @param dataSeries
* @param scaleName
* @param domain
*/
filterDataByDomain(data, dataSeries, scaleName, domain) {
const accessor = dataSeries.accessors.data?.[scaleName];
// if (isNil(accessor)) {
// throw new Error("accessor is not defined");
// }
return data.filter((d, i) => {
// @ts-ignore
const value = accessor(d, i, data, dataSeries);
return value >= domain[0] && value <= domain[1];
});
}
/**
* Get the definitions of lasagna layers required for visualizing data
*
* @returns {ILasagnaLayer[]} lasagna layer definitions
*/
getRequiredLayers() {
return [STANDARD_RENDER_LAYERS[RenderLayerName.data]];
}
setupInteraction(path, nativeEvent, target, dataPointSubject, dataPoint) {
const eventList = get(this.interaction, path, {})[nativeEvent];
if (!eventList) {
return;
}
each(eventList, (targetEvent) => {
target.on(nativeEvent, () => {
const bbox = target.node().getBoundingClientRect();
dataPointSubject.next({
eventName: targetEvent,
data: {
...dataPoint,
position: bbox,
},
});
});
});
}
}
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
/** Mouse interaction types */
var InteractionType;
(function (InteractionType) {
/** Indicates that an element has been clicked */
InteractionType["Click"] = "click";
/** Indicates that an element is hovered */
InteractionType["Hover"] = "hover";
/** Indicates a 'mousedown' event */
InteractionType["MouseDown"] = "mousedown";
/** Indicates that the mouse has entered the bounds of an element */
InteractionType["MouseEnter"] = "mouseenter";
/** Indicates that the mouse has left the bounds of an element */
InteractionType["MouseLeave"] = "mouseleave";
/** Indicates a movement of the mouse across the chart */
InteractionType["MouseMove"] = "mousemove";
/** Indicates a 'mouseup' event */
InteractionType["MouseUp"] = "mouseup";
})(InteractionType || (InteractionType = {}));
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
/**
* Default configuration for Radial Renderer
*/
const DEFAULT_RADIAL_RENDERER_CONFIG = {
annularWidth: 20,
annularPadding: 5,
maxThickness: 30,
annularGrowth: 0.15,
cursor: "default",
enableSeriesHighlighting: true,
enableDataPointHighlighting: true,
};
/**
* Radial renderer is a generic renderer that is able to draw pie and donut charts
*/
class RadialRenderer extends Renderer {
config;
segmentWidth;
/**
* Creates an instance of RadialRenderer.
* @param {IRadialRendererConfig} [config]
* Renderer configuration object. Defaults to `DEFAULT_RADIAL_RENDERER_CONFIG` constant value.
*/
constructor(config = {}) {
super(config);
this.config = config;
this.config = defaultsDeep(this.config, DEFAULT_RADIAL_RENDERER_CONFIG);
}
/** See {@link Renderer#draw} */
draw(renderSeries, rendererSubject) {
const dataContainer = renderSeries.containers[RenderLayerName.data];
const data = renderSeries.dataSeries.data;
const accessors = renderSeries.dataSeries.accessors;
// TODO: This handles several data points withing one series as well. Most probably this can be abstracted to other renderer
const arcGenerator = (d, index) => {
const separateArc = this.getArc(renderSeries.scales.r.range(), arc(), index);
return separateArc(d);
};
this.segmentWidth = this.getSegmentWidth(renderSeries);
const g = dataContainer.selectAll("path.arc").data(data);
g.exit().remove();
g.enter()
.append("path")
.attr("class", "arc pointer-events nui-chart--path__outline")
.style("stroke-width", this.config.strokeWidth)
.style("cursor", this.config.cursor)
.on("mouseenter", (d, i) => {
this.emitDataPointHighlight(renderSeries, d, i, rendererSubject);
})
.on("mouseleave", (d, i) => {
this.emitDataPointHighlight(renderSeries, null, DATA_POINT_NOT_FOUND, rendererSubject);
})
// TODO: testing event, remove in favor or generic interaction setup
.on("click", (d, i) => {
rendererSubject.next({
eventName: SELECT_DATA_POINT_EVENT,
data: {
seriesId: renderSeries.dataSeries.id,
index: i,
data: d.data,
position: this.getDataPointPosition(renderSeries.dataSeries, i, renderSeries.scales),
},
});
})
.merge(g)
.attr("d", arcGenerator)
.attr("fill", (d, i) => accessors.data.color
? accessors.data.color(d.data, i, data, renderSeries.dataSeries)
: accessors.series.color?.(renderSeries.dataSeries.id, renderSeries.dataSeries));
}
/** See {@link Renderer#getDataPointPosition} */
getDataPointPosition(dataSeries, index, scales) {
if (index < 0) {
return undefined;
}
const pieArcData = dataSeries.data[index];
const dataPointArc = this.getArc(scales.r.range(), arc(), index);
const centroid = dataPointArc.centroid(pieArcData);
return {
x: centroid[0],
y: centroid[1],
width: 0,
height: 0,
};
}
getInnerRadius(range, index) {
if (isUndefined(this.segmentWidth) ||
isUndefined(this.config.annularPadding)) {
throw new Error("Can't compute inner radius");
}
const calculatedRadius = range[1] -
range[0] -
this.segmentWidth -
index * (this.config.annularPadding + this.segmentWidth);
return calculatedRadius >= 0 ? calculatedRadius : 0;
}
getOuterRadius(range, index) {
if (isUndefined(this.segmentWidth) ||
isUndefined(this.config.annularPadding)) {
throw new Error("Can't compute outer radius");
}
const calculatedRadius = range[1] -
range[0] -
index * (this.config.annularPadding + this.segmentWidth);
return calculatedRadius >= 0 ? calculatedRadius : 0;
}
getArc(range, generatedArc, index) {
const innerRadius = this.getInnerRadius(range, index);
return generatedArc
.outerRadius(this.getOuterRadius(range, index))
.innerRadius(innerRadius);
}
getSegmentWidth(renderSeries) {
if (!(this.config.maxThickness && this.config.annularGrowth)) {
return this.config.annularWidth;
}
return Math.min((renderSeries.scales.r.range()[1] -
renderSeries.scales.r.range()[0]) *
this.config.annularGrowth, this.config.maxThickness);
}
emitDataPointHighlight(renderSeries, data, i, rendererSubject) {
const position = this.getDataPointPosition(renderSeries.dataSeries, i, renderSeries.scales);
const dataPoint = {
seriesId: renderSeries.dataSeries.id,
dataSeries: renderSeries.dataSeries,
index: i,
data: data,
position: position,
};
if (this.config.enableSeriesHighlighting) {
rendererSubject.next({
eventName: HIGHLIGHT_SERIES_EVENT,
data: dataPoint,
});
}
if (this.config.enableDataPointHighlighting) {
rendererSubject.next({
eventName: HIGHLIGHT_DATA_POINT_EVENT,
data: dataPoint,
});
}
// we're emitting this event manually, because it's not triggered by mouse interactive area in this case
rendererSubject.next({
eventName: INTERACTION_DATA_POINTS_EVENT,
data: {
interactionType: InteractionType.MouseMove,
dataPoints: {
[renderSeries.dataSeries.id]: dataPoint,
},
},
});
}
}
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
class ChartPlugin {
constructor() { }
chart;
initialize() { }
update() { }
updateDimensions() { }
destroy() { }
}
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
/**
* This plugin calculates new size and position for content inside donut chart
*/
class ChartDonutContentPlugin extends ChartPlugin {
/** Subject for getting updates on the content position */
contentPositionUpdateSubject = new Subject();
/** The current content position */
contentPosition = {
top: 0,
left: 0,
width: 0,
height: 0,
};
updateDimensions() {
const radius = this.chart
.getDataManager()
.chartSeriesSet.reduce((prev, current) => {
if (current.renderer instanceof RadialRenderer &&
!isUndefined(prev)) {
return Math.min(prev, current.renderer.getInnerRadius(current.scales.r.range(), current.data.length - 1));
}
return prev;
}, Infinity);
if (isUndefined(radius)) {
throw new Error("Radius is undefined");
}
this.contentPosition = this.getContentPosition(radius);
this.contentPositionUpdateSubject.next(this.contentPosition);
}
destroy() {
this.contentPositionUpdateSubject.complete();
}
getContentPosition(areaSize) {
const basics = [
this.chart.getGrid().config().dimension.outerHeight() / 2,
this.chart.getGrid().config().dimension.outerWidth() / 2,
];
return {
top: basics[0] - areaSize / Math.sqrt(2),
left: basics[1] - areaSize / Math.sqrt(2),
width: areaSize * Math.sqrt(2),
height: areaSize * Math.sqrt(2),
};
}
}
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
class ChartDonutContentComponent {
/** The plugin instance */
plugin;
/** The current content position */
contentPosition;
contentPositionUpdateSubscription;
ngOnChanges(changes) {
if (changes.plugin) {
this.contentPositionUpdateSubscription?.unsubscribe();
this.contentPositionUpdateSubscription =
this.plugin.contentPositionUpdateSubject.subscribe((contentPosition) => {
this.contentPosition = contentPosition;
});
this.plugin.chart.updateDimensions();
}
}
ngOnDestroy() {
if (this.contentPositionUpdateSubscription) {
this.contentPositionUpdateSubscription.unsubscribe();
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ChartDonutContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: ChartDonutContentComponent, selector: "nui-chart-donut-content", inputs: { plugin: "plugin" }, usesOnChanges: true, ngImport: i0, template: "<div\n *ngIf=\"plugin && contentPosition\"\n class=\"nui-chart-donut-content d-flex justify-content-center align-items-center text-center\"\n [style.top.px]=\"contentPosition.top\"\n [style.left.px]=\"contentPosition.left\"\n [style.height.px]=\"contentPosition.height\"\n [style.width.px]=\"contentPosition.width\"\n>\n <ng-content></ng-content>\n</div>\n", styles: [".nui-chart-donut-content{position:absolute;pointer-events:auto;width:100%;height:100%;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ChartDonutContentComponent, decorators: [{
type: Component,
args: [{ selector: "nui-chart-donut-content", template: "<div\n *ngIf=\"plugin && contentPosition\"\n class=\"nui-chart-donut-content d-flex justify-content-center align-items-center text-center\"\n [style.top.px]=\"contentPosition.top\"\n [style.left.px]=\"contentPosition.left\"\n [style.height.px]=\"contentPosition.height\"\n [style.width.px]=\"contentPosition.width\"\n>\n <ng-content></ng-content>\n</div>\n", styles: [".nui-chart-donut-content{position:absolute;pointer-events:auto;width:100%;height:100%;overflow:hidden;text-overflow:ellipsis}\n"] }]
}], propDecorators: { plugin: [{
type: Input
}] } });
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
/** How far away from the data point position will the tooltip be positioned */
const TOOLTIP_POSITION_OFFSET = 10;
/** @ignore
* Used for charts where tooltips should be placed aside of some vertical line */
const getVerticalSetup = (offset) => [
{
originX: "end",
originY: "top",
overlayX: "start",
overlayY: "center",
offsetX: offset,
},
{
originX: "start",
originY: "center",
overlayX: "end",
overlayY: "center",
offsetX: -offset,
},
];
/** @ignore
* Used for charts where tooltips should be placed aligned to some horizontal line (as Horizontal Bar Charts) */
const getHorizontalSetup = (offset) => [
{
originX: "end",
originY: "top",
overlayX: "center",
overlayY: "bottom",
offsetY: -offset,
},
{
originX: "end",
originY: "bottom",
overlayX: "center",
overlayY: "top",
offsetY: offset,
},
];
/**
* This plugin listens to the INTERACTION_DATA_POINTS_EVENT and transforms received data into tooltips inputs.
* The actual tooltips are handled by the ChartTooltipsComponent.
*/
class ChartTooltipsPlugin extends ChartPlugin {
tooltipPositionOffset;
orientation;
/** Highlighted data points received from the chart */
dataPoints;
/** Calculated positions for the data point tooltips */
dataPointPositions = {};
/**
* This publishes an event to show tooltips
*/
showSubject = new Subject();
/**
* This publishes an event to hide tooltips
*/
hideSubject = new Subject();
overlaySetup;
isChartInView = false;
destroy$ = new Subject();
seriesVisibilityMap = {};
/**
* @param tooltipPositionOffset Offset of a tooltip from edge of a highlighted element
* @param orientation
*/
constructor(tooltipPositionOffset = TOOLTIP_POSITION_OFFSET, orientation = "right") {
super();
this.tooltipPositionOffset = tooltipPositionOffset;
this.orientation = orientation;
if (orientation === "right") {
this.overlaySetup = getVerticalSetup(tooltipPositionOffset);
}
else if (orientation === "top") {
this.overlaySetup = getHorizontalSetup(tooltipPositionOffset);
}
}
initialize() {
this.chart
.getEventBus()
.getStream(INTERACTION_DATA_POINTS_EVENT)
.pipe(takeUntil(this.destroy$))
.subscribe((event) => {
if (event.data.interactionType === InteractionType.MouseMove &&
this.isChartInView) {
const dataPoints = event.data.dataPoints;
this.processHighlightedDataPoints(dataPoints);
}
});
this.chart
.getEventBus()
.getStream(SERIES_STATE_CHANGE_EVENT)
.pipe(takeUntil(this.destroy$))
.subscribe((event) => {
event.data.forEach((series) => {
this.seriesVisibilityMap[series.seriesId] =
series.state !== RenderState.hidden;
});
});
this.chart
.getEventBus()
.getStream(CHART_VIEW_STATUS_EVENT)
.pipe(takeUntil(this.destroy$))
.subscribe((event) => {
this.isChartInView = event.data.isChartInView;
if (!this.isChartInView) {
this.hideSubject.next();
}
});
}
destroy() {
this.destroy$.next();
this.destroy$.complete();
}
processHighlightedDataPoints(dataPoints) {
const validDataPoints = pickBy(dataPoints, (d) => d.index >= 0 &&
d.position &&
this.seriesVisibilityMap[d.seriesId] !== false);
if (values(validDataPoints).length === 0) {
this.hideSubject.next();
return;
}
this.dataPoints = validDataPoints;
const chartElement = this.chart.target?.node()?.parentNode; // the one above svg
if (!chartElement) {
throw new Error("Chart parent node is not defined");
}
const bbox = chartElement.getBoundingClientRect();
const offsetParentBbox = chartElement.offsetParent.getBoundingClientRect();
const chartPosition = {
x: bbox.left - offsetParentBbox.left,
y: bbox.top - offsetParentBbox.top,
};
each(Object.keys(this.dataPoints), (seriesId) => {
const dataPoint = this.dataPoints[seriesId];
const chartSeries = this.chart
.getDataManager()
.getChartSeries(dataPoint.seriesId);
const tooltipRelativePosition = this.getTooltipPosition(dataPoint, chartSeries);
this.dataPointPositions[seriesId] = this.getAbsolutePosition(tooltipRelativePosition, chartPosition);
});
this.showSubject.next();
}
/**
* Calculate tooltip position. Default implementation shows the tooltip on left / right with
* @param dataPoint
* @param chartSeries
*/
getTooltipPosition(dataPoint, chartSeries) {
if (!dataPoint.position) {
throw new Error("Unable to get tooltip position");
}
return {
x: dataPoint.position.x,
y: dataPoint.position.y,
height: dataPoint.position?.height || 1,
width: dataPoint.position?.width || 1,
overlayPositions: this.overlaySetup,
};
}
/**
* Converts the relative position within a chart into an absolute position on the screen
*
* @param relativePosition
* @param chartPosition
*/
getAbsolutePosition(relativePosition, chartPosition) {
return Object.assign({}, relativePosition, {
x: chartPosition.x +
this.chart.getGrid().config().dimension.margin.left +
relativePosition.x,
y: chartPosition.y +
this.chart.getGrid().config().dimension.margin.top +
relativePosition.y,
});
}
}
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
class ChartTooltipsComponent {
changeDetector;
plugin;
template;
tooltips;
openTooltips = new Subject();
closeTooltips = new Subject();
unsubscribe$ = new Subject();
simulation;
// index we use for fast access of tooltip directives by seriesId
toolt