feat(plugin): add plugin-chart-cartodiagram (#25869)

Co-authored-by: Jakob Miksch <jakob@meggsimum.de>
This commit is contained in:
Jan Suleiman
2025-01-06 17:58:03 +01:00
committed by GitHub
parent 5484db34f9
commit a986a61b5f
72 changed files with 8434 additions and 193 deletions

View File

@@ -0,0 +1,251 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ControlHeader } from '@superset-ui/chart-controls';
import { css, styled, t } from '@superset-ui/core';
import { Form, Tag } from 'antd';
import { FC, useState } from 'react';
import { isZoomConfigsLinear, isZoomConfigsExp } from './typeguards';
import { ZoomConfigs, ZoomConfigsControlProps } from './types';
import {
computeConfigValues,
toFixedConfig,
toLinearConfig,
toExpConfig,
} from './zoomUtil';
import ZoomConfigsChart from './ZoomConfigsChart';
import { ControlFormItem } from '../ColumnConfigControl/ControlForm';
export const StyledControlFormItem = styled(ControlFormItem)`
${({ theme }) => css`
border-radius: ${theme.borderRadius}px;
`}
`;
export const ZoomConfigControl: FC<ZoomConfigsControlProps> = ({
value,
onChange = () => {},
name,
label,
description,
renderTrigger,
hovered,
validationErrors,
}) => {
const initBaseWidth = value ? value.configs.width : 0;
const initBaseHeight = value ? value.configs.height : 0;
const initBaseSlope =
value?.configs.slope !== undefined ? value.configs.slope : 0;
const initBaseExponent =
value?.configs.exponent !== undefined ? value.configs.exponent : 0;
const [baseWidth, setBaseWidth] = useState<number>(initBaseWidth);
const [baseHeight, setBaseHeight] = useState<number>(initBaseHeight);
const [baseSlope, setBaseSlope] = useState<number>(initBaseSlope);
const [baseExponent, setBaseExponent] = useState<number>(initBaseExponent);
const onChartChange = (newConfig: ZoomConfigs) => {
onChange(newConfig);
};
const onBaseWidthChange = (width: number) => {
console.log('now in onbasewidthcahnge');
setBaseWidth(width);
if (!value) {
return;
}
const newValue = { ...value };
newValue.configs.width = width;
newValue.values = computeConfigValues(newValue);
onChange(newValue);
};
const onBaseHeightChange = (height: number) => {
setBaseHeight(height);
if (!value) {
return;
}
const newValue = { ...value };
newValue.configs.height = height;
newValue.values = computeConfigValues(newValue);
onChange(newValue);
};
const onBaseSlopeChange = (slope: number) => {
setBaseSlope(slope);
if (value && isZoomConfigsLinear(value)) {
const newValue = { ...value };
newValue.configs.slope = slope;
newValue.values = computeConfigValues(newValue);
onChange(newValue);
}
};
const onBaseExponentChange = (exponent: number) => {
setBaseExponent(exponent);
if (value && isZoomConfigsExp(value)) {
const newValue = { ...value };
newValue.configs.exponent = exponent;
newValue.values = computeConfigValues(newValue);
onChange(newValue);
}
};
const onShapeChange = (shape: ZoomConfigs['type']) => {
if (!value) return;
const baseValues = {
width: baseWidth,
height: baseHeight,
slope: baseSlope,
exponent: baseExponent,
zoom: value?.configs.zoom,
};
switch (shape) {
case 'FIXED': {
const newFixedConfig = toFixedConfig(baseValues);
onChange(newFixedConfig);
break;
}
case 'LINEAR': {
const newLinearConfig = toLinearConfig(baseValues);
onChange(newLinearConfig);
break;
}
case 'EXP': {
const newLogConfig = toExpConfig(baseValues);
onChange(newLogConfig);
break;
}
default:
break;
}
};
const controlHeaderProps = {
name,
label,
description,
renderTrigger,
hovered,
validationErrors,
};
const shapeLabel = t('Shape');
const shapeDescription = t(
'Select shape for computing values. "FIXED" sets all zoom levels to the same size. "LINEAR" increases sizes linearly based on specified slope. "EXP" increases sizes exponentially based on specified exponent',
);
const baseWidthLabel = t('Base width');
const baseWidthDescription = t(
'The width of the current zoom level to compute all widths from',
);
const baseHeightLabel = t('Base height');
const baseHeightDescription = t(
'The height of the current zoom level to compute all heights from',
);
const baseSlopeLabel = t('Base slope');
const baseSlopeDescription = t(
'The slope to compute all sizes from. "LINEAR" only',
);
const baseExponentLabel = t('Base exponent');
const baseExponentDescription = t(
'The exponent to compute all sizes from. "EXP" only',
);
return (
<div>
<ControlHeader {...controlHeaderProps} />
<Form>
<StyledControlFormItem
controlType="RadioButtonControl"
label={shapeLabel}
description={shapeDescription}
options={[
['FIXED', 'FIXED'],
['LINEAR', 'LINEAR'],
['EXP', 'EXP'],
]}
value={value ? value.type : undefined}
name="shape"
onChange={onShapeChange}
/>
<StyledControlFormItem
controlType="Slider"
label={baseWidthLabel}
description={baseWidthDescription}
value={baseWidth}
name="baseWidth"
// @ts-ignore
onAfterChange={onBaseWidthChange}
step={1}
min={0}
max={500}
/>
<StyledControlFormItem
controlType="Slider"
label={baseHeightLabel}
description={baseHeightDescription}
value={baseHeight}
name="baseHeight"
// @ts-ignore
onAfterChange={onBaseHeightChange}
step={1}
min={0}
max={500}
/>
<StyledControlFormItem
controlType="Slider"
label={baseSlopeLabel}
description={baseSlopeDescription}
value={baseSlope}
name="slope"
// @ts-ignore
onAfterChange={onBaseSlopeChange}
disabled={!!(value && !isZoomConfigsLinear(value))}
step={1}
min={0}
max={100}
/>
<StyledControlFormItem
controlType="Slider"
label={baseExponentLabel}
description={baseExponentDescription}
value={baseExponent}
name="exponent"
// @ts-ignore
onAfterChange={onBaseExponentChange}
disabled={!!(value && !isZoomConfigsExp(value))}
step={0.2}
min={0}
max={3}
/>
<Tag>Current Zoom: {value?.configs.zoom}</Tag>
</Form>
<ZoomConfigsChart
name="zoomlevels"
value={value}
onChange={onChartChange}
/>
</div>
);
};
export default ZoomConfigControl;

