chartjs-chart-graph
Version:
Chart.js module for charting graphs
1 lines • 46 kB
Source Map (JSON)
{"version":3,"file":"index.d.ts","sources":["../src/elements/EdgeLine.ts","../src/controllers/GraphController.ts","../src/controllers/ForceDirectedGraphController.ts","../src/controllers/DendrogramController.ts","../src/controllers/TreeController.ts"],"sourcesContent":["import {\n ChartType,\n LineElement,\n LineOptions,\n PointElement,\n ScriptableAndArrayOptions,\n ScriptableContext,\n} from 'chart.js';\n\nfunction horizontal(from: { x: number }, to: { x: number }, options: { tension: number }) {\n return {\n fx: (to.x - from.x) * options.tension,\n fy: 0,\n tx: (from.x - to.x) * options.tension,\n ty: 0,\n };\n}\n\nfunction vertical(from: { y: number }, to: { y: number }, options: { tension: number }) {\n return {\n fx: 0,\n fy: (to.y - from.y) * options.tension,\n tx: 0,\n ty: (from.y - to.y) * options.tension,\n };\n}\n\nfunction radial(\n from: { x: number; angle?: number; y: number },\n to: { x: number; angle?: number; y: number },\n options: { tension: number }\n) {\n const angleHelper = Math.hypot(to.x - from.x, to.y - from.y) * options.tension;\n return {\n fx: Number.isNaN(from.angle) ? 0 : Math.cos(from.angle || 0) * angleHelper,\n fy: Number.isNaN(from.angle) ? 0 : Math.sin(from.angle || 0) * -angleHelper,\n tx: Number.isNaN(to.angle) ? 0 : Math.cos(to.angle || 0) * -angleHelper,\n ty: Number.isNaN(to.angle) ? 0 : Math.sin(to.angle || 0) * angleHelper,\n };\n}\n\nexport interface IEdgeLineOptions extends LineOptions {\n directed: boolean;\n arrowHeadSize: number;\n arrowHeadOffset: number;\n}\n\nexport interface IEdgeLineProps extends LineOptions {\n points: { x: number; y: number }[];\n}\n\nexport class EdgeLine extends LineElement {\n /**\n * @hidden\n */\n declare _orientation: 'vertical' | 'radial' | 'horizontal';\n\n /**\n * @hidden\n */\n declare source: PointElement;\n\n /**\n * @hidden\n */\n declare target: PointElement;\n\n /**\n * @hidden\n */\n declare options: IEdgeLineOptions;\n\n /**\n * @hidden\n */\n draw(ctx: CanvasRenderingContext2D): void {\n const { options } = this;\n\n ctx.save();\n\n // Stroke Line Options\n ctx.lineCap = options.borderCapStyle;\n ctx.setLineDash(options.borderDash || []);\n ctx.lineDashOffset = options.borderDashOffset;\n ctx.lineJoin = options.borderJoinStyle;\n ctx.lineWidth = options.borderWidth;\n ctx.strokeStyle = options.borderColor;\n\n const orientations = {\n horizontal,\n vertical,\n radial,\n };\n const layout = orientations[this._orientation] || orientations.horizontal;\n\n const renderLine = (\n from: { x: number; y: number; angle?: number },\n to: { x: number; y: number; angle?: number }\n ) => {\n const shift = layout(from, to, options);\n\n const fromX = {\n cpx: from.x + shift.fx,\n cpy: from.y + shift.fy,\n };\n const toX = {\n cpx: to.x + shift.tx,\n cpy: to.y + shift.ty,\n };\n\n // Line to next point\n if (options.stepped === 'middle') {\n const midpoint = (from.x + to.x) / 2.0;\n ctx.lineTo(midpoint, from.y);\n ctx.lineTo(midpoint, to.y);\n ctx.lineTo(to.x, to.y);\n } else if (options.stepped === 'after') {\n ctx.lineTo(from.x, to.y);\n ctx.lineTo(to.x, to.y);\n } else if (options.stepped) {\n ctx.lineTo(to.x, from.y);\n ctx.lineTo(to.x, to.y);\n } else if (options.tension) {\n ctx.bezierCurveTo(fromX.cpx, fromX.cpy, toX.cpx, toX.cpy, to.x, to.y);\n } else {\n ctx.lineTo(to.x, to.y);\n }\n return to;\n };\n\n const source = this.source.getProps(['x', 'y', 'angle']) as { x: number; y: number; angle?: number };\n const target = this.target.getProps(['x', 'y', 'angle']) as { x: number; y: number; angle?: number };\n const points = (this.getProps(['points'] as any) as any).points as {\n x: number;\n y: number;\n angle: number;\n }[];\n\n // Stroke Line\n ctx.beginPath();\n\n let from = source;\n ctx.moveTo(from.x, from.y);\n if (points && points.length > 0) {\n from = points.reduce(renderLine, from);\n }\n renderLine(from, target);\n\n ctx.stroke();\n\n if (options.directed) {\n const to = target;\n // compute the rotation based on from and to\n const shift = layout(from, to, options);\n const s = options.arrowHeadSize;\n const offset = options.arrowHeadOffset;\n ctx.save();\n ctx.translate(to.x, target.y);\n if (options.stepped === 'middle') {\n const midpoint = (from.x + to.x) / 2.0;\n ctx.rotate(Math.atan2(to.y - to.y, to.x - midpoint));\n } else if (options.stepped === 'after') {\n ctx.rotate(Math.atan2(to.y - to.y, to.x - from.x));\n } else if (options.stepped) {\n ctx.rotate(Math.atan2(to.y - from.y, to.x - to.x));\n } else if (options.tension) {\n const toX = {\n x: to.x + shift.tx,\n y: to.y + shift.ty,\n };\n const f = 0.1;\n ctx.rotate(Math.atan2(to.y - toX.y * (1 - f) - from.y * f, to.x - toX.x * (1 - f) - from.x * f));\n } else {\n ctx.rotate(Math.atan2(to.y - from.y, to.x - from.x));\n }\n ctx.translate(-offset, 0);\n ctx.beginPath();\n\n ctx.moveTo(0, 0);\n ctx.lineTo(-s, -s / 2);\n ctx.lineTo(-s * 0.9, 0);\n ctx.lineTo(-s, s / 2);\n ctx.closePath();\n ctx.fillStyle = ctx.strokeStyle;\n ctx.fill();\n\n ctx.restore();\n }\n\n ctx.restore();\n\n // point helper\n // ctx.save();\n // ctx.strokeStyle = 'blue';\n // ctx.beginPath();\n // ctx.moveTo(from.x, from.y);\n // ctx.lineTo(from.x + shift.fx, from.y + shift.fy, 3, 3);\n // ctx.stroke();\n // ctx.strokeStyle = 'red';\n // ctx.beginPath();\n // ctx.moveTo(to.x, to.y);\n // ctx.lineTo(to.x + shift.tx, to.y + shift.ty, 3, 3);\n // ctx.stroke();\n // ctx.restore();\n }\n\n static readonly id = 'edgeLine';\n\n /**\n * @hidden\n */\n static readonly defaults: any = /* #__PURE__ */ {\n ...LineElement.defaults,\n tension: 0,\n directed: false,\n arrowHeadSize: 15,\n arrowHeadOffset: 5,\n };\n\n /**\n * @hidden\n */\n static readonly defaultRoutes = LineElement.defaultRoutes;\n\n /**\n * @hidden\n */\n static readonly descriptors = /* #__PURE__ */ {\n _scriptable: true,\n _indexable: (name: keyof IEdgeLineOptions): boolean => name !== 'borderDash',\n };\n}\n\ndeclare module 'chart.js' {\n export interface ElementOptionsByType<TType extends ChartType> {\n edgeLine: ScriptableAndArrayOptions<IEdgeLineOptions, ScriptableContext<TType>>;\n }\n}\n","import {\n defaults,\n Chart,\n ScatterController,\n registry,\n LinearScale,\n PointElement,\n UpdateMode,\n TooltipItem,\n ChartItem,\n ChartConfiguration,\n ControllerDatasetOptions,\n ScriptableAndArrayOptions,\n LineHoverOptions,\n PointPrefixedOptions,\n PointPrefixedHoverOptions,\n ScriptableContext,\n Element,\n CartesianScaleTypeRegistry,\n CoreChartOptions,\n} from 'chart.js';\nimport { merge, clipArea, unclipArea, listenArrayEvents, unlistenArrayEvents } from 'chart.js/helpers';\nimport { EdgeLine, IEdgeLineOptions } from '../elements';\nimport interpolatePoints from './interpolatePoints';\nimport patchController from './patchController';\n\nexport type AnyObject = Record<string, unknown>;\n\nexport interface IExtendedChartMeta {\n edges: EdgeLine[];\n _parsedEdges: ITreeEdge[];\n}\n\nexport interface ITreeNode extends IGraphDataPoint {\n x: number;\n y: number;\n index?: number;\n}\n\nexport interface ITreeEdge {\n source: number;\n target: number;\n points?: { x: number; y: number }[];\n}\n\nexport class GraphController extends ScatterController {\n /**\n * @hidden\n */\n declare _ctx: CanvasRenderingContext2D;\n\n /**\n * @hidden\n */\n declare _cachedDataOpts: any;\n\n /**\n * @hidden\n */\n declare _type: string;\n\n /**\n * @hidden\n */\n declare _data: any[];\n\n /**\n * @hidden\n */\n declare _edges: any[];\n\n /**\n * @hidden\n */\n declare _sharedOptions: any;\n\n /**\n * @hidden\n */\n declare _edgeSharedOptions: any;\n\n /**\n * @hidden\n */\n declare dataElementType: any;\n\n /**\n * @hidden\n */\n private _scheduleResyncLayoutId = -1;\n\n /**\n * @hidden\n */\n edgeElementType: any;\n\n /**\n * @hidden\n */\n private readonly _edgeListener = {\n _onDataPush: (...args: any[]) => {\n const count = args.length;\n const start = (this.getDataset() as any).edges.length - count;\n const parsed = (this._cachedMeta as unknown as IExtendedChartMeta)._parsedEdges;\n args.forEach((edge) => {\n parsed.push(this._parseDefinedEdge(edge));\n });\n this._insertEdgeElements(start, count);\n },\n _onDataPop: () => {\n (this._cachedMeta as unknown as IExtendedChartMeta).edges.pop();\n (this._cachedMeta as unknown as IExtendedChartMeta)._parsedEdges.pop();\n this._scheduleResyncLayout();\n },\n _onDataShift: () => {\n (this._cachedMeta as unknown as IExtendedChartMeta).edges.shift();\n (this._cachedMeta as unknown as IExtendedChartMeta)._parsedEdges.shift();\n this._scheduleResyncLayout();\n },\n _onDataSplice: (start: number, count: number, ...args: any[]) => {\n (this._cachedMeta as unknown as IExtendedChartMeta).edges.splice(start, count);\n (this._cachedMeta as unknown as IExtendedChartMeta)._parsedEdges.splice(start, count);\n if (args.length > 0) {\n const parsed = (this._cachedMeta as unknown as IExtendedChartMeta)._parsedEdges;\n parsed.splice(start, 0, ...args.map((edge) => this._parseDefinedEdge(edge)));\n this._insertEdgeElements(start, args.length);\n } else {\n this._scheduleResyncLayout();\n }\n },\n _onDataUnshift: (...args: any[]) => {\n const parsed = (this._cachedMeta as unknown as IExtendedChartMeta)._parsedEdges;\n parsed.unshift(...args.map((edge) => this._parseDefinedEdge(edge)));\n this._insertEdgeElements(0, args.length);\n },\n };\n\n /**\n * @hidden\n */\n initialize(): void {\n const type = this._type;\n const defaultConfig = defaults.datasets[type as 'graph'] as any;\n this.edgeElementType = registry.getElement(defaultConfig.edgeElementType as string);\n super.initialize();\n this.enableOptionSharing = true;\n this._scheduleResyncLayout();\n }\n\n /**\n * @hidden\n */\n parse(start: number, count: number): void {\n const meta = this._cachedMeta;\n const data = this._data;\n const { iScale, vScale } = meta;\n for (let i = 0; i < count; i += 1) {\n const index = i + start;\n const d = data[index];\n const v = (meta._parsed[index] || {}) as { x: number; y: number };\n if (d && typeof d.x === 'number') {\n v.x = d.x;\n }\n if (d && typeof d.y === 'number') {\n v.y = d.y;\n }\n meta._parsed[index] = v;\n }\n if (meta._parsed.length > data.length) {\n meta._parsed.splice(data.length, meta._parsed.length - data.length);\n }\n this._cachedMeta._sorted = false;\n (iScale as any)._dataLimitsCached = false;\n (vScale as any)._dataLimitsCached = false;\n\n this._parseEdges();\n }\n\n /**\n * @hidden\n */\n reset(): void {\n this.resetLayout();\n super.reset();\n }\n\n /**\n * @hidden\n */\n update(mode: UpdateMode): void {\n super.update(mode);\n\n const meta = this._cachedMeta as unknown as IExtendedChartMeta;\n const edges = meta.edges || [];\n\n this.updateEdgeElements(edges, 0, mode);\n }\n\n /**\n * @hidden\n */\n _destroy(): void {\n (ScatterController.prototype as any)._destroy.call(this);\n if (this._edges) {\n unlistenArrayEvents(this._edges, this._edgeListener);\n }\n this.stopLayout();\n }\n\n declare getContext: (index: number, active: boolean, mode: UpdateMode) => unknown;\n\n /**\n * @hidden\n */\n updateEdgeElements(edges: EdgeLine[], start: number, mode: UpdateMode): void {\n const bak = {\n _cachedDataOpts: this._cachedDataOpts,\n dataElementType: this.dataElementType,\n _sharedOptions: this._sharedOptions,\n // getDataset: this.getDataset,\n // getParsed: this.getParsed,\n };\n this._cachedDataOpts = {};\n this.dataElementType = this.edgeElementType;\n this._sharedOptions = this._edgeSharedOptions;\n\n const dataset = this.getDataset();\n const meta = this._cachedMeta;\n const nodeElements = meta.data;\n const data = (this._cachedMeta as unknown as IExtendedChartMeta)._parsedEdges;\n\n // get generic context to prefill cache\n this.getContext(-1, false, mode);\n this.getDataset = () => {\n return new Proxy(dataset, {\n get(obj: any, prop: string) {\n return prop === 'data' ? (obj.edges ?? []) : obj[prop];\n },\n });\n };\n this.getParsed = (index: number) => {\n return data[index] as any;\n };\n // patch meta to store edges\n meta.data = (meta as any).edges;\n\n const reset = mode === 'reset';\n\n const firstOpts = this.resolveDataElementOptions(start, mode);\n const dummyShared = {};\n const sharedOptions = this.getSharedOptions(firstOpts) ?? dummyShared;\n const includeOptions = this.includeOptions(mode, sharedOptions);\n\n const { xScale, yScale } = meta;\n\n const base = {\n x: xScale?.getBasePixel() ?? 0,\n y: yScale?.getBasePixel() ?? 0,\n };\n\n function copyPoint(point: { x: number; y: number; angle?: number }) {\n const x = reset ? base.x : (xScale?.getPixelForValue(point.x, 0) ?? 0);\n const y = reset ? base.y : (yScale?.getPixelForValue(point.y, 0) ?? 0);\n return {\n x,\n y,\n angle: point.angle,\n };\n }\n\n for (let i = 0; i < edges.length; i += 1) {\n const edge = edges[i];\n const index = start + i;\n const parsed = data[index];\n\n const properties: any = {\n source: nodeElements[parsed.source],\n target: nodeElements[parsed.target],\n points: Array.isArray(parsed.points) ? parsed.points.map((p) => copyPoint(p)) : [],\n };\n properties.points._source = nodeElements[parsed.source];\n if (includeOptions) {\n if (sharedOptions !== dummyShared) {\n properties.options = sharedOptions;\n } else {\n properties.options = this.resolveDataElementOptions(index, mode);\n }\n }\n this.updateEdgeElement(edge, index, properties, mode);\n }\n this.updateSharedOptions(sharedOptions, mode, firstOpts);\n\n this._edgeSharedOptions = this._sharedOptions;\n Object.assign(this, bak);\n delete (this as any).getDataset;\n delete (this as any).getParsed;\n // patch meta to store edges\n meta.data = nodeElements;\n }\n\n /**\n * @hidden\n */\n\n updateEdgeElement(edge: EdgeLine, index: number, properties: any, mode: UpdateMode): void {\n super.updateElement(edge as unknown as Element<AnyObject, AnyObject>, index, properties, mode);\n }\n\n /**\n * @hidden\n */\n\n updateElement(point: Element<AnyObject, AnyObject>, index: number, properties: any, mode: UpdateMode): void {\n if (mode === 'reset') {\n // start in center also in x\n const { xScale } = this._cachedMeta;\n\n properties.x = xScale?.getBasePixel() ?? 0;\n }\n super.updateElement(point, index, properties, mode);\n }\n\n /**\n * @hidden\n */\n resolveNodeIndex(nodes: any[], ref: string | number | any): number {\n if (typeof ref === 'number') {\n // index\n return ref;\n }\n if (typeof ref === 'string') {\n // label\n const labels = this.chart.data.labels as string[];\n return labels.indexOf(ref);\n }\n const nIndex = nodes.indexOf(ref);\n if (nIndex >= 0) {\n // hit\n return nIndex;\n }\n\n const data = this.getDataset().data as any[];\n const index = data.indexOf(ref);\n if (index >= 0) {\n return index;\n }\n\n console.warn('cannot resolve edge ref', ref);\n return -1;\n }\n\n /**\n * @hidden\n */\n buildOrUpdateElements(): void {\n const dataset = this.getDataset() as any;\n const edges = dataset.edges || [];\n\n // In order to correctly handle data addition/deletion animation (an thus simulate\n // real-time charts), we need to monitor these data modifications and synchronize\n // the internal meta data accordingly.\n if (this._edges !== edges) {\n if (this._edges) {\n // This case happens when the user replaced the data array instance.\n unlistenArrayEvents(this._edges, this._edgeListener);\n }\n\n if (edges && Object.isExtensible(edges)) {\n listenArrayEvents(edges, this._edgeListener);\n }\n this._edges = edges;\n }\n super.buildOrUpdateElements();\n }\n\n /**\n * @hidden\n */\n draw(): void {\n const meta = this._cachedMeta;\n const edges = (this._cachedMeta as unknown as IExtendedChartMeta).edges || [];\n const elements = (meta.data || []) as unknown[] as PointElement[];\n\n const area = this.chart.chartArea;\n const ctx = this._ctx;\n\n if (edges.length > 0) {\n clipArea(ctx, area);\n edges.forEach((edge) => (edge.draw.call as any)(edge, ctx, area));\n unclipArea(ctx);\n }\n\n elements.forEach((elem) => (elem.draw.call as any)(elem, ctx, area));\n }\n\n protected _resyncElements(): void {\n (ScatterController.prototype as any)._resyncElements.call(this);\n\n const meta = this._cachedMeta as unknown as IExtendedChartMeta;\n const edges = meta._parsedEdges;\n const metaEdges = meta.edges || (meta.edges = []);\n const numMeta = metaEdges.length;\n const numData = edges.length;\n\n if (numData < numMeta) {\n metaEdges.splice(numData, numMeta - numData);\n this._scheduleResyncLayout();\n } else if (numData > numMeta) {\n this._insertEdgeElements(numMeta, numData - numMeta);\n }\n }\n\n getTreeRootIndex(): number {\n const ds = this.getDataset() as any;\n const nodes = ds.data as any[];\n if (ds.derivedEdges) {\n // find the one with no parent\n return nodes.findIndex((d) => d.parent == null);\n }\n // find the one with no edge\n const edges = (this._cachedMeta as unknown as IExtendedChartMeta)._parsedEdges || [];\n const nodeIndices = new Set(nodes.map((_, i) => i));\n edges.forEach((edge) => {\n nodeIndices.delete(edge.target);\n });\n return Array.from(nodeIndices)[0];\n }\n\n getTreeRoot(): ITreeNode {\n const index = this.getTreeRootIndex();\n const p = this.getParsed(index) as ITreeNode;\n p.index = index;\n return p;\n }\n\n getTreeChildren(node: { index?: number }): ITreeNode[] {\n const edges = (this._cachedMeta as unknown as IExtendedChartMeta)._parsedEdges;\n const index = node.index ?? 0;\n return edges\n .filter((d) => d.source === index)\n .map((d) => {\n const p = this.getParsed(d.target) as ITreeNode;\n p.index = d.target;\n return p;\n });\n }\n\n /**\n * @hidden\n */\n _parseDefinedEdge(edge: { source: number; target: number }): ITreeEdge {\n const ds = this.getDataset();\n const { data } = ds;\n return {\n source: this.resolveNodeIndex(data, edge.source),\n target: this.resolveNodeIndex(data, edge.target),\n points: [],\n };\n }\n\n /**\n * @hidden\n */\n _parseEdges(): ITreeEdge[] {\n const ds = this.getDataset() as any;\n const data = ds.data as { parent?: number }[];\n const meta = this._cachedMeta as unknown as IExtendedChartMeta;\n if (ds.edges) {\n const edges = ds.edges.map((edge: any) => this._parseDefinedEdge(edge));\n meta._parsedEdges = edges;\n return edges;\n }\n\n const edges: ITreeEdge[] = [];\n meta._parsedEdges = edges as any;\n // try to derive edges via parent links\n data.forEach((node, i) => {\n if (node.parent != null) {\n // tree edge\n const parent = this.resolveNodeIndex(data, node.parent);\n edges.push({\n source: parent,\n target: i,\n points: [],\n });\n }\n });\n return edges;\n }\n\n /**\n * @hidden\n */\n addElements(): void {\n super.addElements();\n\n const meta = this._cachedMeta as unknown as IExtendedChartMeta;\n const edges = this._parseEdges();\n const metaData = new Array(edges.length);\n meta.edges = metaData;\n\n for (let i = 0; i < edges.length; i += 1) {\n metaData[i] = new this.edgeElementType();\n }\n }\n\n /**\n * @hidden\n */\n _resyncEdgeElements(): void {\n const meta = this._cachedMeta as unknown as IExtendedChartMeta;\n const edges = this._parseEdges();\n const metaData = meta.edges || (meta.edges = []);\n\n for (let i = 0; i < edges.length; i += 1) {\n metaData[i] = metaData[i] || new this.edgeElementType();\n }\n if (edges.length < metaData.length) {\n metaData.splice(edges.length, metaData.length);\n }\n }\n\n /**\n * @hidden\n */\n _insertElements(start: number, count: number): void {\n (ScatterController.prototype as any)._insertElements.call(this, start, count);\n if (count > 0) {\n this._resyncEdgeElements();\n }\n }\n\n /**\n * @hidden\n */\n _removeElements(start: number, count: number): void {\n (ScatterController.prototype as any)._removeElements.call(this, start, count);\n if (count > 0) {\n this._resyncEdgeElements();\n }\n }\n\n /**\n * @hidden\n */\n _insertEdgeElements(start: number, count: number): void {\n const elements = [];\n for (let i = 0; i < count; i += 1) {\n elements.push(new this.edgeElementType());\n }\n (this._cachedMeta as unknown as IExtendedChartMeta).edges.splice(start, 0, ...elements);\n this.updateEdgeElements(elements, start, 'reset');\n this._scheduleResyncLayout();\n }\n\n reLayout(): void {\n // hook\n }\n\n resetLayout(): void {\n // hook\n }\n\n stopLayout(): void {\n // hook\n }\n\n /**\n * @hidden\n */\n _scheduleResyncLayout(): void {\n if (this._scheduleResyncLayoutId != null && this._scheduleResyncLayoutId >= 0) {\n return;\n }\n this._scheduleResyncLayoutId = requestAnimationFrame(() => {\n this._scheduleResyncLayoutId = -1;\n this.resyncLayout();\n });\n }\n\n resyncLayout(): void {\n // hook\n }\n\n static readonly id: string = 'graph';\n\n /**\n * @hidden\n */\n static readonly defaults: any = /* #__PURE__ */ merge({}, [\n ScatterController.defaults,\n {\n clip: 10, // some space in combination with padding\n animations: {\n points: {\n fn: interpolatePoints,\n properties: ['points'],\n },\n },\n edgeElementType: EdgeLine.id,\n },\n ]);\n\n /**\n * @hidden\n */\n static readonly overrides: any = /* #__PURE__ */ merge({}, [\n (ScatterController as any).overrides,\n {\n layout: {\n padding: 10,\n },\n scales: {\n x: {\n display: false,\n ticks: {\n maxTicksLimit: 2,\n precision: 100,\n minRotation: 0,\n maxRotation: 0,\n },\n },\n y: {\n display: false,\n ticks: {\n maxTicksLimit: 2,\n precision: 100,\n minRotation: 0,\n maxRotation: 0,\n },\n },\n },\n plugins: {\n tooltip: {\n callbacks: {\n label(item: TooltipItem<'graph'>) {\n return item.chart.data?.labels?.[item.dataIndex];\n },\n },\n },\n },\n },\n ]);\n}\n\nexport interface IGraphDataPoint {\n parent?: number;\n}\n\nexport interface IGraphEdgeDataPoint {\n source: number | string;\n target: number | string;\n}\n\nexport interface IGraphChartControllerDatasetOptions\n extends ControllerDatasetOptions,\n ScriptableAndArrayOptions<PointPrefixedOptions, ScriptableContext<'graph'>>,\n ScriptableAndArrayOptions<PointPrefixedHoverOptions, ScriptableContext<'graph'>>,\n ScriptableAndArrayOptions<IEdgeLineOptions, ScriptableContext<'graph'>>,\n ScriptableAndArrayOptions<LineHoverOptions, ScriptableContext<'graph'>> {\n edges: IGraphEdgeDataPoint[];\n}\n\ndeclare module 'chart.js' {\n export interface ChartTypeRegistry {\n graph: {\n chartOptions: CoreChartOptions<'graph'>;\n datasetOptions: IGraphChartControllerDatasetOptions;\n defaultDataPoint: IGraphDataPoint;\n metaExtensions: Record<string, never>;\n parsedDataType: ITreeNode;\n scales: keyof CartesianScaleTypeRegistry;\n };\n }\n}\n\nexport class GraphChart<DATA extends unknown[] = IGraphDataPoint[], LABEL = string> extends Chart<\n 'graph',\n DATA,\n LABEL\n> {\n static id = GraphController.id;\n\n constructor(item: ChartItem, config: Omit<ChartConfiguration<'graph', DATA, LABEL>, 'type'>) {\n super(item, patchController('graph', config, GraphController, [EdgeLine, PointElement], LinearScale));\n }\n}\n","import {\n Chart,\n ChartItem,\n ChartConfiguration,\n LinearScale,\n PointElement,\n CoreChartOptions,\n CartesianScaleTypeRegistry,\n} from 'chart.js';\nimport { merge } from 'chart.js/helpers';\nimport {\n forceCenter,\n forceCollide,\n forceLink,\n ForceLink,\n forceManyBody,\n forceRadial,\n forceSimulation,\n forceX,\n forceY,\n Simulation,\n SimulationLinkDatum,\n SimulationNodeDatum,\n} from 'd3-force';\nimport { EdgeLine } from '../elements';\nimport {\n GraphController,\n IGraphChartControllerDatasetOptions,\n IGraphDataPoint,\n ITreeNode,\n IExtendedChartMeta,\n} from './GraphController';\nimport patchController from './patchController';\n\nexport interface ITreeSimNode extends ITreeNode {\n _sim: { x?: number; y?: number; vx?: number; vy?: number; index?: number };\n reset?: boolean;\n}\n\nexport interface IForceDirectedControllerOptions {\n simulation: {\n /**\n * auto restarts the simulation upon dataset change, one can manually restart by calling: `chart.getDatasetMeta(0).controller.reLayout();`\n *\n * @default true\n */\n autoRestart: boolean;\n\n initialIterations: number;\n\n forces: {\n /**\n * center force\n * https://github.com/d3/d3-force/#centering\n *\n * @default true\n */\n center: boolean | ICenterForce;\n\n /**\n * collision between nodes\n * https://github.com/d3/d3-force/#collision\n *\n * @default false\n */\n collide: boolean | ICollideForce;\n\n /**\n * link force\n * https://github.com/d3/d3-force/#links\n *\n * @default true\n */\n link: boolean | ILinkForce;\n\n /**\n * link force\n * https://github.com/d3/d3-force/#many-body\n *\n * @default true\n */\n manyBody: boolean | IManyBodyForce;\n\n /**\n * x positioning force\n * https://github.com/d3/d3-force/#forceX\n *\n * @default false\n */\n x: boolean | IForceXForce;\n\n /**\n * y positioning force\n * https://github.com/d3/d3-force/#forceY\n *\n * @default false\n */\n y: boolean | IForceYForce;\n\n /**\n * radial positioning force\n * https://github.com/d3/d3-force/#forceRadial\n *\n * @default false\n */\n radial: boolean | IRadialForce;\n };\n };\n}\n\nexport declare type ID3NodeCallback = (d: any, i: number) => number;\nexport declare type ID3EdgeCallback = (d: any, i: number) => number;\n\nexport interface ICenterForce {\n x?: number;\n y?: number;\n}\n\nexport interface ICollideForce {\n radius?: number | ID3NodeCallback;\n strength?: number | ID3NodeCallback;\n}\n\nexport interface ILinkForce {\n id?: (d: { source: any; target: any }) => string | number;\n distance?: number | ID3EdgeCallback;\n strength?: number | ID3EdgeCallback;\n}\n\nexport interface IManyBodyForce {\n strength?: number | ID3NodeCallback;\n theta?: number;\n distanceMin?: number;\n distanceMax?: number;\n}\n\nexport interface IForceXForce {\n x?: number;\n strength?: number;\n}\n\nexport interface IForceYForce {\n y?: number;\n strength?: number;\n}\n\nexport interface IRadialForce {\n x?: number;\n y?: number;\n radius?: number;\n strength?: number;\n}\n\nexport class ForceDirectedGraphController extends GraphController {\n /**\n * @hidden\n */\n declare options: IForceDirectedControllerOptions;\n\n /**\n * @hidden\n */\n private readonly _simulation: Simulation<SimulationNodeDatum, undefined>;\n\n private _animTimer: number = -1;\n\n constructor(chart: Chart, datasetIndex: number) {\n super(chart, datasetIndex);\n this._simulation = forceSimulation()\n .on('tick', () => {\n if (this.chart.canvas && this._animTimer !== -2) {\n this._copyPosition();\n this.chart.render();\n } else {\n this._simulation.stop();\n }\n })\n .on('end', () => {\n if (this.chart.canvas && this._animTimer !== -2) {\n this._copyPosition();\n this.chart.render();\n // trigger a full update\n this.chart.update('default');\n }\n });\n const sim = this.options.simulation;\n\n const fs = {\n center: forceCenter,\n collide: forceCollide,\n link: forceLink,\n manyBody: forceManyBody,\n x: forceX,\n y: forceY,\n radial: forceRadial,\n };\n\n (Object.keys(fs) as (keyof typeof fs)[]).forEach((key) => {\n const options = sim.forces[key] as any;\n if (!options) {\n return;\n }\n const f = (fs[key] as any)();\n if (typeof options !== 'boolean') {\n Object.keys(options).forEach((attr) => {\n f[attr](options[attr]);\n });\n }\n this._simulation.force(key, f);\n });\n this._simulation.stop();\n }\n\n _destroy() {\n if (this._animTimer >= 0) {\n cancelAnimationFrame(this._animTimer);\n }\n this._animTimer = -2;\n return super._destroy();\n }\n\n /**\n * @hidden\n */\n _copyPosition(): void {\n const nodes = this._cachedMeta._parsed as ITreeSimNode[];\n\n const minmax = nodes.reduce(\n (acc, v) => {\n const s = v._sim;\n if (!s || s.x == null || s.y == null) {\n return acc;\n }\n if (s.x < acc.minX) {\n acc.minX = s.x;\n }\n if (s.x > acc.maxX) {\n acc.maxX = s.x;\n }\n if (s.y < acc.minY) {\n acc.minY = s.y;\n }\n if (s.y > acc.maxY) {\n acc.maxY = s.y;\n }\n return acc;\n },\n {\n minX: Number.POSITIVE_INFINITY,\n maxX: Number.NEGATIVE_INFINITY,\n minY: Number.POSITIVE_INFINITY,\n maxY: Number.NEGATIVE_INFINITY,\n }\n );\n\n const rescaleX = (v: number) => ((v - minmax.minX) / (minmax.maxX - minmax.minX)) * 2 - 1;\n const rescaleY = (v: number) => ((v - minmax.minY) / (minmax.maxY - minmax.minY)) * 2 - 1;\n\n nodes.forEach((node) => {\n if (node._sim) {\n node.x = rescaleX(node._sim.x ?? 0);\n\n node.y = rescaleY(node._sim.y ?? 0);\n }\n });\n\n const { xScale, yScale } = this._cachedMeta;\n const elems = this._cachedMeta.data;\n elems.forEach((elem, i) => {\n const parsed = nodes[i];\n Object.assign(elem, {\n x: xScale?.getPixelForValue(parsed.x, i) ?? 0,\n y: yScale?.getPixelForValue(parsed.y, i) ?? 0,\n skip: false,\n });\n });\n }\n\n resetLayout(): void {\n super.resetLayout();\n this._simulation.stop();\n\n const nodes = (this._cachedMeta._parsed as ITreeSimNode[]).map((node, i) => {\n const simNode: ITreeSimNode['_sim'] = { ...node };\n simNode.index = i;\n\n node._sim = simNode;\n if (!node.reset) {\n return simNode;\n }\n delete simNode.x;\n delete simNode.y;\n delete simNode.vx;\n delete simNode.vy;\n return simNode;\n });\n this._simulation.nodes(nodes);\n this._simulation.alpha(1).restart();\n }\n\n resyncLayout(): void {\n super.resyncLayout();\n this._simulation.stop();\n\n const meta = this._cachedMeta;\n\n const nodes = (meta._parsed as ITreeSimNode[]).map((node, i) => {\n const simNode: ITreeSimNode['_sim'] = { ...node };\n simNode.index = i;\n\n node._sim = simNode;\n if (simNode.x === null) {\n delete simNode.x;\n }\n if (simNode.y === null) {\n delete simNode.y;\n }\n if (simNode.x == null && simNode.y == null) {\n node.reset = true;\n }\n return simNode;\n });\n const link =\n this._simulation.force<ForceLink<SimulationNodeDatum, SimulationLinkDatum<SimulationNodeDatum>>>('link');\n if (link) {\n link.links([]);\n }\n this._simulation.nodes(nodes);\n if (link) {\n // console.assert(ds.edges.length === meta.edges.length);\n // work on copy to avoid change\n link.links(((meta as unknown as IExtendedChartMeta)._parsedEdges || []).map((l) => ({ ...l })));\n }\n\n if (this.options.simulation.initialIterations > 0) {\n this._simulation.alpha(1);\n this._simulation.tick(this.options.simulation.initialIterations);\n this._copyPosition();\n if (this.options.simulation.autoRestart) {\n this._simulation.restart();\n } else if (this.chart.canvas != null && this._animTimer !== -2) {\n const chart = this.chart;\n this._animTimer = requestAnimationFrame(() => {\n if (chart.canvas) {\n chart.update();\n }\n });\n }\n } else if (this.options.simulation.autoRestart && this.chart.canvas != null && this._animTimer !== -2) {\n this._simulation.alpha(1).restart();\n }\n }\n\n reLayout(): void {\n this._simulation.alpha(1).restart();\n }\n\n stopLayout(): void {\n super.stopLayout();\n this._simulation.stop();\n }\n\n static readonly id = 'forceDirectedGraph';\n\n /**\n * @hidden\n */\n static readonly defaults: any = /* #__PURE__ */ merge({}, [\n GraphController.defaults,\n {\n animation: false,\n simulation: {\n initialIterations: 0,\n autoRestart: true,\n forces: {\n center: true,\n collide: false,\n link: true,\n manyBody: true,\n x: false,\n y: false,\n radial: false,\n },\n },\n },\n ]);\n\n /**\n * @hidden\n */\n static readonly overrides: any = /* #__PURE__ */ merge({}, [\n GraphController.overrides,\n {\n scales: {\n x: {\n min: -1,\n max: 1,\n },\n y: {\n min: -1,\n max: 1,\n },\n },\n },\n ]);\n}\n\nexport interface IForceDirectedGraphChartControllerDatasetOptions\n extends IGraphChartControllerDatasetOptions,\n IForceDirectedControllerOptions {}\n\ndeclare module 'chart.js' {\n export interface ChartTypeRegistry {\n forceDirectedGraph: {\n chartOptions: CoreChartOptions<'forceDirectedGraph'> & IForceDirectedControllerOptions;\n datasetOptions: IForceDirectedGraphChartControllerDatasetOptions;\n defaultDataPoint: IGraphDataPoint & Record<string, unknown>;\n metaExtensions: Record<string, never>;\n parsedDataType: ITreeSimNode;\n scales: keyof CartesianScaleTypeRegistry;\n };\n }\n}\n\nexport class ForceDirectedGraphChart<DATA extends unknown[] = IGraphDataPoint[], LABEL = string> extends Chart<\n 'forceDirectedGraph',\n DATA,\n LABEL\n> {\n static id = ForceDirectedGraphController.id;\n\n constructor(item: ChartItem, config: Omit<ChartConfiguration<'forceDirectedGraph', DATA, LABEL>, 'type'>) {\n super(\n item,\n patchController('forceDirectedGraph', config, ForceDirectedGraphController, [EdgeLine, PointElement], LinearScale)\n );\n }\n}\n","import {\n Chart,\n ChartItem,\n ChartConfiguration,\n LinearScale,\n PointElement,\n UpdateMode,\n Element,\n CartesianScaleTypeRegistry,\n CoreChartOptions,\n} from 'chart.js';\nimport { merge } from 'chart.js/helpers';\nimport { cluster, hierarchy, HierarchyNode, tree } from 'd3-hierarchy';\nimport { EdgeLine } from '../elements';\nimport {\n GraphController,\n IGraphChartControllerDatasetOptions,\n IGraphDataPoint,\n ITreeNode,\n AnyObject,\n} from './GraphController';\nimport patchController from './patchController';\n\nexport interface ITreeOptions {\n /**\n * tree (cluster) or dendrogram layout default depends on the chart type\n */\n mode: 'dendrogram' | 'tree' | 'dendrogram';\n /**\n * orientation of the tree layout\n * @default horizontal\n */\n orientation: 'horizontal' | 'vertical' | 'radial';\n}\n\nexport class DendrogramController extends GraphController {\n /**\n * @hidden\n */\n declare options: { tree: ITreeOptions };\n\n private _animTimer: number = -1;\n\n /**\n * @hidden\n */\n\n updateEdgeElement(line: EdgeLine, index: number, properties: any, mode: UpdateMode): void {\n properties._orientation = this.options.tree.orientation;\n super.updateEdgeElement(line, index, properties, mode);\n }\n\n _destroy() {\n if (this._animTimer >= 0) {\n cancelAnimationFrame(this._animTimer);\n }\n this._animTimer = -2;\n return super._destroy();\n }\n\n /**\n * @hidden\n */\n\n updateElement(point: Element<AnyObject, AnyObject>, index: number, properties: any, mode: UpdateMode): void {\n if (index != null) {\n properties.angle = (this.getParsed(index) as { angle: number }).angle;\n }\n super.updateElement(point, index, properties, mode);\n }\n\n resyncLayout(): void {\n const meta = this._cachedMeta as any;\n\n meta.root = hierarchy(this.getTreeRoot(), (d) => this.getTreeChildren(d))\n .count()\n .sort((a, b) => b.height - a.height || (b.data.index ?? 0) - (a.data.index ?? 0));\n\n this.doLayout(meta.root);\n\n super.resyncLayout();\n }\n\n reLayout(newOptions: Partial<ITreeOptions> = {}): void {\n if (newOptions) {\n Object.assign(this.options.tree, newOptions);\n const ds = this.getDataset() as any;\n if (ds.tree) {\n Object.assign(ds.tree, newOptions);\n } else {\n ds.tree = newOptions;\n }\n }\n this.doLayout((this._cachedMeta as any).root);\n }\n\n doLayout(root: HierarchyNode<{ x: number; y: number; angle?: number }>): void {\n const options = this.options.tree;\n\n const layout =\n options.mode === 'tree'\n ? tree<{ x: number; y: number; angle?: number }>()\n : cluster<{ x: number; y: number; angle?: number }>();\n\n if (options.orientation === 'radial') {\n layout.size([Math.PI * 2, 1]);\n } else {\n layout.size([2, 2]);\n }\n\n const orientation = {\n horizontal: (d: { x: number; y: number; data: { x: number; y: number } }) => {\n d.data.x = d.y - 1;\n\n d.data.y = -d.x + 1;\n },\n vertical: (d: { x: number; y: number; data: { x: number; y: number } }) => {\n d.data.x = d.x - 1;\n\n d.data.y = -d.y + 1;\n },\n radial: (d: { x: number; y: number; data: { x: number; y: number; angle?: number } }) => {\n d.data.x = Math.cos(d.x) * d.y;\n\n d.data.y = Math.sin(d.x) * d.y;\n\n d.data.angle = d.y === 0 ? Number.NaN : d.x;\n },\n };\n\n layout(root).each((orientation[options.orientation] || orientation.horizontal) as any);\n\n const chart = this.chart;\n if (this._animTimer !== -2) {\n this._animTimer = requestAnimationFrame(() => {\n if (chart.canvas) {\n chart.update();\n }\n });\n }\n }\n\n static readonly id: string = 'dendrogram';\n\n /**\n * @hidden\n */\n static readonly defaults: any = /* #__PURE__ */ merge({}, [\n GraphController.defaults,\n {\n tree: {\n mode: 'dendrogram', // dendrogram, tree\n orientation: 'horizontal', // vertical, horizontal, radial\n },\n animations: {\n numbers: {\n type: 'number',\n properties: ['x', 'y', 'angle', 'radius', 'rotation', 'borderWidth'],\n },\n },\n tension: 0.4,\n },\n ]);\n\n /**\n * @hidden\n */\n static readonly overrides: any = /* #__PURE__ */ merge({}, [\n GraphController.overrides,\n {\n scales: {\n x: {\n min: -1,\n max: 1,\n },\n y: {\n min: -1,\n max: 1,\n },\n },\n },\n ]);\n}\n\nexport interface IDendrogramChartControllerDatasetOptions extends IGraphChartControllerDatasetOptions {\n tree: ITreeOptions;\n}\n\ndeclare module 'chart.js' {\n export interface ChartTypeRegistry {\n dendogram: {\n chartOptions: CoreChartOptions<'dendrogram'> & { tree: ITreeOptions };\n datasetOptions: IDendrogramChartControllerDatasetOptions;\n defaultDataPoint: IGraphDataPoint & Record<string, unknown>;\n metaExtensions: Record<string, never>;\n parsedDataType: ITreeNode & { angle?: number };\n scales: keyof CartesianScaleTypeRegistry;\n };\n dendrogram: {\n chartOptions: CoreChartOptions<'dendrogram'> & { tree: ITreeOptions };\n datasetOptions: IDendrogramChartControllerDatasetOptions;\n defaultDataPoint: IGraphDataPoint & Record<string, unknown>;\n metaExtensions: Record<string, never>;\n parsedDataType: ITreeNode & { angle?: number };\n scales: keyof CartesianScaleTypeRegistry;\n };\n }\n}\n\nexport class DendrogramChart<DATA extends unknown[] = IGraphDataPoint[], LABEL = string> extends Chart<\n 'dendrogram',\n DATA,\n LABEL\n> {\n static id = DendrogramController.id;\n\n constructor(item: ChartItem, config: Omit<ChartConfiguration<'dendrogram', DATA, LABEL>, 'type'>) {\n super(item, patchController('dendrogram', config, DendrogramController, [EdgeLine, PointElement], LinearScale));\n }\n}\n\nexport class DendogramController extends DendrogramController {\n static readonly id: string = 'dendogram';\n\n /**\n * @hidden\n */\n static readonly defaults: any = /* #__PURE__ */ merge({}, [\n DendrogramController.defaults,\n {\n tree: {\n mode: 'dendrogram', // dendrogram, tree\n },\n },\n ]);\n}\n\nexport const DendogramChart = DendrogramChart;\n","import {\n CartesianScaleTypeRegistry,\n Chart,\n ChartConfiguration,\n ChartItem,\n CoreChartOptions,\n LinearScale,\n PointElement,\n} from 'chart.js';\nimport { merge } from 'chart.js/helpers';\nimport { EdgeLine } from '../elements';\nimport { DendrogramController, IDendrogramChartControllerDatasetOptions, ITreeOptions } from './DendrogramController';\nimport type { IGraphDataPoint, ITreeNode } from './GraphController';\nimport patchController from './patchController';\n\nexport class TreeController extends DendrogramController {\n static readonly id = 'tree';\n\n /**\n * @hidden\n */\n static readonly defaults: any = /* #__PURE__ */ merge({}, [\n DendrogramController.defaults,\n {\n tree: {\n mode: 'tree',\n },\n },\n ]);\n\n /**\n * @hidden\n */\n static readonly overrides: any = /* #__PURE__ */ DendrogramController.overrides;\n}\n\ndeclare module 'chart.js' {\n export interface ChartTypeRegistry {\n tree: {\n chartOptions: CoreChartOptions<'tree'> & { tree: ITreeOptions };\n datasetOptions: IDendrogramChartControllerDatasetOptions;\n defaultDataPoint: IGraphDataPoint & Record<string, unknown>;\n metaExtensions: Record<string, never>;\n parsedDataType: ITreeNode;\n scales: keyof CartesianScaleTypeRegistry;\n };\n }\n}\n\nexport class TreeChart<DATA extends unknown[] = IGraphDataPoint[], LABEL = string> extends Chart<'tree', DATA, LABEL> {\n static id = TreeController.id;\n\n constructor(item: ChartItem, config: Omit<ChartConfiguration<'tree', DATA, LABEL>, 'type'>) {\n super(item, patchController('tree', config, TreeController, [EdgeLine, PointElement], LinearScale));\n }\n}\n"],"names":[],"mappings":";;;;;;;;;;AACO;AACP;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC9BO;AACA;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;AC5FO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACA;AACA;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;AC7FO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACO;;AC/DA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;;"}