chartjs-chart-geo
Version:
Chart.js module for charting maps
233 lines (203 loc) • 5.94 kB
text/typescript
import { Scale, CoreScaleOptions } from 'chart.js';
import {
geoPath,
geoAzimuthalEqualArea,
geoAzimuthalEquidistant,
geoGnomonic,
geoOrthographic,
geoStereographic,
geoEqualEarth,
geoAlbers,
geoAlbersUsa,
geoConicConformal,
geoConicEqualArea,
geoConicEquidistant,
geoEquirectangular,
geoMercator,
geoTransverseMercator,
geoNaturalEarth1,
GeoProjection,
GeoPath,
GeoPermissibleObjects,
ExtendedFeatureCollection,
ExtendedFeature,
GeoGeometryObjects,
ExtendedGeometryCollection,
} from 'd3-geo';
const lookup: { [key: string]: () => GeoProjection } = {
geoAzimuthalEqualArea,
geoAzimuthalEquidistant,
geoGnomonic,
geoOrthographic,
geoStereographic,
geoEqualEarth,
geoAlbers,
geoAlbersUsa,
geoConicConformal,
geoConicEqualArea,
geoConicEquidistant,
geoEquirectangular,
geoMercator,
geoTransverseMercator,
geoNaturalEarth1,
};
Object.keys(lookup).forEach((key) => {
lookup[`${key.charAt(3).toLowerCase()}${key.slice(4)}`] = lookup[key];
});
export interface IProjectionScaleOptions extends CoreScaleOptions {
/**
* projection method used
* @default albersUsa
*/
projection:
| GeoProjection
| 'azimuthalEqualArea'
| 'azimuthalEquidistant'
| 'gnomonic'
| 'orthographic'
| 'stereographic'
| 'equalEarth'
| 'albers'
| 'albersUsa'
| 'conicConformal'
| 'conicEqualArea'
| 'conicEquidistant'
| 'equirectangular'
| 'mercator'
| 'transverseMercator'
| 'naturalEarth1';
/**
* extra scale factor applied to projection
*/
projectionScale: number;
/**
* extra offset applied after projection
*/
projectionOffset: [number, number];
/**
* padding applied during auto scaling of the map in pixels
* i.e. the chart size is reduce by the padding before fitting the map
*/
padding: number | { top: number; left: number; right: number; bottom: number };
}
export class ProjectionScale extends Scale<IProjectionScaleOptions> {
/**
* @hidden
*/
readonly geoPath: GeoPath<any, GeoPermissibleObjects>;
/**
* @hidden
*/
projection!: GeoProjection;
private outlineBounds: {
refX: number;
refY: number;
refScale: number;
width: number;
height: number;
aspectRatio: number;
} | null = null;
private oldChartBounds: { chartWidth: number; chartHeight: number } | null = null;
constructor(cfg: any) {
super(cfg);
this.geoPath = geoPath();
}
/**
* @hidden
*/
init(options: IProjectionScaleOptions): void {
(options as any).position = 'chartArea';
super.init(options);
if (typeof options.projection === 'function') {
this.projection = options.projection;
} else {
this.projection = (lookup[options.projection] || lookup.albersUsa)();
}
this.geoPath.projection(this.projection);
this.outlineBounds = null;
this.oldChartBounds = null;
}
/**
* @hidden
*/
computeBounds(outline: ExtendedFeature): void;
computeBounds(outline: ExtendedFeatureCollection): void;
computeBounds(outline: GeoGeometryObjects): void;
computeBounds(outline: ExtendedGeometryCollection): void;
computeBounds(outline: any): void {
const bb = geoPath(this.projection.fitWidth(1000, outline)).bounds(outline);
const bHeight = Math.ceil(bb[1][1] - bb[0][1]);
const bWidth = Math.ceil(bb[1][0] - bb[0][0]);
const t = this.projection.translate();
this.outlineBounds = {
width: bWidth,
height: bHeight,
aspectRatio: bWidth / bHeight,
refScale: this.projection.scale(),
refX: t[0],
refY: t[1],
};
}
/**
* @hidden
*/
updateBounds(): boolean {
const area = this.chart.chartArea;
const bb = this.outlineBounds;
if (!bb) {
return false;
}
const padding = this.options.padding;
const paddingTop = typeof padding === 'number' ? padding : padding.top;
const paddingLeft = typeof padding === 'number' ? padding : padding.left;
const paddingBottom = typeof padding === 'number' ? padding : padding.bottom;
const paddingRight = typeof padding === 'number' ? padding : padding.right;
const chartWidth = area.right - area.left - paddingLeft - paddingRight;
const chartHeight = area.bottom - area.top - paddingTop - paddingBottom;
const bak = this.oldChartBounds;
this.oldChartBounds = {
chartWidth,
chartHeight,
};
const scale = Math.min(chartWidth / bb.width, chartHeight / bb.height);
const viewWidth = bb.width * scale;
const viewHeight = bb.height * scale;
const x = (chartWidth - viewWidth) * 0.5 + area.left + paddingLeft;
const y = (chartHeight - viewHeight) * 0.5 + area.top + paddingTop;
// this.mapScale = scale;
// this.mapTranslate = {x, y};
const o = this.options;
this.projection
.scale(bb.refScale * scale * o.projectionScale)
.translate([scale * bb.refX + x + o.projectionOffset[0], scale * bb.refY + y + o.projectionOffset[1]]);
return (
!bak || bak.chartWidth !== this.oldChartBounds.chartWidth || bak.chartHeight !== this.oldChartBounds.chartHeight
);
}
static readonly id = 'projection';
/**
* @hidden
*/
static readonly defaults: Partial<IProjectionScaleOptions> = {
projection: 'albersUsa',
projectionScale: 1,
projectionOffset: [0, 0],
padding: 0,
};
/**
* @hidden
*/
static readonly descriptors = /* #__PURE__ */ {
_scriptable: (name: keyof IProjectionScaleOptions): boolean => name !== 'projection',
_indexable: (name: keyof IProjectionScaleOptions): boolean => name !== 'projectionOffset',
};
}
declare module 'chart.js' {
export interface ProjectionScaleTypeRegistry {
projection: {
options: IProjectionScaleOptions;
};
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface ScaleTypeRegistry extends ProjectionScaleTypeRegistry {}
}