View File

@@ -0,0 +1,183 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@superset-ui/core';
import * as echarts from 'echarts';
import { createRef, FC, useEffect } from 'react';
import { ZoomConfigsChartProps } from './types';
import {
createDragGraphicOptions,
dataToZoomConfigs,
MAX_ZOOM_LEVEL,
MIN_ZOOM_LEVEL,
zoomConfigsToData,
} from './zoomUtil';
export const ZoomConfigsChart: FC<ZoomConfigsChartProps> = ({
value,
onChange = () => {},
}) => {
const ref = createRef<HTMLDivElement>();
useEffect(() => {
if (!ref.current) {
return undefined;
}
// TODO check if this can be applied here
if (value === null || value === undefined) {
return undefined;
}
let timer: number;
const barWidth = 15;
const data = zoomConfigsToData(value.values);
const chart = echarts.init(ref.current);
const option = {
xAxis: {
min: 0,
name: t('Size in pixels'),
nameLocation: 'center',
nameGap: 25,
},
yAxis: {
type: 'category',
min: MIN_ZOOM_LEVEL,
max: MAX_ZOOM_LEVEL,
name: t('Zoom level'),
nameLocation: 'center',
nameRotate: 90,
nameGap: 25,
},
dataset: {
dimensions: ['width', 'height', 'zoom'],
source: data,
},
grid: {
top: 12,
left: 40,
},
series: [
{
id: 'width',
name: 'width',
type: 'bar',
animation: false,
showBackground: true,
barWidth,
barGap: '0%',
label: {
show: true,
formatter: '{a}: {@width}',
},
encode: {
x: 'width',
y: 'zoom',
},
},
{
id: 'height',
name: 'height',
type: 'bar',
animation: false,
showBackground: true,
barWidth,
barGap: '0%',
label: {
show: true,
formatter: '{a}: {@height}',
},
encode: {
x: 'height',
y: 'zoom',
},
},
],
};
chart.setOption(option);
const onDrag = function (
this: any,
dataIndex: number | undefined,
itemIndex: number,
) {
if (dataIndex === undefined) {
return;
}
// eslint-disable-next-line react/no-this-in-sfc
const newPosition = chart.convertFromPixel('grid', [this.x, this.y]);
if (typeof newPosition === 'number') {
return;
}
const roundedPosition = Math.round(newPosition[0]);
const newRoundedPosition = roundedPosition < 0 ? 0 : roundedPosition;
data[dataIndex][itemIndex] = newRoundedPosition;
chart.setOption({
dataset: {
source: data,
},
});
if (timer !== undefined) {
clearTimeout(timer);
}
timer = window.setTimeout(() => {
const newValues = dataToZoomConfigs(data);
onChange({ ...value, values: newValues });
}, 200);
};
const onWidthDrag = function (this: any, dataIndex: number | undefined) {
onDrag.call(this, dataIndex, 0);
};
const onHeightDrag = function (this: any, dataIndex: number | undefined) {
onDrag.call(this, dataIndex, 1);
};
// TODO listen to resize event and redraw chart
// TODO rearrange the draghandlers when the chart range changes
chart.setOption({
graphic: createDragGraphicOptions({
data,
onWidthDrag,
onHeightDrag,
barWidth,
chart,
}),
});
// chart.on('click', 'series', (params) => {
// const clickedData: number[] = params.data as number[];
// const zoomLevel: number = clickedData[2];
// // TODO we have to set a flag on value that indicates, which zoomLevel should be active
// // TODO maybe it's better to add a callback to the map that triggers when the zoom
// // in the map changes. This can then be displayed on the zoom chart.
// });
return () => {
clearTimeout(timer);
};
}, [value]);
return <div ref={ref} style={{ height: '1300px', width: '100%' }} />;
};
export default ZoomConfigsChart;

