import { Chart, ChartItem, ChartConfiguration, CommonHoverOptions, ControllerDatasetOptions, PointOptions, PointProps, ScriptableContext, TooltipItem, PointElement, PointHoverOptions, Element, Scale, ScriptableAndArrayOptions, UpdateMode, AnimationOptions, } from 'chart.js'; import { merge } from 'chart.js/helpers'; import { GeoFeature, IGeoFeatureOptions } from '../elements'; import { ProjectionScale, SizeScale } from '../scales'; import { GeoController, geoDefaults, geoOverrides, IGeoChartOptions } from './GeoController'; import patchController from './patchController'; type MyPointElement = PointElement & Element>; export class BubbleMapController extends GeoController<'bubbleMap', MyPointElement> { initialize(): void { super.initialize(); this.enableOptionSharing = true; } linkScales(): void { super.linkScales(); const dataset = this.getGeoDataset(); const meta = this.getMeta(); meta.vAxisID = 'size'; meta.rAxisID = 'size'; dataset.vAxisID = 'size'; dataset.rAxisID = 'size'; meta.rScale = this.getScaleForId('size'); meta.vScale = meta.rScale; meta.iScale = meta.xScale; meta.iAxisID = meta.xAxisID!; dataset.iAxisID = meta.xAxisID!; } _getOtherScale(scale: Scale): Scale { // for strange get min max with other scale return scale; } parse(start: number, count: number): void { const rScale = this.getMeta().rScale!; const data = this.getDataset().data as unknown as IBubbleMapDataPoint[]; const meta = this._cachedMeta; for (let i = start; i < start + count; i += 1) { const d = data[i]; meta._parsed[i] = { x: d.longitude == null ? d.x : d.longitude, y: d.latitude == null ? d.y : d.latitude, [rScale.axis]: rScale.parse(d, i), }; } } updateElements(elems: MyPointElement[], start: number, count: number, mode: UpdateMode): void { const reset = mode === 'reset'; const firstOpts = this.resolveDataElementOptions(start, mode); const sharedOptions = this.getSharedOptions(firstOpts)!; const includeOptions = this.includeOptions(mode, sharedOptions); const scale = this.getProjectionScale(); (this.getMeta().rScale as unknown as SizeScale)._model = firstOpts as unknown as PointOptions; // for legend rendering styling this.updateSharedOptions(sharedOptions, mode, firstOpts); for (let i = start; i < start + count; i += 1) { const elem = elems[i]; const parsed = this.getParsed(i); const projection = scale.projection([parsed.x, parsed.y]); const properties: PointProps & { options?: PointOptions; skip: boolean } = { x: projection ? projection[0] : 0, y: projection ? projection[1] : 0, skip: Number.isNaN(parsed.x) || Number.isNaN(parsed.y), }; if (includeOptions) { properties.options = (sharedOptions || this.resolveDataElementOptions(i, mode)) as unknown as PointOptions; if (reset) { properties.options.radius = 0; } } this.updateElement(elem, i, properties as unknown as Record, mode); } } indexToRadius(index: number): number { const rScale = this.getMeta().rScale as SizeScale; return rScale.getSizeForValue(this.getParsed(index)[rScale.axis as 'r']); } static readonly id = 'bubbleMap'; /** * @hidden */ static readonly defaults: any = /* #__PURE__ */ merge({}, [ geoDefaults, { dataElementType: PointElement.id, datasetElementType: GeoFeature.id, showOutline: true, clipMap: 'outline+graticule', }, ]); /** * @hidden */ static readonly overrides: any = /* #__PURE__ */ merge({}, [ geoOverrides, { plugins: { tooltip: { callbacks: { title() { // Title doesn't make sense for scatter since we format the data as a point return ''; }, label(item: TooltipItem<'bubbleMap'>) { if (item.formattedValue == null) { return item.chart.data?.labels?.[item.dataIndex]; } return `${item.chart.data?.labels?.[item.dataIndex]}: ${item.formattedValue}`; }, }, }, }, scales: { size: { axis: 'x', type: SizeScale.id, }, }, elements: { point: { radius(context: ScriptableContext<'bubbleMap'>) { if (context.dataIndex == null) { return null; } const controller = (context.chart as Chart<'bubbleMap'>).getDatasetMeta(context.datasetIndex) .controller as BubbleMapController; return controller.indexToRadius(context.dataIndex); }, hoverRadius(context: ScriptableContext<'bubbleMap'>) { if (context.dataIndex == null) { return null; } const controller = (context.chart as Chart<'bubbleMap'>).getDatasetMeta(context.datasetIndex) .controller as BubbleMapController; return controller.indexToRadius(context.dataIndex) + 1; }, }, }, }, ]); } export interface IBubbleMapDataPoint { longitude: number; latitude: number; x?: number; y?: number; value: number; } export interface IBubbleMapControllerDatasetOptions extends ControllerDatasetOptions, IGeoChartOptions, ScriptableAndArrayOptions>, ScriptableAndArrayOptions>, AnimationOptions<'bubbleMap'> {} declare module 'chart.js' { export interface ChartTypeRegistry { bubbleMap: { chartOptions: IGeoChartOptions; datasetOptions: IBubbleMapControllerDatasetOptions; defaultDataPoint: IBubbleMapDataPoint; scales: keyof (ProjectionScaleTypeRegistry & SizeScaleTypeRegistry); metaExtensions: Record; parsedDataType: { r: number; x: number; y: number }; }; } } export class BubbleMapChart extends Chart< 'bubbleMap', DATA, LABEL > { static id = BubbleMapController.id; constructor(item: ChartItem, config: Omit, 'type'>) { super(item, patchController('bubbleMap', config, BubbleMapController, GeoFeature, [SizeScale, ProjectionScale])); } }