{"version":3,"file":"index.d.ts","sources":["../src/visualizations/interfaces.ts","../src/visualizations/bar.ts","../src/visualizations/boxplot.ts","../src/visualizations/histogram.ts","../src/visualizations/sparkline.ts","../src/visualizations/binarySparkline.ts","../src/visualizations/symbol.ts","../src/overlays.ts","../src/index.ts"],"sourcesContent":["import type cy from 'cytoscape';\n\nexport type OverlayPosition =\n  | 'top'\n  | 'bottom'\n  | 'right'\n  | 'left'\n  | 'right'\n  | 'top-left'\n  | 'top-right'\n  | 'bottom-left'\n  | 'bottom-right';\n\nexport interface IDimension {\n  position: OverlayPosition;\n  width: number;\n  height: number;\n}\n\nexport interface IVisualization {\n  init?: (nodes: cy.NodeCollection) => void;\n\n  (ctx: CanvasRenderingContext2D, node: cy.NodeSingular, dim: IDimension): void;\n\n  defaultWidth?: number;\n  defaultHeight?: number;\n  defaultPosition?: OverlayPosition;\n}\n\nexport declare type IAttrAccessor<T> = string | ((v: cy.NodeSingular) => T | null);\n\nexport declare type IScale = [number, number] | ((v: number) => number);\n\nexport declare type INodeFunction<T> = T | ((v: cy.NodeSingular) => T);\n","import type { IAttrAccessor, IScale, IVisualization, INodeFunction } from './interfaces';\nimport { resolveAccessor, resolveScale, resolveFunction, autoResolveScale } from './utils';\n\nexport interface IBarOptions {\n  /**\n   * the domain scale to use to map to the 0...1 scale\n   * in the array version a value of NaN indicate to automatically derive it from the data\n   * @default [0, NaN]\n   */\n  scale: IScale;\n  /**\n   * background color of the bar\n   * @default '#cccccc'\n   */\n  backgroundColor: INodeFunction<string>;\n  /**\n   * border color to frame the bar\n   * @default '#a0a0a0'\n   */\n  borderColor: INodeFunction<string>;\n}\n\n/**\n * @internal\n */\nexport const defaultColorOptions = {\n  backgroundColor: '#cccccc',\n  borderColor: '#a0a0a0',\n};\n\n/**\n * renders a horizontal or vertical bar\n * @param attr the accessor to the value of the node\n * @param options additional customization options\n */\nexport function renderBar(attr: IAttrAccessor<number>, options: Partial<IBarOptions> = {}): IVisualization {\n  const o = Object.assign(\n    {\n      scale: [0, Number.NaN],\n    },\n    defaultColorOptions,\n    options\n  );\n  const acc = resolveAccessor(attr);\n  let scale = resolveScale(o.scale);\n  const backgroundColor = resolveFunction(o.backgroundColor);\n  const borderColor = resolveFunction(o.borderColor);\n\n  const r: IVisualization = (ctx, node, dim) => {\n    const value = acc(node);\n\n    if (value != null && !Number.isNaN(value)) {\n      ctx.fillStyle = backgroundColor(node);\n      const v = scale(value);\n      if (dim.position === 'left' || dim.position === 'right') {\n        ctx.fillRect(0, dim.height * (1 - v), dim.width, v * dim.height);\n      } else {\n        ctx.fillRect(0, 0, dim.width * v, dim.height);\n      }\n    }\n\n    const b = borderColor(node);\n    if (b) {\n      ctx.strokeStyle = b;\n      ctx.strokeRect(0, 0, dim.width, dim.height);\n    }\n  };\n  r.init = (nodes) => {\n    scale = autoResolveScale(o.scale, () => nodes.map(acc));\n  };\n  r.defaultHeight = 5;\n  r.defaultWidth = 5;\n  r.defaultPosition = 'bottom';\n  return r;\n}\n","import type { IAttrAccessor, IVisualization, IDimension, INodeFunction } from './interfaces';\nimport { resolveAccessor, resolveScale, resolveFunction, autoResolveScale } from './utils';\nimport { type IBoxPlot, boxplot, type BoxplotStatsOptions } from '@sgratzl/boxplots';\nimport { type IBarOptions, defaultColorOptions } from './bar';\nimport type cy from 'cytoscape';\nimport seedrandom from 'seedrandom';\n\nexport interface IBoxplotOptions extends BoxplotStatsOptions, IBarOptions {\n  /**\n   * pixel radius when rendering outliers\n   * @default 2\n   */\n  outlierRadius: number;\n  /**\n   * color for rendering outliers\n   */\n  outlierBackgroundColor: INodeFunction<string>;\n  /**\n   * pixel radius when rendering items\n   * @default 0\n   */\n  itemRadius: number;\n  /**\n   * color for rendering items\n   */\n  itemBackgroundColor: INodeFunction<string>;\n\n  /**\n   * padding for a smaller boxplot box\n   * @default 1\n   */\n  boxPadding: number;\n}\n\nconst defaultOptions: IBoxplotOptions = {\n  scale: [0, Number.NaN],\n  ...defaultColorOptions,\n\n  outlierRadius: 2,\n  get outlierBackgroundColor() {\n    return this.backgroundColor;\n  },\n  itemRadius: 0,\n  get itemBackgroundColor() {\n    return this.backgroundColor;\n  },\n\n  boxPadding: 1,\n};\n\nfunction renderPoints(\n  ctx: CanvasRenderingContext2D,\n  points: Iterable<number>,\n  radius: number,\n  x: (v: number) => number,\n  y: (v: number) => number\n) {\n  for (const p of points) {\n    const px = x(p);\n    const py = y(p);\n    ctx.beginPath();\n    ctx.arc(px, py, radius, 0, Math.PI * 2);\n    ctx.fill();\n  }\n}\n\nexport function renderBoxplot(\n  attr: IAttrAccessor<readonly number[] | IBoxPlot>,\n  options: Partial<IBoxplotOptions> = {}\n): IVisualization {\n  const o = Object.assign({}, defaultOptions, options);\n  const acc = resolveAccessor(attr);\n  let scale01 = resolveScale(o.scale);\n\n  const r: IVisualization = (ctx, node, dim) => {\n    const value = acc(node);\n\n    if (value == null) {\n      return;\n    }\n\n    const b = Array.isArray(value) ? boxplot(value, o) : (value as IBoxPlot);\n\n    const scale = (v: number) => scale01(v) * dim.width;\n\n    if (b == null || Number.isNaN(b.max)) {\n      return;\n    }\n\n    renderBoxplotImpl(ctx, node, o, scale, b, dim);\n  };\n  r.init = (nodes) => {\n    scale01 = autoResolveScale(o.scale, () =>\n      nodes\n        .map((n) => {\n          const b = acc(n);\n          if (Array.isArray(b)) {\n            return b;\n          }\n          return [(b as IBoxPlot).min, (b as IBoxPlot).max];\n        })\n        .flat()\n    );\n  };\n  r.defaultHeight = 10;\n  r.defaultPosition = 'bottom';\n  return r;\n}\n\nfunction renderBoxplotImpl(\n  ctx: CanvasRenderingContext2D,\n  node: cy.NodeSingular,\n  o: IBoxplotOptions,\n  scale: (v: number) => number,\n  b: IBoxPlot,\n  dim: IDimension\n) {\n  if (o.itemRadius > 0 && b.items.length > 0) {\n    const rnd = seedrandom(node.id());\n    ctx.fillStyle = resolveFunction(o.itemBackgroundColor)(node);\n    const yDim = dim.height - o.itemRadius * 2;\n    renderPoints(\n      ctx,\n      Array.from(b.items),\n      o.itemRadius,\n      (v) => scale(v),\n      () => o.itemRadius + rnd() * yDim\n    );\n  }\n\n  ctx.strokeStyle = resolveFunction(o.borderColor)(node);\n  ctx.fillStyle = resolveFunction(o.backgroundColor)(node);\n\n  // draw box\n  const q1 = scale(b.q1);\n  const q3 = scale(b.q3);\n  const boxHeight = dim.height - 2 * o.boxPadding;\n  ctx.fillRect(q1, o.boxPadding, q3 - q1, boxHeight);\n\n  // draw median line\n  ctx.beginPath();\n  const median = scale(b.median);\n  const whiskerLow = scale(b.whiskerLow);\n  const whiskerHigh = scale(b.whiskerHigh);\n\n  // whisker line\n  ctx.moveTo(whiskerLow, 0);\n  ctx.lineTo(whiskerLow, dim.height);\n  ctx.moveTo(whiskerHigh, 0);\n  ctx.lineTo(whiskerHigh, dim.height);\n  ctx.moveTo(whiskerLow, dim.height / 2);\n  ctx.lineTo(q1, dim.height / 2);\n  ctx.moveTo(whiskerHigh, dim.height / 2);\n  ctx.lineTo(q3, dim.height / 2);\n\n  // box stroke\n  ctx.rect(q1, o.boxPadding, q3 - q1, boxHeight);\n\n  // draw median line\n  ctx.moveTo(median, o.boxPadding);\n  ctx.lineTo(median, dim.height - o.boxPadding);\n\n  ctx.stroke();\n\n  if (o.outlierRadius > 0 && b.outlier.length > 0) {\n    ctx.fillStyle = resolveFunction(o.outlierBackgroundColor)(node);\n    renderPoints(\n      ctx,\n      b.outlier,\n      o.outlierRadius,\n      (v) => scale(v),\n      () => dim.height / 2\n    );\n  }\n}\n","import { bin } from 'd3-array';\nimport { defaultColorOptions } from './bar';\nimport type { IAttrAccessor, INodeFunction, IVisualization } from './interfaces';\nimport { resolveAccessor, resolveFunction } from './utils';\n\nexport interface IHist {\n  bins: readonly number[];\n}\n\nfunction isHist(value: readonly number[] | IHist): value is IHist {\n  return Array.isArray((value as IHist).bins);\n}\nexport interface IHistogramOptions {\n  scale: [number, number];\n  maxBin: number;\n  backgroundColor: INodeFunction<string>;\n  borderColor: INodeFunction<string>;\n  barPadding: number;\n}\n\nfunction generateHist(value: readonly number[] | IHist, scale: [number, number]): readonly number[] {\n  if (isHist(value)) {\n    return value.bins;\n  }\n  const b = bin<number, number>();\n  b.domain(scale);\n  return b(value).map((d) => d.length);\n}\n\nexport function renderHistogram(\n  attr: IAttrAccessor<readonly number[] | IHist>,\n  options: Partial<IHistogramOptions> = {}\n): IVisualization {\n  const o = Object.assign(\n    {\n      scale: [Number.NaN, Number.NaN] as [number, number],\n      maxBin: Number.NaN,\n      barPadding: 0,\n    },\n    defaultColorOptions,\n    options\n  );\n  const acc = resolveAccessor(attr);\n  let maxBin = o.maxBin;\n  let scale = o.scale;\n  const backgroundColor = resolveFunction(o.backgroundColor);\n  const borderColor = resolveFunction(o.borderColor);\n\n  const viz: IVisualization = (ctx, node, dim) => {\n    const value = acc(node);\n    ctx.strokeStyle = borderColor(node);\n    ctx.strokeRect(0, 0, dim.width, dim.height);\n\n    if (value == null || !Array.isArray(value)) {\n      return;\n    }\n    const hist = generateHist(value, scale);\n\n    ctx.fillStyle = backgroundColor(node);\n\n    const binWidth = (dim.width - (hist.length - 1) * o.barPadding) / hist.length;\n    const yScale = (v: number) => (v / maxBin) * dim.height;\n\n    let offset = 0;\n    for (const histBin of hist) {\n      const height = yScale(histBin);\n      ctx.fillRect(offset, dim.height - height, binWidth, height);\n      offset += binWidth + o.barPadding;\n    }\n  };\n  viz.init = (nodes) => {\n    if (!Number.isNaN(maxBin) && !Number.isNaN(scale[0]) && !Number.isNaN(scale[1])) {\n      return;\n    }\n\n    // derive hist borders\n    const output = nodes.reduce(\n      (out, node) => {\n        const v = acc(node);\n        if (v == null || !Array.isArray(v)) {\n          return out;\n        }\n        if (isHist(v)) {\n          out.maxBin = v.reduce((m, b) => Math.max(m, b), out.maxBin);\n          return out;\n        }\n\n        const b = bin<number, number>();\n        const hist = b(v);\n        out.maxBin = hist.reduce((m, histBin) => Math.max(m, histBin.length), out.maxBin);\n        if (hist.length > 0) {\n          out.min = Math.min(out.min, hist[0]!.x0!);\n          out.max = Math.max(out.max, hist[hist.length - 1]!.x1!);\n        }\n        return out;\n      },\n      {\n        min: Number.POSITIVE_INFINITY,\n        max: Number.NEGATIVE_INFINITY,\n        maxBin: 0,\n      }\n    );\n    if (Number.isNaN(maxBin)) {\n      maxBin = output.maxBin;\n    }\n    scale = [Number.isNaN(scale[0]) ? output.min : scale[0], Number.isNaN(scale[1]) ? output.max : scale[0]];\n  };\n  viz.defaultHeight = 20;\n  viz.defaultPosition = 'bottom';\n  return viz;\n}\n","import type { IAttrAccessor, IVisualization, IScale, INodeFunction } from './interfaces';\nimport { resolveAccessor, resolveScale, resolveFunction, autoResolveScale } from './utils';\nimport { defaultColorOptions } from './bar';\nimport { renderLine, renderArea } from './lineUtils';\n\nexport interface ISparkLineOptions {\n  scale: IScale;\n  backgroundColor: INodeFunction<string>;\n  lineColor: INodeFunction<string>;\n  borderColor: INodeFunction<string>;\n  padding: number;\n}\n\nexport function renderSparkLine(\n  attr: IAttrAccessor<readonly (number | null)[]>,\n  options: Partial<ISparkLineOptions> = {}\n): IVisualization {\n  const o: ISparkLineOptions = Object.assign(\n    {\n      scale: [0, Number.NaN],\n      backgroundColor: '',\n      padding: 1,\n      borderColor: defaultColorOptions.borderColor,\n      lineColor: defaultColorOptions.borderColor,\n    },\n    options\n  );\n  const acc = resolveAccessor(attr);\n  let yScale01 = resolveScale(o.scale);\n  const backgroundColor = resolveFunction(o.backgroundColor);\n  const lineColor = resolveFunction(o.lineColor);\n  const borderColor = resolveFunction(o.borderColor);\n\n  const r: IVisualization = (ctx, node, dim) => {\n    const value = acc(node);\n\n    const bc = borderColor(node);\n    if (bc) {\n      ctx.strokeStyle = bc;\n      ctx.strokeRect(0, 0, dim.width, dim.height);\n    }\n\n    if (value == null || !Array.isArray(value) || value.length === 0) {\n      return;\n    }\n\n    const step = (dim.width - 2 * o.padding) / (value.length - 1);\n    const xScale = (i: number) => i * step + o.padding;\n    const yScale = (v: number) => (1 - yScale01(v)) * dim.height;\n\n    const values = value.map((y, x) => ({ x, y: y! }));\n\n    const bg = backgroundColor(node);\n    if (bg) {\n      ctx.fillStyle = bg;\n      renderArea(ctx, values, xScale, yScale, dim.height);\n    }\n\n    const lc = lineColor(node);\n    if (lc) {\n      ctx.lineCap = 'round';\n      ctx.strokeStyle = lc;\n      renderLine(ctx, values, xScale, yScale);\n    }\n  };\n\n  r.init = (nodes) => {\n    yScale01 = autoResolveScale(o.scale, () => nodes.map((v) => acc(v) || []).flat());\n  };\n  r.defaultHeight = 20;\n  r.defaultPosition = 'bottom';\n  return r;\n}\n","import type { IAttrAccessor, IVisualization, IScale, INodeFunction } from './interfaces';\nimport { resolveAccessor, resolveScale, resolveFunction, autoResolveScale } from './utils';\nimport { defaultColorOptions } from './bar';\nimport { renderLine, renderArea } from './lineUtils';\n\nexport interface IBinarySparkLineOptions {\n  scale: IScale;\n  centerValue: number;\n  aboveBackgroundColor: INodeFunction<string>;\n  belowBackgroundColor: INodeFunction<string>;\n  aboveLineColor: INodeFunction<string>;\n  belowLineColor: INodeFunction<string>;\n  centerValueColor: INodeFunction<string>;\n  borderColor: INodeFunction<string>;\n  padding: number;\n}\n\nfunction lineSplit(x1: number, y1: number, x2: number, y2: number, centerValue: number) {\n  const m = (y1 - y2) / (x1 - x2);\n  // y − y1 = m(x − x1)\n  // x = (y - y1) / m + x1;\n  return (centerValue - y1) / m + x1;\n}\n\nfunction splitSegments(values: readonly (number | null)[], centerValue: number) {\n  const below: { x: number; y: number }[] = [];\n  const above: { x: number; y: number }[] = [];\n\n  let previousIndex: number | null = null;\n  for (let i = 0; i < values.length; i++) {\n    const v = values[i];\n    if (v == null || Number.isNaN(v)) {\n      previousIndex = null;\n      if (below.length > 0 && !Number.isNaN(below[below.length - 1].y)) {\n        below.push({ x: i, y: Number.NaN });\n      }\n      if (above.length > 0 && !Number.isNaN(above[above.length - 1].y)) {\n        above.push({ x: i, y: Number.NaN });\n      }\n      continue;\n    }\n\n    if (previousIndex != null && values[previousIndex]! < centerValue !== v < centerValue) {\n      // crossed the line\n      const xc = lineSplit(previousIndex, values[previousIndex]!, i, v, centerValue);\n      below.push({ x: xc, y: centerValue });\n      above.push({ x: xc, y: centerValue });\n    }\n\n    if (v < centerValue) {\n      below.push({ x: i, y: v });\n    } else {\n      above.push({ x: i, y: v });\n    }\n\n    previousIndex = i;\n  }\n\n  return [above, below];\n}\n\nexport function renderBinarySparkLine(\n  attr: IAttrAccessor<readonly (number | null)[]>,\n  options: Partial<IBinarySparkLineOptions> = {}\n): IVisualization {\n  const o: IBinarySparkLineOptions = Object.assign(\n    {\n      scale: [Number.NaN, Number.NaN],\n      centerValue: 0,\n      aboveBackgroundColor: 'green',\n      belowBackgroundColor: 'red',\n      aboveLineColor: '',\n      belowLineColor: '',\n      borderColor: defaultColorOptions.borderColor,\n      centerValueColor: '',\n      padding: 1,\n    },\n    options\n  );\n  const acc = resolveAccessor(attr);\n  let yScale01 = resolveScale(o.scale);\n  const borderColor = resolveFunction(o.borderColor);\n  const belowBackgroundColor = resolveFunction(o.belowBackgroundColor);\n  const belowLineColor = resolveFunction(o.belowLineColor);\n  const aboveBackgroundColor = resolveFunction(o.aboveBackgroundColor);\n  const aboveLineColor = resolveFunction(o.aboveLineColor);\n  const centerValueColor = resolveFunction(o.centerValueColor);\n\n  const r: IVisualization = (ctx, node, dim) => {\n    const value = acc(node);\n\n    const bc = borderColor(node);\n    if (bc) {\n      ctx.strokeStyle = bc;\n      ctx.strokeRect(0, 0, dim.width, dim.height);\n    }\n\n    if (value == null || !Array.isArray(value) || value.length === 0) {\n      return;\n    }\n\n    const step = (dim.width - 2 * o.padding) / (value.length - 1);\n    const xScale = (i: number) => i * step + o.padding;\n    const yScale = (v: number) => (1 - yScale01(v)) * dim.height;\n\n    const mLC = centerValueColor(node);\n    const y = yScale(o.centerValue);\n    if (mLC) {\n      ctx.strokeStyle = mLC;\n      ctx.beginPath();\n      ctx.moveTo(0, y);\n      ctx.lineTo(dim.width, y);\n      ctx.stroke();\n    }\n\n    const values = value.map((vy, x) => ({ x, y: vy! }));\n    const [above, below] = splitSegments(value, o.centerValue);\n\n    const bBG = belowBackgroundColor(node);\n    const aBG = aboveBackgroundColor(node);\n\n    if (aBG) {\n      ctx.fillStyle = aBG;\n      renderArea(ctx, above, xScale, yScale, y);\n    }\n    if (bBG) {\n      ctx.fillStyle = bBG;\n      renderArea(ctx, below, xScale, yScale, y);\n    }\n\n    const bLC = belowLineColor(node);\n    const aLC = aboveLineColor(node);\n\n    if (aLC || bLC) {\n      ctx.lineCap = 'round';\n      if (aLC === bLC) {\n        ctx.strokeStyle = aLC;\n        renderLine(ctx, values, xScale, yScale);\n      } else if (aLC) {\n        ctx.strokeStyle = aLC;\n        renderLine(ctx, above, xScale, yScale);\n      } else if (bLC) {\n        ctx.strokeStyle = bLC;\n        renderLine(ctx, below, xScale, yScale);\n      }\n    }\n  };\n\n  r.init = (nodes) => {\n    yScale01 = autoResolveScale(o.scale, () => nodes.map((v) => acc(v) || []).flat());\n  };\n  r.defaultHeight = 20;\n  r.defaultPosition = 'bottom';\n  return r;\n}\n","import {\n  symbolCircle,\n  symbolCross,\n  symbolDiamond,\n  symbolSquare,\n  symbolStar,\n  symbolTriangle,\n  type SymbolType,\n  symbolWye,\n} from 'd3-shape';\nimport type { INodeFunction, IVisualization } from './interfaces';\nimport { resolveFunction } from './utils';\n\nconst symbols = {\n  circle: symbolCircle,\n  cross: symbolCross,\n  diamond: symbolDiamond,\n  square: symbolSquare,\n  star: symbolStar,\n  triangle: symbolTriangle,\n  wye: symbolWye,\n};\n\nexport interface ITextSymbol {\n  text: string;\n  font?: string;\n}\n\nexport interface ISymbolOptions {\n  symbol: INodeFunction<keyof typeof symbols | CanvasImageSource | SymbolType | ITextSymbol | null>;\n  color: INodeFunction<string | null>;\n}\n\nfunction isSymbol(s: any): s is SymbolType {\n  return typeof (s as SymbolType).draw === 'function';\n}\n\nfunction isTextSymbol(s: any): s is ITextSymbol {\n  return typeof (s as ITextSymbol).text === 'string';\n}\n\nexport function renderSymbol(options: Partial<ISymbolOptions> = {}): IVisualization {\n  const o = Object.assign(\n    {\n      symbol: 'circle',\n      color: '#cccccc',\n    } as ISymbolOptions,\n    options\n  );\n  const symbol = resolveFunction(o.symbol);\n  const backgroundColor = resolveFunction(o.color);\n\n  const r: IVisualization = (ctx, node, dim) => {\n    const bg = backgroundColor(node);\n    const s = symbol(node);\n\n    if (bg == null || s == null) {\n      return;\n    }\n    ctx.fillStyle = bg;\n\n    if (isSymbol(s) || typeof s === 'string') {\n      const sym = isSymbol(s) ? s : symbols[s as keyof typeof symbols] || symbolCircle;\n      ctx.translate(dim.width / 2, dim.height / 2);\n      ctx.beginPath();\n      sym.draw(ctx, 0.5 * (dim.width * dim.height));\n      ctx.fill();\n      ctx.translate(-dim.width / 2, -dim.height / 2);\n    } else if (isTextSymbol(s)) {\n      ctx.save();\n      if (s.font) {\n        ctx.font = s.font;\n      }\n      ctx.textAlign = 'center';\n      ctx.textBaseline = 'middle';\n      ctx.fillText(s.text, dim.width / 2, dim.height / 2);\n      ctx.restore();\n    } else {\n      // image source\n      ctx.drawImage(s as CanvasImageSource, 0, 0, dim.width, dim.height);\n    }\n  };\n  r.defaultHeight = 8;\n  r.defaultWidth = 8;\n  r.defaultPosition = 'top-left';\n  return r;\n}\n","import type cy from 'cytoscape';\nimport type { IVisualization, OverlayPosition } from './visualizations';\nimport {\n  layers,\n  type ICanvasLayer,\n  type ICanvasLayerOptions,\n  renderPerNode,\n  type INodeCanvasLayerOption,\n} from 'cytoscape-layers';\n\nexport interface IOverlayVisualization {\n  position?: OverlayPosition;\n  width?: number;\n  height?: number;\n  vis: IVisualization;\n}\n\nexport interface IOverlayPluginOptions extends ICanvasLayerOptions, INodeCanvasLayerOption {\n  layer: ICanvasLayer;\n  backgroundColor: string;\n  padding: number;\n}\n\nfunction toFullVisualization(o: IOverlayVisualization | IVisualization): Required<IOverlayVisualization> {\n  const vis = typeof o === 'function' ? o : o.vis;\n  return Object.assign(\n    {\n      vis,\n      height: vis.defaultHeight || 5,\n      width: vis.defaultWidth || 5,\n      position: vis.defaultPosition || 'bottom',\n    },\n    typeof o === 'function' ? { vis: o } : o\n  );\n}\n\nfunction pick<T>(o: T, keys: (keyof T)[]): Partial<T> {\n  const r: Partial<T> = {};\n  for (const key of keys) {\n    const v = o[key];\n    if (v !== undefined) {\n      r[key] = v;\n    }\n  }\n  return r;\n}\n\nfunction stackVertical(pos: OverlayPosition) {\n  return pos === 'top' || pos === 'bottom';\n}\nexport function overlays(\n  this: cy.Core,\n  definitions: readonly (IOverlayVisualization | IVisualization)[],\n  options: Partial<IOverlayPluginOptions> = {}\n): { remove(): void } {\n  const layer = options.layer || layers(this).nodeLayer.insertAfter('canvas', options);\n\n  const overlayObjects = definitions.map(toFullVisualization);\n\n  const someInit = overlayObjects.filter((d) => d.vis.init != null);\n\n  const padding = options.padding == null ? 1 : options.padding;\n\n  const positions: OverlayPosition[] = [\n    'bottom-left',\n    'bottom-right',\n    'bottom',\n    'left',\n    'right',\n    'top',\n    'top-left',\n    'top-right',\n  ];\n\n  const infos = positions\n    .map((pos) => {\n      const subset = overlayObjects.filter((d) => d.position === pos);\n      const vertical = stackVertical(pos);\n      return {\n        pos,\n        overlays: subset,\n        total: subset.reduce((acc, overlay) => acc + (vertical ? overlay.height : overlay.width) + padding, -padding),\n        maxOther: subset.reduce((acc, overlay) => Math.max(acc, vertical ? overlay.width : overlay.height), 0),\n      };\n    })\n    .filter((d) => d.total > 0);\n\n  const renderPerNodeOptions: Partial<INodeCanvasLayerOption> = Object.assign(\n    {\n      position: 'top-left',\n    },\n    someInit\n      ? {\n          initCollection: (nodes: cy.NodeCollection) => {\n            for (const o of overlayObjects) {\n              if (o.vis.init) {\n                o.vis.init(nodes);\n              }\n            }\n          },\n        }\n      : {},\n    pick(options, ['boundingBox', 'checkBounds', 'queryEachTime', 'selector', 'updateOn'])\n  );\n\n  function cleanArea(ctx: CanvasRenderingContext2D, bb: cy.BoundingBox12 & cy.BoundingBoxWH) {\n    if (!options.backgroundColor) {\n      return;\n    }\n    ctx.fillStyle = options.backgroundColor;\n    for (const info of infos) {\n      switch (info.pos) {\n        case 'bottom':\n          ctx.fillRect(0, bb.h, bb.w, info.total);\n          break;\n        case 'bottom-left':\n          ctx.fillRect(-info.overlays[0]!.width / 2, bb.h - info.maxOther / 2, info.total, info.maxOther);\n          break;\n        case 'bottom-right':\n          ctx.fillRect(\n            bb.w - info.total - info.overlays[0]!.width / 2,\n            bb.h - info.maxOther / 2,\n            info.total,\n            info.maxOther\n          );\n          break;\n        case 'left':\n          ctx.fillRect(-info.total, 0, info.total, bb.h);\n          break;\n        case 'right':\n          ctx.fillRect(bb.w, 0, info.total, bb.h);\n          break;\n        case 'top':\n          ctx.fillRect(0, -info.total, bb.w, info.total);\n          break;\n        case 'top-left':\n          ctx.fillRect(-info.overlays[0]!.width / 2, -info.maxOther / 2, info.total, info.maxOther);\n          break;\n        case 'top-right':\n          ctx.fillRect(bb.w - info.total - info.overlays[0]!.width / 2, -info.maxOther / 2, info.total, info.maxOther);\n          break;\n      }\n    }\n  }\n\n  function renderInfo(\n    position: OverlayPosition,\n    visualizations: Required<IOverlayVisualization>[],\n    ctx: CanvasRenderingContext2D,\n    node: cy.NodeSingular,\n    bb: cy.BoundingBox12 & cy.BoundingBoxWH\n  ) {\n    switch (position) {\n      case 'bottom':\n        ctx.translate(0, bb.h);\n        for (const overlay of visualizations) {\n          overlay.vis(ctx, node, { width: bb.w, height: overlay.height, position });\n          ctx.translate(0, overlay.height + padding);\n        }\n        break;\n      case 'left':\n        for (const overlay of visualizations) {\n          ctx.translate(-overlay.width, 0);\n          overlay.vis(ctx, node, { width: overlay.width, height: bb.h, position });\n          ctx.translate(-padding, 0);\n        }\n        break;\n      case 'right':\n        ctx.translate(bb.w, 0);\n        for (const overlay of visualizations) {\n          overlay.vis(ctx, node, { width: overlay.width, height: bb.h, position });\n          ctx.translate(overlay.width + padding, 0);\n        }\n        break;\n      case 'top':\n        for (const overlay of visualizations) {\n          ctx.translate(0, -overlay.height);\n          overlay.vis(ctx, node, { width: bb.w, height: overlay.height, position });\n          ctx.translate(0, -padding);\n        }\n        break;\n      case 'top-left':\n      case 'bottom-left':\n        // along the top line\n        ctx.translate(-visualizations[0]!.width / 2, position === 'bottom-left' ? bb.h : 0);\n        for (const overlay of visualizations) {\n          ctx.translate(0, -overlay.height / 2);\n          overlay.vis(ctx, node, { width: overlay.width, height: overlay.height, position });\n          ctx.translate(padding + overlay.width, overlay.height / 2);\n        }\n        break;\n      case 'top-right':\n      case 'bottom-right':\n        ctx.translate(bb.w + visualizations[0]!.width / 2, position === 'bottom-right' ? bb.h : 0);\n        for (const overlay of visualizations) {\n          ctx.translate(-overlay.width, -overlay.height / 2);\n          overlay.vis(ctx, node, { width: overlay.width, height: overlay.height, position });\n          ctx.translate(-padding, overlay.height / 2);\n        }\n        break;\n    }\n  }\n\n  return renderPerNode(\n    layer,\n    (ctx, node, bb) => {\n      const bak = ctx.getTransform();\n      cleanArea(ctx, bb);\n      for (const info of infos) {\n        renderInfo(info.pos, info.overlays, ctx, node, bb);\n        ctx.setTransform(bak);\n      }\n    },\n    renderPerNodeOptions\n  );\n}\n","import { overlays } from './overlays';\n\nexport * from './visualizations';\nexport * from './overlays';\n\nexport default function register(\n  cytoscape: (type: 'core' | 'collection' | 'layout', name: string, extension: any) => void\n) {\n  cytoscape('core', 'overlays', overlays);\n}\n\n// auto register\nif (typeof (window as any).cytoscape !== 'undefined') {\n  register((window as any).cytoscape);\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport declare namespace cytoscape {\n  type Ext2 = (cytoscape: (type: 'core' | 'collection' | 'layout', name: string, extension: any) => void) => void;\n  function use(module: Ext2): void;\n\n  interface Core {\n    overlays: typeof overlays;\n  }\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AACO;AACA;AACP;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACA;AACA;;ACfA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;;ACrBA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;;AC3BA;AACP;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;;ACVA;AACP;AACA;AACA;AACA;AACA;AACA;AACO;;ACPA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;;ACVP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACO;;AChBA;AACP;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACO;AACP;AACA;;ACbe;AACR;AACP;AACA;AACA;AACA;AACA;AACA;;;"}