View File

@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
ZoomConfigs,
ZoomConfigsFixed,
ZoomConfigsLinear,
ZoomConfigsExp,
} from './types';
export const isZoomConfigsFixed = (
zoomConfigs: ZoomConfigs,
): zoomConfigs is ZoomConfigsFixed => zoomConfigs.type === 'FIXED';
export const isZoomConfigsLinear = (
zoomConfigs: ZoomConfigs,
): zoomConfigs is ZoomConfigsLinear => zoomConfigs.type === 'LINEAR';
export const isZoomConfigsExp = (
zoomConfigs: ZoomConfigs,
): zoomConfigs is ZoomConfigsExp => zoomConfigs.type === 'EXP';

View File

@@ -0,0 +1,93 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ControlComponentProps } from '@superset-ui/chart-controls';
export type ZoomConfigs = ZoomConfigsFixed | ZoomConfigsLinear | ZoomConfigsExp;
export type ChartSizeValues = {
[index: number]: { width: number; height: number };
};
export interface ZoomConfigsBase {
type: string;
configs: {
zoom: number;
width: number;
height: number;
slope?: number;
exponent?: number;
};
values: ChartSizeValues;
}
export interface ZoomConfigsFixed extends ZoomConfigsBase {
type: 'FIXED';
}
export interface ZoomConfigsLinear extends ZoomConfigsBase {
type: 'LINEAR';
configs: {
zoom: number;
width: number;
height: number;
slope: number;
exponent?: number;
};
}
export interface ZoomConfigsExp extends ZoomConfigsBase {
type: 'EXP';
configs: {
zoom: number;
width: number;
height: number;
slope?: number;
exponent: number;
};
}
export type ZoomConfigsControlProps = ControlComponentProps<ZoomConfigs>;
export interface CreateDragGraphicOptions {
data: number[][];
onWidthDrag: (...arg: any[]) => any;
onHeightDrag: (...args: any[]) => any;
barWidth: number;
chart: any;
}
export interface CreateDragGraphicOption {
dataItem: number[];
dataItemIndex: number;
dataIndex: number;
onDrag: (...arg: any[]) => any;
barWidth: number;
chart: any;
add: boolean;
}
export interface GetDragGraphicPositionOptions {
chart: any;
x: number;
y: number;
barWidth: number;
add: boolean;
}
export type ZoomConfigsChartProps = ZoomConfigsControlProps;

View File

