@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering.
1,319 lines (1,143 loc) • 34.3 kB
text/typescript
import { KeyValue } from '../types'
import { Dom, FunctionExt } from '../util'
import { Point, Rectangle } from '../geometry'
import { Cell, Edge, Model } from '../model'
import { View, CellView, EdgeView } from '../view'
import { FlagManager } from '../view/flag'
import { Graph } from './graph'
import { Base } from './base'
export class Renderer extends Base {
protected views: KeyValue<CellView>
protected zPivots: KeyValue<Comment>
protected updates: Renderer.Updates
protected init() {
this.resetUpdates()
this.startListening()
// Renders existing cells in the model.
this.resetViews(this.model.getCells())
// Starts rendering loop.
if (!this.isFrozen() && this.isAsync()) {
this.updateViewsAsync()
}
}
protected startListening() {
this.model.on('sorted', this.onSortModel, this)
this.model.on('reseted', this.onModelReseted, this)
this.model.on('batch:stop', this.onBatchStop, this)
this.model.on('cell:added', this.onCellAdded, this)
this.model.on('cell:removed', this.onCellRemoved, this)
this.model.on('cell:change:zIndex', this.onCellZIndexChanged, this)
this.model.on('cell:change:visible', this.onCellVisibleChanged, this)
}
protected stopListening() {
this.model.off('sorted', this.onSortModel, this)
this.model.off('reseted', this.onModelReseted, this)
this.model.off('batch:stop', this.onBatchStop, this)
this.model.off('cell:added', this.onCellAdded, this)
this.model.off('cell:removed', this.onCellRemoved, this)
this.model.off('cell:change:zIndex', this.onCellZIndexChanged, this)
this.model.off('cell:change:visible', this.onCellVisibleChanged, this)
}
protected resetUpdates() {
this.updates = {
priorities: [{}, {}, {}],
mounted: {},
mountedCids: [],
unmounted: {},
unmountedCids: [],
count: 0,
sort: false,
frozen: false,
freezeKey: null,
animationId: null,
}
}
protected onSortModel() {
if (this.model.hasActiveBatch(Renderer.SORT_DELAYING_BATCHES)) {
return
}
this.sortViews()
}
protected onModelReseted({ options }: Model.EventArgs['reseted']) {
this.removeZPivots()
this.resetViews(this.model.getCells(), options)
}
protected onBatchStop({ name, data }: Model.EventArgs['batch:stop']) {
if (this.isFrozen()) {
return
}
const model = this.model
if (!this.isAsync()) {
const updateDelayingBatches = Renderer.UPDATE_DELAYING_BATCHES
if (
updateDelayingBatches.includes(name as Model.BatchName) &&
!model.hasActiveBatch(updateDelayingBatches)
) {
this.updateViews(data)
}
}
const sortDelayingBatches = Renderer.SORT_DELAYING_BATCHES
if (
sortDelayingBatches.includes(name as Model.BatchName) &&
!model.hasActiveBatch(sortDelayingBatches)
) {
this.sortViews()
}
}
protected onCellAdded({ cell, options }: Model.EventArgs['cell:added']) {
const position = options.position
if (this.isAsync() || typeof position !== 'number') {
this.renderView(cell, options)
} else {
if (options.maxPosition === position) {
this.freeze({ key: 'addCells' })
}
this.renderView(cell, options)
if (position === 0) {
this.unfreeze({ key: 'addCells' })
}
}
}
protected onCellRemoved({ cell, options }: Model.EventArgs['cell:removed']) {
const view = this.findViewByCell(cell)
if (view) {
this.requestViewUpdate(view, Renderer.FLAG_REMOVE, view.priority, options)
}
}
protected onCellZIndexChanged({
cell,
options,
}: Model.EventArgs['cell:change:zIndex']) {
if (this.options.sorting === 'approx') {
const view = this.findViewByCell(cell)
if (view) {
this.requestViewUpdate(
view,
Renderer.FLAG_INSERT,
view.priority,
options,
)
}
}
}
protected onCellVisibleChanged({
cell,
current: visible,
options,
}: Model.EventArgs['cell:change:visible']) {
// Hide connected edges before cell
if (!visible) {
this.processEdgeOnTerminalVisibleChanged(cell, false)
}
const view = this.findViewByCell(cell)
if (!visible && view) {
this.removeView(cell)
} else if (visible && view == null) {
this.renderView(cell, options)
}
// Show connected edges after cell rendered
if (visible) {
this.processEdgeOnTerminalVisibleChanged(cell, true)
}
}
protected processEdgeOnTerminalVisibleChanged(node: Cell, visible: boolean) {
const getOpposite = (edge: Edge, currentTerminal: Cell) => {
const sourceId = edge.getSourceCellId()
if (sourceId !== currentTerminal.id) {
return edge.getSourceCell()
}
const targetId = edge.getTargetCellId()
if (targetId !== currentTerminal.id) {
return edge.getTargetCell()
}
return null
}
this.model.getConnectedEdges(node).forEach((edge) => {
const opposite = getOpposite(edge, node)
if (opposite == null || opposite.isVisible()) {
visible ? edge.show() : edge.hide()
}
})
}
protected isEdgeTerminalVisible(edge: Edge, terminal: Edge.TerminalType) {
const cellId =
terminal === 'source' ? edge.getSourceCellId() : edge.getTargetCellId()
const cell = cellId ? this.model.getCell(cellId) : null
if (cell && !cell.isVisible()) {
return false
}
return true
}
requestConnectedEdgesUpdate(
view: CellView,
options: Renderer.RequestViewUpdateOptions = {},
) {
if (CellView.isCellView(view)) {
const cell = view.cell
const edges = this.model.getConnectedEdges(cell)
for (let j = 0, n = edges.length; j < n; j += 1) {
const edge = edges[j]
const edgeView = this.findViewByCell(edge)
if (!edgeView) {
continue
}
const flagLabels: FlagManager.Action[] = ['update']
if (edge.getTargetCell() === cell) {
flagLabels.push('target')
}
if (edge.getSourceCell() === cell) {
flagLabels.push('source')
}
this.scheduleViewUpdate(
edgeView,
edgeView.getFlag(flagLabels),
edgeView.priority,
options,
)
}
}
}
forcePostponedViewUpdate(view: CellView, flag: number) {
if (!view || !CellView.isCellView(view)) {
return false
}
const cell = view.cell
if (cell.isNode()) {
return false
}
const edgeView = view as EdgeView
if (cell.isEdge() && (flag & view.getFlag(['source', 'target'])) === 0) {
// EdgeView is waiting for the source/target cellView to be rendered.
// This can happen when the cells are not in the viewport.
let sourceFlag = 0
const sourceView = this.findViewByCell(cell.getSourceCell())
if (sourceView && !this.isViewMounted(sourceView)) {
sourceFlag = this.dumpView(sourceView)
edgeView.updateTerminalMagnet('source')
}
let targetFlag = 0
const targetView = this.findViewByCell(cell.getTargetCell())
if (targetView && !this.isViewMounted(targetView)) {
targetFlag = this.dumpView(targetView)
edgeView.updateTerminalMagnet('target')
}
if (sourceFlag === 0 && targetFlag === 0) {
// If leftover flag is 0, all view updates were done.
return !this.dumpView(edgeView)
}
}
return false
}
scheduleViewUpdate(
view: View,
flag: number,
priority: number,
options: Renderer.RequestViewUpdateOptions = {},
) {
const cid = view.cid
const updates = this.updates
let cache = updates.priorities[priority]
if (!cache) {
cache = updates.priorities[priority] = {}
}
const currentFlag = cache[cid] || 0
if ((currentFlag & flag) === flag) {
return
}
if (!currentFlag) {
updates.count += 1
}
if (flag & Renderer.FLAG_REMOVE && currentFlag & Renderer.FLAG_INSERT) {
// When a view is removed we need to remove the
// insert flag as this is a reinsert.
cache[cid] ^= Renderer.FLAG_INSERT
} else if (
flag & Renderer.FLAG_INSERT &&
currentFlag & Renderer.FLAG_REMOVE
) {
// When a view is added we need to remove the remove
// flag as this is view was previously removed.
cache[cid] ^= Renderer.FLAG_REMOVE
}
cache[cid] |= flag
this.graph.hook.onViewUpdated(view as CellView, flag, options)
}
requestViewUpdate(
view: CellView,
flag: number,
priority: number,
options: Renderer.RequestViewUpdateOptions = {},
) {
this.scheduleViewUpdate(view, flag, priority, options)
const isAsync = this.isAsync()
if (
this.isFrozen() ||
(isAsync && options.async !== false) ||
this.model.hasActiveBatch(Renderer.UPDATE_DELAYING_BATCHES)
) {
return
}
const stats = this.updateViews(options)
if (isAsync) {
this.graph.trigger('render:done', { stats, options })
}
}
/**
* Adds view into the DOM and update it.
*/
dumpView(view: CellView, options: any = {}) {
if (view == null) {
return 0
}
const cid = view.cid
const updates = this.updates
const cache = updates.priorities[view.priority]
const flag = this.registerMountedView(view) | cache[cid]
delete cache[cid]
if (!flag) {
return 0
}
return this.updateView(view, flag, options)
}
/**
* Adds all views into the DOM and update them.
*/
dumpViews(options: Renderer.UpdateViewOptions = {}) {
this.checkView(options)
this.updateViews(options)
}
/**
* Ensure the view associated with the cell is attached
* to the DOM and updated.
*/
requireView(cell: Cell, options: any = {}) {
const view = this.findViewByCell(cell)
if (view == null) {
return null
}
this.dumpView(view, options)
return view
}
updateView(view: View, flag: number, options: any = {}) {
if (view == null) {
return 0
}
if (CellView.isCellView(view)) {
if (flag & Renderer.FLAG_REMOVE) {
this.removeView(view.cell as any)
return 0
}
if (flag & Renderer.FLAG_INSERT) {
this.insertView(view)
flag ^= Renderer.FLAG_INSERT // eslint-disable-line
}
}
if (!flag) {
return 0
}
return view.confirmUpdate(flag, options)
}
updateViews(options: Renderer.UpdateViewOptions = {}) {
let result: ReturnType<typeof Renderer.prototype.updateViewsBatch>
let batchCount = 0
let updatedCount = 0
let priority = Renderer.MIN_PRIORITY
do {
result = this.updateViewsBatch(options)
batchCount += 1
updatedCount += result.updatedCount
priority = Math.min(result.priority, priority)
} while (!result.empty)
return {
priority,
batchCount,
updatedCount,
}
}
protected updateViewsBatch(options: Renderer.UpdateViewOptions = {}) {
const updates = this.updates
const priorities = updates.priorities
const batchSize = options.batchSize || Renderer.UPDATE_BATCH_SIZE
let empty = true
let priority = Renderer.MIN_PRIORITY
let mountedCount = 0
let unmountedCount = 0
let updatedCount = 0
let postponedCount = 0
let checkView = options.checkView || this.options.checkView
if (typeof checkView !== 'function') {
checkView = null
}
// eslint-disable-next-line
main: for (let p = 0, n = priorities.length; p < n; p += 1) {
const cache = priorities[p]
// eslint-disable-next-line
for (const cid in cache) {
if (updatedCount >= batchSize) {
empty = false // goto next batch
break main // eslint-disable-line no-labels
}
const view = View.views[cid]
if (!view) {
delete cache[cid]
continue
}
let currentFlag = cache[cid]
// Do not check a view for viewport if we are about to remove the view.
if ((currentFlag & Renderer.FLAG_REMOVE) === 0) {
const isUnmounted = cid in updates.unmounted
if (
checkView &&
!FunctionExt.call(checkView, this.graph, {
view: view as CellView,
unmounted: isUnmounted,
})
) {
// Unmount view
if (!isUnmounted) {
this.registerUnmountedView(view)
view.unmount()
}
updates.unmounted[cid] |= currentFlag
delete cache[cid]
unmountedCount += 1
continue
}
// Mount view
if (isUnmounted) {
currentFlag |= Renderer.FLAG_INSERT
mountedCount += 1
}
currentFlag |= this.registerMountedView(view)
}
const cellView = view as CellView
let leftoverFlag = this.updateView(view, currentFlag, options)
if (leftoverFlag > 0) {
const cell = cellView.cell
if (cell && cell.isEdge()) {
// remove edge view when source cell is invisible
if (
cellView.hasAction(leftoverFlag, 'source') &&
!this.isEdgeTerminalVisible(cell, 'source')
) {
leftoverFlag = cellView.removeAction(leftoverFlag, 'source')
leftoverFlag |= Renderer.FLAG_REMOVE
}
// remove edge view when target cell is invisible
if (
cellView.hasAction(leftoverFlag, 'target') &&
!this.isEdgeTerminalVisible(cell, 'target')
) {
leftoverFlag = cellView.removeAction(leftoverFlag, 'target')
leftoverFlag |= Renderer.FLAG_REMOVE
}
}
}
if (leftoverFlag > 0) {
// update has not finished
cache[cid] = leftoverFlag
if (
!this.graph.hook.onViewPostponed(cellView, leftoverFlag, options) ||
cache[cid]
) {
postponedCount += 1
empty = false
continue
}
}
if (priority > p) {
priority = p
}
updatedCount += 1
delete cache[cid]
}
}
return {
empty,
priority,
mountedCount,
unmountedCount,
updatedCount,
postponedCount,
}
}
protected updateViewsAsync(
options: Renderer.UpdateViewsAsyncOptions = {},
data: {
processed: number
priority: number
} = {
processed: 0,
priority: Renderer.MIN_PRIORITY,
},
) {
const updates = this.updates
const animationId = updates.animationId
if (animationId) {
Dom.cancelAnimationFrame(animationId)
if (data.processed === 0) {
const beforeFn = options.before
if (typeof beforeFn === 'function') {
FunctionExt.call(beforeFn, this.graph, this.graph)
}
}
const stats = this.updateViewsBatch(options)
const checkout = this.checkViewImpl({
checkView: options.checkView,
mountedBatchSize: Renderer.MOUNT_BATCH_SIZE - stats.mountedCount,
unmountedBatchSize: Renderer.MOUNT_BATCH_SIZE - stats.unmountedCount,
})
let processed = data.processed
const total = updates.count
const mountedCount = checkout.mountedCount
const unmountedCount = checkout.unmountedCount
if (stats.updatedCount > 0) {
// Some updates have been just processed
processed += stats.updatedCount + stats.unmountedCount
data.priority = Math.min(stats.priority, data.priority)
if (stats.empty && mountedCount === 0) {
stats.priority = data.priority
stats.mountedCount += mountedCount
stats.unmountedCount += unmountedCount
this.graph.trigger('render:done', { stats, options })
data.processed = 0
updates.count = 0
} else {
data.processed = processed
}
}
// Progress callback
const progressFn = options.progress
if (total && typeof progressFn === 'function') {
FunctionExt.call(progressFn, this.graph, {
total,
done: stats.empty,
current: processed,
})
}
// The current frame could have been canceled in a callback
if (updates.animationId !== animationId) {
return
}
}
updates.animationId = Dom.requestAnimationFrame(() => {
this.updateViewsAsync(options, data)
})
}
protected registerMountedView(view: View) {
const cid = view.cid
const updates = this.updates
if (cid in updates.mounted) {
return 0
}
updates.mounted[cid] = true
updates.mountedCids.push(cid)
const flag = updates.unmounted[cid] || 0
delete updates.unmounted[cid]
return flag
}
protected registerUnmountedView(view: View) {
const cid = view.cid
const updates = this.updates
if (cid in updates.unmounted) {
return 0
}
updates.unmounted[cid] |= Renderer.FLAG_INSERT
const flag = updates.unmounted[cid]
updates.unmountedCids.push(cid)
delete updates.mounted[cid]
return flag
}
isViewMounted(view: CellView) {
if (view == null) {
return false
}
const cid = view.cid
return cid in this.updates.mounted
}
getMountedViews() {
return Object.keys(this.updates.mounted).map((cid) => CellView.views[cid])
}
getUnmountedViews() {
return Object.keys(this.updates.unmounted).map((cid) => CellView.views[cid])
}
protected checkMountedViews(
viewportFn?: Renderer.CheckViewFn | null,
batchSize?: number,
) {
let unmountCount = 0
if (typeof viewportFn !== 'function') {
return unmountCount
}
const updates = this.updates
const mounted = updates.mounted
const mountedCids = updates.mountedCids
const size =
batchSize == null
? mountedCids.length
: Math.min(mountedCids.length, batchSize)
for (let i = 0; i < size; i += 1) {
const cid = mountedCids[i]
if (!(cid in mounted)) {
continue
}
const view = CellView.views[cid]
if (view == null) {
continue
}
const shouldMount = FunctionExt.call(viewportFn, this.graph, {
view: view as CellView,
unmounted: true,
})
if (shouldMount) {
// Push at the end of all mounted ids
mountedCids.push(cid)
continue
}
unmountCount += 1
const flag = this.registerUnmountedView(view)
if (flag) {
view.unmount()
}
}
// Get rid of views, that have been unmounted
mountedCids.splice(0, size)
return unmountCount
}
protected checkUnmountedViews(
checkView?: Renderer.CheckViewFn | null,
batchSize?: number,
) {
let mountCount = 0
if (typeof checkView !== 'function') {
checkView = null // eslint-disable-line
}
const updates = this.updates
const unmounted = updates.unmounted
const unmountedCids = updates.unmountedCids
const size =
batchSize == null
? unmountedCids.length
: Math.min(unmountedCids.length, batchSize)
for (let i = 0; i < size; i += 1) {
const cid = unmountedCids[i]
if (!(cid in unmounted)) {
continue
}
const view = CellView.views[cid] as CellView
if (view == null) {
continue
}
if (
checkView &&
!FunctionExt.call(checkView, this.graph, { view, unmounted: false })
) {
unmountedCids.push(cid)
continue
}
mountCount += 1
const flag = this.registerMountedView(view)
if (flag) {
this.scheduleViewUpdate(view, flag, view.priority, {
mounting: true,
})
}
}
// Get rid of views, that have been mounted
unmountedCids.splice(0, size)
return mountCount
}
protected checkViewImpl(
options: Renderer.CheckViewOptions & {
mountedBatchSize?: number
unmountedBatchSize?: number
} = {
mountedBatchSize: Number.MAX_SAFE_INTEGER,
unmountedBatchSize: Number.MAX_SAFE_INTEGER,
},
) {
const checkView = options.checkView || this.options.checkView
const unmountedCount = this.checkMountedViews(
checkView,
options.unmountedBatchSize,
)
const mountedCount = this.checkUnmountedViews(
checkView,
// Do not check views, that have been just unmounted
// and pushed at the end of the cids array
unmountedCount > 0
? Math.min(
this.updates.unmountedCids.length - unmountedCount,
options.mountedBatchSize as number,
)
: options.mountedBatchSize,
)
return { mountedCount, unmountedCount }
}
/**
* Determine every view in the graph should be attached/detached.
*/
protected checkView(options: Renderer.CheckViewOptions = {}) {
return this.checkViewImpl(options)
}
isFrozen() {
return !!this.options.frozen
}
/**
* Freeze the graph then the graph does not automatically re-render upon
* changes in the graph. This is useful when adding large numbers of cells.
*/
freeze(options: Renderer.FreezeOptions = {}) {
const key = options.key
const updates = this.updates
const frozen = this.options.frozen
const freezeKey = updates.freezeKey
if (key && key !== freezeKey) {
if (frozen && freezeKey) {
// key passed, but the graph is already freezed with another key
return
}
updates.frozen = frozen
updates.freezeKey = key
}
this.options.frozen = true
const animationId = updates.animationId
updates.animationId = null
if (this.isAsync() && animationId != null) {
Dom.cancelAnimationFrame(animationId)
}
this.graph.trigger('freeze', { key })
}
unfreeze(options: Renderer.UnfreezeOptions = {}) {
const key = options.key
const updates = this.updates
const freezeKey = updates.freezeKey
// key passed, but the graph is already freezed with another key
if (key && freezeKey && key !== freezeKey) {
return
}
updates.freezeKey = null
// key passed, but the graph is already freezed
if (key && key === freezeKey && updates.frozen) {
return
}
const callback = () => {
this.options.frozen = updates.frozen = false
if (updates.sort) {
this.sortViews()
updates.sort = false
}
const afterFn = options.after
if (afterFn) {
FunctionExt.call(afterFn, this.graph, this.graph)
}
this.graph.trigger('unfreeze', { key })
}
if (this.isAsync()) {
this.freeze()
const onProgress = options.progress
this.updateViewsAsync({
...options,
progress: ({ done, current, total }) => {
if (onProgress) {
FunctionExt.call(onProgress, this.graph, { done, current, total })
}
// sort views after async render
if (done) {
callback()
}
},
})
} else {
this.updateViews(options)
callback()
}
}
isAsync() {
return !!this.options.async
}
setAsync(async: boolean) {
this.options.async = async
}
protected onRemove() {
this.freeze()
this.removeViews()
}
protected resetViews(cells: Cell[] = [], options: any = {}) {
this.resetUpdates()
this.removeViews()
this.freeze({ key: 'reset' })
for (let i = 0, n = cells.length; i < n; i += 1) {
this.renderView(cells[i], options)
}
this.unfreeze({ key: 'reset' })
this.sortViews()
}
protected removeView(cell: Cell) {
const view = this.views[cell.id]
if (view) {
const cid = view.cid
const updates = this.updates
const mounted = updates.mounted
const unmounted = updates.unmounted
view.remove()
delete this.views[cell.id]
delete mounted[cid]
delete unmounted[cid]
}
return view
}
protected removeViews() {
if (this.views) {
Object.keys(this.views).forEach((id) => {
const view = this.views[id]
if (view) {
this.removeView(view.cell)
}
})
}
this.views = {}
}
protected renderView(cell: Cell, options: any = {}) {
const id = cell.id
const views = this.views
let flag = 0
let view = views[id]
if (!cell.isVisible()) {
return
}
if (cell.isEdge()) {
if (
!this.isEdgeTerminalVisible(cell, 'source') ||
!this.isEdgeTerminalVisible(cell, 'target')
) {
return
}
}
if (view) {
flag = Renderer.FLAG_INSERT
} else {
const tmp = this.graph.hook.createCellView(cell)
if (tmp) {
view = views[cell.id] = tmp
view.graph = this.graph
flag = this.registerUnmountedView(view) | view.getBootstrapFlag()
}
}
if (view) {
this.requestViewUpdate(view, flag, view.priority, options)
}
}
protected isExactSorting() {
return this.options.sorting === 'exact'
}
sortViews() {
if (!this.isExactSorting()) {
return
}
if (this.isFrozen()) {
// sort views once unfrozen
this.updates.sort = true
return
}
this.sortViewsExact()
}
protected sortElements(
elems: Element[],
comparator: (a: Element, b: Element) => number,
) {
// Highly inspired by the jquery.sortElements plugin by Padolsey.
// See http://james.padolsey.com/javascript/sorting-elements-with-jquery/.
const placements = elems.map((elem) => {
const parentNode = elem.parentNode!
// Since the element itself will change position, we have
// to have some way of storing it's original position in
// the DOM. The easiest way is to have a 'flag' node:
const nextSibling = parentNode.insertBefore(
document.createTextNode(''),
elem.nextSibling,
)
return (targetNode: Element) => {
if (parentNode === targetNode) {
throw new Error(
"You can't sort elements if any one is a descendant of another.",
)
}
// Insert before flag
parentNode.insertBefore(targetNode, nextSibling)
// Remove flag
parentNode.removeChild(nextSibling)
}
})
elems.sort(comparator).forEach((elem, index) => placements[index](elem))
}
sortViewsExact() {
// const elems = this.view.stage.querySelectorAll('[data-cell-id]')
// const length = elems.length
// const cells = []
// for (let i = 0; i < length; i++) {
// const cell = this.model.getCell(elems[i].getAttribute('data-cell-id') || '')
// cells.push({
// id: cell.id,
// zIndex: cell.getZIndex() || 0,
// elem: elems[i],
// })
// }
// const sortedCells = [...cells].sort((cell1, cell2) => cell1.zIndex - cell2.zIndex)
// const moves = ArrayExt.diff(cells, sortedCells, 'zIndex').moves
// if (moves && moves.length) {
// moves.forEach((move) => {
// if (move.type) {
// const elem = move.item.elem as Element
// const parentNode = elem.parentNode
// const index = move.index
// if (parentNode) {
// if (index === length - 1) {
// parentNode.appendChild(elem)
// } else if (index < length - 1) {
// parentNode.insertBefore(elem, elems[index + 1])
// }
// }
// }
// })
// }
// Run insertion sort algorithm in order to efficiently sort DOM
// elements according to their associated cell `zIndex` attribute.
const elems = this.view
.$(this.view.stage)
.children('[data-cell-id]')
.toArray() as Element[]
const model = this.model
this.sortElements(elems, (a, b) => {
const cellA = model.getCell(a.getAttribute('data-cell-id') || '')
const cellB = model.getCell(b.getAttribute('data-cell-id') || '')
const z1 = cellA.getZIndex() || 0
const z2 = cellB.getZIndex() || 0
return z1 === z2 ? 0 : z1 < z2 ? -1 : 1
})
}
protected addZPivot(zIndex = 0) {
if (this.zPivots == null) {
this.zPivots = {}
}
const pivots = this.zPivots
let pivot = pivots[zIndex]
if (pivot) {
return pivot
}
pivot = pivots[zIndex] = document.createComment(`z-index:${zIndex + 1}`)
let neighborZ = -Infinity
// eslint-disable-next-line
for (const key in pivots) {
const currentZ = +key
if (currentZ < zIndex && currentZ > neighborZ) {
neighborZ = currentZ
if (neighborZ === zIndex - 1) {
continue
}
}
}
const layer = this.view.stage
if (neighborZ !== -Infinity) {
const neighborPivot = pivots[neighborZ]
layer.insertBefore(pivot, neighborPivot.nextSibling)
} else {
layer.insertBefore(pivot, layer.firstChild)
}
return pivot
}
protected removeZPivots() {
if (this.zPivots) {
Object.keys(this.zPivots).forEach((z) => {
const elem = this.zPivots[z]
if (elem && elem.parentNode) {
elem.parentNode.removeChild(elem)
}
})
}
this.zPivots = {}
}
insertView(view: CellView) {
const stage = this.view.stage
switch (this.options.sorting) {
case 'approx': {
const zIndex = view.cell.getZIndex()
const pivot = this.addZPivot(zIndex)
stage.insertBefore(view.container, pivot)
break
}
case 'exact':
default:
stage.appendChild(view.container)
break
}
}
findViewByCell(cellId: string | number): CellView | null
findViewByCell(cell: Cell | null): CellView | null
findViewByCell(
cell: Cell | string | number | null | undefined,
): CellView | null {
if (cell == null) {
return null
}
const id = Cell.isCell(cell) ? cell.id : cell
return this.views[id]
}
findViewByElem(elem: string | JQuery | Element | undefined | null) {
if (elem == null) {
return null
}
const target =
typeof elem === 'string'
? this.view.stage.querySelector(elem)
: elem instanceof Element
? elem
: elem[0]
if (target) {
const id = this.view.findAttr('data-cell-id', target)
if (id) {
return this.views[id]
}
}
return null
}
findViewsFromPoint(p: Point.PointLike) {
const ref = { x: p.x, y: p.y }
return this.model
.getCells()
.map((cell) => this.findViewByCell(cell))
.filter((view) => {
if (view != null) {
return Dom.getBBox(view.container as SVGElement, {
target: this.view.stage,
}).containsPoint(ref)
}
return false
}) as CellView[]
}
findEdgeViewsInArea(
rect: Rectangle.RectangleLike,
options: Renderer.FindViewsInAreaOptions = {},
) {
const area = Rectangle.create(rect)
return this.model
.getEdges()
.map((edge) => this.findViewByCell(edge))
.filter((view) => {
if (view) {
const bbox = Dom.getBBox(view.container as SVGElement, {
target: this.view.stage,
})
return options.strict
? area.containsRect(bbox)
: area.isIntersectWithRect(bbox)
}
return false
}) as CellView[]
}
findViewsInArea(
rect: Rectangle.RectangleLike,
options: Renderer.FindViewsInAreaOptions = {},
) {
const area = Rectangle.create(rect)
return this.model
.getNodes()
.map((node) => this.findViewByCell(node))
.filter((view) => {
if (view) {
const bbox = Dom.getBBox(view.container as SVGElement, {
target: this.view.stage,
})
return options.strict
? area.containsRect(bbox)
: area.isIntersectWithRect(bbox)
}
return false
}) as CellView[]
}
.dispose()
dispose() {
this.resetUpdates()
this.stopListening()
}
}
export namespace Renderer {
export interface Updates {
priorities: KeyValue<number>[]
mounted: KeyValue<boolean>
unmounted: KeyValue<number>
mountedCids: string[]
unmountedCids: string[]
animationId: number | null
count: number
sort: boolean
/**
* The last frozen state of graph.
*/
frozen: boolean
/**
* The current freeze key of graph.
*/
freezeKey: string | null
}
export type CheckViewFn = (
this: Graph,
args: {
view: CellView
unmounted: boolean
},
) => boolean
export interface CheckViewOptions {
/**
* Callback function to determine whether a given view
* should be added to the DOM.
*/
checkView?: CheckViewFn
}
export interface UpdateViewOptions extends CheckViewOptions {
/**
* For async graph, how many views should there be per
* one asynchronous process?
*/
batchSize?: number
}
export interface RequestViewUpdateOptions
extends UpdateViewOptions,
Cell.SetOptions {
async?: boolean
}
export interface UpdateViewsAsyncOptions extends UpdateViewOptions {
before?: (this: Graph, graph: Graph) => void
after?: (this: Graph, graph: Graph) => void
/**
* Callback function that is called whenever a batch is
* finished processing.
*/
progress?: (
this: Graph,
args: { done: boolean; current: number; total: number },
) => void
}
export interface FreezeOptions {
key?: string
}
export interface UnfreezeOptions
extends FreezeOptions,
UpdateViewsAsyncOptions {}
export interface FindViewsInAreaOptions {
strict?: boolean
}
}
export namespace Renderer {
export const FLAG_INSERT = 1 << 30
export const FLAG_REMOVE = 1 << 29
export const MOUNT_BATCH_SIZE = 1000
export const UPDATE_BATCH_SIZE = 1000
export const MIN_PRIORITY = 2
export const SORT_DELAYING_BATCHES: Model.BatchName[] = [
'add',
'to-front',
'to-back',
]
export const UPDATE_DELAYING_BATCHES: Model.BatchName[] = ['translate']
}