@unovis/ts
Advanced tools
Comparing version
@@ -30,2 +30,4 @@ export { ComponentCore } from "./core/component"; | ||
export { Annotations } from "./components/annotations"; | ||
export { Treemap } from './components/treemap'; | ||
export * from './components/donut/constants'; | ||
export type { LineConfigInterface } from './components/line/config'; | ||
@@ -55,1 +57,2 @@ export type { StackedBarConfigInterface } from './components/stacked-bar/config'; | ||
export type { AnnotationsConfigInterface } from './components/annotations/config'; | ||
export type { TreemapConfigInterface } from './components/treemap/config'; |
@@ -28,4 +28,6 @@ export { ComponentCore } from './core/component/index.js'; | ||
export { Annotations } from './components/annotations/index.js'; | ||
export { Treemap } from './components/treemap/index.js'; | ||
export { DONUT_HALF_ANGLE_RANGES, DONUT_HALF_ANGLE_RANGE_BOTTOM, DONUT_HALF_ANGLE_RANGE_LEFT, DONUT_HALF_ANGLE_RANGE_RIGHT, DONUT_HALF_ANGLE_RANGE_TOP } from './components/donut/constants.js'; | ||
// Core | ||
//# sourceMappingURL=components.js.map |
@@ -45,3 +45,3 @@ import { select } from 'd3-selection'; | ||
const stacked = getStackedData(data, config.baseline, yAccessors, this._prevNegative); | ||
this._prevNegative = stacked.map(s => !!s.negative); | ||
this._prevNegative = stacked.map(s => !!s.isMostlyNegative); | ||
const stackedData = stacked.map(arr => arr.map((d, j) => ({ | ||
@@ -48,0 +48,0 @@ y0: this.yScale(d[0]), |
@@ -33,3 +33,3 @@ import { XYComponentConfigInterface } from "../../core/xy-component/config"; | ||
/** Tick label formatter function. Default: `undefined` */ | ||
tickFormat?: ((tick: number | Date, i: number, ticks: number[] | Date[]) => string); | ||
tickFormat?: ((tick: number, i: number, ticks: number[]) => string) | ((tick: Date, i: number, ticks: Date[]) => string); | ||
/** Explicitly set tick values. Default: `undefined` */ | ||
@@ -36,0 +36,0 @@ tickValues?: number[]; |
@@ -43,4 +43,3 @@ import { Selection } from 'd3-selection'; | ||
private _alignTickLabels; | ||
private _getTickTextAnchor; | ||
private _getYTickTextTranslate; | ||
} |
@@ -8,3 +8,3 @@ import { select } from 'd3-selection'; | ||
import { smartTransition } from '../../utils/d3.js'; | ||
import { trimSVGText, renderTextToSvgTextElement } from '../../utils/text.js'; | ||
import { trimSVGText, renderTextToSvgTextElement, textAlignToAnchor } from '../../utils/text.js'; | ||
import { isEqual } from '../../utils/data.js'; | ||
@@ -342,3 +342,3 @@ import { rectIntersect } from '../../utils/misc.js'; | ||
const tickText = this.g.selectAll('g.tick > text'); | ||
const textAnchor = this._getTickTextAnchor(tickTextAlign); | ||
const textAnchor = textAlignToAnchor(tickTextAlign); | ||
const translateX = type === AxisType.X | ||
@@ -352,10 +352,2 @@ ? 0 | ||
} | ||
_getTickTextAnchor(textAlign) { | ||
switch (textAlign) { | ||
case TextAlign.Left: return 'start'; | ||
case TextAlign.Right: return 'end'; | ||
case TextAlign.Center: return 'middle'; | ||
default: return null; | ||
} | ||
} | ||
_getYTickTextTranslate(textAlign, axisPosition = Position.Left) { | ||
@@ -362,0 +354,0 @@ const defaultTickTextSpacingPx = 9; // Default in D3 |
@@ -28,3 +28,3 @@ import { D3BrushEvent } from 'd3-brush'; | ||
/** Position of the handle: `Arrangement.Inside` or `Arrangement.Outside`. Default: `Arrangement.Inside` */ | ||
handlePosition?: Arrangement | string; | ||
handlePosition?: Arrangement.Inside | Arrangement.Outside | string; | ||
/** Constraint Brush selection to a minimal length in data units. Default: `undefined` */ | ||
@@ -31,0 +31,0 @@ selectionMinLength?: number; |
@@ -41,3 +41,7 @@ import { ComponentConfigInterface } from "../../core/component/config"; | ||
backgroundAngleRange?: [number, number]; | ||
/** Central label and sub-label horizontal offset in pixels. Default: `undefined` */ | ||
centralLabelOffsetX?: number; | ||
/** Central label and sub-label vertical offset in pixels. Default: `undefined` */ | ||
centralLabelOffsetY?: number; | ||
} | ||
export declare const DonutDefaultConfig: DonutConfigInterface<unknown>; |
import { ComponentDefaultConfig } from '../../core/component/config.js'; | ||
// Core | ||
const DonutDefaultConfig = Object.assign(Object.assign({}, ComponentDefaultConfig), { id: (d, i) => { var _a; return (_a = d.id) !== null && _a !== void 0 ? _a : i; }, value: undefined, angleRange: [0, 2 * Math.PI], padAngle: 0, sortFunction: undefined, cornerRadius: 0, color: undefined, radius: undefined, arcWidth: 20, centralLabel: undefined, centralSubLabel: undefined, centralSubLabelWrap: true, showEmptySegments: false, emptySegmentAngle: 0.5 * Math.PI / 180, showBackground: true, backgroundAngleRange: undefined }); | ||
const DonutDefaultConfig = Object.assign(Object.assign({}, ComponentDefaultConfig), { id: (d, i) => { var _a; return (_a = d.id) !== null && _a !== void 0 ? _a : i; }, value: undefined, angleRange: [0, 2 * Math.PI], padAngle: 0, sortFunction: undefined, cornerRadius: 0, color: undefined, radius: undefined, arcWidth: 20, centralLabel: undefined, centralSubLabel: undefined, centralSubLabelWrap: true, showEmptySegments: false, emptySegmentAngle: 0.5 * Math.PI / 180, showBackground: true, backgroundAngleRange: undefined, centralLabelOffsetX: undefined, centralLabelOffsetY: undefined }); | ||
export { DonutDefaultConfig }; | ||
//# sourceMappingURL=config.js.map |
@@ -9,2 +9,3 @@ import { arc, pie } from 'd3-shape'; | ||
import { createArc, updateArc, removeArc } from './modules/arc.js'; | ||
import { DONUT_HALF_ANGLE_RANGES } from './constants.js'; | ||
import * as style from './style.js'; | ||
@@ -44,4 +45,18 @@ import { centralLabel, centralSubLabel, segment, segmentExit, background } from './style.js'; | ||
const duration = isNumber(customDuration) ? customDuration : config.duration; | ||
const outerRadius = config.radius || Math.min(this._width - bleed.left - bleed.right, this._height - bleed.top - bleed.bottom) / 2; | ||
// Handle half-donut cases, which adjust the scaling and positioning. | ||
// One of these is true if we are dealing with a half-donut. | ||
const [isHalfDonutTop, isHalfDonutRight, isHalfDonutBottom, isHalfDonutLeft,] = DONUT_HALF_ANGLE_RANGES.map(angleRange => config.angleRange && (config.angleRange[0] === angleRange[0] && | ||
config.angleRange[1] === angleRange[1])); | ||
const isVerticalHalfDonut = isHalfDonutTop || isHalfDonutBottom; | ||
const isHorizontalHalfDonut = isHalfDonutRight || isHalfDonutLeft; | ||
// Compute the bounding box of the donut, | ||
// considering it may be a half-donut | ||
const width = this._width * (isHorizontalHalfDonut ? 2 : 1); | ||
const height = this._height * (isVerticalHalfDonut ? 2 : 1); | ||
const outerRadius = config.radius || Math.min(width - bleed.left - bleed.right, height - bleed.top - bleed.bottom) / 2; | ||
const innerRadius = config.arcWidth === 0 ? 0 : clamp(outerRadius - config.arcWidth, 0, outerRadius - 1); | ||
const translateY = this._height / 2 + (isHalfDonutTop ? outerRadius / 2 : isHalfDonutBottom ? -outerRadius / 2 : 0); | ||
const translateX = this._width / 2 + (isHalfDonutLeft ? outerRadius / 2 : isHalfDonutRight ? -outerRadius / 2 : 0); | ||
const translate = `translate(${translateX},${translateY})`; | ||
this.arcGroup.attr('transform', translate); | ||
this.arcGen | ||
@@ -60,3 +75,2 @@ .startAngle(d => d.startAngle) | ||
.sort((a, b) => { var _a; return (_a = config.sortFunction) === null || _a === void 0 ? void 0 : _a.call(config, a.datum, b.datum); }); | ||
this.arcGroup.attr('transform', `translate(${this._width / 2},${this._height / 2})`); | ||
const arcData = pieGen(data).map(d => { | ||
@@ -85,16 +99,32 @@ const arc = Object.assign(Object.assign({}, d), { data: d.data.datum, index: d.data.index, innerRadius, | ||
// Label | ||
const labelTextAnchor = isHalfDonutRight ? 'start' : isHalfDonutLeft ? 'end' : 'middle'; | ||
this.centralLabel | ||
.attr('transform', `translate(${this._width / 2},${this._height / 2})`) | ||
.attr('dy', config.centralSubLabel ? '-0.55em' : null) | ||
.style('text-anchor', labelTextAnchor) | ||
.text((_e = config.centralLabel) !== null && _e !== void 0 ? _e : null); | ||
this.centralSubLabel | ||
.attr('transform', `translate(${this._width / 2},${this._height / 2})`) | ||
.attr('dy', config.centralLabel ? '0.55em' : null) | ||
.style('text-anchor', labelTextAnchor) | ||
.text((_f = config.centralSubLabel) !== null && _f !== void 0 ? _f : null); | ||
if (config.centralSubLabelWrap) | ||
wrapSVGText(this.centralSubLabel, innerRadius * 1.9); | ||
// Label placement | ||
const labelTranslateX = (config.centralLabelOffsetX || 0) + translateX; | ||
let labelTranslateY = (config.centralLabelOffsetY || 0) + translateY; | ||
// Special case label placement for half donut | ||
if (isVerticalHalfDonut && config.centralLabelOffsetX === undefined && config.centralLabelOffsetY === undefined) { | ||
const halfDonutLabelOffsetY = isHalfDonutTop | ||
? -this.centralSubLabel.node().getBoundingClientRect().height | ||
: isHalfDonutBottom | ||
? this.centralLabel.node().getBoundingClientRect().height | ||
: 0; | ||
labelTranslateY = halfDonutLabelOffsetY + translateY; | ||
} | ||
const labelTranslate = `translate(${labelTranslateX},${labelTranslateY})`; | ||
this.centralLabel.attr('transform', labelTranslate); | ||
this.centralSubLabel.attr('transform', labelTranslate); | ||
// Background | ||
this.arcBackground.attr('class', background) | ||
.attr('visibility', config.showBackground ? null : 'hidden') | ||
.attr('transform', `translate(${this._width / 2},${this._height / 2})`); | ||
.attr('transform', translate); | ||
smartTransition(this.arcBackground, duration) | ||
@@ -101,0 +131,0 @@ .attr('d', this.arcGen({ |
@@ -5,5 +5,7 @@ import { D3BrushEvent } from 'd3-brush'; | ||
import { Selection } from 'd3-selection'; | ||
import { ElkShape } from 'elkjs'; | ||
import type { GraphDataModel } from "../../data-models/graph"; | ||
import { ComponentConfigInterface } from "../../core/component/config"; | ||
import { TrimMode } from "../../types/text"; | ||
import { GraphInputLink, GraphInputNode } from "../../types/graph"; | ||
import { GraphInputLink, GraphInputNode, GraphInputData } from "../../types/graph"; | ||
import { BooleanAccessor, ColorAccessor, NumericAccessor, StringAccessor, GenericAccessor } from "../../types/accessor"; | ||
@@ -83,2 +85,8 @@ import { GraphLayoutType, GraphCircleLabel, GraphLinkStyle, GraphLinkArrowStyle, GraphPanelConfig, GraphForceLayoutSettings, GraphElkLayoutSettings, GraphNodeShape, GraphDagreLayoutSetting, GraphNode, GraphLink, GraphNodeSelectionHighlightMode } from './types'; | ||
layoutElkNodeGroups?: StringAccessor<N>[]; | ||
/** A function to be called per graph node to get the ELK shape object. | ||
* This enables you to provide custom node dimensions (through the `width` and `height` properties) | ||
* and coordinates (through the `x` and `y` properties) if needed. | ||
* Default: `undefined` | ||
*/ | ||
layoutElkGetNodeShape?: (d: GraphNode<N, L>, i: number) => ElkShape; | ||
/** Link width accessor function ot constant value. Default: `1` */ | ||
@@ -99,5 +107,5 @@ linkWidth?: NumericAccessor<L>; | ||
/** Animation duration of the flow (traffic) circles. Default: `20000` */ | ||
linkFlowAnimDuration?: number; | ||
linkFlowAnimDuration?: NumericAccessor<L>; | ||
/** Size of the moving particles that represent traffic flow. Default: `2` */ | ||
linkFlowParticleSize?: number; | ||
linkFlowParticleSize?: NumericAccessor<L>; | ||
/** Link label accessor function or constant value. Default: `undefined` */ | ||
@@ -115,2 +123,8 @@ linkLabel?: GenericAccessor<GraphCircleLabel | GraphCircleLabel[], L> | undefined; | ||
linkCurvature?: NumericAccessor<L>; | ||
/** Highlight links on hover. Default: `true` */ | ||
linkHighlightOnHover?: boolean; | ||
/** Offset [x,y] in pixels from the source node's center point where the link should start. Default: `undefined` */ | ||
linkSourcePointOffset?: GenericAccessor<[number, number], GraphLink<N, L>>; | ||
/** Offset [x,y] in pixels from the target node's center point where the link should end. Default: `undefined` */ | ||
linkTargetPointOffset?: GenericAccessor<[number, number], GraphLink<N, L>>; | ||
/** Set selected link by its unique id. Default: `undefined` */ | ||
@@ -213,3 +227,10 @@ selectedLinkId?: number | string; | ||
onRenderComplete?: (g: Selection<SVGGElement, unknown, null, undefined>, nodes: GraphNode<N, L>[], links: GraphLink<N, L>[], config: GraphConfigInterface<N, L>, duration: number, zoomLevel: number, width: number, height: number) => void; | ||
/** Determines whether the component should update when new data is provided. | ||
* This function takes the previous and new data as parameters and returns a boolean | ||
* indicating whether the update should proceed. Useful for fine-grained control over | ||
* update behavior when your data has a complex nested structure. | ||
* By default the `isEqual` function from Unovis will be used to do the comparison. | ||
*/ | ||
shouldDataUpdate?: (prevData: GraphInputData<N, L>, nextData: GraphInputData<N, L>, datamodel: GraphDataModel<N, L, GraphNode<N, L>, GraphLink<N, L>>) => boolean; | ||
} | ||
export declare const GraphDefaultConfig: GraphConfigInterface<GraphInputNode, GraphInputLink>; |
@@ -0,1 +1,2 @@ | ||
import { isEqual } from '../../utils/data.js'; | ||
import { ComponentDefaultConfig } from '../../core/component/config.js'; | ||
@@ -5,3 +6,3 @@ import { TrimMode } from '../../types/text.js'; | ||
// Config | ||
// Utils | ||
const GraphDefaultConfig = Object.assign(Object.assign({}, ComponentDefaultConfig), { duration: 1000, zoomScaleExtent: [0.35, 1.25], disableZoom: false, zoomEventFilter: undefined, disableDrag: false, disableBrush: false, zoomThrottledUpdateNodeThreshold: 100, layoutType: GraphLayoutType.Force, layoutAutofit: true, layoutAutofitTolerance: 8.0, layoutNonConnectedAside: false, layoutGroupOrder: [], layoutParallelSubGroupsPerRow: 1, layoutParallelNodesPerColumn: 6, layoutParallelGroupSpacing: undefined, layoutParallelSortConnectionsByGroup: undefined, layoutNodeGroup: (n) => n.group, layoutParallelNodeSubGroup: (n) => n.subgroup, forceLayoutSettings: { | ||
@@ -18,5 +19,7 @@ linkDistance: 60, | ||
ranker: 'longest-path', | ||
}, layoutElkSettings: undefined, layoutElkNodeGroups: undefined, linkFlowAnimDuration: 20000, linkFlowParticleSize: 2, linkWidth: 1, linkStyle: GraphLinkStyle.Solid, linkBandWidth: 0, linkArrow: undefined, linkStroke: undefined, linkFlow: false, linkLabel: undefined, linkLabelShiftFromCenter: true, linkNeighborSpacing: 8, linkDisabled: false, linkCurvature: 0, selectedLinkId: undefined, nodeGaugeAnimDuration: 1500, nodeSize: 30, nodeStrokeWidth: 3, nodeShape: GraphNodeShape.Circle, nodeGaugeValue: 0, nodeIcon: (n) => n.icon, nodeIconSize: undefined, nodeLabel: (n) => n.label, nodeLabelTrim: true, nodeLabelTrimLength: 15, nodeLabelTrimMode: TrimMode.Middle, nodeSubLabel: '', nodeSubLabelTrim: true, nodeSubLabelTrimLength: 15, nodeSubLabelTrimMode: TrimMode.Middle, nodeSideLabels: undefined, nodeBottomIcon: undefined, nodeDisabled: false, nodeFill: (n) => n.fill, nodeGaugeFill: undefined, nodeStroke: (n) => n.stroke, nodeEnterPosition: undefined, nodeEnterScale: 0.75, nodeExitPosition: undefined, nodeExitScale: 0.75, nodeSort: undefined, nodeSelectionHighlightMode: GraphNodeSelectionHighlightMode.GreyoutNonConnected, selectedNodeId: undefined, selectedNodeIds: undefined, panels: undefined, onNodeDragStart: undefined, onNodeDrag: undefined, onNodeDragEnd: undefined, onZoom: undefined, onZoomStart: undefined, onZoomEnd: undefined, onLayoutCalculated: undefined, onNodeSelectionBrush: undefined, onNodeSelectionDrag: undefined, onRenderComplete: undefined }); | ||
}, layoutElkSettings: undefined, layoutElkNodeGroups: undefined, layoutElkGetNodeShape: undefined, linkFlowAnimDuration: 20000, linkFlowParticleSize: 2, linkWidth: 1, linkStyle: GraphLinkStyle.Solid, linkBandWidth: 0, linkArrow: undefined, linkStroke: undefined, linkFlow: false, linkLabel: undefined, linkLabelShiftFromCenter: true, linkNeighborSpacing: 8, linkDisabled: false, linkCurvature: 0, linkHighlightOnHover: true, linkSourcePointOffset: undefined, linkTargetPointOffset: undefined, selectedLinkId: undefined, nodeSize: 30, nodeStrokeWidth: 3, nodeShape: GraphNodeShape.Circle, nodeGaugeValue: 0, nodeIcon: (n) => n.icon, nodeIconSize: undefined, nodeLabel: (n) => n.label, nodeLabelTrim: true, nodeLabelTrimLength: 15, nodeLabelTrimMode: TrimMode.Middle, nodeSubLabel: '', nodeSubLabelTrim: true, nodeSubLabelTrimLength: 15, nodeSubLabelTrimMode: TrimMode.Middle, nodeSideLabels: undefined, nodeBottomIcon: undefined, nodeDisabled: false, nodeFill: (n) => n.fill, nodeGaugeFill: undefined, nodeStroke: (n) => n.stroke, nodeEnterPosition: undefined, nodeEnterScale: 0.75, nodeExitPosition: undefined, nodeExitScale: 0.75, nodeSort: undefined, nodeSelectionHighlightMode: GraphNodeSelectionHighlightMode.GreyoutNonConnected, nodeGaugeAnimDuration: 1500, selectedNodeId: undefined, selectedNodeIds: undefined, panels: undefined, onNodeDragStart: undefined, onNodeDrag: undefined, onNodeDragEnd: undefined, onZoom: undefined, onZoomStart: undefined, onZoomEnd: undefined, onLayoutCalculated: undefined, onNodeSelectionBrush: undefined, onNodeSelectionDrag: undefined, onRenderComplete: undefined, shouldDataUpdate: (prevData, nextData) => { | ||
return !isEqual(prevData, nextData); | ||
} }); | ||
export { GraphDefaultConfig }; | ||
//# sourceMappingURL=config.js.map |
import { Selection } from 'd3-selection'; | ||
import { ComponentCore } from "../../core/component"; | ||
import { GraphDataModel } from "../../data-models/graph"; | ||
import { GraphInputLink, GraphInputNode } from "../../types/graph"; | ||
import { GraphInputLink, GraphInputNode, GraphInputData } from "../../types/graph"; | ||
import { Spacing } from "../../types/spacing"; | ||
@@ -9,6 +9,3 @@ import { GraphNode, GraphLink } from './types'; | ||
import * as nodeSelectors from './modules/node/style'; | ||
export declare class Graph<N extends GraphInputNode, L extends GraphInputLink> extends ComponentCore<{ | ||
nodes: N[]; | ||
links?: L[]; | ||
}, GraphConfigInterface<N, L>> { | ||
export declare class Graph<N extends GraphInputNode, L extends GraphInputLink> extends ComponentCore<GraphInputData<N, L>, GraphConfigInterface<N, L>> { | ||
static selectors: { | ||
@@ -26,2 +23,3 @@ root: string; | ||
linkLine: string; | ||
linkLabel: string; | ||
dimmedLink: string; | ||
@@ -68,2 +66,3 @@ panel: string; | ||
private _groupDragInit; | ||
private _linkPathLengthMap; | ||
events: { | ||
@@ -88,6 +87,3 @@ [x: string]: { | ||
constructor(config?: GraphConfigInterface<N, L>); | ||
setData(data: { | ||
nodes: N[]; | ||
links?: L[]; | ||
}): void; | ||
setData(data: GraphInputData<N, L>): void; | ||
setConfig(config: GraphConfigInterface<N, L>): void; | ||
@@ -150,2 +146,9 @@ get bleed(): Spacing; | ||
} | undefined; | ||
/** Set the node state by id */ | ||
setNodeStateById(nodeId: string, state: GraphNode<N, L>['_state']): void; | ||
/** Call a partial render to update the positions of the nodes and their links. | ||
* This can be useful when you've changed the node positions manually outside | ||
* of the component and want to update the graph. | ||
*/ | ||
updateNodePositions(duration?: number): void; | ||
} |
@@ -10,3 +10,3 @@ import { __awaiter } from 'tslib'; | ||
import { GraphDataModel } from '../../data-models/graph.js'; | ||
import { isEqual, isNumber, isFunction, clamp, getBoolean, shallowDiff, isPlainObject } from '../../utils/data.js'; | ||
import { isNumber, isFunction, clamp, getBoolean, getNumber, shallowDiff, isPlainObject, isEqual } from '../../utils/data.js'; | ||
import { smartTransition } from '../../utils/d3.js'; | ||
@@ -18,7 +18,7 @@ import { GraphLayoutType, GraphNodeSelectionHighlightMode, GraphLinkArrowStyle } from './types.js'; | ||
import { nodes, gNode, gNodeExit, brushed, brushable, node, nodeGauge, sideLabelGroup, label, greyedOutNode } from './modules/node/style.js'; | ||
import { links, gLink, gLinkExit, link, greyedOutLink } from './modules/link/style.js'; | ||
import { links, gLink, gLinkExit, link, linkLabelGroup, greyedOutLink } from './modules/link/style.js'; | ||
import { panels, gPanel, panel, panelSelection, label as label$1, labelText, sideIconGroup, sideIconShape, sideIconSymbol } from './modules/panel/style.js'; | ||
import { createNodes, updateNodes, removeNodes, updateNodesPartial, zoomNodesThrottled, zoomNodes } from './modules/node/index.js'; | ||
import { createNodes, updateNodes, removeNodes, updateNodesPartial, zoomNodesThrottled, zoomNodes, updateNodePositions } from './modules/node/index.js'; | ||
import { getMaxNodeSize, getX, getY, getNodeSize } from './modules/node/helper.js'; | ||
import { createLinks, updateLinks, removeLinks, updateLinksPartial, animateLinkFlow, zoomLinksThrottled, zoomLinks } from './modules/link/index.js'; | ||
import { createLinks, updateLinks, removeLinks, updateLinksPartial, animateLinkFlow, zoomLinksThrottled, zoomLinks, updateLinkLines } from './modules/link/index.js'; | ||
import { getArrowPath, getDoubleArrowPath } from './modules/link/helper.js'; | ||
@@ -40,2 +40,4 @@ import { removePanels, createPanels, updatePanels } from './modules/panel/index.js'; | ||
this._isDragging = false; | ||
// A map for storing link total path lengths to optimize rendering performance | ||
this._linkPathLengthMap = new Map(); | ||
this.events = { | ||
@@ -88,3 +90,3 @@ [Graph.selectors.background]: { | ||
const { config } = this; | ||
if (isEqual(this.datamodel.data, data)) | ||
if (!config.shouldDataUpdate(this.datamodel.data, data, this.datamodel)) | ||
return; | ||
@@ -185,3 +187,3 @@ this.datamodel.nodeSort = config.nodeSort; | ||
const selectedIds = (_a = this.config.selectedNodeIds) !== null && _a !== void 0 ? _a : [this.config.selectedNodeId]; | ||
const selectedNodes = selectedIds.map(id => datamodel.getNodeFromId(id)); | ||
const selectedNodes = selectedIds.map(id => datamodel.getNodeById(id)); | ||
this._setNodeSelectionState(selectedNodes); | ||
@@ -206,10 +208,2 @@ } | ||
this.g.call(this._zoomBehavior).on('dblclick.zoom', null); | ||
// While the graph is animating we disable pointer events on the graph group | ||
if (animDuration) { | ||
this._graphGroup.attr('pointer-events', 'none'); | ||
} | ||
smartTransition(this._graphGroup, animDuration) | ||
.on('end interrupt', () => { | ||
this._graphGroup.attr('pointer-events', null); | ||
}); | ||
// We need to set up events and attributes again because the rendering might have been delayed by the layout | ||
@@ -256,3 +250,3 @@ // calculation and they were not set up properly (see the render function of `ComponentCore`) | ||
const linkGroups = this._linksGroup | ||
.selectAll(`.${gLink}`) | ||
.selectAll(`.${gLink}:not(.${gLinkExit}`) | ||
.data(links, (d) => String(d._id)); | ||
@@ -263,3 +257,3 @@ const linkGroupsEnter = linkGroups.enter().append('g') | ||
const linkGroupsMerged = linkGroups.merge(linkGroupsEnter); | ||
linkGroupsMerged.call(updateLinks, config, duration, this._scale, this._getLinkArrowDefId); | ||
linkGroupsMerged.call(updateLinks, config, duration, this._scale, this._getLinkArrowDefId, this._linkPathLengthMap); | ||
const linkGroupsExit = linkGroups.exit(); | ||
@@ -500,3 +494,4 @@ linkGroupsExit | ||
return; | ||
d._state.hovered = true; | ||
if (this.config.linkHighlightOnHover) | ||
d._state.hovered = true; | ||
this._updateNodesLinksPartial(); | ||
@@ -511,11 +506,13 @@ } | ||
_onLinkFlowTimerFrame(elapsed = 0) { | ||
const { config: { linkFlow, linkFlowAnimDuration }, datamodel: { links } } = this; | ||
const hasLinksWithFlow = links.some((d, i) => getBoolean(d, linkFlow, i)); | ||
const { config, datamodel: { links } } = this; | ||
const hasLinksWithFlow = links.some((d, i) => getBoolean(d, config.linkFlow, i)); | ||
if (!hasLinksWithFlow) | ||
return; | ||
const t = (elapsed % linkFlowAnimDuration) / linkFlowAnimDuration; | ||
const linkElements = this._linksGroup.selectAll(`.${gLink}`); | ||
const linksToAnimate = linkElements.filter(d => !d._state.greyout); | ||
linksToAnimate.each(d => { d._state.flowAnimTime = t; }); | ||
animateLinkFlow(linksToAnimate, this.config, this._scale); | ||
linksToAnimate.each((l, i, els) => { | ||
const linkFlowAnimDuration = getNumber(l, config.linkFlowAnimDuration, l._indexGlobal); | ||
l._state.flowAnimTime = (elapsed % linkFlowAnimDuration) / linkFlowAnimDuration; | ||
}); | ||
animateLinkFlow(linksToAnimate, this.config, this._scale, this._linkPathLengthMap); | ||
} | ||
@@ -664,6 +661,6 @@ _onZoom(t, event) { | ||
}); | ||
linksToUpdate.call(updateLinks, config, 0, scale, this._getLinkArrowDefId); | ||
linksToUpdate.call(updateLinks, config, 0, scale, this._getLinkArrowDefId, this._linkPathLengthMap); | ||
const linksToAnimate = linksToUpdate.filter(d => d._state.greyout); | ||
if (linksToAnimate.size()) | ||
animateLinkFlow(linksToAnimate, config, this._scale); | ||
animateLinkFlow(linksToAnimate, config, this._scale, this._linkPathLengthMap); | ||
(_a = config.onNodeDrag) === null || _a === void 0 ? void 0 : _a.call(config, d, event); | ||
@@ -699,3 +696,3 @@ } | ||
.filter(l => { var _a, _b, _c, _d; return ((_b = (_a = l.source) === null || _a === void 0 ? void 0 : _a._state) === null || _b === void 0 ? void 0 : _b.isDragged) || ((_d = (_c = l.target) === null || _c === void 0 ? void 0 : _c._state) === null || _d === void 0 ? void 0 : _d.isDragged); })); | ||
connectedLinks.call(updateLinks, this.config, 0, this._scale, this._getLinkArrowDefId); | ||
connectedLinks.call(updateLinks, this.config, 0, this._scale, this._getLinkArrowDefId, this._linkPathLengthMap); | ||
} | ||
@@ -827,2 +824,18 @@ else { | ||
} | ||
/** Set the node state by id */ | ||
setNodeStateById(nodeId, state) { | ||
this.datamodel.setNodeStateById(nodeId, state); | ||
} | ||
/** Call a partial render to update the positions of the nodes and their links. | ||
* This can be useful when you've changed the node positions manually outside | ||
* of the component and want to update the graph. | ||
*/ | ||
updateNodePositions(duration) { | ||
const { config } = this; | ||
const animDuration = isNumber(duration) ? duration : config.duration; | ||
const linkElements = this._linksGroup.selectAll(`.${gLink}:not(.${gLinkExit}`); | ||
updateLinkLines(linkElements, config, animDuration, this._scale, this._getLinkArrowDefId, this._linkPathLengthMap); | ||
const nodeElements = this._nodesGroup.selectAll(`.${gNode}:not(.${gNodeExit})`); | ||
updateNodePositions(nodeElements, animDuration); | ||
} | ||
} | ||
@@ -841,2 +854,3 @@ Graph.selectors = { | ||
linkLine: link, | ||
linkLabel: linkLabelGroup, | ||
dimmedLink: greyedOutLink, | ||
@@ -843,0 +857,0 @@ panel: gPanel, |
@@ -26,3 +26,3 @@ import { isPlainObject, merge, getValue } from '../../../utils/data.js'; | ||
if (key) { | ||
const layoutOps = isPlainObject(layoutOptions) ? DEFAULT_ELK_SETTINGS : merge(DEFAULT_ELK_SETTINGS, getValue(key, layoutOptions)); | ||
const layoutOps = isPlainObject(layoutOptions) ? merge(DEFAULT_ELK_SETTINGS, layoutOptions) : merge(DEFAULT_ELK_SETTINGS, getValue(key, layoutOptions)); | ||
return { | ||
@@ -29,0 +29,0 @@ id: key, |
@@ -383,3 +383,3 @@ import { __awaiter } from 'tslib'; | ||
const labelApprxHeight = 30; | ||
const nodes = datamodel.nodes.map(n => (Object.assign(Object.assign({}, n), { id: n._id, width: getNumber(n, config.nodeSize, n._index) + getNumber(n, config.nodeStrokeWidth, n._index), height: getNumber(n, config.nodeSize, n._index) + labelApprxHeight }))); | ||
const nodes = datamodel.nodes.map((n, i) => (Object.assign(Object.assign(Object.assign({}, n), { id: n._id, width: getNumber(n, config.nodeSize, n._index) + getNumber(n, config.nodeStrokeWidth, n._index), height: getNumber(n, config.nodeSize, n._index) + labelApprxHeight }), (config.layoutElkGetNodeShape ? config.layoutElkGetNodeShape(n, i) : {})))); | ||
let elkNodes; | ||
@@ -386,0 +386,0 @@ if (config.layoutElkNodeGroups) { |
@@ -8,6 +8,7 @@ import { Selection } from 'd3-selection'; | ||
export declare function updateLinksPartial<N extends GraphInputNode, L extends GraphInputLink>(selection: Selection<SVGGElement, GraphLink<N, L>, SVGGElement, unknown>, config: GraphConfigInterface<N, L>, scale: number): void; | ||
export declare function updateLinks<N extends GraphInputNode, L extends GraphInputLink>(selection: Selection<SVGGElement, GraphLink<N, L>, SVGGElement, unknown>, config: GraphConfigInterface<N, L>, duration: number, scale: number, getLinkArrowDefId: (arrow: GraphLinkArrowStyle | undefined) => string): void; | ||
export declare function updateLinkLines<N extends GraphInputNode, L extends GraphInputLink>(selection: Selection<SVGGElement, GraphLink<N, L>, SVGGElement, unknown>, config: GraphConfigInterface<N, L>, duration: number, scale: number, getLinkArrowDefId: (arrow: GraphLinkArrowStyle | undefined) => string, linkPathLengthMap: Map<string, number>): Selection<SVGGElement, GraphLink<N, L>, SVGGElement, unknown>; | ||
export declare function updateLinks<N extends GraphInputNode, L extends GraphInputLink>(selection: Selection<SVGGElement, GraphLink<N, L>, SVGGElement, unknown>, config: GraphConfigInterface<N, L>, duration: number, scale: number, getLinkArrowDefId: (arrow: GraphLinkArrowStyle | undefined) => string, linkPathLengthMap: Map<string, number>): void; | ||
export declare function removeLinks<N extends GraphInputNode, L extends GraphInputLink>(selection: Selection<SVGGElement, GraphLink<N, L>, SVGGElement, unknown>, config: GraphConfigInterface<N, L>, duration: number): void; | ||
export declare function animateLinkFlow<N extends GraphInputNode, L extends GraphInputLink>(selection: Selection<SVGGElement, GraphLink<N, L>, SVGGElement, unknown>, config: GraphConfigInterface<N, L>, scale: number): void; | ||
export declare function animateLinkFlow<N extends GraphInputNode, L extends GraphInputLink>(selection: Selection<SVGGElement, GraphLink<N, L>, SVGGElement, unknown>, config: GraphConfigInterface<N, L>, scale: number, linkPathLengthMap: Map<string, number>): void; | ||
export declare function zoomLinks<N extends GraphInputNode, L extends GraphInputLink>(selection: Selection<SVGGElement, GraphLink<N, L>, SVGGElement, unknown>, config: GraphConfigInterface<N, L>, scale: number): void; | ||
export declare const zoomLinksThrottled: import("throttle-debounce").throttle<typeof zoomLinks>; |
import { select } from 'd3-selection'; | ||
import { range, sum } from 'd3-array'; | ||
import toPx from 'to-px'; | ||
import { getBoolean, getValue, ensureArray, getNumber, throttle } from '../../../../utils/data.js'; | ||
import { getBoolean, ensureArray, getValue, getNumber, throttle } from '../../../../utils/data.js'; | ||
import { smartTransition } from '../../../../utils/d3.js'; | ||
@@ -51,9 +51,4 @@ import { getCSSVariableValueInPixels } from '../../../../utils/misc.js'; | ||
} | ||
function updateLinks(selection, config, duration, scale = 1, getLinkArrowDefId) { | ||
const { linkFlowParticleSize, linkStyle, linkFlow, linkLabel, linkLabelShiftFromCenter } = config; | ||
if (!selection.size()) | ||
return; | ||
selection | ||
.classed(linkDashed, d => getValue(d, linkStyle, d._indexGlobal) === GraphLinkStyle.Dashed); | ||
selection.each((d, i, elements) => { | ||
function updateLinkLines(selection, config, duration, scale = 1, getLinkArrowDefId, linkPathLengthMap) { | ||
return selection.each((d, i, elements) => { | ||
var _a; | ||
@@ -66,10 +61,11 @@ const element = elements[i]; | ||
const linkArrow$1 = linkGroup.select(`.${linkArrow}`); | ||
const flowGroup$1 = linkGroup.select(`.${flowGroup}`); | ||
const linkColor = getLinkColor(d, config); | ||
const linkShiftTransform = getLinkShiftTransform(d, config.linkNeighborSpacing); | ||
const linkLabelData = ensureArray(getValue(d, linkLabel, d._indexGlobal)); | ||
const x1 = getX(d.source); | ||
const y1 = getY(d.source); | ||
const x2 = getX(d.target); | ||
const y2 = getY(d.target); | ||
const linkLabelData = ensureArray(getValue(d, config.linkLabel, d._indexGlobal)); | ||
const offsetSource = getValue(d, config.linkSourcePointOffset, i); | ||
const offsetTarget = getValue(d, config.linkTargetPointOffset, i); | ||
const x1 = getX(d.source) + ((offsetSource === null || offsetSource === void 0 ? void 0 : offsetSource[0]) || 0); | ||
const y1 = getY(d.source) + ((offsetSource === null || offsetSource === void 0 ? void 0 : offsetSource[1]) || 0); | ||
const x2 = getX(d.target) + ((offsetTarget === null || offsetTarget === void 0 ? void 0 : offsetTarget[0]) || 0); | ||
const y2 = getY(d.target) + ((offsetTarget === null || offsetTarget === void 0 ? void 0 : offsetTarget[1]) || 0); | ||
const curvature = (_a = getNumber(d, config.linkCurvature, i)) !== null && _a !== void 0 ? _a : 0; | ||
@@ -81,2 +77,10 @@ const cp1x = x1 + (x2 - x1) * 0.5 * curvature; | ||
const pathData = `M${x1},${y1} C${cp1x},${cp1y} ${cp2x},${cp2y} ${x2},${y2}`; | ||
const linkPathElement = linkSupport$1.attr('d', pathData).node(); | ||
const cachedLinkPathLength = linkPathLengthMap.get(pathData); | ||
const pathLength = cachedLinkPathLength !== null && cachedLinkPathLength !== void 0 ? cachedLinkPathLength : linkPathElement.getTotalLength(); | ||
if (!cachedLinkPathLength) | ||
linkPathLengthMap.set(pathData, pathLength); | ||
linkSupport$1 | ||
.style('stroke', linkColor) | ||
.attr('transform', linkShiftTransform); | ||
link$1 | ||
@@ -96,10 +100,4 @@ .attr('class', link) | ||
.attr('d', pathData); | ||
linkSupport$1 | ||
.style('stroke', linkColor) | ||
.attr('transform', linkShiftTransform) | ||
.attr('d', pathData); | ||
// Arrow | ||
const linkArrowStyle = getLinkArrowStyle(d, config); | ||
const linkPathElement = linkSupport$1.node(); | ||
const pathLength = linkPathElement.getTotalLength(); | ||
if (linkArrowStyle) { | ||
@@ -121,2 +119,23 @@ const arrowPos = pathLength * (linkLabelData.length ? 0.65 : 0.5); | ||
} | ||
}); | ||
} | ||
function updateLinks(selection, config, duration, scale = 1, getLinkArrowDefId, linkPathLengthMap) { | ||
const { linkStyle, linkFlow, linkLabel, linkLabelShiftFromCenter } = config; | ||
if (!selection.size()) | ||
return; | ||
selection | ||
.classed(linkDashed, d => getValue(d, linkStyle, d._indexGlobal) === GraphLinkStyle.Dashed); | ||
// Update line and arrow positions | ||
updateLinkLines(selection, config, duration, scale, getLinkArrowDefId, linkPathLengthMap); | ||
// Update labels and link flow (particles) groups | ||
selection.each((d, i, elements) => { | ||
const element = elements[i]; | ||
const linkGroup = select(element); | ||
const flowGroup$1 = linkGroup.select(`.${flowGroup}`); | ||
const linkSupport$1 = linkGroup.select(`.${linkSupport}`); | ||
const linkPathElement = linkSupport$1.node(); | ||
const linkColor = getLinkColor(d, config); | ||
const linkShiftTransform = getLinkShiftTransform(d, config.linkNeighborSpacing); | ||
const linkLabelData = ensureArray(getValue(d, linkLabel, d._indexGlobal)); | ||
const linkFlowParticleSize = getNumber(d, config.linkFlowParticleSize, d._indexGlobal); | ||
// Particle Flow | ||
@@ -128,3 +147,3 @@ flowGroup$1 | ||
.selectAll(`.${flowCircle}`) | ||
.attr('r', linkFlowParticleSize / scale) | ||
.attr('r', linkFlowParticleSize / Math.sqrt(scale)) | ||
.style('fill', linkColor); | ||
@@ -165,2 +184,5 @@ smartTransition(flowGroup$1, duration) | ||
let linkLabelShiftCumulative = -sum(linkLabelsDataPrepared, d => d._backgroundWidth + linkLabelMargin) / 2; // Centering the labels | ||
const cachedLinkPathLength = linkPathLengthMap.get(linkPathElement.getAttribute('d')); | ||
const pathLength = cachedLinkPathLength !== null && cachedLinkPathLength !== void 0 ? cachedLinkPathLength : linkPathElement.getTotalLength(); | ||
const linkArrowStyle = getLinkArrowStyle(d, config); | ||
linkLabelGroupsMerged.each((linkLabelDatum, i, elements) => { | ||
@@ -189,3 +211,4 @@ var _a, _b; | ||
.attr('height', linkLabelDatum._fontSizePx) | ||
.style('fill', linkLabelColor); | ||
.style('fill', linkLabelColor) | ||
.style('color', linkLabelColor); // Setting `color` to be passed to SVGs that use `currentColor` | ||
} | ||
@@ -239,3 +262,3 @@ else { | ||
} | ||
function animateLinkFlow(selection, config, scale) { | ||
function animateLinkFlow(selection, config, scale, linkPathLengthMap) { | ||
const { linkFlow } = config; | ||
@@ -249,3 +272,4 @@ if (scale < ZoomLevel.Level2) | ||
const linkPathElement = linkGroup.select(`.${linkSupport}`).node(); | ||
const pathLength = linkPathElement.getTotalLength(); | ||
const cachedLinkPathLength = linkPathLengthMap.get(linkPathElement.getAttribute('d')); | ||
const pathLength = cachedLinkPathLength !== null && cachedLinkPathLength !== void 0 ? cachedLinkPathLength : linkPathElement.getTotalLength(); | ||
if (!getBoolean(d, linkFlow, d._indexGlobal) || !pathLength) | ||
@@ -264,8 +288,9 @@ return; | ||
function zoomLinks(selection, config, scale) { | ||
const { linkFlowParticleSize } = config; | ||
selection.classed(zoomOutLevel2, scale < ZoomLevel.Level2); | ||
selection.select(`.${flowGroup}`) | ||
.style('opacity', scale < ZoomLevel.Level2 ? 0 : 1); | ||
selection.selectAll(`.${flowCircle}`) | ||
.attr('r', linkFlowParticleSize / scale); | ||
selection.each((l, i, els) => { | ||
const r = getNumber(l, config.linkFlowParticleSize, l._indexGlobal) / Math.sqrt(scale); | ||
select(els[i]).selectAll(`.${flowCircle}`).attr('r', r); | ||
}); | ||
const linkElements = selection.selectAll(`.${link}`); | ||
@@ -280,3 +305,3 @@ linkElements | ||
export { animateLinkFlow, createLinks, removeLinks, updateLinks, updateLinksPartial, zoomLinks, zoomLinksThrottled }; | ||
export { animateLinkFlow, createLinks, removeLinks, updateLinkLines, updateLinks, updateLinksPartial, zoomLinks, zoomLinksThrottled }; | ||
//# sourceMappingURL=index.js.map |
@@ -41,3 +41,2 @@ import { css, injectGlobal } from '@emotion/css'; | ||
stroke-linecap: round; | ||
pointer-events: stroke; | ||
stroke-width: var(--vis-graph-link-support-stroke-width); | ||
@@ -100,3 +99,2 @@ stroke-opacity: 0; | ||
label: label-group; | ||
pointer-events: all; | ||
`; | ||
@@ -103,0 +101,0 @@ const linkLabelBackground = css ` |
import { Selection } from 'd3-selection'; | ||
import { Transition } from 'd3-transition'; | ||
import { GraphInputLink, GraphInputNode } from "../../../../types/graph"; | ||
import { Selection$Transition } from "../../../../utils/d3"; | ||
import { GraphNode } from '../../types'; | ||
@@ -13,2 +14,3 @@ import { GraphConfigInterface } from '../../config'; | ||
export declare function updateNodesPartial<N extends GraphInputNode, L extends GraphInputLink>(selection: Selection<SVGGElement, GraphNode<N, L>, SVGGElement, unknown>, config: GraphConfigInterface<N, L>, duration: number, scale?: number): void; | ||
export declare function updateNodePositions<N extends GraphInputNode, L extends GraphInputLink>(selection: Selection<SVGGElement, GraphNode<N, L>, SVGGElement, unknown>, duration: number): Selection$Transition<SVGGElement, GraphNode<N, L>, SVGGElement, unknown>; | ||
export declare function updateNodes<N extends GraphInputNode, L extends GraphInputLink>(selection: Selection<SVGGElement, GraphNode<N, L>, SVGGElement, unknown>, config: GraphConfigInterface<N, L>, duration: number, scale?: number): Selection<SVGGElement, GraphNode<N, L>, SVGGElement, unknown> | Transition<SVGGElement, GraphNode<N, L>, SVGGElement, unknown>; | ||
@@ -15,0 +17,0 @@ export declare function removeNodes<N extends GraphInputNode, L extends GraphInputLink>(selection: Selection<SVGGElement, GraphNode<N, L>, SVGGElement, unknown>, config: GraphConfigInterface<N, L>, duration: number, scale?: number): void; |
@@ -89,7 +89,10 @@ import { select } from 'd3-selection'; | ||
} | ||
function updateNodePositions(selection, duration) { | ||
return smartTransition(selection, duration) | ||
.attr('transform', d => `translate(${getX(d)}, ${getY(d)}) scale(1)`) | ||
.attr('opacity', 1); | ||
} | ||
function updateNodes(selection, config, duration, scale = 1) { | ||
const { nodeGaugeAnimDuration, nodeStrokeWidth, nodeShape, nodeSize, nodeGaugeValue, nodeGaugeFill, nodeIcon: nodeIcon$1, nodeIconSize, nodeLabel, nodeLabelTrim, nodeLabelTrimMode, nodeLabelTrimLength, nodeSubLabel, nodeSubLabelTrim, nodeSubLabelTrimMode, nodeSubLabelTrimLength, nodeSideLabels, nodeStroke, nodeFill, nodeBottomIcon: nodeBottomIcon$1, } = config; | ||
const nodeGroupsUpdate = smartTransition(selection, duration) | ||
.attr('transform', d => `translate(${getX(d)}, ${getY(d)}) scale(1)`) | ||
.attr('opacity', 1); | ||
const nodeGroupsUpdate = updateNodePositions(selection, duration); | ||
// If there's a custom render function, use it | ||
@@ -322,3 +325,3 @@ if (config.nodeUpdateCustomRenderFunction) { | ||
export { createNodes, removeNodes, updateNodes, updateNodesPartial, zoomNodes, zoomNodesThrottled }; | ||
export { createNodes, removeNodes, updateNodePositions, updateNodes, updateNodesPartial, zoomNodes, zoomNodesThrottled }; | ||
//# sourceMappingURL=index.js.map |
@@ -75,3 +75,3 @@ import { select } from 'd3-selection'; | ||
panelLabel.select(`.${labelText}`) | ||
.text(d => trimString(d.label)); | ||
.text(d => trimString(d.label, d.labelTrimLength, d.labelTrimMode)); | ||
smartTransition(panelLabel, duration) | ||
@@ -88,3 +88,3 @@ .attr('transform', getLabelTranslateTransform); | ||
const label = select(event.currentTarget); | ||
const labelContent = trimString(d.label); | ||
const labelContent = trimString(d.label, d.labelTrimLength, d.labelTrimMode); | ||
label.select('text').text(labelContent); | ||
@@ -91,0 +91,0 @@ setLabelRect(label, labelContent, labelText); |
import { Position } from "../../types/position"; | ||
import { GraphInputLink, GraphInputNode, GraphNodeCore, GraphLinkCore } from "../../types/graph"; | ||
import { Spacing } from "../../types/spacing"; | ||
import { TrimMode } from "../../types/text"; | ||
export declare type GraphNode<N extends GraphInputNode = GraphInputNode, L extends GraphInputLink = GraphInputLink> = GraphNodeCore<N, L> & { | ||
@@ -77,2 +78,6 @@ x?: number; | ||
label?: string; | ||
/** Trim label if it's longer than this number of characters */ | ||
labelTrimLength?: number; | ||
/** Trim mode of the label */ | ||
labelTrimMode?: TrimMode; | ||
/** Position of the label */ | ||
@@ -79,0 +84,0 @@ labelPosition?: Position.Top | Position.Bottom | string; |
import { LeafletMapDefaultConfig } from '../leaflet-map/config.js'; | ||
/* eslint-disable dot-notation */ | ||
// Config | ||
const LeafletFlowMapDefaultConfig = Object.assign(Object.assign({}, LeafletMapDefaultConfig), { sourceLongitude: (f) => f.sourceLongitude, sourceLatitude: (f) => f.sourceLatitude, targetLongitude: (f) => f.targetLongitude, targetLatitude: (f) => f.targetLatitude, sourcePointRadius: 3, sourcePointColor: '#88919f', flowParticleColor: '#949dad', flowParticleRadius: 1.1, flowParticleSpeed: 0.07, flowParticleDensity: 0.6, onSourcePointClick: undefined, onSourcePointMouseEnter: undefined, onSourcePointMouseLeave: undefined }); | ||
@@ -5,0 +6,0 @@ |
@@ -5,2 +5,3 @@ import { ComponentDefaultConfig } from '../../core/component/config.js'; | ||
/* eslint-disable no-irregular-whitespace */ | ||
// Core | ||
const LeafletMapDefaultConfig = Object.assign(Object.assign({}, ComponentDefaultConfig), { | ||
@@ -7,0 +8,0 @@ // General |
@@ -79,6 +79,6 @@ import { __awaiter } from 'tslib'; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const { style, renderer, topoJSONLayer } = config; | ||
const { style: style$1, renderer, topoJSONLayer } = config; | ||
const leaflet = yield import('leaflet'); | ||
const L = leaflet.default; | ||
if (!style) { | ||
if (!style$1) { | ||
console.error('Unovis | Leaflet Map: Please provide style settings in the map configuration object'); | ||
@@ -120,3 +120,3 @@ return; | ||
case LeafletMapRenderer.Raster: | ||
layer = L.tileLayer(style); | ||
layer = L.tileLayer(style$1); | ||
layer.addTo(leafletMap); | ||
@@ -123,0 +123,0 @@ break; |
@@ -29,3 +29,3 @@ import { ComponentConfigInterface } from "../../core/component/config"; | ||
sort?: (a: NestedDonutSegment<Datum>, b: NestedDonutSegment<Datum>) => number; | ||
/** Array of accessor functions to defined the nested groups */ | ||
/** Array of accessor functions to defined the nested groups. Default: `[]` */ | ||
layers: StringAccessor<Datum>[]; | ||
@@ -32,0 +32,0 @@ /** |
@@ -62,3 +62,3 @@ import { min, max } from 'd3-array'; | ||
const stacked = getStackedData(this._barData, 0, yAccessors, this._prevNegative); | ||
this._prevNegative = stacked.map(s => !!s.negative); | ||
this._prevNegative = stacked.map(s => !!s.isMostlyNegative); | ||
const barGroups = this.g | ||
@@ -94,3 +94,6 @@ .selectAll(`.${barGroup}`) | ||
.selectAll(`.${bar}`) | ||
.data((d, j) => stacked.map((s) => (Object.assign(Object.assign({}, d), { _stacked: s[j], _negative: s.negative, _ending: s.ending })))); | ||
.data((d, j) => stacked.map((s, stackIndex) => (Object.assign(Object.assign({}, d), { _index: j, _stacked: s[j], | ||
// Ending bar if the next stack is not the same as the current one | ||
_ending: (stackIndex === stacked.length - 1) || | ||
((stackIndex <= stacked.length - 1) && stacked[stackIndex + 1][j][0] !== s[j][1]) })))); | ||
const barsEnter = bars.enter().append('path') | ||
@@ -155,6 +158,5 @@ .attr('class', bar) | ||
const barWidth = this._getBarWidth(); | ||
const isNegative = d._negative; | ||
const isNegative = d._stacked[1] < 0; | ||
const isEnding = d._ending; // The most top bar or, if the value is negative, the most bottom bar | ||
// Todo: Find a way to pass the datum index to `getNumber` below | ||
const value = getNumber(d, yAccessors[accessorIndex]); | ||
const value = getNumber(d, yAccessors[accessorIndex], d._index); | ||
const height = isEntering ? 0 : Math.abs(this.valueScale(d._stacked[0]) - this.valueScale(d._stacked[1])); | ||
@@ -161,0 +163,0 @@ const h = !isEntering && config.barMinHeight1Px && (height < 1) && isFinite(value) && (value !== config.barMinHeightZeroValue) ? 1 : height; |
export declare type StackedBarDataRecord<D> = D & { | ||
_index: number; | ||
_stacked: [number, number]; | ||
_negative: boolean; | ||
_ending: boolean; | ||
}; |
import { XYComponentConfigInterface } from "../../core/xy-component/config"; | ||
import { WithOptional } from "../../types/misc"; | ||
import { ColorAccessor, NumericAccessor, StringAccessor } from "../../types/accessor"; | ||
import { ColorAccessor, NumericAccessor, StringAccessor, GenericAccessor } from "../../types/accessor"; | ||
import { TextAlign } from "../../types/text"; | ||
import { Arrangement } from "../../types/position"; | ||
import type { TimelineArrow, TimelineLineRenderState, TimelineRowIcon, TimelineRowLabel } from './types'; | ||
export interface TimelineConfigInterface<Datum> extends WithOptional<XYComponentConfigInterface<Datum>, 'y'> { | ||
/** @deprecated This property has been renamed to `key` */ | ||
type?: StringAccessor<Datum>; | ||
/** @deprecated This property has been renamed to `lineDuration` */ | ||
length?: NumericAccessor<Datum>; | ||
/** @deprecated This property has been renamed to `lineCursor` */ | ||
cursor?: StringAccessor<Datum>; | ||
/** Timeline item row accessor function. Records with the `lineRow` will be plotted in one row. Default: `undefined` */ | ||
lineRow?: StringAccessor<Datum>; | ||
/** Timeline item duration accessor function. Default: `undefined`. Falls back to the deprecated `length` property */ | ||
lineDuration?: NumericAccessor<Datum>; | ||
/** Timeline item color accessor function. Default: `d => d.color` */ | ||
@@ -11,23 +24,61 @@ color?: ColorAccessor<Datum>; | ||
lineCap?: boolean; | ||
/** Provide a href to an SVG defined in container's `svgDefs` to display an icon at the start of the line. Default: undefined */ | ||
lineStartIcon?: StringAccessor<Datum>; | ||
/** Line start icon color accessor function. Default: `undefined` */ | ||
lineStartIconColor?: StringAccessor<Datum>; | ||
/** Line start icon size accessor function. Default: `undefined` */ | ||
lineStartIconSize?: NumericAccessor<Datum>; | ||
/** Line start icon arrangement configuration. Controls how the icon is positioned relative to the line. | ||
* Accepts values from the Arrangement enum: `Arrangement.Start`, `Arrangement.Middle`, `Arrangement.End` or a string equivalent. | ||
* Default: `Arrangement.Inside` */ | ||
lineStartIconArrangement?: GenericAccessor<Arrangement | `${Arrangement}`, Datum>; | ||
/** Provide a href to an SVG defined in container's `svgDefs` to display an icon at the end of the line. Default: undefined */ | ||
lineEndIcon?: StringAccessor<Datum>; | ||
/** Line end icon color accessor function. Default: `undefined` */ | ||
lineEndIconColor?: StringAccessor<Datum>; | ||
/** Line end icon size accessor function. Default: `undefined` */ | ||
lineEndIconSize?: NumericAccessor<Datum>; | ||
/** Line end icon arrangement configuration. Controls how the icon is positioned relative to the line. | ||
* Accepts values from the Arrangement enum: `Arrangement.Start`, `Arrangement.Middle`, `Arrangement.End` or a string equivalent. | ||
* Default: `Arrangement.Inside` */ | ||
lineEndIconArrangement?: GenericAccessor<Arrangement | `${Arrangement}`, Datum>; | ||
/** Configurable Timeline item cursor when hovering over. Default: `undefined` */ | ||
lineCursor?: StringAccessor<Datum>; | ||
/** Sets the minimum line length to 1 pixel for better visibility of small values. Default: `false` */ | ||
showEmptySegments?: boolean; | ||
/** Timeline row height. Default: `22` */ | ||
rowHeight?: number; | ||
/** Timeline item length accessor function. Default: `d => d.length` */ | ||
length?: NumericAccessor<Datum>; | ||
/** Timeline item type accessor function. Records of one type will be plotted in one row. Default: `d => d.type` */ | ||
type?: StringAccessor<Datum>; | ||
/** Configurable Timeline item cursor when hovering over. Default: `null` */ | ||
cursor?: StringAccessor<Datum>; | ||
/** Show item type labels when set to `true`. Default: `false` */ | ||
/** Alternating row colors. Default: `true` */ | ||
alternatingRowColors?: boolean; | ||
/** @deprecated This property has been renamed to `showRowLabels */ | ||
showLabels?: boolean; | ||
/** Fixed label width in pixels. Labels longer than the specified value will be trimmed. Default: `undefined` */ | ||
/** @deprecated This property has been renamed to `rowLabelWidth */ | ||
labelWidth?: number; | ||
/** Maximum label width in pixels. Labels longer than the specified value will be trimmed. Default: `120` */ | ||
/** @deprecated This property has been renamed to `rowMaxLabelWidth */ | ||
maxLabelWidth?: number; | ||
/** Alternating row colors. Default: `true` */ | ||
alternatingRowColors?: boolean; | ||
/** Show row labels when set to `true`. Default: `false`. Falls back to deprecated `showLabels` */ | ||
showRowLabels?: boolean; | ||
/** Row label style as an object with the `{ [property-name]: value }` format. Default: `undefined` */ | ||
rowLabelStyle?: GenericAccessor<Record<string, string>, TimelineRowLabel<Datum>>; | ||
/** Row label formatter function. Default: `undefined` */ | ||
rowLabelFormatter?: (key: string, items: Datum[], i: number) => string; | ||
/** Provide an icon href to be displayed before the row label. Default: `undefined` */ | ||
rowIcon?: (key: string, items: Datum[], i: number) => TimelineRowIcon | undefined; | ||
/** Fixed label width in pixels. Labels longer than the specified value will be trimmed. Default: `undefined`. Falls back to deprecated `labelWidth`. */ | ||
rowLabelWidth?: number; | ||
/** Maximum label width in pixels. Labels longer than the specified value will be trimmed. Default: `undefined`. Falls back to deprecated `maxLabelWidth`. */ | ||
rowMaxLabelWidth?: number; | ||
/** Text alignment for labels: `TextAlign.Left`, `TextAlign.Center` or `TextAlign.Right`. Default: `TextAlign.Right` */ | ||
rowLabelTextAlign?: TextAlign | `${TextAlign}`; | ||
arrows?: TimelineArrow[]; | ||
/** Control the animation by specify the initial position for new lines as [x, y]. Default: `undefined` */ | ||
animationLineEnterPosition?: [ | ||
number | undefined | null, | ||
number | undefined | null | ||
] | ((d: Datum & TimelineLineRenderState, i: number, data: (Datum & TimelineLineRenderState)[]) => [number | undefined, number | undefined]) | undefined; | ||
/** Control the animation by specify the destination position for exiting lines as [x, y]. Default: `undefined` */ | ||
animationLineExitPosition?: [number | undefined | null, number | undefined | null] | ((d: Datum & TimelineLineRenderState, i: number, data: (Datum & TimelineLineRenderState)[]) => [number | undefined, number | undefined]) | undefined; | ||
/** Scrolling callback function: `(scrollTop: number) => void`. Default: `undefined` */ | ||
onScroll?: (scrollTop: number) => void; | ||
/** Sets the minimum line length to 1 pixel for better visibility of small values. Default: `false` */ | ||
showEmptySegments?: boolean; | ||
} | ||
export declare const TimelineDefaultConfig: TimelineConfigInterface<unknown>; |
import { XYComponentDefaultConfig } from '../../core/xy-component/config.js'; | ||
import { TextAlign } from '../../types/text.js'; | ||
import { Arrangement } from '../../types/position.js'; | ||
const TimelineDefaultConfig = Object.assign(Object.assign({}, XYComponentDefaultConfig), { id: undefined, color: (d) => d.color, lineWidth: 8, lineCap: false, rowHeight: 22, length: (d) => d.length, type: (d) => d.type, cursor: null, labelWidth: undefined, showLabels: false, maxLabelWidth: 120, alternatingRowColors: true, onScroll: undefined, showEmptySegments: false }); | ||
const TimelineDefaultConfig = Object.assign(Object.assign({}, XYComponentDefaultConfig), { id: undefined, | ||
// Items (Lines) | ||
cursor: undefined, type: (d) => d.type, length: (d) => d.length, color: (d) => d.color, lineRow: undefined, lineDuration: undefined, lineWidth: 8, lineCap: false, lineCursor: undefined, showEmptySegments: false, lineStartIcon: undefined, lineStartIconColor: undefined, lineStartIconSize: undefined, lineStartIconArrangement: Arrangement.Inside, lineEndIcon: undefined, lineEndIconColor: undefined, lineEndIconSize: undefined, lineEndIconArrangement: Arrangement.Inside, | ||
// Rows | ||
rowHeight: 22, alternatingRowColors: true, | ||
// Row Labels | ||
showLabels: false, labelWidth: undefined, maxLabelWidth: 120, showRowLabels: undefined, rowLabelFormatter: undefined, rowIcon: undefined, rowLabelStyle: undefined, rowLabelWidth: undefined, rowMaxLabelWidth: undefined, rowLabelTextAlign: TextAlign.Right, | ||
// Arrows | ||
arrows: undefined, | ||
// Animation | ||
animationLineEnterPosition: undefined, | ||
// Callbacks | ||
onScroll: undefined }); | ||
export { TimelineDefaultConfig }; | ||
//# sourceMappingURL=config.js.map |
import { XYComponentCore } from "../../core/xy-component"; | ||
import { Spacing } from "../../types"; | ||
import { TimelineConfigInterface } from './config'; | ||
@@ -15,4 +16,6 @@ import * as s from './style'; | ||
private _rowsGroup; | ||
private _arrowsGroup; | ||
private _linesGroup; | ||
private _labelsGroup; | ||
private _rowIconsGroup; | ||
private _scrollBarGroup; | ||
@@ -27,18 +30,26 @@ private _scrollBarBackground; | ||
private _labelMargin; | ||
private _labelWidth; | ||
private _rowIconBleed; | ||
private _lineBleed; | ||
/** We define a dedicated clipping path for this component because it needs to behave | ||
* differently than the regular XYContainer's clipPath */ | ||
private _clipPathId; | ||
private _clipPath; | ||
constructor(config?: TimelineConfigInterface<Datum>); | ||
get bleed(): { | ||
top: number; | ||
bottom: number; | ||
left: number; | ||
right: number; | ||
}; | ||
setConfig(config: TimelineConfigInterface<Datum>): void; | ||
setData(data: Datum[]): void; | ||
get bleed(): Spacing; | ||
_render(customDuration?: number): void; | ||
private _positionLines; | ||
private _getLineLength; | ||
private _getLineWidth; | ||
private _getLineDuration; | ||
private _prepareLinesData; | ||
private _prepareArrowsData; | ||
private _renderLines; | ||
private _onScrollbarDrag; | ||
private _onMouseWheel; | ||
private _updateScrollPosition; | ||
private _getMaxLineWidth; | ||
private _getRecordType; | ||
private _getRecordLabels; | ||
private _getRecordKey; | ||
private _getRowLabels; | ||
getXDataExtent(): number[]; | ||
} |
import { select } from 'd3-selection'; | ||
import { max, min, minIndex } from 'd3-array'; | ||
import { scaleOrdinal } from 'd3-scale'; | ||
import { drag } from 'd3-drag'; | ||
import { max } from 'd3-array'; | ||
import { XYComponentCore } from '../../core/xy-component/index.js'; | ||
import { isNumber, unique, arrayOfIndices, getString, getNumber, getMin, getMax } from '../../utils/data.js'; | ||
import { getNumber, isNumber, arrayOfIndices, getValue, isPlainObject, isFunction, getString, groupBy, getMin, getMax } from '../../utils/data.js'; | ||
import { smartTransition } from '../../utils/d3.js'; | ||
import { getColor } from '../../utils/color.js'; | ||
import { trimSVGText } from '../../utils/text.js'; | ||
import { trimSVGText, textAlignToAnchor } from '../../utils/text.js'; | ||
import { arrowPolylinePath } from '../../utils/path.js'; | ||
import { guid } from '../../utils/misc.js'; | ||
import '../../types.js'; | ||
import { TimelineDefaultConfig } from './config.js'; | ||
import * as style from './style.js'; | ||
import { background, rows, lines, labels, scrollbar, scrollbarBackground, scrollbarHandle, label, row, rowOdd, line } from './style.js'; | ||
import { background, rows, arrows, lines, labels, rowIcons, scrollbar, scrollbarBackground, scrollbarHandle, label, rowIcon, row, rowOdd, lineGroup, line, lineStartIcon, lineEndIcon, arrow } from './style.js'; | ||
import { TIMELINE_DEFAULT_ARROW_HEAD_LENGTH, TIMELINE_DEFAULT_ARROW_HEAD_WIDTH, TIMELINE_DEFAULT_ARROW_MARGIN } from './constants.js'; | ||
import { getIconBleed } from './utils.js'; | ||
import { TextAlign } from '../../types/text.js'; | ||
import { Arrangement } from '../../types/position.js'; | ||
@@ -20,2 +27,8 @@ class Timeline extends XYComponentCore { | ||
this.events = { | ||
[Timeline.selectors.background]: { | ||
wheel: this._onMouseWheel.bind(this), | ||
}, | ||
[Timeline.selectors.label]: { | ||
wheel: this._onMouseWheel.bind(this), | ||
}, | ||
[Timeline.selectors.rows]: { | ||
@@ -34,2 +47,8 @@ wheel: this._onMouseWheel.bind(this), | ||
this._labelMargin = 5; | ||
this._labelWidth = 0; // Will be overridden in `get bleed ()` | ||
this._rowIconBleed = [0, 0]; | ||
this._lineBleed = [0, 0]; | ||
/** We define a dedicated clipping path for this component because it needs to behave | ||
* differently than the regular XYContainer's clipPath */ | ||
this._clipPathId = guid(); | ||
if (config) | ||
@@ -39,6 +58,15 @@ this.setConfig(config); | ||
this._background = this.g.append('rect').attr('class', background); | ||
// Clip path | ||
this._clipPath = this.g.append('clipPath') | ||
.attr('id', this._clipPathId); | ||
this._clipPath.append('rect'); | ||
// Group for content | ||
this._rowsGroup = this.g.append('g').attr('class', rows); | ||
this._linesGroup = this.g.append('g').attr('class', lines); | ||
this._rowsGroup = this.g.append('g').attr('class', rows) | ||
.style('clip-path', `url(#${this._clipPathId})`); | ||
this._arrowsGroup = this.g.append('g').attr('class', arrows) | ||
.style('clip-path', `url(#${this._clipPathId})`); | ||
this._linesGroup = this.g.append('g').attr('class', lines) | ||
.style('clip-path', `url(#${this._clipPathId})`); | ||
this._labelsGroup = this.g.append('g').attr('class', labels); | ||
this._rowIconsGroup = this.g.append('g').attr('class', rowIcons); | ||
this._scrollBarGroup = this.g.append('g').attr('class', scrollbar); | ||
@@ -55,31 +83,76 @@ // Scroll bar | ||
} | ||
setConfig(config) { | ||
super.setConfig(config); | ||
} | ||
setData(data) { | ||
super.setData(data); | ||
} | ||
get bleed() { | ||
var _a, _b, _c, _d; | ||
const { config, datamodel: { data } } = this; | ||
const rowLabels = this._getRowLabels(data); | ||
const rowHeight = config.rowHeight || (this._height / rowLabels.length); | ||
const hasIcons = rowLabels.some(l => l.iconHref); | ||
const maxIconSize = max(rowLabels.map(l => l.iconSize || 0)); | ||
// We calculate the longest label width to set the bleed values accordingly | ||
let labelsBleed = 0; | ||
if (config.showLabels) { | ||
if (config.labelWidth) | ||
labelsBleed = config.labelWidth + this._labelMargin; | ||
if ((_a = config.showRowLabels) !== null && _a !== void 0 ? _a : config.showLabels) { | ||
if ((_b = config.rowLabelWidth) !== null && _b !== void 0 ? _b : config.labelWidth) | ||
this._labelWidth = ((_c = config.rowLabelWidth) !== null && _c !== void 0 ? _c : config.labelWidth) + this._labelMargin; | ||
else { | ||
const recordLabels = this._getRecordLabels(data); | ||
const longestLabel = recordLabels.reduce((acc, val) => acc.length > val.length ? acc : val, ''); | ||
const longestLabel = rowLabels.reduce((longestLabel, l) => longestLabel.formattedLabel.length > l.formattedLabel.length ? longestLabel : l, rowLabels[0]); | ||
const label$1 = this._labelsGroup.append('text') | ||
.attr('class', label) | ||
.text(longestLabel) | ||
.call(trimSVGText, config.maxLabelWidth); | ||
.text((longestLabel === null || longestLabel === void 0 ? void 0 : longestLabel.formattedLabel) || '') | ||
.call(trimSVGText, (_d = config.rowMaxLabelWidth) !== null && _d !== void 0 ? _d : config.maxLabelWidth); | ||
const labelWidth = label$1.node().getBBox().width; | ||
this._labelsGroup.empty(); | ||
label$1.remove(); | ||
const tolerance = 1.15; // Some characters are wider than others so we add a little of extra space to take that into account | ||
labelsBleed = labelWidth ? tolerance * labelWidth + this._labelMargin : 0; | ||
this._labelWidth = labelWidth ? tolerance * labelWidth + this._labelMargin : 0; | ||
} | ||
} | ||
const maxLineWidth = this._getMaxLineWidth(); | ||
// There can be multiple start / end items with the same timestamp, so we need to find the shortest one | ||
const minTimestamp = min(data, (d, i) => getNumber(d, config.x, i)); | ||
const dataMin = data.filter((d, i) => getNumber(d, config.x, i) === minTimestamp); | ||
const dataMinShortestItemIdx = minIndex(dataMin, (d, i) => this._getLineDuration(d, i)); | ||
const firstItemIdx = data.findIndex(d => d === dataMin[dataMinShortestItemIdx]); | ||
const firstItem = data[firstItemIdx]; | ||
const maxTimestamp = max(data, (d, i) => getNumber(d, config.x, i) + this._getLineDuration(d, i)); | ||
const dataMax = data.filter((d, i) => getNumber(d, config.x, i) + this._getLineDuration(d, i) === maxTimestamp); | ||
const dataMaxShortestItemIdx = minIndex(dataMax, (d, i) => this._getLineDuration(d, i)); | ||
const lastItemIdx = data.findIndex(d => d === dataMax[dataMaxShortestItemIdx]); | ||
const lastItem = data[lastItemIdx]; | ||
// Small segments bleed | ||
const lineBleed = [1, 1]; | ||
if (config.showEmptySegments && config.lineCap && firstItem && lastItem) { | ||
const firstItemStart = getNumber(firstItem, config.x, firstItemIdx); | ||
const firstItemEnd = getNumber(firstItem, config.x, firstItemIdx) + this._getLineDuration(firstItem, firstItemIdx); | ||
const lastItemStart = getNumber(lastItem, config.x, lastItemIdx); | ||
const lastItemEnd = getNumber(lastItem, config.x, lastItemIdx) + this._getLineDuration(lastItem, lastItemIdx); | ||
const fullTimeRange = lastItemEnd - firstItemStart; | ||
const firstItemHeight = this._getLineWidth(firstItem, firstItemIdx, rowHeight); | ||
const lastItemHeight = this._getLineWidth(lastItem, lastItemIdx, rowHeight); | ||
if ((firstItemEnd - firstItemStart) / fullTimeRange * this._width < firstItemHeight) | ||
lineBleed[0] = firstItemHeight / 2; | ||
if ((lastItemEnd - lastItemStart) / fullTimeRange * this._width < lastItemHeight) | ||
lineBleed[1] = lastItemHeight / 2; | ||
} | ||
this._lineBleed = lineBleed; | ||
// Icon bleed | ||
const iconBleed = [0, 0]; | ||
if (config.lineStartIcon && firstItem) { | ||
iconBleed[0] = getIconBleed(firstItem, firstItemIdx, config.lineStartIcon, config.lineStartIconSize, config.lineStartIconArrangement, rowHeight); | ||
} | ||
if (config.lineEndIcon && lastItem) { | ||
iconBleed[1] = getIconBleed(lastItem, lastItemIdx, config.lineEndIcon, config.lineEndIconSize, config.lineEndIconArrangement, rowHeight); | ||
} | ||
this._rowIconBleed = iconBleed; | ||
return { | ||
top: 0, | ||
bottom: 0, | ||
left: maxLineWidth / 2 + labelsBleed, | ||
right: maxLineWidth / 2 + this._scrollBarWidth + this._scrollBarMargin, | ||
left: this._labelWidth + iconBleed[0] + (hasIcons ? maxIconSize : 0) + lineBleed[0], | ||
right: this._scrollBarWidth + this._scrollBarMargin + iconBleed[1] + lineBleed[1], | ||
}; | ||
} | ||
_render(customDuration) { | ||
var _a; | ||
super._render(customDuration); | ||
@@ -92,9 +165,9 @@ const { config, datamodel: { data } } = this; | ||
const yHeight = Math.abs(yRange[1] - yRange[0]); | ||
const maxLineWidth = this._getMaxLineWidth(); | ||
const recordLabels = this._getRecordLabels(data); | ||
const recordLabelsUnique = unique(recordLabels); | ||
const numUniqueRecords = recordLabelsUnique.length; | ||
// Ordinal scale to handle records on the same type | ||
const ordinalScale = scaleOrdinal(); | ||
ordinalScale.range(arrayOfIndices(numUniqueRecords)); | ||
const rowLabels = this._getRowLabels(data); | ||
const numRowLabels = rowLabels.length; | ||
const rowHeight = config.rowHeight || (yHeight / numRowLabels); | ||
const yOrdinalScale = scaleOrdinal() | ||
.range(arrayOfIndices(numRowLabels)) | ||
.domain(rowLabels.map(l => l.label)); | ||
const lineDataPrepared = this._prepareLinesData(data, yOrdinalScale, rowHeight); | ||
// Invisible Background rect to track events | ||
@@ -105,57 +178,171 @@ this._background | ||
.attr('opacity', 0); | ||
// Row Icons | ||
const rowIcons = this._rowIconsGroup.selectAll(`.${rowIcon}`) | ||
.data(rowLabels.filter(d => d.iconSize), l => l === null || l === void 0 ? void 0 : l.label); | ||
const rowIconsEnter = rowIcons.enter().append('use') | ||
.attr('class', rowIcon) | ||
.attr('x', 0) | ||
.attr('width', l => l.iconSize) | ||
.attr('height', l => l.iconSize) | ||
.attr('y', l => yStart + (yOrdinalScale(l.label) + 0.5) * rowHeight - l.iconSize / 2) | ||
.style('opacity', 0); | ||
smartTransition(rowIconsEnter.merge(rowIcons), duration) | ||
.attr('href', l => l.iconHref) | ||
.attr('x', 0) | ||
.attr('y', l => yStart + (yOrdinalScale(l.label) + 0.5) * rowHeight - l.iconSize / 2) | ||
.attr('width', l => l.iconSize) | ||
.attr('height', l => l.iconSize) | ||
.style('color', l => l.iconColor) | ||
.style('opacity', 1); | ||
smartTransition(rowIcons.exit(), duration) | ||
.style('opacity', 0) | ||
.remove(); | ||
// Labels | ||
const labels = this._labelsGroup.selectAll(`.${label}`) | ||
.data(config.showLabels ? recordLabelsUnique : []); | ||
.data(((_a = config.showRowLabels) !== null && _a !== void 0 ? _a : config.showLabels) ? rowLabels : [], l => l === null || l === void 0 ? void 0 : l.label); | ||
const labelOffset = config.rowLabelTextAlign === TextAlign.Center ? this._labelWidth / 2 | ||
: config.rowLabelTextAlign === TextAlign.Left ? this._labelWidth | ||
: this._labelMargin; | ||
const xStart = xRange[0] - this._rowIconBleed[0] - this._lineBleed[0]; | ||
const labelXStart = xStart - labelOffset; | ||
const labelsEnter = labels.enter().append('text') | ||
.attr('class', label); | ||
labelsEnter.merge(labels) | ||
.attr('x', xRange[0] - maxLineWidth / 2 - this._labelMargin) | ||
.attr('y', (label, i) => yStart + (ordinalScale(label) + 0.5) * config.rowHeight) | ||
.text(label => label) | ||
.attr('class', label) | ||
.attr('x', labelXStart) | ||
.attr('y', l => yStart + (yOrdinalScale(l.label) + 0.5) * rowHeight) | ||
.style('opacity', 0); | ||
const labelsMerged = labelsEnter.merge(labels) | ||
.text(l => l.formattedLabel) | ||
.each((label, i, els) => { | ||
trimSVGText(select(els[i]), config.labelWidth || config.maxLabelWidth); | ||
}); | ||
labels.exit().remove(); | ||
var _a, _b; | ||
const labelSelection = select(els[i]); | ||
trimSVGText(labelSelection, ((_a = config.rowLabelWidth) !== null && _a !== void 0 ? _a : config.labelWidth) || ((_b = config.rowMaxLabelWidth) !== null && _b !== void 0 ? _b : config.maxLabelWidth)); | ||
// Apply custom label style if it has been provided | ||
const customStyle = getValue(label, config.rowLabelStyle); | ||
if (!isPlainObject(customStyle)) | ||
return; | ||
for (const [prop, value] of Object.entries(customStyle)) { | ||
labelSelection.style(prop, value); | ||
} | ||
}) | ||
.style('text-anchor', textAlignToAnchor(config.rowLabelTextAlign)); | ||
smartTransition(labelsMerged, duration) | ||
.attr('x', labelXStart) | ||
.attr('y', l => yStart + (yOrdinalScale(l.label) + 0.5) * rowHeight) | ||
.style('opacity', 1); | ||
smartTransition(labels.exit(), duration) | ||
.style('opacity', 0) | ||
.remove(); | ||
// Row background rects | ||
const xStart = xRange[0]; | ||
const numRows = Math.max(Math.floor(yHeight / config.rowHeight), numUniqueRecords); | ||
const recordTypes = Array(numRows).fill(null).map((_, i) => recordLabelsUnique[i]); | ||
const timelineWidth = xRange[1] - xRange[0] + this._rowIconBleed[0] + this._rowIconBleed[1] + this._lineBleed[0] + this._lineBleed[1]; | ||
const numRows = Math.max(Math.floor(yHeight / rowHeight), numRowLabels); | ||
const recordTypes = Array(numRows).fill(null).map((_, i) => rowLabels[i]); | ||
const rects = this._rowsGroup.selectAll(`.${row}`) | ||
.data(recordTypes); | ||
const rectsEnter = rects.enter().append('rect') | ||
.attr('class', row); | ||
rectsEnter.merge(rects) | ||
.classed(rowOdd, config.alternatingRowColors ? (_, i) => !(i % 2) : null) | ||
.attr('x', xStart - maxLineWidth / 2) | ||
.attr('width', xRange[1] - xStart + maxLineWidth) | ||
.attr('y', (_, i) => yStart + i * config.rowHeight) | ||
.attr('height', config.rowHeight); | ||
rects.exit().remove(); | ||
.attr('class', row) | ||
.attr('x', xStart) | ||
.attr('width', timelineWidth) | ||
.attr('y', (_, i) => yStart + i * rowHeight) | ||
.attr('height', rowHeight) | ||
.style('opacity', 0); | ||
const rectsMerged = rectsEnter.merge(rects) | ||
.classed(rowOdd, config.alternatingRowColors ? (_, i) => !(i % 2) : null); | ||
smartTransition(rectsMerged, duration) | ||
.attr('x', xStart) | ||
.attr('width', timelineWidth) | ||
.attr('y', (_, i) => yStart + i * rowHeight) | ||
.attr('height', rowHeight) | ||
.style('opacity', 1); | ||
smartTransition(rects.exit(), duration) | ||
.style('opacity', 0) | ||
.remove(); | ||
// Lines | ||
const lines = this._linesGroup.selectAll(`.${line}`) | ||
.data(data, (d, i) => { | ||
var _a; | ||
return (_a = getString(d, config.id, i)) !== null && _a !== void 0 ? _a : [ | ||
this._getRecordType(d, i), getNumber(d, config.x, i), | ||
].join('-'); | ||
const lines = this._linesGroup.selectAll(`.${lineGroup}`) | ||
.data(lineDataPrepared, (d) => d._id); | ||
const linesEnter = lines.enter().append('g') | ||
.attr('class', lineGroup) | ||
.style('opacity', 0) | ||
.attr('transform', (d, i) => { | ||
var _a, _b; | ||
const configuredPos = isFunction(config.animationLineEnterPosition) | ||
? config.animationLineEnterPosition(d, i, lineDataPrepared) | ||
: config.animationLineEnterPosition; | ||
const [x, y] = [(_a = configuredPos === null || configuredPos === void 0 ? void 0 : configuredPos[0]) !== null && _a !== void 0 ? _a : d._xPx, (_b = configuredPos === null || configuredPos === void 0 ? void 0 : configuredPos[1]) !== null && _b !== void 0 ? _b : d._yPx]; | ||
return `translate(${x}, ${y})`; | ||
}); | ||
const linesEnter = lines.enter().append('rect') | ||
linesEnter.append('rect') | ||
.attr('class', line) | ||
.classed(rowOdd, config.alternatingRowColors | ||
? (d, i) => !(recordLabelsUnique.indexOf(this._getRecordType(d, i)) % 2) | ||
: null) | ||
.style('fill', (d, i) => getColor(d, config.color, ordinalScale(this._getRecordType(d, i)))) | ||
.call(this._positionLines.bind(this), ordinalScale) | ||
.attr('transform', 'translate(0, 10)') | ||
.style('opacity', 0); | ||
const linesMerged = linesEnter.merge(lines) | ||
.style('fill', (d, i) => getColor(d, config.color, ordinalScale(this._getRecordType(d, i)))) | ||
.style('cursor', (d, i) => getString(d, config.cursor, i)) | ||
.call(this._positionLines.bind(this), ordinalScale); | ||
.style('fill', (d, i) => getColor(d, config.color, yOrdinalScale(this._getRecordKey(d, i)))) | ||
.call(this._renderLines.bind(this), rowHeight); | ||
linesEnter.append('use').attr('class', lineStartIcon); | ||
linesEnter.append('use').attr('class', lineEndIcon); | ||
const linesMerged = linesEnter.merge(lines); | ||
smartTransition(linesMerged, duration) | ||
.attr('transform', 'translate(0, 0)') | ||
.attr('transform', d => `translate(${d._xPx + d._xOffsetPx}, ${d._yPx})`) | ||
.style('opacity', 1); | ||
smartTransition(lines.exit(), duration) | ||
const lineRectElementsSelection = linesMerged.selectAll(`.${line}`) | ||
.data(d => [d]); | ||
smartTransition(lineRectElementsSelection, duration) | ||
.style('fill', (d, i) => getColor(d, config.color, yOrdinalScale(this._getRecordKey(d, i)))) | ||
.style('cursor', (d, i) => { var _a; return getString(d, (_a = config.lineCursor) !== null && _a !== void 0 ? _a : config.cursor, i); }) | ||
.call(this._renderLines.bind(this), rowHeight); | ||
linesMerged.selectAll(`.${lineStartIcon}`) | ||
.data(d => [d]) | ||
.attr('href', (d, i) => getString(d, config.lineStartIcon, i)) | ||
.attr('x', (d, i) => { | ||
const iconSize = d._startIconSize; | ||
const iconArrangement = d._startIconArrangement; | ||
const offset = iconArrangement === Arrangement.Inside ? 0 | ||
: iconArrangement === Arrangement.Center ? -iconSize / 2 | ||
: -iconSize; | ||
return offset; | ||
}) | ||
.attr('y', d => (-(d._startIconSize - d._height) / 2) || 0) | ||
.attr('width', d => d._startIconSize) | ||
.attr('height', d => d._startIconSize) | ||
.style('color', d => d._startIconColor); | ||
linesMerged.selectAll(`.${lineEndIcon}`) | ||
.data(d => [d]) | ||
.attr('href', (d, i) => getString(d, config.lineEndIcon, i)) | ||
.attr('x', (d, i) => { | ||
const lineLength = d._lengthCorrected; | ||
const iconSize = d._endIconSize; | ||
const iconArrangement = d._endIconArrangement; | ||
const offset = iconArrangement === Arrangement.Inside ? -iconSize | ||
: iconArrangement === Arrangement.Center ? -iconSize / 2 | ||
: 0; | ||
return lineLength + offset; | ||
}) | ||
.attr('y', d => -((d._endIconSize - d._height) / 2) || 0) | ||
.attr('width', d => d._endIconSize) | ||
.attr('height', d => d._endIconSize) | ||
.style('color', d => d._endIconColor); | ||
const linesExit = lines.exit(); | ||
smartTransition(linesExit, duration) | ||
.style('opacity', 0) | ||
.attr('transform', (d, i) => { | ||
var _a, _b; | ||
const configuredPos = isFunction(config.animationLineExitPosition) | ||
? config.animationLineExitPosition(d, i, lineDataPrepared) | ||
: config.animationLineExitPosition; | ||
const [x, y] = [(_a = configuredPos === null || configuredPos === void 0 ? void 0 : configuredPos[0]) !== null && _a !== void 0 ? _a : d._xPx, (_b = configuredPos === null || configuredPos === void 0 ? void 0 : configuredPos[1]) !== null && _b !== void 0 ? _b : d._yPx]; | ||
return `translate(${x}, ${y})`; | ||
}) | ||
.remove(); | ||
// Arrows | ||
const arrowsData = this._prepareArrowsData(data, yOrdinalScale, rowHeight); | ||
const arrows = this._arrowsGroup.selectAll(`.${arrow}`) | ||
.data(arrowsData !== null && arrowsData !== void 0 ? arrowsData : [], d => d.id); | ||
const arrowsEnter = arrows.enter().append('path') | ||
.attr('class', arrow) | ||
.style('opacity', 0); | ||
smartTransition(arrowsEnter.merge(arrows), duration) | ||
.attr('d', (d) => { | ||
var _a, _b; | ||
return arrowPolylinePath(d._points, (_a = d.arrowHeadLength) !== null && _a !== void 0 ? _a : TIMELINE_DEFAULT_ARROW_HEAD_LENGTH, (_b = d.arrowHeadWidth) !== null && _b !== void 0 ? _b : TIMELINE_DEFAULT_ARROW_HEAD_WIDTH); | ||
}) | ||
.style('opacity', 1); | ||
smartTransition(arrows.exit(), duration) | ||
.style('opacity', 0) | ||
.remove(); | ||
// Scroll Bar | ||
@@ -180,28 +367,109 @@ const contentBBox = this._rowsGroup.node().getBBox(); // We determine content size using the rects group because lines are animated | ||
this._updateScrollPosition(0); | ||
// Clip path | ||
const clipPathRect = this._clipPath.select('rect'); | ||
smartTransition(clipPathRect, clipPathRect.attr('width') ? duration : 0) | ||
.attr('x', xStart) | ||
.attr('width', timelineWidth) | ||
.attr('height', this._height); | ||
} | ||
_positionLines(selection, ordinalScale) { | ||
_getLineLength(d, i) { | ||
var _a, _b; | ||
const { config, xScale } = this; | ||
const x = getNumber(d, config.x, i); | ||
const length = (_b = getNumber(d, (_a = config.lineDuration) !== null && _a !== void 0 ? _a : config.length, i)) !== null && _b !== void 0 ? _b : 0; | ||
const lineLength = xScale(x + length) - xScale(x); | ||
return lineLength; | ||
} | ||
_getLineWidth(d, i, rowHeight) { | ||
var _a; | ||
const { config } = this; | ||
return (_a = getNumber(d, config.lineWidth, i)) !== null && _a !== void 0 ? _a : Math.max(Math.floor(rowHeight / 2), 1); | ||
} | ||
_getLineDuration(d, i) { | ||
var _a, _b; | ||
const { config } = this; | ||
return (_b = getNumber(d, (_a = config.lineDuration) !== null && _a !== void 0 ? _a : config.length, i)) !== null && _b !== void 0 ? _b : 0; | ||
} | ||
_prepareLinesData(data, rowOrdinalScale, rowHeight) { | ||
const { config, xScale, yScale } = this; | ||
const yRange = yScale.range(); | ||
const yStart = Math.min(...yRange); | ||
selection.each((d, i, elements) => { | ||
var _a; | ||
const x = getNumber(d, config.x, i); | ||
const y = ordinalScale(this._getRecordType(d, i)) * config.rowHeight; | ||
const length = (_a = getNumber(d, config.length, i)) !== null && _a !== void 0 ? _a : 0; | ||
// Rect dimensions | ||
const height = getNumber(d, config.lineWidth, i); | ||
const width = xScale(x + length) - xScale(x); | ||
if (width < 0) { | ||
return data.map((d, i) => { | ||
var _a, _b, _c, _d, _e; | ||
const id = (_a = getString(d, config.id, i)) !== null && _a !== void 0 ? _a : [ | ||
this._getRecordKey(d, i), getNumber(d, config.x, i), | ||
].join('-'); | ||
const lineHeight = this._getLineWidth(d, i, rowHeight); | ||
const lineLength = this._getLineLength(d, i); | ||
if (lineLength < 0) { | ||
console.warn('Unovis | Timeline: Line segments should not have negative lengths. Setting to 0.'); | ||
} | ||
select(elements[i]) | ||
.attr('x', xScale(x)) | ||
.attr('y', yStart + y + (config.rowHeight - height) / 2) | ||
.attr('width', config.showEmptySegments | ||
? Math.max(config.lineCap ? height : 1, width) | ||
: Math.max(0, width)) | ||
.attr('height', height) | ||
.attr('rx', config.lineCap ? height / 2 : null); | ||
const isLineTooShort = config.showEmptySegments && config.lineCap && (lineLength < lineHeight); | ||
const lineLengthCorrected = config.showEmptySegments | ||
? Math.max(config.lineCap ? lineHeight : 1, lineLength) | ||
: Math.max(0, lineLength); | ||
const x = xScale(getNumber(d, config.x, i)); | ||
const y = yStart + rowOrdinalScale(this._getRecordKey(d, i)) * rowHeight + (rowHeight - lineHeight) / 2; | ||
const xOffset = isLineTooShort ? -(lineLengthCorrected - lineLength) / 2 : 0; | ||
return Object.assign(Object.assign({}, d), { _id: id, _xPx: x, _yPx: y, _xOffsetPx: xOffset, _length: lineLength, _height: lineHeight, _lengthCorrected: lineLengthCorrected, _startIconSize: (_b = getNumber(d, config.lineStartIconSize, i)) !== null && _b !== void 0 ? _b : lineHeight, _endIconSize: (_c = getNumber(d, config.lineEndIconSize, i)) !== null && _c !== void 0 ? _c : lineHeight, _startIconColor: getString(d, config.lineStartIconColor, i), _endIconColor: getString(d, config.lineEndIconColor, i), _startIconArrangement: (_d = getValue(d, config.lineStartIconArrangement, i)) !== null && _d !== void 0 ? _d : Arrangement.Outside, _endIconArrangement: (_e = getValue(d, config.lineEndIconArrangement, i)) !== null && _e !== void 0 ? _e : Arrangement.Outside }); | ||
}); | ||
} | ||
_prepareArrowsData(data, rowOrdinalScale, rowHeight) { | ||
var _a; | ||
const { config } = this; | ||
const arrowsData = (_a = config.arrows) === null || _a === void 0 ? void 0 : _a.map(a => { | ||
var _a, _b, _c, _d; | ||
const sourceLineIndex = data.findIndex((d, i) => getString(d, config.id, i) === a.lineSourceId); | ||
const targetLineIndex = data.findIndex((d, i) => getString(d, config.id, i) === a.lineTargetId); | ||
const sourceLine = data[sourceLineIndex]; | ||
const targetLine = data[targetLineIndex]; | ||
if (!sourceLine || !targetLine) { | ||
console.warn('Unovis | Timeline: Arrow references a non-existent line. Skipping...', a); | ||
return undefined; | ||
} | ||
const sourceLineY = rowOrdinalScale(this._getRecordKey(sourceLine, sourceLineIndex)) * rowHeight + rowHeight / 2; | ||
const targetLineY = rowOrdinalScale(this._getRecordKey(targetLine, targetLineIndex)) * rowHeight + rowHeight / 2; | ||
const sourceLineWidth = this._getLineWidth(sourceLine, sourceLineIndex, rowHeight); | ||
const targetLineWidth = this._getLineWidth(targetLine, targetLineIndex, rowHeight); | ||
const x1 = (a.xSource | ||
? this.xScale(a.xSource) | ||
: this.xScale(getNumber(sourceLine, config.x, sourceLineIndex)) + this._getLineLength(sourceLine, sourceLineIndex)) + ((_a = a.xSourceOffsetPx) !== null && _a !== void 0 ? _a : 0); | ||
const targetLineStart = this.xScale(getNumber(targetLine, config.x, targetLineIndex)); | ||
const x2 = (a.xTarget ? this.xScale(a.xTarget) : targetLineStart) + ((_b = a.xTargetOffsetPx) !== null && _b !== void 0 ? _b : 0); | ||
const isX2OutsideTargetLineStart = (x2 < targetLineStart) || (x2 > targetLineStart); | ||
// Points array | ||
const sourceMargin = (_c = a.lineSourceMarginPx) !== null && _c !== void 0 ? _c : TIMELINE_DEFAULT_ARROW_MARGIN; | ||
const targetMargin = (_d = a.lineTargetMarginPx) !== null && _d !== void 0 ? _d : TIMELINE_DEFAULT_ARROW_MARGIN; | ||
const y1 = sourceLineY < targetLineY ? sourceLineY + sourceLineWidth / 2 + sourceMargin : sourceLineY - sourceLineWidth / 2 - sourceMargin; | ||
const y2 = sourceLineY < targetLineY ? targetLineY - targetLineWidth / 2 - targetMargin : targetLineY + targetLineWidth / 2 + targetMargin; | ||
const points = [[x1, y1]]; | ||
const threshold = 5; | ||
if (Math.abs(x2 - x1) > threshold) { | ||
if ((x1 < x2) && !isX2OutsideTargetLineStart) { | ||
points.push([x1, (y1 + targetLineY) / 2]); // A dummy point to enable smooth transitions when arrows change | ||
points.push([x1, targetLineY]); | ||
points.push([x2 - targetMargin, targetLineY]); | ||
} | ||
else { | ||
points.push([x1, y2 - Math.sign(targetLineY - sourceLineY) * (rowHeight / 4)]); | ||
points.push([x2, y2 - Math.sign(targetLineY - sourceLineY) * (rowHeight / 4)]); | ||
points.push([x2, y2]); | ||
} | ||
} | ||
else { | ||
points.push([x1, y1 + (y2 - y1) / 4]); // A dummy point to enable smooth transitions | ||
points.push([x1, y1 + 3 * (y2 - y1) / 4]); // A dummy point to enable smooth transitions | ||
points.push([x1, y2]); | ||
} | ||
return Object.assign(Object.assign({}, a), { _points: points }); | ||
}).filter(Boolean); | ||
return arrowsData; | ||
} | ||
_renderLines(selection) { | ||
const { config } = this; | ||
selection | ||
.attr('width', d => d._lengthCorrected) | ||
.attr('height', d => d._height) | ||
.attr('rx', d => config.lineCap ? d._height / 2 : null); | ||
} | ||
_onScrollbarDrag(event) { | ||
@@ -229,19 +497,31 @@ const yRange = this.yScale.range(); | ||
this._scrollDistance = Math.min(this._maxScroll, this._scrollDistance); | ||
this._clipPath.attr('transform', `translate(0,${this._scrollDistance})`); | ||
this._linesGroup.attr('transform', `translate(0,${-this._scrollDistance})`); | ||
this._rowsGroup.attr('transform', `translate(0,${-this._scrollDistance})`); | ||
this._labelsGroup.attr('transform', `translate(0,${-this._scrollDistance})`); | ||
this._rowIconsGroup.attr('transform', `translate(0,${-this._scrollDistance})`); | ||
this._arrowsGroup.attr('transform', `translate(0,${-this._scrollDistance})`); | ||
const scrollBarPosition = (this._scrollDistance / this._maxScroll * (yHeight - this._scrollbarHeight)) || 0; | ||
this._scrollBarHandle.attr('y', scrollBarPosition); | ||
} | ||
_getMaxLineWidth() { | ||
_getRecordKey(d, i) { | ||
var _a; | ||
const { config, datamodel: { data } } = this; | ||
return (_a = max(data, (d, i) => getNumber(d, config.lineWidth, i))) !== null && _a !== void 0 ? _a : 0; | ||
return getString(d, (_a = this.config.lineRow) !== null && _a !== void 0 ? _a : this.config.type) || `__${i}`; | ||
} | ||
_getRecordType(d, i) { | ||
return getString(d, this.config.type) || `__${i}`; | ||
_getRowLabels(data) { | ||
const grouped = groupBy(data, (d, i) => { var _a; return getString(d, (_a = this.config.lineRow) !== null && _a !== void 0 ? _a : this.config.type) || `${i + 1}`; }); | ||
const rowLabels = Object.entries(grouped).map(([key, items], i) => { | ||
var _a, _b, _c, _d, _e; | ||
const icon = (_b = (_a = this.config).rowIcon) === null || _b === void 0 ? void 0 : _b.call(_a, key, items, i); | ||
return { | ||
label: key, | ||
formattedLabel: (_e = (_d = (_c = this.config).rowLabelFormatter) === null || _d === void 0 ? void 0 : _d.call(_c, key, items, i)) !== null && _e !== void 0 ? _e : key, | ||
iconHref: icon === null || icon === void 0 ? void 0 : icon.href, | ||
iconSize: icon === null || icon === void 0 ? void 0 : icon.size, | ||
iconColor: icon === null || icon === void 0 ? void 0 : icon.color, | ||
data: items, | ||
}; | ||
}); | ||
return rowLabels; | ||
} | ||
_getRecordLabels(data) { | ||
return data.map((d, i) => getString(d, this.config.type) || `${i + 1}`); | ||
} | ||
// Override the default XYComponent getXDataExtent method to take into account line lengths | ||
@@ -251,3 +531,3 @@ getXDataExtent() { | ||
const min = getMin(datamodel.data, config.x); | ||
const max = getMax(datamodel.data, (d, i) => { var _a; return getNumber(d, config.x, i) + ((_a = getNumber(d, config.length, i)) !== null && _a !== void 0 ? _a : 0); }); | ||
const max = getMax(datamodel.data, (d, i) => { var _a, _b; return getNumber(d, config.x, i) + ((_b = getNumber(d, (_a = config.lineDuration) !== null && _a !== void 0 ? _a : config.length, i)) !== null && _b !== void 0 ? _b : 0); }); | ||
return [min, max]; | ||
@@ -254,0 +534,0 @@ } |
@@ -5,3 +5,8 @@ export declare const root: string; | ||
export declare const lines: string; | ||
export declare const lineGroup: string; | ||
export declare const line: string; | ||
export declare const lineStartIcon: string; | ||
export declare const lineEndIcon: string; | ||
export declare const arrows: string; | ||
export declare const arrow: string; | ||
export declare const rows: string; | ||
@@ -15,1 +20,3 @@ export declare const row: string; | ||
export declare const label: string; | ||
export declare const rowIcons: string; | ||
export declare const rowIcon: string; |
@@ -17,5 +17,11 @@ import { css, injectGlobal } from '@emotion/css'; | ||
--vis-timeline-arrow-color: #6C778C; | ||
--vis-timeline-arrow-stroke-width: 1.5; | ||
--vis-timeline-cursor: default; | ||
--vis-timeline-line-color: var(--vis-color-main); | ||
--vis-timeline-line-stroke-width: 0; | ||
--vis-timeline-line-hover-stroke-width: 0; | ||
--vis-timeline-line-hover-stroke-color: #6C778C; | ||
// The line stroke color variable is not defined by default | ||
@@ -30,2 +36,4 @@ // to allow it to fallback to the corresponding row background color | ||
--vis-dark-timeline-label-color: #EFF5F8; | ||
--vis-dark-timeline-arrow-color: #EFF5F8; | ||
--vis-dark-timeline-line-hover-stroke-color: #EFF5F8; | ||
} | ||
@@ -39,2 +47,4 @@ | ||
--vis-timeline-label-color: var(--vis-dark-timeline-label-color); | ||
--vis-timeline-arrow-color: var(--vis-dark-timeline-arrow-color); | ||
--vis-timeline-line-hover-stroke-color: var(--vis-dark-timeline-line-hover-stroke-color); | ||
} | ||
@@ -48,2 +58,5 @@ `; | ||
`; | ||
const lineGroup = css ` | ||
label: line-group; | ||
`; | ||
const line = css ` | ||
@@ -60,3 +73,23 @@ label: line; | ||
} | ||
:hover { | ||
stroke-width: var(--vis-timeline-line-hover-stroke-width); | ||
stroke: var(--vis-timeline-line-hover-stroke-color); | ||
} | ||
`; | ||
const lineStartIcon = css ` | ||
label: line-start-icon; | ||
`; | ||
const lineEndIcon = css ` | ||
label: line-end-icon; | ||
`; | ||
const arrows = css ` | ||
label: arrows; | ||
`; | ||
const arrow = css ` | ||
label: arrow; | ||
fill: none; | ||
stroke: var(--vis-timeline-arrow-color); | ||
stroke-width: var(--vis-timeline-arrow-stroke-width); | ||
`; | ||
const rows = css ` | ||
@@ -96,4 +129,10 @@ label: rows; | ||
`; | ||
const rowIcons = css ` | ||
label: row-icons; | ||
`; | ||
const rowIcon = css ` | ||
label: row-icon; | ||
`; | ||
export { background, globalStyles, label, labels, line, lines, root, row, rowOdd, rows, scrollbar, scrollbarBackground, scrollbarHandle }; | ||
export { arrow, arrows, background, globalStyles, label, labels, line, lineEndIcon, lineGroup, lineStartIcon, lines, root, row, rowIcon, rowIcons, rowOdd, rows, scrollbar, scrollbarBackground, scrollbarHandle }; | ||
//# sourceMappingURL=style.js.map |
@@ -5,2 +5,3 @@ import { ContainerConfigInterface } from "../../core/container/config"; | ||
import { Annotations } from "../../components/annotations"; | ||
import { Spacing } from "../../types/spacing"; | ||
export interface SingleContainerConfigInterface<Datum> extends ContainerConfigInterface { | ||
@@ -13,3 +14,5 @@ /** Visualization component. Default: `undefined` */ | ||
annotations?: Annotations | undefined; | ||
/** Callback function to be called when the chart rendering is complete. Default: `undefined` */ | ||
onRenderComplete?: (svgNode: SVGSVGElement, margin: Spacing, containerWidth: number, containerHeight: number, componentWidth: number, componentHeight: number) => void; | ||
} | ||
export declare const SingleContainerDefaultConfig: SingleContainerConfigInterface<unknown>; |
@@ -86,3 +86,3 @@ import '../../styles/index.js'; | ||
_render(duration) { | ||
var _a; | ||
var _a, _b; | ||
const { config, component } = this; | ||
@@ -95,2 +95,3 @@ super._render(duration); | ||
config.tooltip.update(); | ||
(_b = config.onRenderComplete) === null || _b === void 0 ? void 0 : _b.call(config, this.svg.node(), config.margin, this.containerWidth, this.containerHeight, this.width, this.height); | ||
} | ||
@@ -97,0 +98,0 @@ // Re-defining the `render()` function to handle different sizing techniques (`Sizing.Extend` and `Sizing.FitWidth`) |
@@ -9,2 +9,3 @@ import { XYComponentCore } from "../../core/xy-component"; | ||
import { Direction } from "../../types/direction"; | ||
import { Spacing } from "../../types/spacing"; | ||
export interface XYContainerConfigInterface<Datum> extends ContainerConfigInterface { | ||
@@ -84,3 +85,7 @@ /** An array of visualization components. Default: `[]` */ | ||
annotations?: Annotations | undefined; | ||
/** Extend the clip path by the specified number of pixels. Default: `2` */ | ||
clipPathExtend?: number; | ||
/** Callback function to be called when the chart rendering is complete. Default: `undefined` */ | ||
onRenderComplete?: (svgNode: SVGSVGElement, margin: Spacing, bleed: Spacing, containerWidth: number, containerHeight: number, componentWidth: number, componentHeight: number) => void; | ||
} | ||
export declare const XYContainerDefaultConfig: XYContainerConfigInterface<unknown>; |
import { ContainerDefaultConfig } from '../../core/container/config.js'; | ||
import { Direction } from '../../types/direction.js'; | ||
const XYContainerDefaultConfig = Object.assign(Object.assign({}, ContainerDefaultConfig), { components: [], tooltip: undefined, crosshair: undefined, annotations: undefined, xAxis: undefined, yAxis: undefined, autoMargin: true, xScale: undefined, xDomain: undefined, xDomainMinConstraint: undefined, xDomainMaxConstraint: undefined, xRange: undefined, yScale: undefined, yDomain: undefined, yDomainMinConstraint: undefined, yDomainMaxConstraint: undefined, yRange: undefined, yDirection: Direction.North, preventEmptyDomain: null, scaleByDomain: false }); | ||
const XYContainerDefaultConfig = Object.assign(Object.assign({}, ContainerDefaultConfig), { components: [], tooltip: undefined, crosshair: undefined, annotations: undefined, xAxis: undefined, yAxis: undefined, autoMargin: true, xScale: undefined, xDomain: undefined, xDomainMinConstraint: undefined, xDomainMaxConstraint: undefined, xRange: undefined, yScale: undefined, yDomain: undefined, yDomainMinConstraint: undefined, yDomainMaxConstraint: undefined, yRange: undefined, yDirection: Direction.North, preventEmptyDomain: null, scaleByDomain: false, clipPathExtend: 2 }); | ||
export { XYContainerDefaultConfig }; | ||
//# sourceMappingURL=config.js.map |
@@ -36,3 +36,4 @@ import { Selection } from 'd3-selection'; | ||
private _getMargin; | ||
private _getBleed; | ||
destroy(): void; | ||
} |
@@ -168,3 +168,3 @@ import { css } from '@emotion/css'; | ||
_render(customDuration) { | ||
var _a, _b, _c, _d, _e; | ||
var _a, _b, _c, _d, _e, _f; | ||
const { config } = this; | ||
@@ -182,5 +182,5 @@ super._render(); | ||
this._renderAxes(this._firstRender ? 0 : customDuration); | ||
// Clip RectsetSize | ||
// Clip Rect | ||
// Extending the clipping path to allow small overflow (e.g. Line will looks better that way when it touches the edges) | ||
const clipPathExtension = 2; | ||
const clipPathExtension = config.clipPathExtend; | ||
this._clipPath.select('rect') | ||
@@ -215,2 +215,3 @@ .attr('x', -clipPathExtension) | ||
this._firstRender = false; | ||
(_f = config.onRenderComplete) === null || _f === void 0 ? void 0 : _f.call(config, this.svg.node(), margin, this._getBleed(this.components), this.containerWidth, this.containerHeight, this.width, this.height); | ||
} | ||
@@ -279,10 +280,3 @@ _updateScales(...components) { | ||
// Get and combine bleed | ||
const bleed = components.map(c => c.bleed).reduce((bleed, b) => { | ||
for (const key of Object.keys(bleed)) { | ||
const k = key; | ||
if (bleed[k] < b[k]) | ||
bleed[k] = b[k]; | ||
} | ||
return bleed; | ||
}, { top: 0, bottom: 0, left: 0, right: 0 }); | ||
const bleed = this._getBleed(components); | ||
// Update scale range | ||
@@ -346,2 +340,12 @@ for (const c of components) { | ||
} | ||
_getBleed(components) { | ||
return components.map(c => c.bleed).reduce((bleed, b) => { | ||
for (const key of Object.keys(bleed)) { | ||
const k = key; | ||
if (bleed[k] < b[k]) | ||
bleed[k] = b[k]; | ||
} | ||
return bleed; | ||
}, { top: 0, bottom: 0, left: 0, right: 0 }); | ||
} | ||
destroy() { | ||
@@ -348,0 +352,0 @@ const { components, config: { tooltip, crosshair, annotations, xAxis, yAxis } } = this; |
import { Sizing } from '../../types/component.js'; | ||
// Core | ||
// Types | ||
const ContainerDefaultConfig = { | ||
@@ -5,0 +6,0 @@ duration: undefined, |
@@ -13,7 +13,7 @@ import { GraphInputLink, GraphInputNode, GraphLinkCore, GraphNodeCore } from "../types/graph"; | ||
private _inputNodesMap; | ||
private _nodeIds; | ||
private _nodesMap; | ||
nodeId: ((n: N) => string | undefined); | ||
linkId: ((n: L) => string | undefined); | ||
nodeSort: ((a: N, b: N) => number); | ||
getNodeFromId(id: string | number): OutNode; | ||
getNodeById(id: string | number): OutNode; | ||
get data(): GraphData<N, L>; | ||
@@ -27,2 +27,3 @@ set data(inputData: GraphData<N, L>); | ||
private transferState; | ||
setNodeStateById(id: string, state: Record<string, any>): void; | ||
} |
@@ -10,3 +10,3 @@ import { isString, cloneDeep, isFunction, isUndefined, without, isNumber, isObject, isEqual } from '../utils/data.js'; | ||
this._inputNodesMap = new Map(); | ||
this._nodeIds = new Map(); | ||
this._nodesMap = new Map(); | ||
// Model configuration | ||
@@ -16,4 +16,4 @@ this.nodeId = n => (isString(n.id) || isFinite(n.id)) ? `${n.id}` : undefined; | ||
} | ||
getNodeFromId(id) { | ||
return this._nodeIds.get(id); | ||
getNodeById(id) { | ||
return this._nodesMap.get(id); | ||
} | ||
@@ -31,3 +31,4 @@ get data() { | ||
this._inputNodesMap.clear(); | ||
this._nodeIds.clear(); | ||
this._nodesMap.clear(); | ||
// Todo: Figure out why TypeScript complains about types | ||
const nodes = cloneDeep((_a = inputData === null || inputData === void 0 ? void 0 : inputData.nodes) !== null && _a !== void 0 ? _a : []); | ||
@@ -44,3 +45,3 @@ const links = cloneDeep((_b = inputData === null || inputData === void 0 ? void 0 : inputData.links) !== null && _b !== void 0 ? _b : []); | ||
this._inputNodesMap.set(node, inputData.nodes[i]); | ||
this._nodeIds.set(node._id, node); | ||
this._nodesMap.set(node._id, node); | ||
}); | ||
@@ -101,3 +102,3 @@ // Sort nodes | ||
if (!foundNode) { | ||
console.warn(`Node ${nodeIdentifier} is missing from the nodes list`); | ||
console.warn(`Unovis | Graph Data Model: Node ${nodeIdentifier} is missing from the nodes list`); | ||
} | ||
@@ -115,2 +116,10 @@ return foundNode; | ||
} | ||
setNodeStateById(id, state) { | ||
const node = this.getNodeById(id); | ||
if (!node) { | ||
console.warn(`Unovis | Graph Data Model: Node ${id} not found`); | ||
return; | ||
} | ||
node._state = state; | ||
} | ||
} | ||
@@ -117,0 +126,0 @@ |
export * from './containers'; | ||
export * from './components'; | ||
export * from './data-models'; | ||
export * from './types'; | ||
@@ -4,0 +5,0 @@ export * from './styles/colors'; |
13
index.js
import './containers.js'; | ||
import './components.js'; | ||
import './data-models/index.js'; | ||
import './types.js'; | ||
@@ -37,2 +38,8 @@ export { colors, colorsDark, getCSSColorVariable, getDarkerColor, getLighterColor } from './styles/colors.js'; | ||
export { Annotations } from './components/annotations/index.js'; | ||
export { Treemap } from './components/treemap/index.js'; | ||
export { DONUT_HALF_ANGLE_RANGES, DONUT_HALF_ANGLE_RANGE_BOTTOM, DONUT_HALF_ANGLE_RANGE_LEFT, DONUT_HALF_ANGLE_RANGE_RIGHT, DONUT_HALF_ANGLE_RANGE_TOP } from './components/donut/constants.js'; | ||
export { GraphDataModel } from './data-models/graph.js'; | ||
export { MapGraphDataModel } from './data-models/map-graph.js'; | ||
export { MapDataModel } from './data-models/map.js'; | ||
export { SeriesDataModel } from './data-models/series.js'; | ||
export { Curve, CurveType } from './types/curve.js'; | ||
@@ -58,6 +65,6 @@ export { Symbol, SymbolType } from './types/symbol.js'; | ||
export { arrayOfIndices, clamp, clean, cloneDeep, countUnique, ensureArray, filterDataByRange, flatten, getBoolean, getExtent, getMax, getMin, getNearest, getNumber, getStackedData, getStackedExtent, getStackedValues, getString, getValue, groupBy, isAClassInstance, isArray, isEmpty, isEqual, isFunction, isNil, isNumber, isNumberWithinRange, isObject, isPlainObject, isString, isUndefined, merge, omit, shallowDiff, sortBy, throttle, unique, without } from './utils/data.js'; | ||
export { allowedSvgTextTags, escapeStringKeepHash, estimateStringPixelLength, estimateTextSize, estimateWrappedTextHeight, getPreciseStringLengthPx, getWrappedText, kebabCase, kebabCaseToCamel, renderTextIntoFrame, renderTextToSvgTextElement, splitString, trimSVGText, trimString, trimStringEnd, trimStringMiddle, trimStringStart, wrapSVGText } from './utils/text.js'; | ||
export { allowedSvgTextTags, escapeStringKeepHash, estimateStringPixelLength, estimateTextSize, estimateWrappedTextHeight, getPreciseStringLengthPx, getWrappedText, kebabCase, kebabCaseToCamel, renderTextIntoFrame, renderTextToSvgTextElement, splitString, textAlignToAnchor, trimSVGText, trimString, trimStringEnd, trimStringMiddle, trimStringStart, wrapSVGText } from './utils/text.js'; | ||
export { allowedSvgTags, getTransformValues, isStringSvg, sanitizeSvgString, transformValuesToString } from './utils/svg.js'; | ||
export { getColor, getHexValue, hexToBrightness, hexToRgb, rgbToBrightness, rgbaToRgb } from './utils/color.js'; | ||
export { circlePath, convertLineToArc, polygon, roundedRectPath, scoreRectPath } from './utils/path.js'; | ||
export { brighter, getColor, getHexValue, hexToBrightness, hexToRgb, isDarkBackground, rgbToBrightness, rgbaToRgb } from './utils/color.js'; | ||
export { arrowPolylinePath, circlePath, convertLineToArc, polygon, roundedRectPath, scoreRectPath } from './utils/path.js'; | ||
export { getCSSVariableValue, getCSSVariableValueInPixels, getHref, getPixelValue, guid, isStringCSSVariable, parseUnit, rectIntersect, stringToHtmlId } from './utils/misc.js'; | ||
@@ -64,0 +71,0 @@ export { DefaultRange } from './utils/scale.js'; |
{ | ||
"name": "@unovis/ts", | ||
"description": "Modular data visualization framework for React, Angular, Svelte, Vue, Solid, and vanilla TypeScript or JavaScript", | ||
"version": "1.5.1-ql.2", | ||
"packageManager": "npm@10.9.1", | ||
"version": "1.5.1-xplg.0", | ||
"repository": { | ||
@@ -37,5 +36,5 @@ "type": "git", | ||
"scripts": { | ||
"build": "sha=$(tar cf - ./src | shasum); if [[ $(echo $sha) == $(< .srcsha) ]] && [[ -d \"./lib\" ]]; then echo \"Lib Build Exists\"; else npm run forcebuild; echo $sha > .srcsha; fi", | ||
"forcebuild": "rimraf lib; rollup -c", | ||
"publish:dist": "rm -rf lib/.cache; cp ./{LICENSE,README.md,package.json} ./lib; cd ./lib; npm publish" | ||
"build": "sha=$(tar cf - ./src | shasum); if [[ $(echo $sha) == $(< .srcsha) ]] && [[ -d \"./lib\" ]]; then echo \"Lib Build Exists\"; else npm run forcebuild; echo $sha > .srcsha; fi; cp ./{LICENSE,README.md,package.json} ./lib", | ||
"forcebuild": "rimraf lib; rollup -c --bundleConfigAsCjs", | ||
"publish:dist": "rm -rf lib/.cache; cd ./lib; npm publish" | ||
}, | ||
@@ -42,0 +41,0 @@ "devDependencies": { |
@@ -29,1 +29,3 @@ export * from "./types/accessor"; | ||
export * from "./components/annotations/types"; | ||
export * from "./components/treemap/types"; | ||
export * from "./components/timeline/types"; |
@@ -29,4 +29,7 @@ import './types/accessor.js'; | ||
import './components/annotations/types.js'; | ||
import './components/treemap/types.js'; | ||
import './components/timeline/types.js'; | ||
/* eslint-disable max-len */ | ||
// Global Types | ||
//# sourceMappingURL=types.js.map |
@@ -5,4 +5,3 @@ /** The most generic data record: an object with unknown properties */ | ||
export declare type StackValuesRecord = Array<[number, number]> & { | ||
negative?: boolean; | ||
ending?: boolean; | ||
isMostlyNegative: boolean; | ||
}; |
@@ -11,2 +11,6 @@ export interface GraphInputNode { | ||
} | ||
export declare type GraphInputData<N extends GraphInputNode = GraphInputNode, L extends GraphInputLink = GraphInputLink> = { | ||
nodes: N[]; | ||
links?: L[]; | ||
}; | ||
export declare type GraphNodeCore<N extends GraphInputNode, L extends GraphInputLink> = N & { | ||
@@ -13,0 +17,0 @@ links: GraphLinkCore<N, L>[]; |
@@ -15,3 +15,4 @@ export declare enum Position { | ||
Inside = "inside", | ||
Outside = "outside" | ||
Outside = "outside", | ||
Center = "center" | ||
} | ||
@@ -18,0 +19,0 @@ export declare enum Orientation { |
@@ -19,2 +19,3 @@ var Position; | ||
Arrangement["Outside"] = "outside"; | ||
Arrangement["Center"] = "center"; | ||
})(Arrangement || (Arrangement = {})); | ||
@@ -21,0 +22,0 @@ var Orientation; |
@@ -14,2 +14,16 @@ import { ColorAccessor } from "../types/accessor"; | ||
export declare function rgbaToRgb(rgba: string, backgroundColor?: string): RGBColor; | ||
/** | ||
* Determines if a background color is considered "dark" based on its brightness | ||
* @param backgroundColor - The color to check (hex, rgb, or rgba) | ||
* @param threshold - Optional brightness threshold (0-1, default 0.5) | ||
* @returns true if the background is dark, false if it's light | ||
*/ | ||
export declare function isDarkBackground(backgroundColor: string, threshold?: number): boolean; | ||
/** | ||
* Makes a color brighter by a certain amount | ||
* @param inputColor - The color to brighten (hex, rgb, or rgba) | ||
* @param amount - Amount to brighten by (0-1) | ||
* @returns The brightened color in hex format | ||
*/ | ||
export declare function brighter(inputColor: string, amount: number): string; | ||
export {}; |
@@ -1,2 +0,2 @@ | ||
import { color } from 'd3-color'; | ||
import { color, hcl } from 'd3-color'; | ||
import { getCSSColorVariable } from '../styles/colors.js'; | ||
@@ -46,4 +46,28 @@ import { getString, isNumber } from './data.js'; | ||
} | ||
/** | ||
* Determines if a background color is considered "dark" based on its brightness | ||
* @param backgroundColor - The color to check (hex, rgb, or rgba) | ||
* @param threshold - Optional brightness threshold (0-1, default 0.5) | ||
* @returns true if the background is dark, false if it's light | ||
*/ | ||
function isDarkBackground(backgroundColor, threshold = 0.55) { | ||
const hex = getHexValue(backgroundColor, document.body); | ||
if (!hex) | ||
return false; | ||
return hexToBrightness(hex) < threshold; | ||
} | ||
/** | ||
* Makes a color brighter by a certain amount | ||
* @param inputColor - The color to brighten (hex, rgb, or rgba) | ||
* @param amount - Amount to brighten by (0-1) | ||
* @returns The brightened color in hex format | ||
*/ | ||
function brighter(inputColor, amount) { | ||
const c = hcl(inputColor); | ||
if (!c) | ||
return inputColor; | ||
return c.brighter(amount).formatHex(); | ||
} | ||
export { getColor, getHexValue, hexToBrightness, hexToRgb, rgbToBrightness, rgbaToRgb }; | ||
export { brighter, getColor, getHexValue, hexToBrightness, hexToRgb, isDarkBackground, rgbToBrightness, rgbaToRgb }; | ||
//# sourceMappingURL=color.js.map |
@@ -14,3 +14,3 @@ import { throttle as _throttle } from 'throttle-debounce'; | ||
export declare const isEmpty: <T>(obj: T) => boolean; | ||
export declare const isEqual: (a?: unknown | null, b?: unknown | null, visited?: Set<any>) => boolean; | ||
export declare const isEqual: (a: unknown | null | undefined, b: unknown | null | undefined, skipKeys?: string[], visited?: Set<any>) => boolean; | ||
export declare const without: <T>(arr: T[], ...args: T[]) => T[]; | ||
@@ -21,3 +21,3 @@ export declare const flatten: <T>(arr: (T | T[])[]) => T[]; | ||
export declare const omit: <T extends Record<string | number | symbol, unknown>>(obj: T, props: (keyof T)[]) => Partial<T>; | ||
export declare const groupBy: <T extends Record<string | number, any>>(arr: T[], accessor: (a: T) => string | number) => Record<string | number, T[]>; | ||
export declare const groupBy: <T extends Record<string | number, any>>(arr: T[], accessor: (a: T, index: number) => string | number) => Record<string | number, T[]>; | ||
export declare const sortBy: <T>(arr: T[], accessor: (a: T) => string | number) => T[]; | ||
@@ -24,0 +24,0 @@ export declare const throttle: <T extends (...args: any[]) => any>(f: T, delay: number, options?: { |
@@ -19,3 +19,3 @@ import { mean, min, max, bisector } from 'd3-array'; | ||
// Based on https://github.com/maplibre/maplibre-gl-js/blob/e78ad7944ef768e67416daa4af86b0464bd0f617/src/style-spec/util/deep_equal.ts, 3-Clause BSD license | ||
const isEqual = (a, b, visited = new Set()) => { | ||
const isEqual = (a, b, skipKeys = [], visited = new Set()) => { | ||
if (Array.isArray(a)) { | ||
@@ -29,3 +29,3 @@ if (!Array.isArray(b) || a.length !== b.length) | ||
for (let i = 0; i < a.length; i++) { | ||
if (!isEqual(a[i], b[i], visited)) | ||
if (!isEqual(a[i], b[i], skipKeys, visited)) | ||
return false; | ||
@@ -43,4 +43,5 @@ } | ||
return true; | ||
const keys = Object.keys(a); | ||
if (keys.length !== Object.keys(b).length) | ||
const keysA = Object.keys(a).filter(key => !skipKeys.includes(key)); | ||
const keysB = Object.keys(b).filter(key => !skipKeys.includes(key)); | ||
if (keysA.length !== keysB.length) | ||
return false; | ||
@@ -51,4 +52,4 @@ if (visited.has(a)) | ||
visited.add(a); | ||
for (const key in a) { | ||
if (!isEqual(a[key], b[key], visited)) | ||
for (const key of keysA) { | ||
if (!isEqual(a[key], b[key], skipKeys, visited)) | ||
return false; | ||
@@ -128,3 +129,3 @@ } | ||
const groupBy = (arr, accessor) => { | ||
return arr.reduce((grouped, v, i, a, k = accessor(v)) => (((grouped[k] || (grouped[k] = [])).push(v), grouped)), {}); | ||
return arr.reduce((grouped, v, i, a, k = accessor(v, i)) => (((grouped[k] || (grouped[k] = [])).push(v), grouped)), {}); | ||
}; | ||
@@ -228,3 +229,3 @@ const sortBy = (arr, accessor) => { | ||
const value = getNumber(d, a, i) || 0; | ||
if (!isNegativeStack[j]) { | ||
if (value >= 0) { | ||
stackedData[j].push([positiveStack, positiveStack += value]); | ||
@@ -239,12 +240,4 @@ } | ||
stackedData.forEach((stack, i) => { | ||
stack.negative = isNegativeStack[i]; | ||
stack.isMostlyNegative = isNegativeStack[i]; | ||
}); | ||
stackedData.filter(s => s.negative) | ||
.forEach((s, i, arr) => { | ||
s.ending = i === arr.length - 1; | ||
}); | ||
stackedData.filter(s => !s.negative) | ||
.forEach((s, i, arr) => { | ||
s.ending = i === arr.length - 1; | ||
}); | ||
return stackedData; | ||
@@ -251,0 +244,0 @@ } |
export { arrayOfIndices, clamp, clean, cloneDeep, countUnique, ensureArray, filterDataByRange, flatten, getBoolean, getExtent, getMax, getMin, getNearest, getNumber, getStackedData, getStackedExtent, getStackedValues, getString, getValue, groupBy, isAClassInstance, isArray, isEmpty, isEqual, isFunction, isNil, isNumber, isNumberWithinRange, isObject, isPlainObject, isString, isUndefined, merge, omit, shallowDiff, sortBy, throttle, unique, without } from './data.js'; | ||
export { allowedSvgTextTags, escapeStringKeepHash, estimateStringPixelLength, estimateTextSize, estimateWrappedTextHeight, getPreciseStringLengthPx, getWrappedText, kebabCase, kebabCaseToCamel, renderTextIntoFrame, renderTextToSvgTextElement, splitString, trimSVGText, trimString, trimStringEnd, trimStringMiddle, trimStringStart, wrapSVGText } from './text.js'; | ||
export { allowedSvgTextTags, escapeStringKeepHash, estimateStringPixelLength, estimateTextSize, estimateWrappedTextHeight, getPreciseStringLengthPx, getWrappedText, kebabCase, kebabCaseToCamel, renderTextIntoFrame, renderTextToSvgTextElement, splitString, textAlignToAnchor, trimSVGText, trimString, trimStringEnd, trimStringMiddle, trimStringStart, wrapSVGText } from './text.js'; | ||
export { allowedSvgTags, getTransformValues, isStringSvg, sanitizeSvgString, transformValuesToString } from './svg.js'; | ||
export { getColor, getHexValue, hexToBrightness, hexToRgb, rgbToBrightness, rgbaToRgb } from './color.js'; | ||
export { circlePath, convertLineToArc, polygon, roundedRectPath, scoreRectPath } from './path.js'; | ||
export { brighter, getColor, getHexValue, hexToBrightness, hexToRgb, isDarkBackground, rgbToBrightness, rgbaToRgb } from './color.js'; | ||
export { arrowPolylinePath, circlePath, convertLineToArc, polygon, roundedRectPath, scoreRectPath } from './path.js'; | ||
export { getCSSVariableValue, getCSSVariableValueInPixels, getHref, getPixelValue, guid, isStringCSSVariable, parseUnit, rectIntersect, stringToHtmlId } from './misc.js'; | ||
@@ -7,0 +7,0 @@ import './type.js'; |
@@ -26,1 +26,9 @@ import { Path } from 'd3-path'; | ||
export declare function convertLineToArc(path: Path | string, r: number): string; | ||
/** | ||
* Generate an SVG path string for an arrow that follows a polyline path. | ||
* The arrow is composed of line segments between points and a triangular arrowhead at the end. | ||
* | ||
* @param opts - ArrowPolylinePathOptions object containing array of points and optional head dimensions. | ||
* @returns SVG path string for the arrow. | ||
*/ | ||
export declare function arrowPolylinePath(points: [number, number][], arrowHeadLength?: number, arrowHeadWidth?: number, smoothing?: number): string; |
@@ -151,4 +151,107 @@ import { range, min, max } from 'd3-array'; | ||
} | ||
/** | ||
* Generate an SVG path string for an arrow that follows a polyline path. | ||
* The arrow is composed of line segments between points and a triangular arrowhead at the end. | ||
* | ||
* @param opts - ArrowPolylinePathOptions object containing array of points and optional head dimensions. | ||
* @returns SVG path string for the arrow. | ||
*/ | ||
function arrowPolylinePath(points, arrowHeadLength = 10, arrowHeadWidth = 6, smoothing = 5) { | ||
if (points.length < 2) | ||
return ''; | ||
// Calculate total path length | ||
let totalLength = 0; | ||
for (let i = 0; i < points.length - 1; i++) { | ||
const [x1, y1] = points[i]; | ||
const [x2, y2] = points[i + 1]; | ||
totalLength += Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); | ||
} | ||
// If the total length is zero or nearly zero, don't draw anything | ||
if (totalLength === 0) | ||
return ''; | ||
// Let the default values be modifiable based on the line length | ||
let headLength = arrowHeadLength; | ||
let headWidth = arrowHeadWidth; | ||
// If the line is very short, scale down the arrow head dimensions | ||
const threshold = arrowHeadLength * 2; | ||
if (totalLength < threshold) { | ||
const scale = totalLength / threshold; | ||
headLength *= scale; | ||
headWidth *= scale; | ||
} | ||
// Ensure the arrow head length is never longer than the line itself | ||
headLength = Math.min(headLength / 2, totalLength); | ||
// Get the last two points for arrowhead calculation | ||
const [lastX, lastY] = points[points.length - 1]; | ||
const [prevX, prevY] = points[points.length - 2]; | ||
// Calculate direction vector for the last segment | ||
const dx = lastX - prevX; | ||
const dy = lastY - prevY; | ||
const segmentLength = Math.sqrt(dx * dx + dy * dy); | ||
const ux = dx / segmentLength; | ||
const uy = dy / segmentLength; | ||
// Tail point of the arrow (where the arrowhead starts) | ||
const tailX = lastX - headLength * ux; | ||
const tailY = lastY - headLength * uy; | ||
// Perpendicular vector for arrowhead width calculation | ||
const perpX = -uy; | ||
const perpY = ux; | ||
// Calculate the two base points of the arrowhead triangle | ||
const leftX = tailX + (headWidth / 2) * perpX; | ||
const leftY = tailY + (headWidth / 2) * perpY; | ||
const rightX = tailX - (headWidth / 2) * perpX; | ||
const rightY = tailY - (headWidth / 2) * perpY; | ||
// Build the path | ||
const pathParts = []; | ||
if (points.length === 2) { | ||
// For a single segment, create a curved path | ||
const [startX, startY] = points[0]; | ||
// Calculate control points for a cubic Bézier curve with absolute smoothing | ||
const cp1x = startX + ux * smoothing; | ||
const cp1y = startY + uy * smoothing + perpY * smoothing * 0.5; | ||
const cp2x = tailX - ux * smoothing; | ||
const cp2y = tailY - uy * smoothing + perpY * smoothing * 0.5; | ||
// Start path and add cubic Bézier curve | ||
pathParts.push(`M${startX},${startY}`); | ||
pathParts.push(`C${cp1x},${cp1y} ${cp2x},${cp2y} ${lastX},${lastY}`); | ||
} | ||
else { | ||
// For multiple segments, use smooth Bézier corners with absolute smoothing | ||
pathParts.push(`M${points[0][0]},${points[0][1]}`); | ||
for (let i = 0; i < points.length - 2; i++) { | ||
const [x1, y1] = points[i]; | ||
const [x2, y2] = points[i + 1]; | ||
const [x3, y3] = points[i + 2]; | ||
// Calculate vectors for the current and next segment | ||
const v1x = x2 - x1; | ||
const v1y = y2 - y1; | ||
const v2x = x3 - x2; | ||
const v2y = y3 - y2; | ||
// Calculate lengths of segments | ||
const len1 = Math.sqrt(v1x * v1x + v1y * v1y); | ||
const len2 = Math.sqrt(v2x * v2x + v2y * v2y); | ||
// Calculate unit vectors | ||
const u1x = v1x / len1; | ||
const u1y = v1y / len1; | ||
const u2x = v2x / len2; | ||
const u2y = v2y / len2; | ||
// Calculate the corner points and control points with absolute smoothing | ||
const corner1x = x2 - u1x * smoothing; | ||
const corner1y = y2 - u1y * smoothing; | ||
const corner2x = x2 + u2x * smoothing; | ||
const corner2y = y2 + u2y * smoothing; | ||
// Add line to approach point | ||
pathParts.push(`L${corner1x},${corner1y}`); | ||
// Add cubic Bézier curve for the corner | ||
pathParts.push(`C${x2},${y2} ${x2},${y2} ${corner2x},${corner2y}`); | ||
} | ||
// Add the final line segment to the tail point | ||
pathParts.push(`L${lastX},${lastY}`); | ||
} | ||
// Add the arrowhead | ||
pathParts.push(`M${leftX},${leftY} L${lastX},${lastY} L${rightX},${rightY}`); | ||
return pathParts.join(' '); | ||
} | ||
export { circlePath, convertLineToArc, polygon, roundedRectPath, scoreRectPath }; | ||
export { arrowPolylinePath, circlePath, convertLineToArc, polygon, roundedRectPath, scoreRectPath }; | ||
//# sourceMappingURL=path.js.map |
import { Selection } from 'd3-selection'; | ||
import { TrimMode, UnovisText, UnovisTextFrameOptions, UnovisTextOptions, UnovisWrappedText } from "../types/text"; | ||
import { TextAlign, TrimMode, UnovisText, UnovisTextFrameOptions, UnovisTextOptions, UnovisWrappedText } from "../types/text"; | ||
export declare const textAlignToAnchor: (textAlign: TextAlign) => string | null; | ||
/** | ||
@@ -19,24 +20,24 @@ * Converts a kebab-case string to camelCase. | ||
* Trims the input string from the start, leaving only the specified maximum length. | ||
* @param {string} [str=''] - The input string to be trimmed. | ||
* @param {string} str - The input string to be trimmed. | ||
* @param {number} [maxLength=15] - The maximum allowed length of the trimmed string. | ||
* @returns {string} - The trimmed string. | ||
*/ | ||
export declare function trimStringStart(str?: string, maxLength?: number): string; | ||
export declare function trimStringStart(str: string | undefined, maxLength?: number): string; | ||
/** | ||
* Trims the input string from the middle, leaving only the specified maximum length. | ||
* @param {string} [str=''] - The input string to be trimmed. | ||
* @param {string} str - The input string to be trimmed. | ||
* @param {number} [maxLength=15] - The maximum allowed length of the trimmed string. | ||
* @returns {string} - The trimmed string. | ||
*/ | ||
export declare function trimStringMiddle(str?: string, maxLength?: number): string; | ||
export declare function trimStringMiddle(str: string | undefined, maxLength?: number): string; | ||
/** | ||
* Trims the input string from the end, leaving only the specified maximum length. | ||
* @param {string} [str=''] - The input string to be trimmed. | ||
* @param {string} str - The input string to be trimmed. | ||
* @param {number} [maxLength=15] - The maximum allowed length of the trimmed string. | ||
* @returns {string} - The trimmed string. | ||
*/ | ||
export declare function trimStringEnd(str?: string, maxLength?: number): string; | ||
export declare function trimStringEnd(str: string | undefined, maxLength?: number): string; | ||
/** | ||
* Trims the input string according to the specified trim mode. | ||
* @param {string} [str=''] - The input string to be trimmed. | ||
* @param {string} str - The input string to be trimmed. | ||
* @param {number} [length=15] - The maximum allowed length of the trimmed string. | ||
@@ -46,3 +47,3 @@ * @param {TrimMode} [type=TrimMode.Middle] - The trim mode to be applied. | ||
*/ | ||
export declare function trimString(str?: string, length?: number, type?: TrimMode): string; | ||
export declare function trimString(str: string | undefined, length?: number, type?: TrimMode): string; | ||
/** | ||
@@ -49,0 +50,0 @@ * Splits the input string according to the specified separators. |
import { sum } from 'd3-array'; | ||
import striptags from 'striptags'; | ||
import { TrimMode, VerticalAlign, TextAlign } from '../types/text.js'; | ||
import { TextAlign, TrimMode, VerticalAlign } from '../types/text.js'; | ||
import { flatten, isArray, merge } from './data.js'; | ||
@@ -8,2 +8,10 @@ import { getTextAnchorFromTextAlign } from '../types/svg.js'; | ||
const textAlignToAnchor = (textAlign) => { | ||
switch (textAlign) { | ||
case TextAlign.Left: return 'start'; | ||
case TextAlign.Right: return 'end'; | ||
case TextAlign.Center: return 'middle'; | ||
default: return null; | ||
} | ||
}; | ||
/** | ||
@@ -40,7 +48,9 @@ * Converts a kebab-case string to camelCase. | ||
* Trims the input string from the start, leaving only the specified maximum length. | ||
* @param {string} [str=''] - The input string to be trimmed. | ||
* @param {string} str - The input string to be trimmed. | ||
* @param {number} [maxLength=15] - The maximum allowed length of the trimmed string. | ||
* @returns {string} - The trimmed string. | ||
*/ | ||
function trimStringStart(str = '', maxLength = 15) { | ||
function trimStringStart(str, maxLength = 15) { | ||
if (!str) | ||
return ''; | ||
return str.length > maxLength ? `…${str.substr(str.length - maxLength, maxLength)}` : str; | ||
@@ -50,7 +60,9 @@ } | ||
* Trims the input string from the middle, leaving only the specified maximum length. | ||
* @param {string} [str=''] - The input string to be trimmed. | ||
* @param {string} str - The input string to be trimmed. | ||
* @param {number} [maxLength=15] - The maximum allowed length of the trimmed string. | ||
* @returns {string} - The trimmed string. | ||
*/ | ||
function trimStringMiddle(str = '', maxLength = 15) { | ||
function trimStringMiddle(str, maxLength = 15) { | ||
if (!str) | ||
return ''; | ||
const dist = Math.floor((maxLength - 3) / 2); | ||
@@ -61,7 +73,9 @@ return str.length > maxLength ? `${str.substr(0, dist)}…${str.substr(-dist, dist)}` : str; | ||
* Trims the input string from the end, leaving only the specified maximum length. | ||
* @param {string} [str=''] - The input string to be trimmed. | ||
* @param {string} str - The input string to be trimmed. | ||
* @param {number} [maxLength=15] - The maximum allowed length of the trimmed string. | ||
* @returns {string} - The trimmed string. | ||
*/ | ||
function trimStringEnd(str = '', maxLength = 15) { | ||
function trimStringEnd(str, maxLength = 15) { | ||
if (!str) | ||
return ''; | ||
return str.length > maxLength ? `${str.substr(0, maxLength)}…` : str; | ||
@@ -71,3 +85,3 @@ } | ||
* Trims the input string according to the specified trim mode. | ||
* @param {string} [str=''] - The input string to be trimmed. | ||
* @param {string} str - The input string to be trimmed. | ||
* @param {number} [length=15] - The maximum allowed length of the trimmed string. | ||
@@ -77,3 +91,5 @@ * @param {TrimMode} [type=TrimMode.Middle] - The trim mode to be applied. | ||
*/ | ||
function trimString(str = '', length = 15, type = TrimMode.Middle) { | ||
function trimString(str, length = 15, type = TrimMode.Middle) { | ||
if (!str) | ||
return ''; | ||
let result = trimStringEnd(str, length); | ||
@@ -485,3 +501,3 @@ if (type === TrimMode.Start) | ||
export { allowedSvgTextTags, escapeStringKeepHash, estimateStringPixelLength, estimateTextSize, estimateWrappedTextHeight, getPreciseStringLengthPx, getWrappedText, kebabCase, kebabCaseToCamel, renderTextIntoFrame, renderTextToSvgTextElement, splitString, trimSVGText, trimString, trimStringEnd, trimStringMiddle, trimStringStart, wrapSVGText }; | ||
export { allowedSvgTextTags, escapeStringKeepHash, estimateStringPixelLength, estimateTextSize, estimateWrappedTextHeight, getPreciseStringLengthPx, getWrappedText, kebabCase, kebabCaseToCamel, renderTextIntoFrame, renderTextToSvgTextElement, splitString, textAlignToAnchor, trimSVGText, trimString, trimStringEnd, trimStringMiddle, trimStringStart, wrapSVGText }; | ||
//# sourceMappingURL=text.js.map |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
10052601
1.71%595
4.75%1197929
0.1%0
-100%