Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 55 additions & 30 deletions packages/ts/src/components/annotations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class Annotations extends ComponentCore<unknown[], AnnotationsConfigInter
const duration = isNumber(customDuration) ? customDuration : config.duration

const annotations = this.g.selectAll<SVGGElement, AnnotationItem[]>(`.${s.annotation}`)
.data(config.items, d => JSON.stringify(d))
.data(config.items ?? [], d => JSON.stringify(d))

const annotationsEnter = annotations.enter().append('g')
.attr('class', s.annotation)
Expand All @@ -58,18 +58,21 @@ export class Annotations extends ComponentCore<unknown[], AnnotationsConfigInter
subject.append('line')

const annotationsMerged = annotationsEnter.merge(annotations)
.attr('cursor', d => d?.cursor)
.attr('cursor', d => d.cursor ?? null)
.each((annotation, i, elements) => {
if (annotation.content) {
const content = typeof annotation.content === 'string' ? { ...UNOVIS_TEXT_DEFAULT, text: annotation.content } : annotation.content
const x = parseUnit(annotation.x, this._width)
const y = parseUnit(annotation.y, this._height)
const width = parseUnit(annotation.width, this._width)
const height = parseUnit(annotation.height, this._height)
const x = parseUnit(annotation.x ?? 0, this._width)
const y = parseUnit(annotation.y ?? 0, this._height)
const width = parseUnit(annotation.width ?? 0, this._width)
const height = parseUnit(annotation.height ?? 0, this._height)
const options = { ...annotation, x, y, width, height }

const contentGroupElement = select(elements[i]).select<SVGGElement>(`.${s.annotationContent}`)
renderTextIntoFrame(contentGroupElement.node(), content, options)
const node = contentGroupElement.node()
if (node) {
renderTextIntoFrame(node, content, options)
}
}

if (annotation.subject) {
Expand All @@ -93,18 +96,32 @@ export class Annotations extends ComponentCore<unknown[], AnnotationsConfigInter
const contentGroup = select(annotationGroupElement).select<SVGGElement>(`.${s.annotationContent}`)
const subjectGroup = select(annotationGroupElement).select<SVGGElement>(`.${s.annotationSubject}`)

const subjectX: number | null = parseUnit(typeof subject?.x === 'function' ? subject.x() : subject?.x, this._width) ?? null
const subjectY: number | null = parseUnit(typeof subject?.y === 'function' ? subject.y() : subject?.y, this._height) ?? null

const subjectStrokeColor: string | null = subject?.strokeColor ?? null
const subjectFillColor: string | null = subject?.fillColor ?? null
const subjectStrokeDasharray: string | null = subject?.strokeDasharray ?? null
const connectorLineColor: string | null = subject?.connectorLineColor ?? null
const connectorLineStrokeDasharray: string | null = subject?.connectorLineStrokeDasharray ?? null
const subjectRadius: number | null = subject?.radius ?? 0
const padding = subject?.padding ?? 5

const contentBbox = contentGroup.node().getBBox()
if (!subject) {
subjectGroup.select('circle').attr('visibility', 'hidden')
subjectGroup.select('line').attr('visibility', 'hidden')
return
}

const subjectX = parseUnit(
typeof subject.x === 'function' ? subject.x() : subject.x,
this._width
)
const subjectY = parseUnit(
typeof subject.y === 'function' ? subject.y() : subject.y,
this._height
)

const subjectStrokeColor = subject.strokeColor ?? null
const subjectFillColor = subject.fillColor ?? null
const subjectStrokeDasharray = subject.strokeDasharray ?? null
const connectorLineColor = subject.connectorLineColor ?? null
const connectorLineStrokeDasharray = subject.connectorLineStrokeDasharray ?? null
const subjectRadius = subject.radius ?? 0
const padding = subject.padding ?? 5

const contentNode = contentGroup.node()
if (!contentNode) return
const contentBbox = contentNode.getBBox()
const dy = Math.abs(subjectY - (contentBbox.y + contentBbox.height / 2))
const dx = Math.abs(subjectX - (contentBbox.x + contentBbox.width / 2))
const annotationPadding = 5
Expand All @@ -120,24 +137,32 @@ export class Annotations extends ComponentCore<unknown[], AnnotationsConfigInter
const x1 = subjectX + Math.cos(angle * Math.PI / 180) * (subjectRadius + padding)
const y1 = subjectY + Math.sin(angle * Math.PI / 180) * (subjectRadius + padding)

subjectGroup.select('circle')
.attr('visibility', subject ? null : 'hidden')
const circle = subjectGroup.select('circle')
.attr('visibility', null)
.attr('cx', subjectX)
.attr('cy', subjectY)
.attr('r', subjectRadius)
.style('stroke', subjectStrokeColor)
.style('fill', subjectFillColor)
.style('stroke-dasharray', subjectStrokeDasharray)

subjectGroup.select('line')
.attr('visibility', subject ? null : 'hidden')
if (subjectStrokeColor === null) circle.style('stroke', null)
else circle.style('stroke', subjectStrokeColor)

if (subjectFillColor === null) circle.style('fill', null)
else circle.style('fill', subjectFillColor)

if (subjectStrokeDasharray === null) circle.style('stroke-dasharray', null)
else circle.style('stroke-dasharray', subjectStrokeDasharray)

const line = subjectGroup.select('line')
.attr('visibility', null)
.attr('x1', x1)
.attr('y1', y1)
.attr('x2', x1)
.attr('y2', y1)
.attr('x2', x2)
.attr('y2', y2)
.style('stroke', connectorLineColor)
.style('stroke-dasharray', connectorLineStrokeDasharray)

if (connectorLineColor === null) line.style('stroke', null)
else line.style('stroke', connectorLineColor)

if (connectorLineStrokeDasharray === null) line.style('stroke-dasharray', null)
else line.style('stroke-dasharray', connectorLineStrokeDasharray)
}
}
25 changes: 15 additions & 10 deletions packages/ts/src/containers/xy-container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { CoreDataModel } from 'data-models/core'
// Types
import { Spacing } from 'types/spacing'
import { AxisType } from 'components/axis/types'
import { Axis } from 'components/axis'
import { ScaleDimension } from 'types/scale'
import { Direction } from 'types/direction'

Expand Down Expand Up @@ -376,7 +377,9 @@ export class XYContainer<Datum> extends ContainerCore {
const { config: { xAxis, yAxis } } = this
const margin = this._getMargin()

const axes = clean([xAxis, yAxis])
const axes = clean<Axis<Datum>>(
[xAxis, yAxis] as Array<Axis<Datum> | null | undefined>
)
axes.forEach(axis => {
const offset = axis.getOffset(margin)
axis.g.attr('transform', `translate(${offset.left},${offset.top})`)
Expand All @@ -388,7 +391,9 @@ export class XYContainer<Datum> extends ContainerCore {
const { config: { xAxis, yAxis } } = this

// At first we need to set the domain to the scales
const components = clean([...this.components, xAxis, yAxis])
const components = clean<XYComponentCore<Datum>>(
[...this.components, xAxis, yAxis] as Array<XYComponentCore<Datum> | null | undefined>
)
this._setScales(...components)
this._updateScalesDomain(...components)

Expand All @@ -405,10 +410,10 @@ export class XYContainer<Datum> extends ContainerCore {
axis.preRender()

const m = axis.getRequiredMargin()
if (axisMargin.top < m.top) axisMargin.top = m.top
if (axisMargin.bottom < m.bottom) axisMargin.bottom = m.bottom
if (axisMargin.left < m.left) axisMargin.left = m.left
if (axisMargin.right < m.right) axisMargin.right = m.right
if (axisMargin.top < (m.top ?? 0)) axisMargin.top = m.top ?? 0
if (axisMargin.bottom < (m.bottom ?? 0)) axisMargin.bottom = m.bottom ?? 0
if (axisMargin.left < (m.left ?? 0)) axisMargin.left = m.left ?? 0
if (axisMargin.right < (m.right ?? 0)) axisMargin.right = m.right ?? 0
})
this._axisMargin = axisMargin
}
Expand All @@ -418,10 +423,10 @@ export class XYContainer<Datum> extends ContainerCore {
const { config: { margin } } = this

return {
top: margin.top + this._axisMargin.top,
bottom: margin.bottom + this._axisMargin.bottom,
left: margin.left + this._axisMargin.left,
right: margin.right + this._axisMargin.right,
top: (margin.top ?? 0) + this._axisMargin.top,
bottom: (margin.bottom ?? 0) + this._axisMargin.bottom,
left: (margin.left ?? 0) + this._axisMargin.left,
right: (margin.right ?? 0) + this._axisMargin.right,
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/ts/src/core/component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class ComponentCore<
CoreDatum,
ConfigInterface extends ComponentConfigInterface = ComponentConfigInterface,
> {
public element: SVGGElement | HTMLElement
public element: SVGGElement | HTMLElement | undefined
public type: ComponentType = ComponentType.SVG
public g: Selection<SVGGElement, unknown, null, undefined> | Selection<HTMLElement, unknown, null, undefined>
public config: ConfigInterface
Expand Down Expand Up @@ -113,6 +113,7 @@ export class ComponentCore<

private _setCustomAttributes (): void {
const attributeMap = this.config.attributes
if (!attributeMap) return

Object.keys(attributeMap).forEach(className => {
Object.keys(attributeMap[className]).forEach(attr => {
Expand Down
28 changes: 16 additions & 12 deletions packages/ts/src/core/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import { ContainerDefaultConfig, ContainerConfigInterface } from './config'

export class ContainerCore {
public svg: Selection<SVGSVGElement, unknown, null, undefined>
public element: SVGSVGElement
public element: SVGSVGElement | null
public prevConfig: ContainerConfigInterface
public config: ContainerConfigInterface

protected _defaultConfig: ContainerConfigInterface = ContainerDefaultConfig
protected _container: HTMLElement
protected _renderAnimationFrameId: number
protected _renderAnimationFrameId: number | null
protected _isFirstRender = true
protected _resizeObserver: ResizeObserver | undefined
protected _resizeObserverAnimationFrameId: number
Expand Down Expand Up @@ -59,7 +59,8 @@ export class ContainerCore {
// Add `svgDefs` if provided in the config
if (config?.svgDefs !== this.prevConfig?.svgDefs) {
this._svgDefsExternal.selectAll('*').remove()
this._svgDefsExternal.html(config.svgDefs)
if (config.svgDefs != null) this._svgDefsExternal.html(config.svgDefs)
else this._svgDefsExternal.html(null)
}
}

Expand All @@ -73,8 +74,9 @@ export class ContainerCore {
const { config } = this

// Apply the `aria-label` attribute
select(this._container)
.attr('aria-label', config.ariaLabel)
const aria = config.ariaLabel
if (aria == null) select(this._container).attr('aria-label', null)
else select(this._container).attr('aria-label', aria)

this._isFirstRender = false
}
Expand All @@ -97,7 +99,7 @@ export class ContainerCore {
if (!this._resizeObserver) this._setUpResizeObserver()

// Schedule the actual rendering in the next frame
cancelAnimationFrame(this._renderAnimationFrameId)
if (this._renderAnimationFrameId !== null) cancelAnimationFrame(this._renderAnimationFrameId)
this._renderAnimationFrameId = requestAnimationFrame(() => {
this._preRender()
this._render(duration)
Expand All @@ -106,26 +108,28 @@ export class ContainerCore {

get containerWidth (): number {
return this.config.width
? this.element.clientWidth
? (this.element ? this.element.clientWidth : 0)
: (this._container.clientWidth || this._container.getBoundingClientRect().width)
}

get containerHeight (): number {
return this.config.height
? this.element.clientHeight
? (this.element ? this.element.clientHeight : 0)
: (this._container.clientHeight || this._container.getBoundingClientRect().height || ContainerCore.DEFAULT_CONTAINER_HEIGHT)
}

get width (): number {
return clamp(this.containerWidth - this.config.margin.left - this.config.margin.right, 0, Number.POSITIVE_INFINITY)
const margin = this.config.margin ?? {}
return clamp(this.containerWidth - (margin.left ?? 0) - (margin.right ?? 0), 0, Number.POSITIVE_INFINITY)
}

get height (): number {
return clamp(this.containerHeight - this.config.margin.top - this.config.margin.bottom, 0, Number.POSITIVE_INFINITY)
const margin = this.config.margin ?? {}
return clamp(this.containerHeight - (margin.top ?? 0) - (margin.bottom ?? 0), 0, Number.POSITIVE_INFINITY)
}

protected _removeAllChildren (): void {
while (this.element.firstChild) {
while (this.element && this.element.firstChild) {
this.element.removeChild(this.element.firstChild)
}
}
Expand Down Expand Up @@ -162,7 +166,7 @@ export class ContainerCore {
}

public destroy (): void {
cancelAnimationFrame(this._renderAnimationFrameId)
if (this._renderAnimationFrameId !== null) cancelAnimationFrame(this._renderAnimationFrameId)
cancelAnimationFrame(this._resizeObserverAnimationFrameId)
this._resizeObserver?.disconnect()
this.svg.remove()
Expand Down
6 changes: 3 additions & 3 deletions packages/ts/src/core/xy-component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,20 @@ export class XYComponentCore<
switch (dimension) {
case ScaleDimension.X: return this.getXDataExtent()
case ScaleDimension.Y: return this.getYDataExtent(scaleByVisibleData)
default: return getExtent(datamodel.data, config[dimension])
default: return getExtent(datamodel.data, config[dimension] as NumericAccessor<Datum>) as number[]
}
}

getXDataExtent (): number[] {
const { config, datamodel } = this
return getExtent(datamodel.data, config.x)
return getExtent(datamodel.data, config.x) as number[]
}

getYDataExtent (scaleByVisibleData: boolean): number[] {
const { config, datamodel } = this

const data = scaleByVisibleData ? filterDataByRange(datamodel.data, this.xScale.domain() as [number, number], config.x) : datamodel.data
const yAccessors = (isArray(config.y) ? config.y : [config.y]) as NumericAccessor<Datum>[]
return getExtent(data, ...yAccessors)
return getExtent(data, ...yAccessors) as number[]
}
}
6 changes: 3 additions & 3 deletions packages/ts/src/data-models/core.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
export class CoreDataModel<T> {
protected _data: T | undefined

get data (): T {
get data (): T | undefined {
return this._data
}

set data (value: T) {
set data (value: T | undefined) {
this._data = value
}

constructor (data?: T) {
this.data = data
this._data = data
}
}
16 changes: 8 additions & 8 deletions packages/ts/src/data-models/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ export class GraphDataModel<
public linkId: ((n: L) => string | undefined) = l => (isString(l.id) || isFinite(l.id as number)) ? `${l.id}` : undefined
public nodeSort: ((a: N, b: N) => number)

public getNodeById (id: string | number): OutNode {
public getNodeById (id: string | number): OutNode | undefined {
return this._nodesMap.get(id)
}

get data (): GraphData<N, L> {
return this._data
return this._data ?? { nodes: [] }
}

set data (inputData: GraphData<N, L>) {
Expand All @@ -47,13 +47,13 @@ export class GraphDataModel<
this._nodesMap.clear()

// Todo: Figure out why TypeScript complains about types
const nodes = cloneDeep(inputData?.nodes ?? []) as undefined as OutNode[]
const links = cloneDeep(inputData?.links ?? []) as undefined as OutLink[]
const nodes = cloneDeep(inputData.nodes ?? []) as unknown as OutNode[]
const links = cloneDeep(inputData.links ?? []) as unknown as OutLink[]

// Every node or link can have a private state used for rendering needs
// On data update we transfer state between objects with same ids
this.transferState(nodes, prevNodes, this.nodeId)
this.transferState(links, prevLinks, this.linkId)
this.transferState(nodes, prevNodes, n => this.nodeId(n) ?? '')
this.transferState(links, prevLinks, l => this.linkId(l) ?? '')

// Set node `_id` and `_index`
nodes.forEach((node, i) => {
Expand All @@ -69,8 +69,8 @@ export class GraphDataModel<
// Fill link source and target
links.forEach((link, i) => {
link._indexGlobal = i
link.source = this.findNode(nodes, link.source)
link.target = this.findNode(nodes, link.target)
link.source = this.findNode(nodes, link.source)!
link.target = this.findNode(nodes, link.target)!
Comment on lines +72 to +73
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Risky non-null assertions could cause runtime errors.

The non-null assertion operators (!) assume that findNode will always return a value, but findNode can return undefined (as seen in its implementation). This could lead to runtime errors if nodes are not found.

Consider adding explicit checks instead:

-      link.source = this.findNode(nodes, link.source)!
-      link.target = this.findNode(nodes, link.target)!
+      const source = this.findNode(nodes, link.source)
+      const target = this.findNode(nodes, link.target)
+      if (!source || !target) {
+        console.warn(`Unovis | Graph Data Model: Skipping link ${i} due to missing source or target node`)
+        return
+      }
+      link.source = source
+      link.target = target

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/ts/src/data-models/graph.ts around lines 72 to 73, the use of
non-null assertions on the results of findNode is risky because findNode can
return undefined, potentially causing runtime errors. Replace the non-null
assertions with explicit checks to verify that the returned nodes are not
undefined before assigning them to link.source and link.target. Handle the case
where a node is not found, for example by throwing an error or skipping the
assignment, to ensure safe and predictable behavior.

})

// Set link index for multiple link rendering
Expand Down
Loading
Loading