{"version":3,"file":"index.d.ts","sources":["../src/elements/WordElement.ts","../src/controllers/WordCloudController.ts"],"sourcesContent":["import { Element, FontSpec, VisualElement, ScriptableAndArrayOptions, ScriptableContext, ChartType } from 'chart.js';\nimport { toFont } from 'chart.js/helpers';\n\nexport interface IWordElementOptions extends FontSpec, Record<string, unknown> {\n  color: CanvasRenderingContext2D['fillStyle'];\n  /**\n   * CanvasContext2D.strokeStyle config for rendering a stroke around the text\n   * @default undefined\n   */\n  strokeStyle: CanvasRenderingContext2D['strokeStyle'];\n  /**\n   * CanvasContext2D.lineWith for stroke\n   * @default undefined\n   */\n  strokeWidth?: CanvasRenderingContext2D['lineWidth'];\n  /**\n   * rotation of the word\n   * @default undefined then it will be randomly derived given the other constraints\n   */\n  rotate: number;\n  /**\n   * number of rotation steps between min and max rotation\n   * @default 2\n   */\n  rotationSteps: number;\n  /**\n   * angle in degree for the min rotation\n   * @default -90\n   */\n  minRotation: number;\n  /**\n   * angle in degree for the max rotation\n   * @default 0\n   */\n  maxRotation: number;\n  /**\n   * padding around each word while doing the layout\n   * @default 1\n   */\n  padding: number;\n}\n\nexport interface IWordElementHoverOptions {\n  /**\n   * hover variant of color\n   */\n  hoverColor: CanvasRenderingContext2D['fillStyle'];\n  /**\n   * hover variant of size\n   */\n  hoverSize: FontSpec['size'];\n  /**\n   * hover variant of style\n   */\n  hoverStyle: FontSpec['style'];\n  /**\n   * hover variant of weight\n   */\n  hoverWeight: FontSpec['weight'];\n  /**\n   * hover variant of stroke style\n   * @default undefined\n   */\n  hoverStrokeStyle: CanvasRenderingContext2D['strokeStyle'];\n  /**\n   * hover variant of stroke width\n   * @default undefined\n   */\n  hoverStrokeWidth?: CanvasRenderingContext2D['lineWidth'];\n}\n\nexport interface IWordElementProps {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n  scale: number;\n  text: string;\n}\n\nexport class WordElement extends Element<IWordElementProps, IWordElementOptions> implements VisualElement {\n  static readonly id = 'word';\n\n  /**\n   * @hidden\n   */\n  static readonly defaults: any = /* #__PURE__ */ {\n    // rotate: 0,\n    minRotation: -90,\n    maxRotation: 0,\n    rotationSteps: 2,\n    padding: 1,\n    strokeStyle: undefined,\n    strokeWidth: undefined,\n    size: (ctx) => {\n      const v = (ctx.parsed as unknown as { y: number }).y;\n      return v;\n    },\n    hoverColor: '#ababab',\n  } as Partial<ScriptableAndArrayOptions<IWordElementOptions, ScriptableContext<'wordCloud'>>>;\n\n  /**\n   * @hidden\n   */\n  static readonly defaultRoutes = /* #__PURE__ */ {\n    color: 'color',\n    family: 'font.family',\n    style: 'font.style',\n    weight: 'font.weight',\n    lineHeight: 'font.lineHeight',\n  };\n\n  /**\n   * @hidden\n   */\n  static computeRotation(o: IWordElementOptions, rnd: () => number): number {\n    if (o.rotationSteps <= 1) {\n      return 0;\n    }\n    if (o.minRotation === o.maxRotation) {\n      return o.minRotation;\n    }\n    const base = Math.min(o.rotationSteps, Math.floor(rnd() * o.rotationSteps)) / (o.rotationSteps - 1);\n    const range = o.maxRotation - o.minRotation;\n    return o.minRotation + base * range;\n  }\n\n  /**\n   * @hidden\n   */\n  inRange(mouseX: number, mouseY: number): boolean {\n    const p = this.getProps(['x', 'y', 'width', 'height', 'scale']);\n    if (p.scale <= 0) {\n      return false;\n    }\n    const x = Number.isNaN(mouseX) ? p.x : mouseX;\n    const y = Number.isNaN(mouseY) ? p.y : mouseY;\n    return x >= p.x - p.width / 2 && x <= p.x + p.width / 2 && y >= p.y - p.height / 2 && y <= p.y + p.height / 2;\n  }\n\n  /**\n   * @hidden\n   */\n  inXRange(mouseX: number): boolean {\n    return this.inRange(mouseX, Number.NaN);\n  }\n\n  /**\n   * @hidden\n   */\n  inYRange(mouseY: number): boolean {\n    return this.inRange(Number.NaN, mouseY);\n  }\n\n  /**\n   * @hidden\n   */\n  getCenterPoint(): { x: number; y: number } {\n    return this.getProps(['x', 'y']);\n  }\n\n  /**\n   * @hidden\n   */\n  tooltipPosition(): { x: number; y: number } {\n    return this.getCenterPoint();\n  }\n\n  /**\n   * @hidden\n   */\n  draw(ctx: CanvasRenderingContext2D): void {\n    const { options } = this;\n    const props = this.getProps(['x', 'y', 'width', 'height', 'text', 'scale']);\n    if (props.scale <= 0) {\n      return;\n    }\n    ctx.save();\n    const f = toFont({ ...options, size: options.size * props.scale });\n    ctx.font = f.string;\n    // console.log(ctx.font);\n    ctx.fillStyle = options.color;\n    ctx.textAlign = 'center';\n    // ctx.textBaseline = 'top';\n    ctx.translate(props.x, props.y);\n    // ctx.strokeRect(-props.width / 2, -props.height / 2, props.width, props.height);\n    ctx.rotate((options.rotate / 180) * Math.PI);\n    if (options.strokeStyle) {\n      if (options.strokeWidth != null) {\n        ctx.lineWidth = options.strokeWidth;\n      }\n      ctx.strokeStyle = options.strokeStyle;\n      ctx.strokeText(props.text, 0, 0);\n    }\n    ctx.fillText(props.text, 0, 0);\n\n    ctx.restore();\n  }\n}\n\ndeclare module 'chart.js' {\n  export interface ElementOptionsByType<TType extends ChartType> {\n    word: ScriptableAndArrayOptions<IWordElementOptions & IWordElementHoverOptions, ScriptableContext<TType>>;\n  }\n}\n","import {\n  AnimationOptions,\n  Chart,\n  DatasetController,\n  UpdateMode,\n  ChartItem,\n  ScriptableAndArrayOptions,\n  ControllerDatasetOptions,\n  CommonHoverOptions,\n  ChartConfiguration,\n  ScriptableContext,\n  VisualElement,\n  CartesianScaleTypeRegistry,\n  CoreChartOptions,\n} from 'chart.js';\nimport { toFont } from 'chart.js/helpers';\nimport layout from 'd3-cloud';\nimport { WordElement, IWordElementOptions, IWordElementProps } from '../elements';\nimport patchController from './patchController';\n\nfunction rnd(seed: string | number = Date.now()) {\n  // Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/\n  let s = typeof seed === 'number' ? seed : Array.from(seed).reduce((acc, v) => acc + v.charCodeAt(0), 0);\n  return () => {\n    s = (s * 9301 + 49297) % 233280;\n    return s / 233280;\n  };\n}\n\ninterface ICloudWord extends IWordElementProps {\n  options: IWordElementOptions;\n  index: number;\n}\n\nexport class WordCloudController extends DatasetController<'wordCloud', WordElement> {\n  static readonly id = 'wordCloud';\n\n  /**\n   * @hidden\n   */\n  static readonly defaults = /* #__PURE__ */ {\n    datasets: {\n      animation: {\n        colors: {\n          properties: ['color', 'strokeStyle'],\n        },\n        numbers: {\n          properties: ['x', 'y', 'size', 'rotate'],\n        },\n      },\n    },\n    maintainAspectRatio: false,\n    dataElementType: WordElement.id,\n  };\n\n  /**\n   * @hidden\n   */\n  static readonly overrides = /* #__PURE__ */ {\n    scales: {\n      x: {\n        type: 'linear',\n        min: -1,\n        max: 1,\n        display: false,\n      },\n      y: {\n        type: 'linear',\n        min: -1,\n        max: 1,\n        display: false,\n      },\n    },\n  };\n\n  private readonly wordLayout = layout<ICloudWord>()\n    .text((d) => d.text)\n    .padding((d) => d.options.padding)\n    .rotate((d) => d.options.rotate)\n    .font((d) => d.options.family)\n    .fontSize((d) => d.options.size)\n    .fontStyle((d) => d.options.style)\n    .fontWeight((d) => d.options.weight ?? 1);\n\n  /**\n   * @hidden\n   */\n  rand: () => number = Math.random;\n\n  /**\n   * @hidden\n   */\n  update(mode: UpdateMode): void {\n    super.update(mode);\n    const dsOptions = (this as any).options as IWordCloudControllerDatasetOptions;\n    this.rand = rnd(dsOptions.randomRotationSeed ?? this.chart.id);\n    const meta = this._cachedMeta;\n\n    const elems = (meta.data || []) as unknown as WordElement[];\n    this.updateElements(elems, 0, elems.length, mode);\n  }\n\n  /**\n   * @hidden\n   */\n  updateElements(elems: WordElement[], start: number, count: number, mode: UpdateMode): void {\n    this.wordLayout.stop();\n    const dsOptions = (this as any).options as IWordCloudControllerDatasetOptions;\n    const xScale = this._cachedMeta.xScale as { left: number; right: number };\n    const yScale = this._cachedMeta.yScale as { top: number; bottom: number };\n\n    const w = xScale.right - xScale.left;\n    const h = yScale.bottom - yScale.top;\n    const labels = this.chart.data.labels as string[];\n\n    const growOptions: IAutoGrowOptions = {\n      maxTries: 3,\n      scalingFactor: 1.2,\n    };\n    // update with configured options\n    Object.assign(growOptions, dsOptions?.autoGrow ?? {});\n\n    const words: (ICloudWord & Record<string, unknown>)[] = [];\n    for (let i = start; i < start + count; i += 1) {\n      const o = this.resolveDataElementOptions(i, mode) as unknown as IWordElementOptions;\n      if (o.rotate == null) {\n        o.rotate = WordElement.computeRotation(o, this.rand);\n      }\n      const properties: ICloudWord & Record<string, unknown> = {\n        options: { ...toFont(o), ...o },\n        x: this._cachedMeta.xScale?.getPixelForDecimal(0.5) ?? 0,\n        y: this._cachedMeta.yScale?.getPixelForDecimal(0.5) ?? 0,\n        width: 10,\n        height: 10,\n        scale: 1,\n        index: i,\n        text: labels[i],\n      };\n      words.push(properties);\n    }\n    if (mode === 'reset') {\n      words.forEach((tag) => {\n        this.updateElement(elems[tag.index], tag.index, tag, mode);\n      });\n      return;\n    }\n    // syncish since no time limit is set\n    this.wordLayout.random(this.rand).words(words);\n\n    const run = (factor = 1, tries = growOptions.maxTries): void => {\n      this.wordLayout\n        .size([w * factor, h * factor])\n        .on('end', (tags, bounds) => {\n          if (tags.length < labels.length) {\n            if (tries > 0) {\n              // try again with a factor of 1.2\n              const f =\n                typeof growOptions.scalingFactor === 'function'\n                  ? growOptions.scalingFactor(factor, tags, labels.length)\n                  : factor * growOptions.scalingFactor;\n              run(f, tries - 1);\n              return;\n            }\n\n            console.warn(`cannot fit all text elements in ${growOptions.maxTries} tries`);\n          }\n          const wb = bounds[1].x - bounds[0].x;\n          const hb = bounds[1].y - bounds[0].y;\n\n          const scale = dsOptions.fit ? Math.min(w / wb, h / hb) : 1;\n          const indices = new Set(labels.map((_, i) => i));\n          tags.forEach((tag) => {\n            indices.delete(tag.index);\n            this.updateElement(\n              elems[tag.index],\n              tag.index,\n              {\n                options: tag.options,\n                scale,\n                x: xScale.left + scale * tag.x + w / 2,\n                y: yScale.top + scale * tag.y + h / 2,\n                width: scale * tag.width,\n                height: scale * tag.height,\n                text: tag.text,\n              },\n              mode\n            );\n          });\n          // hide rest\n          indices.forEach((i) => this.updateElement(elems[i], i, { scale: 0 }, mode));\n        })\n        .start();\n    };\n    run();\n  }\n\n  /**\n   * @hidden\n   */\n  draw(): void {\n    const elements = this._cachedMeta.data as unknown as VisualElement[];\n    const { ctx } = this.chart;\n    elements.forEach((elem) => elem.draw(ctx));\n  }\n\n  /**\n   * @hidden\n   */\n  getLabelAndValue(index: number): { label: string; value: any } {\n    const r = super.getLabelAndValue(index);\n    const labels = this.chart.data.labels as string[];\n    r.label = labels[index];\n    return r;\n  }\n}\n\nexport interface IAutoGrowOptions {\n  /**\n   * @default 3\n   */\n  maxTries: number;\n  /**\n   * @default 1.2\n   */\n  scalingFactor: number | ((currentFactor: number, fitted: ICloudWord[], total: number) => number);\n}\n\nexport interface IWordCloudControllerDatasetOptions\n  extends ControllerDatasetOptions,\n    ScriptableAndArrayOptions<IWordElementOptions, ScriptableContext<'wordCloud'>>,\n    ScriptableAndArrayOptions<CommonHoverOptions, ScriptableContext<'wordCloud'>>,\n    AnimationOptions<'wordCloud'> {\n  /**\n   * whether to fit the word cloud to the map, by scaling to the actual bounds\n   * @default false\n   */\n  fit: boolean;\n\n  /**\n   * configures the automatic growing of the canvas in case not all words can be fitted onto the screen\n   * @default { maxTries: 3, scalingFactor: 1.2}\n   */\n  autoGrow: IAutoGrowOptions;\n\n  /**\n   * specifies the random seed that should be used for randomly rotating words if needed\n   * @default the current chart id\n   */\n  randomRotationSeed: string;\n}\n\ndeclare module 'chart.js' {\n  interface ChartTypeRegistry {\n    wordCloud: {\n      chartOptions: CoreChartOptions<'wordCloud'>;\n      datasetOptions: IWordCloudControllerDatasetOptions;\n      defaultDataPoint: number;\n      metaExtensions: Record<string, never>;\n      parsedDataType: { x: number };\n      scales: keyof CartesianScaleTypeRegistry;\n    };\n  }\n}\n\nexport class WordCloudChart<DATA extends unknown[] = number[], LABEL = string> extends Chart<'wordCloud', DATA, LABEL> {\n  static id = WordCloudController.id;\n\n  constructor(item: ChartItem, config: Omit<ChartConfiguration<'wordCloud', DATA, LABEL>, 'type'>) {\n    super(item, patchController('wordCloud', config, WordCloudController, WordElement));\n  }\n}\n"],"names":[],"mappings":";;;;;;;;;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;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;;ACrDA;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;AACO;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;;;"}