@@ -0,0 +1,199 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ZoomConfigs } from './types';
import {
computeConfigValues,
MAX_ZOOM_LEVEL,
MIN_ZOOM_LEVEL,
toExpConfig,
toFixedConfig,
toLinearConfig,
zoomConfigsToData,
} from './zoomUtil';
const zoomConfigValues = {
...Array.from({ length: MAX_ZOOM_LEVEL - MIN_ZOOM_LEVEL + 1 }, () => ({
width: 100,
height: 100,
})),
};
describe('zoomUtil', () => {
describe('computeConfigValues', () => {
it('computes fixed values', () => {
const height = 100;
const width = 100;
const zoomConfigs: ZoomConfigs = {
type: 'FIXED',
values: {},
configs: {
zoom: 2,
width,
height,
},
};
const result = computeConfigValues(zoomConfigs);
expect(Object.keys(result).length).toEqual(
Object.keys(zoomConfigValues).length,
);
expect(result[4]).toEqual({
width,
height,
});
});
it('computes linear values', () => {
const height = 100;
const width = 100;
const zoomConfigs: ZoomConfigs = {
type: 'LINEAR',
values: {},
configs: {
zoom: 2,
width,
height,
slope: 2,
},
};
const result = computeConfigValues(zoomConfigs);
expect(Object.keys(result).length).toEqual(
Object.keys(zoomConfigValues).length,
);
expect(result[4]).toEqual({
width: 104,
height: 104,
});
});
it('computes exponential values', () => {
const height = 100;
const width = 100;
const zoomConfigs: ZoomConfigs = {
type: 'EXP',
values: {},
configs: {
zoom: 2,
width,
height,
exponent: 1.6,
},
};
const result = computeConfigValues(zoomConfigs);
expect(Object.keys(result).length).toEqual(
Object.keys(zoomConfigValues).length,
);
expect(result[4]).toEqual({
width: 119,
height: 119,
});
});
});
describe('zoomConfigsToData', () => {
it('returns correct output', () => {
const result = zoomConfigsToData(zoomConfigValues);
expect(result.length).toEqual(Object.keys(zoomConfigValues).length);
expect(result[12]).toEqual([100, 100, 12]);
});
});
describe('toFixedConfig', () => {
const configs: ZoomConfigs['configs'] = {
width: 100,
height: 100,
zoom: 5,
};
const result = toFixedConfig(configs);
it('has correct type', () => {
expect(result.type).toEqual('FIXED');
});
it('returns correct result', () => {
expect(result.values[4]).toEqual({
width: 100,
height: 100,
});
expect(result.values[6]).toEqual({
width: 100,
height: 100,
});
});
});
describe('toLinearConfig', () => {
const configs: ZoomConfigs['configs'] = {
width: 100,
height: 100,
zoom: 5,
slope: 2,
};
const result = toLinearConfig(configs);
it('has correct type', () => {
expect(result.type).toEqual('LINEAR');
});
it('returns correct result', () => {
expect(result.values[4]).toEqual({
width: 98,
height: 98,
});
expect(result.values[6]).toEqual({
width: 102,
height: 102,
});
});
});
describe('toExpConfig', () => {
const configs: ZoomConfigs['configs'] = {
width: 100,
height: 100,
zoom: 5,
exponent: 1.5,
};
// @ts-ignore
const result = toExpConfig(configs);
it('has correct type', () => {
expect(result.type).toEqual('EXP');
});
it('returns correct result', () => {
expect(result.values[4]).toEqual({
width: 93,
height: 93,
});
expect(result.values[6]).toEqual({
width: 107,
height: 107,
});
});
});
});

View File

