mirror of
https://github.com/apache/superset.git
synced 2026-06-01 13:49:21 +00:00
feat(plugin): add plugin-chart-cartodiagram (#25869)
Co-authored-by: Jakob Miksch <jakob@meggsimum.de>
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user