PromucFlow_constructor/app/client/src/widgets/ChartWidget/component/index.tsx
2023-08-03 11:28:37 +05:30

371 lines
11 KiB
TypeScript

import { get } from "lodash";
import React from "react";
import styled from "styled-components";
import * as echarts from "echarts";
import { invisible } from "constants/DefaultTheme";
import { getAppsmithConfigs } from "@appsmith/configs";
import type {
ChartType,
CustomFusionChartConfig,
AllChartData,
ChartSelectedDataPoint,
LabelOrientation,
} from "../constants";
import log from "loglevel";
import equal from "fast-deep-equal/es6";
import type { WidgetPositionProps } from "widgets/BaseWidget";
import { ChartErrorComponent } from "./ChartErrorComponent";
import { EChartsConfigurationBuilder } from "./EChartsConfigurationBuilder";
import { EChartsDatasetBuilder } from "./EChartsDatasetBuilder";
// Leaving this require here. Ref: https://stackoverflow.com/questions/41292559/could-not-find-a-declaration-file-for-module-module-name-path-to-module-nam/42505940#42505940
// FusionCharts comes with its own typings so there is no need to separately import them. But an import from fusioncharts/core still requires a declaration file.
const FusionCharts = require("fusioncharts");
const plugins: Record<string, any> = {
Charts: require("fusioncharts/fusioncharts.charts"),
FusionTheme: require("fusioncharts/themes/fusioncharts.theme.fusion"),
Widgets: require("fusioncharts/fusioncharts.widgets"),
ZoomScatter: require("fusioncharts/fusioncharts.zoomscatter"),
ZoomLine: require("fusioncharts/fusioncharts.zoomline"),
PowerCharts: require("fusioncharts/fusioncharts.powercharts"),
TimeSeries: require("fusioncharts/fusioncharts.timeseries"),
OverlappedColumn: require("fusioncharts/fusioncharts.overlappedcolumn2d"),
OverlappedBar: require("fusioncharts/fusioncharts.overlappedbar2d"),
TreeMap: require("fusioncharts/fusioncharts.treemap"),
Maps: require("fusioncharts/fusioncharts.maps"),
Gantt: require("fusioncharts/fusioncharts.gantt"),
VML: require("fusioncharts/fusioncharts.vml"),
};
// Enable all plugins.
// This is needed to support custom chart configs
Object.keys(plugins).forEach((key: string) =>
(plugins[key] as any)(FusionCharts),
);
const { fusioncharts } = getAppsmithConfigs();
FusionCharts.options.license({
key: fusioncharts.licenseKey,
creditLabel: false,
});
export interface ChartComponentState {
eChartsError: Error | undefined;
chartType: ChartType;
}
export interface ChartComponentProps extends WidgetPositionProps {
allowScroll: boolean;
chartData: AllChartData;
chartName: string;
chartType: ChartType;
customFusionChartConfig: CustomFusionChartConfig;
hasOnDataPointClick: boolean;
isVisible?: boolean;
isLoading: boolean;
setAdaptiveYMin: boolean;
labelOrientation?: LabelOrientation;
onDataPointClick: (selectedDataPoint: ChartSelectedDataPoint) => void;
widgetId: string;
xAxisName: string;
yAxisName: string;
borderRadius: string;
boxShadow?: string;
primaryColor?: string;
fontFamily?: string;
dimensions: {
componentWidth: number;
componentHeight: number;
};
}
const ChartsContainer = styled.div`
position: relative;
height: 100%;
width: 100%;
`;
const CanvasContainer = styled.div<
Omit<ChartComponentProps, "onDataPointClick" | "hasOnDataPointClick">
>`
border-radius: ${({ borderRadius }) => borderRadius};
box-shadow: ${({ boxShadow }) => `${boxShadow}`} !important;
height: 100%;
width: 100%;
background: var(--ads-v2-color-bg);
overflow: hidden;
position: relative;
${(props) => (!props.isVisible ? invisible : "")};
padding: 10px 0 0 0;
}`;
class ChartComponent extends React.Component<
ChartComponentProps,
ChartComponentState
> {
fusionChartsInstance: any = null;
echartsInstance: echarts.ECharts | undefined;
customFusionChartContainerId =
this.props.widgetId + "custom-fusion-chart-container";
eChartsContainerId = this.props.widgetId + "echart-container";
eChartsHTMLContainer: HTMLElement | null = null;
eChartsData: AllChartData = {};
echartsConfigurationBuilder: EChartsConfigurationBuilder;
echartConfiguration: Record<string, any> = {};
constructor(props: ChartComponentProps) {
super(props);
this.echartsConfigurationBuilder = new EChartsConfigurationBuilder();
this.state = {
eChartsError: undefined,
chartType: this.props.chartType,
};
}
getEChartsOptions = () => {
const options = {
...this.echartsConfigurationBuilder.prepareEChartConfig(
this.props,
this.eChartsData,
),
dataset: {
...EChartsDatasetBuilder.datasetFromData(this.eChartsData),
},
};
return options;
};
dataClickCallback = (params: echarts.ECElementEvent) => {
const eventData: unknown[] = params.data as unknown[];
const x: unknown = eventData[0];
const index = (params.seriesIndex ?? 0) + 1;
const y: unknown = eventData[index];
const seriesName =
params.seriesName && params.seriesName?.length > 0
? params.seriesName
: "null";
this.props.onDataPointClick({
x: x,
y: y,
seriesTitle: seriesName,
});
};
initializeEchartsInstance = () => {
this.eChartsHTMLContainer = document.getElementById(
this.eChartsContainerId,
);
if (!this.eChartsHTMLContainer) {
return;
}
if (!this.echartsInstance || this.echartsInstance.isDisposed()) {
this.echartsInstance = echarts.init(
this.eChartsHTMLContainer,
undefined,
{
renderer: "svg",
},
);
}
};
shouldResizeECharts = () => {
return (
this.echartsInstance?.getHeight() !=
this.props.dimensions.componentHeight ||
this.echartsInstance?.getWidth() != this.props.dimensions.componentWidth
);
};
renderECharts = () => {
this.initializeEchartsInstance();
if (!this.echartsInstance) {
return;
}
const newConfiguration = this.getEChartsOptions();
const needsNewConfig = !equal(newConfiguration, this.echartConfiguration);
const resizedNeeded = this.shouldResizeECharts();
if (needsNewConfig) {
this.echartConfiguration = newConfiguration;
this.echartsInstance.off("click");
this.echartsInstance.on("click", this.dataClickCallback);
try {
this.echartsInstance.setOption(this.echartConfiguration, true);
if (this.state.eChartsError) {
this.setState({ eChartsError: undefined });
}
} catch (error) {
this.disposeECharts();
this.setState({ eChartsError: error as Error });
}
}
if (resizedNeeded) {
this.echartsInstance.resize({
width: this.props.dimensions.componentWidth,
height: this.props.dimensions.componentHeight,
});
}
};
disposeECharts = () => {
this.echartsInstance?.dispose();
};
componentDidMount() {
this.eChartsData = EChartsDatasetBuilder.chartData(this.props);
this.renderChartingLibrary();
}
componentWillUnmount() {
this.disposeECharts();
this.disposeFusionCharts();
}
renderChartingLibrary() {
if (this.state.chartType === "CUSTOM_FUSION_CHART") {
this.disposeECharts();
this.renderFusionCharts();
} else {
this.disposeFusionCharts();
this.initializeEchartsInstance();
this.renderECharts();
}
}
componentDidUpdate() {
if (
this.props.chartType == "CUSTOM_FUSION_CHART" &&
this.state.chartType != "CUSTOM_FUSION_CHART"
) {
this.setState({
eChartsError: undefined,
chartType: "CUSTOM_FUSION_CHART",
});
} else if (
this.props.chartType != "CUSTOM_FUSION_CHART" &&
this.state.chartType === "CUSTOM_FUSION_CHART"
) {
// User has selected one of the ECharts option
this.setState({ chartType: "AREA_CHART" });
} else {
this.eChartsData = EChartsDatasetBuilder.chartData(this.props);
this.renderChartingLibrary();
}
}
disposeFusionCharts = () => {
this.fusionChartsInstance = null;
};
renderFusionCharts = () => {
if (this.fusionChartsInstance) {
const { dataSource, type } = this.getCustomFusionChartDataSource();
this.fusionChartsInstance.chartType(type);
this.fusionChartsInstance.setChartData(dataSource);
} else {
const config = this.customFusionChartConfig();
this.fusionChartsInstance = new FusionCharts(config);
FusionCharts.ready(() => {
/* Component could be unmounted before FusionCharts is ready,
this check ensure we don't render on unmounted component */
if (this.fusionChartsInstance) {
try {
this.fusionChartsInstance.render();
} catch (e) {
log.error(e);
}
}
});
}
};
customFusionChartConfig() {
const chartConfig = {
renderAt: this.customFusionChartContainerId,
width: "100%",
height: "100%",
events: {
dataPlotClick: (evt: any) => {
const data = evt.data;
const seriesTitle = get(data, "datasetName", "");
this.props.onDataPointClick({
x: data.categoryLabel,
y: data.dataValue,
seriesTitle,
});
},
},
...this.getCustomFusionChartDataSource(),
};
return chartConfig;
}
getCustomFusionChartDataSource = () => {
// in case of evaluation error, customFusionChartConfig can be undefined
let config = this.props.customFusionChartConfig as CustomFusionChartConfig;
if (config && config.dataSource) {
config = {
...config,
dataSource: {
chart: {
...config.dataSource.chart,
caption: this.props.chartName || config.dataSource.chart.caption,
setAdaptiveYMin: this.props.setAdaptiveYMin ? "1" : "0",
},
...config.dataSource,
},
};
}
return config || {};
};
render() {
//eslint-disable-next-line @typescript-eslint/no-unused-vars
const { hasOnDataPointClick, onDataPointClick, ...rest } = this.props;
// Avoid propagating the click events to upwards
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onClick = hasOnDataPointClick
? (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => e.stopPropagation()
: undefined;
return (
<CanvasContainer
className={this.props.isLoading ? "bp3-skeleton" : ""}
onClick={onClick}
{...rest}
>
{this.state.chartType !== "CUSTOM_FUSION_CHART" && (
<ChartsContainer id={this.eChartsContainerId} />
)}
{this.state.chartType === "CUSTOM_FUSION_CHART" && (
<ChartsContainer id={this.customFusionChartContainerId} />
)}
{this.state.eChartsError && (
<ChartErrorComponent error={this.state.eChartsError} />
)}
</CanvasContainer>
);
}
}
export default ChartComponent;