@@ -0,0 +1,348 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import * as echarts from 'echarts';
import { isZoomConfigsFixed, isZoomConfigsLinear } from './typeguards';
import {
CreateDragGraphicOption,
CreateDragGraphicOptions,
GetDragGraphicPositionOptions,
ZoomConfigs,
ZoomConfigsFixed,
ZoomConfigsLinear,
ZoomConfigsExp,
} from './types';
/**
* Compute the position for a drag graphic.
*
* @param param0 configuration
* @param param0.chart The eChart instance.
* @param param0.x The x value of the data item.
* @param param0.y The y value of the data item.
* @param param0.barWidth The width of the bar.
* @param param0.add True, if barWidth should be added. False, if barWidth should be subtracted.
* @returns
*/
export const getDragGraphicPosition = ({
chart,
x,
y,
barWidth,
add,
}: GetDragGraphicPositionOptions) => {
const valuePosition = chart.convertToPixel('grid', [x, y]);
const xPos = Math.round(valuePosition[0]);
let yPos = valuePosition[1] - barWidth / 2;
if (add) {
yPos = valuePosition[1] + barWidth / 2;
}
return [xPos, yPos];
};
/**
* Create a single drag graphic with drag handler.
* @param param0 configuration
* @param param0.dataItem The data item to create the graphic for.
* @param param0.dataItemIndex The index of the height/width value in the item.
* @param param0.dataIndex The index of the dataItem in the data.
* @param param0.onDrag Callback for dragging the bar.
* @param param0.barWidth The width of the bar.
* @param param0.chart The eChart instance.
* @param param0.add True, if barWidth should be added for positioning. False, if barWidth should be subtracted.
* @returns eChart Option for a drag graphic.
*/
export const createDragGraphicOption = ({
dataItem,
dataItemIndex,
dataIndex,
onDrag,
barWidth,
chart,
add,
}: CreateDragGraphicOption) => {
const position = getDragGraphicPosition({
chart,
x: dataItem[dataItemIndex],
y: dataItem[2],
barWidth,
add,
});
return {
type: 'circle',
shape: {
// The radius of the circle.
r: barWidth / 4,
},
x: position[0],
y: position[1],
invisible: false,
style: {
// eslint-disable-next-line theme-colors/no-literal-colors
fill: '#ffffff',
// eslint-disable-next-line theme-colors/no-literal-colors
stroke: '#aaa',
},
cursor: 'ew-resize',
draggable: 'horizontal',
// Give a big z value, which makes the circle cover the symbol
// in bar series.
z: 100,
// Util method `echarts.util.curry` is used here to generate a
// new function the same as `onDrag`, except that the
// first parameter is fixed to be the `dataIndex` here.
ondrag: echarts.util.curry(onDrag, dataIndex),
};
};
/**
* Create a drag graphic with dragHandler for each bar.
*
* @param param0 configuration
* @param param0.data The eChart data.
* @param param0.onWidthDrag Callback for dragging width bars.
* @param param0.onHeightDrag Callback for dragging height bars.
* @param param0.barWidth The width of a single bar.
* @param param0.chart The eChart instance.
* @returns List of eChart options for the drag graphics.
*/
export const createDragGraphicOptions = ({
data,
onWidthDrag,
onHeightDrag,
barWidth,
chart,
}: CreateDragGraphicOptions) => {
const graphics: any[] = [];
data.forEach((dataItem: number[], dataIndex: number) => {
const widthGraphic = createDragGraphicOption({
dataItem,
dataIndex,
barWidth,
chart,
dataItemIndex: 0,
onDrag: onWidthDrag,
add: false,
});
graphics.push(widthGraphic);
const heightGraphic = createDragGraphicOption({
dataItem,
dataIndex,
barWidth,
chart,
dataItemIndex: 1,
onDrag: onHeightDrag,
add: true,
});
graphics.push(heightGraphic);
});
return graphics;
};
/**
* Convert ZoomConfigs to eChart data.
*
* @param zoomConfigs The config to convert.
* @returns eChart data representing the zoom configs.
*/
export const zoomConfigsToData = (zoomConfigs: ZoomConfigs['values']) =>
Object.keys(zoomConfigs)
.map((k: string) => parseInt(k, 10))
.map((k: number) => [zoomConfigs[k].width, zoomConfigs[k].height, k]);
/**
* Convert eChart data to ZoomConfigs.
*
* @param data The eChart data to convert.
* @returns ZoomConfigs representing the eChart data.
*/
export const dataToZoomConfigs = (data: number[][]): ZoomConfigs['values'] =>
data.reduce((prev: ZoomConfigs['values'], cur: number[]) => {
// eslint-disable-next-line no-param-reassign
prev[cur[2]] = { width: cur[0], height: cur[1] };
return prev;
}, {});
export const MAX_ZOOM_LEVEL = 28;
export const MIN_ZOOM_LEVEL = 0;
/**
* Compute values for all zoom levels with fixed shape.
*
* @param zoomConfigsFixed The config to base the computation upon.
* @returns The computed values for each zoom level.
*/
const computeFixedConfigValues = (zoomConfigsFixed: ZoomConfigsFixed) => {
const values: ZoomConfigsFixed['values'] = {};
for (let i = MIN_ZOOM_LEVEL; i <= MAX_ZOOM_LEVEL; i += 1) {
const width = Math.round(zoomConfigsFixed.configs.width);
const height = Math.round(zoomConfigsFixed.configs.height);
values[i] = {
width: width > 0 ? width : 0,
height: height > 0 ? height : 0,
};
}
return values;
};
/**
* Compute values for all zoom levels with linear shape.
*
* @param zoomConfigsLinear The config to base the computation upon.
* @returns The computed values for each zoom level.
*/
const computeLinearConfigValues = (zoomConfigsLinear: ZoomConfigsLinear) => {
const values: ZoomConfigsLinear['values'] = {};
for (let i = MIN_ZOOM_LEVEL; i <= MAX_ZOOM_LEVEL; i += 1) {
const aspectRatio =
zoomConfigsLinear.configs.height / zoomConfigsLinear.configs.width;
const width = Math.round(
zoomConfigsLinear.configs.width -
(zoomConfigsLinear.configs.zoom - i) * zoomConfigsLinear.configs.slope,
);
const height = Math.round(aspectRatio * width);
values[i] = {
width: width > 0 ? width : 0,
height: height > 0 ? height : 0,
};
}
return values;
};
/**
* Compute values for all zoom levels with exponential shape.
*
* @param zoomConfigsExp The config to base the computation upon.
* @returns The computed values for each zoom level.
*/
const computeExpConfigValues = (zoomConfigsExp: ZoomConfigsExp) => {
const values: ZoomConfigsExp['values'] = {};
const x = Math.pow(
zoomConfigsExp.configs.width,
1 / zoomConfigsExp.configs.exponent,
);
for (let i = MIN_ZOOM_LEVEL; i <= MAX_ZOOM_LEVEL; i += 1) {
const aspectRatio =
zoomConfigsExp.configs.height / zoomConfigsExp.configs.width;
const width = Math.round(
Math.pow(
x - (zoomConfigsExp.configs.zoom - i),
zoomConfigsExp.configs.exponent,
),
);
const height = Math.round(aspectRatio * width);
values[i] = {
width: width > 0 ? width : 0,
height: height > 0 ? height : 0,
};
}
return values;
};
/**
* Compute values for all zoom levels.
*
* @param zoomConfigs The config to base the computation upon.
* @returns The computed values for each zoom level.
*/
export const computeConfigValues = (zoomConfigs: ZoomConfigs) => {
if (isZoomConfigsFixed(zoomConfigs)) {
return computeFixedConfigValues(zoomConfigs);
}
if (isZoomConfigsLinear(zoomConfigs)) {
return computeLinearConfigValues(zoomConfigs);
}
return computeExpConfigValues(zoomConfigs);
};
/**
* Convert ZoomConfigs to ZoomConfigsFixed.
*
* @param baseConfig The base config.
* @returns The converted config.
*/
export const toFixedConfig = (
baseConfig: ZoomConfigsFixed['configs'],
): ZoomConfigsFixed => {
const zoomConfigFixed: ZoomConfigsFixed = {
type: 'FIXED',
configs: {
zoom: baseConfig.zoom,
width: baseConfig.width,
height: baseConfig.height,
},
values: {},
};
zoomConfigFixed.values = computeFixedConfigValues(zoomConfigFixed);
return zoomConfigFixed;
};
/**
* Convert ZoomConfigs to ZoomConfigsLinear.
*
* @param baseConfig The base config.
* @returns The converted config.
*/
export const toLinearConfig = (
baseConfig: ZoomConfigsFixed['configs'],
): ZoomConfigsLinear => {
const zoomConfigsLinear: ZoomConfigsLinear = {
type: 'LINEAR',
configs: {
zoom: baseConfig.zoom,
width: baseConfig.width,
height: baseConfig.height,
slope: baseConfig.slope,
},
values: {},
} as ZoomConfigsLinear;
zoomConfigsLinear.values = computeLinearConfigValues(zoomConfigsLinear);
return zoomConfigsLinear;
};
/**
* Convert ZoomConfigs to ZoomConfigsExp.
*
* @param baseConfig The base config.
* @returns The converted config.
*/
export const toExpConfig = (
baseConfig: ZoomConfigsExp['configs'],
): ZoomConfigsExp => {
const zoomConfigExp: ZoomConfigsExp = {
type: 'EXP',
configs: {
zoom: baseConfig.zoom,
width: baseConfig.width,
height: baseConfig.height,
exponent: baseConfig.exponent,
},
values: {},
} as ZoomConfigsExp;
zoomConfigExp.values = computeExpConfigValues(zoomConfigExp);
return zoomConfigExp;
};