diff --git a/.gitignore b/.gitignore index b044fc2c8..d2a57a1fe 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ application-dev-localhost.yaml server/api-service/lowcoder-server/src/main/resources/application-local-dev.yaml translations/locales/node_modules/ server/api-service/lowcoder-server/src/main/resources/application-local-dev-ee.yaml +node_modules diff --git a/README.md b/README.md index 6ba2fff86..ede4bcc29 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,22 @@

Create software applications (internal and customer-facing!) and Meeting/Collaboration tools for your Company and your Customers with minimal coding experience.

-

Lowcoder is the best Retool, Appsmith or Tooljet Alternative.

+

We think, Lowcoder is simply better than Retool, Appsmith Tooljet, Outsystems or Mendix.

+--- - - +## 🎥 Lowcoder Intro Video +
+ + Lowcoder Intro Video + +

Click the image above to watch the video on YouTube 📺

+
+--- ## 📢 Use Lowcoder in 3 steps 1. Connect to any data sources or APIs. -2. Build flexible and responsive UI with 100+ components and free layout / design possibilities. +2. Build flexible and responsive UI with 120+ components and free layout / design possibilities. 3. Share with colleagues and customers. ## 💡 Why Lowcoder @@ -23,9 +30,9 @@ One platform for everything instead so many different softwares. (like Website B It's cumbersome to create a single app. You had to design user interfaces, write code in multiple languages and frameworks, and understand how all of that code works together. -NewGen Lowcode Platforms like Retool and others are great for their simplicity and flexibility - like Lowcoder too, but they can also be limited in different ways, especially when it comes to "external" applications for everyone. +NewGen Lowcode Platforms like Retool and others are great for their simplicity and flexibility - like Lowcoder too, but they can also be limited in different ways, especially when it comes to "external" applications for everyone - because their pricing focusses to internal apps and "pay per User". -Lowcoder wants to take a step forward. More specifically, Lowcoder is: +With Lowcoder we did a step forward. More specifically, Lowcoder is: - An all-in-one IDE to create internal or customer-facing (external) apps. - A place to create, build and share building blocks of web applications and whole websites. - The tool and community to support your business, and lower the cost and time to develop interactive applications. @@ -34,9 +41,9 @@ Lowcoder wants to take a step forward. More specifically, Lowcoder is: - The only platform which has extensibility plugin architecture [Check Community Contributions](https://www.npmjs.com/search?q=lowcoder-comp) ## 🪄 Features -- **Visual UI builder** with 100+ built-in components. Save 90% of time to build apps. +- **Visual UI builder** with 120+ built-in components. Save 90% of time to build apps. - **Modules** for reusable (!) embedable component sets in the UI builder. -- **Embed Lowcoder Apps as native parts of any Website** instead of iFrame (!). [Demo](https://lowcoder.cloud/about), [Docu](https://docs.lowcoder.cloud/lowcoder-documentation/lowcoder-extension/native-embed-sdk) +- **Embed Lowcoder Apps as native parts of any Website** instead of iFrame (!). [Demo](http://demo-lowcoder.42web.io/ecommerce/), [Docu](https://docs.lowcoder.cloud/lowcoder-documentation/lowcoder-extension/native-embed-sdk) - **Video Meeting Components** to create your own individual Web-Meeting tool. - **Query Library** for reusable data queries of your data sources. - **Custom components** to develop own components and use them in the UI builder. @@ -107,7 +114,3 @@ Accelerate the growth of Lowcoder and unleash its potential with your Sponsorshi [Be a Sponsor](https://github.com/sponsors/lowcoder-org) Like ... [@Darkjamin](https://github.com/Darkjamin), [@spacegoats-io](https://github.com/spacegoats-io), [@Jomedya](https://github.com/Jomedya), [@CHSchuepfer](https://github.com/CHSchuepfer), Thank you very much!! - -## Intro Video - -[![Watch the video](https://i.ytimg.com/vi/s4ltAqS0hzM/maxresdefault.jpg?sqp=-oaymwEmCIAKENAF8quKqQMa8AEB-AH-CYAC0AWKAgwIABABGD0gSShyMA8=&rs=AOn4CLAlPOIFdtauythoBKNPXhi6XGwlDQ)](https://youtu.be/s4ltAqS0hzM?feature=shared) diff --git a/client/VERSION b/client/VERSION index bd4053bfb..e46a05b19 100644 --- a/client/VERSION +++ b/client/VERSION @@ -1 +1 @@ -2.6.3 \ No newline at end of file +2.6.4 \ No newline at end of file diff --git a/client/config/test/transform/babelTransform.js b/client/config/test/transform/babelTransform.js index 703cac21a..36f6cf0d8 100644 --- a/client/config/test/transform/babelTransform.js +++ b/client/config/test/transform/babelTransform.js @@ -8,6 +8,13 @@ export default babelJest.createTransformer({ runtime: "automatic", }, ], + [ + "babel-preset-vite", + { + "env": true, + "glob": false + } + ] ], babelrc: false, configFile: false, diff --git a/client/package.json b/client/package.json index e84c1dbea..f8a736710 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-frontend", - "version": "2.6.3", + "version": "2.6.4", "type": "module", "private": true, "workspaces": [ @@ -43,6 +43,7 @@ "add": "^2.0.6", "babel-jest": "^29.3.0", "babel-preset-react-app": "^10.0.1", + "babel-preset-vite": "^1.1.3", "husky": "^8.0.1", "jest": "^29.5.0", "jest-canvas-mock": "^2.5.2", diff --git a/client/packages/lowcoder-cli/actions/build.js b/client/packages/lowcoder-cli/actions/build.js index 04e754e99..7ed38e8f5 100644 --- a/client/packages/lowcoder-cli/actions/build.js +++ b/client/packages/lowcoder-cli/actions/build.js @@ -3,6 +3,7 @@ import fsExtra from "fs-extra"; import { build } from "vite"; import { writeFileSync, existsSync, readFileSync, readdirSync } from "fs"; import { resolve } from "path"; +import { pathToFileURL } from "url"; import paths from "../config/paths.js"; import "../util/log.js"; import chalk from "chalk"; @@ -80,7 +81,9 @@ export default async function buildAction(options) { console.log(""); console.cyan("Building..."); - const viteConfig = await import(paths.appViteConfigJs).default; + const viteConfigURL = pathToFileURL(paths.appViteConfigJs); + const viteConfig = await import(viteConfigURL).default; + console.log(paths.appViteConfigJs); await build(viteConfig); // write package.json diff --git a/client/packages/lowcoder-comps/package.json b/client/packages/lowcoder-comps/package.json index 7cf6fc1af..4fb56a02f 100644 --- a/client/packages/lowcoder-comps/package.json +++ b/client/packages/lowcoder-comps/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-comps", - "version": "2.6.3", + "version": "2.6.5", "type": "module", "license": "MIT", "dependencies": { @@ -23,6 +23,7 @@ "agora-rtm-sdk": "^1.5.1", "big.js": "^6.2.1", "echarts-extension-gmap": "^1.6.0", + "echarts-gl": "^2.0.9", "echarts-wordcloud": "^2.1.0", "lowcoder-cli": "workspace:^", "lowcoder-sdk": "workspace:^", @@ -58,6 +59,62 @@ "h": 40 } }, + "barChart": { + "name": "Bar Chart", + "icon": "./icons/icon-chart.svg", + "layoutInfo": { + "w": 12, + "h": 40 + } + }, + "lineChart": { + "name": "Line Chart", + "icon": "./icons/icon-chart.svg", + "layoutInfo": { + "w": 12, + "h": 40 + } + }, + "pieChart": { + "name": "Pie Chart", + "icon": "./icons/icon-chart.svg", + "layoutInfo": { + "w": 12, + "h": 40 + } + }, + "scatterChart": { + "name": "Scatter Chart", + "icon": "./icons/icon-chart.svg", + "layoutInfo": { + "w": 12, + "h": 40 + } + }, + "boxplotChart": { + "name": "Boxplot Chart", + "icon": "./icons/icon-chart.svg", + "layoutInfo": { + "w": 12, + "h": 40 + } + }, + "parallelChart": { + "name": "Parallel Chart", + "icon": "./icons/icon-chart.svg", + "layoutInfo": { + "w": 12, + "h": 40 + } + }, + "line3dChart": { + "name": "Line3D Chart", + "icon": "./icons/icon-chart.svg", + "layoutInfo": { + "w": 12, + "h": 40 + } + }, "imageEditor": { "name": "Image Editor", "icon": "./icons/icon-chart.svg", diff --git a/client/packages/lowcoder-comps/src/comps/barChartComp/barChartComp.tsx b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartComp.tsx new file mode 100644 index 000000000..e13818586 --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartComp.tsx @@ -0,0 +1,320 @@ +import { + changeChildAction, + changeValueAction, + CompAction, + CompActionTypes, + wrapChildAction, +} from "lowcoder-core"; +import { AxisFormatterComp, EchartsAxisType } from "../basicChartComp/chartConfigs/cartesianAxisConfig"; +import { barChartChildrenMap, ChartSize, getDataKeys } from "./barChartConstants"; +import { barChartPropertyView } from "./barChartPropertyView"; +import _ from "lodash"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; +import ReactResizeDetector from "react-resize-detector"; +import ReactECharts from "../basicChartComp/reactEcharts"; +import { + childrenToProps, + depsConfig, + genRandomKey, + NameConfig, + UICompBuilder, + withDefault, + withExposingConfigs, + withViewFn, + ThemeContext, + chartColorPalette, + getPromiseAfterDispatch, + dropdownControl, + JSONObject, +} from "lowcoder-sdk"; +import { getEchartsLocale, trans } from "i18n/comps"; +import { ItemColorComp } from "comps/basicChartComp/chartConfigs/lineChartConfig"; +import { + echartsConfigOmitChildren, + getEchartsConfig, + getSelectedPoints, +} from "./barChartUtils"; +import 'echarts-extension-gmap'; +import log from "loglevel"; + +let clickEventCallback = () => {}; + +const chartModeOptions = [ + { + label: "ECharts JSON", + value: "json", + } +] as const; + +let BarChartTmpComp = (function () { + return new UICompBuilder({mode:dropdownControl(chartModeOptions,'ui'),...barChartChildrenMap}, () => null) + .setPropertyViewFn(barChartPropertyView) + .build(); +})(); + +BarChartTmpComp = withViewFn(BarChartTmpComp, (comp) => { + const mode = comp.children.mode.getView(); + const onUIEvent = comp.children.onUIEvent.getView(); + const onEvent = comp.children.onEvent.getView(); + const echartsCompRef = useRef(); + const [chartSize, setChartSize] = useState(); + const firstResize = useRef(true); + const theme = useContext(ThemeContext); + const defaultChartTheme = { + color: chartColorPalette, + backgroundColor: "#fff", + }; + + let themeConfig = defaultChartTheme; + try { + themeConfig = theme?.theme.chart ? JSON.parse(theme?.theme.chart) : defaultChartTheme; + } catch (error) { + log.error('theme chart error: ', error); + } + + const triggerClickEvent = async (dispatch: any, action: CompAction) => { + await getPromiseAfterDispatch( + dispatch, + action, + { autoHandleAfterReduce: true } + ); + onEvent('click'); + } + + useEffect(() => { + const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance(); + if (!echartsCompInstance) { + return _.noop; + } + echartsCompInstance?.on("click", (param: any) => { + document.dispatchEvent(new CustomEvent("clickEvent", { + bubbles: true, + detail: { + action: 'click', + data: param.data, + } + })); + triggerClickEvent( + comp.dispatch, + changeChildAction("lastInteractionData", param.data, false) + ); + }); + return () => { + echartsCompInstance?.off("click"); + document.removeEventListener('clickEvent', clickEventCallback) + }; + }, []); + + useEffect(() => { + // bind events + const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance(); + if (!echartsCompInstance) { + return _.noop; + } + echartsCompInstance?.on("selectchanged", (param: any) => { + const option: any = echartsCompInstance?.getOption(); + document.dispatchEvent(new CustomEvent("clickEvent", { + bubbles: true, + detail: { + action: param.fromAction, + data: getSelectedPoints(param, option) + } + })); + + if (param.fromAction === "select") { + comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false)); + onUIEvent("select"); + } else if (param.fromAction === "unselect") { + comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false)); + onUIEvent("unselect"); + } + + triggerClickEvent( + comp.dispatch, + changeChildAction("lastInteractionData", getSelectedPoints(param, option), false) + ); + }); + // unbind + return () => { + echartsCompInstance?.off("selectchanged"); + document.removeEventListener('clickEvent', clickEventCallback) + }; + }, [onUIEvent]); + + const echartsConfigChildren = _.omit(comp.children, echartsConfigOmitChildren); + const childrenProps = childrenToProps(echartsConfigChildren); + const option = useMemo(() => { + return getEchartsConfig( + childrenProps as ToViewReturn, + chartSize, + themeConfig + ); + }, [theme, childrenProps, chartSize, ...Object.values(echartsConfigChildren)]); + + useEffect(() => { + comp.children.mapInstance.dispatch(changeValueAction(null, false)) + if(comp.children.mapInstance.value) return; + }, [option]) + + return ( + { + if (w && h) { + setChartSize({ w: w, h: h }); + } + if (!firstResize.current) { + // ignore the first resize, which will impact the loading animation + echartsCompRef.current?.getEchartsInstance().resize(); + } else { + firstResize.current = false; + } + }} + > + (echartsCompRef.current = e)} + style={{ height: "100%" }} + notMerge + lazyUpdate + opts={{ locale: getEchartsLocale() }} + option={option} + mode={mode} + /> + + ); +}); + +function getYAxisFormatContextValue( + data: Array, + yAxisType: EchartsAxisType, + yAxisName?: string +) { + const dataSample = yAxisName && data.length > 0 && data[0][yAxisName]; + let contextValue = dataSample; + if (yAxisType === "time") { + // to timestamp + const time = + typeof dataSample === "number" || typeof dataSample === "string" + ? new Date(dataSample).getTime() + : null; + if (time) contextValue = time; + } + return contextValue; +} + +BarChartTmpComp = class extends BarChartTmpComp { + private lastYAxisFormatContextVal?: JSONValue; + private lastColorContext?: JSONObject; + + updateContext(comp: this) { + // the context value of axis format + let resultComp = comp; + const data = comp.children.data.getView(); + const sampleSeries = comp.children.series.getView().find((s) => !s.getView().hide); + const yAxisContextValue = getYAxisFormatContextValue( + data, + comp.children.yConfig.children.yAxisType.getView(), + sampleSeries?.children.columnName.getView() + ); + if (yAxisContextValue !== comp.lastYAxisFormatContextVal) { + comp.lastYAxisFormatContextVal = yAxisContextValue; + resultComp = comp.setChild( + "yConfig", + comp.children.yConfig.reduce( + wrapChildAction( + "formatter", + AxisFormatterComp.changeContextDataAction({ value: yAxisContextValue }) + ) + ) + ); + } + // item color context + const colorContextVal = { + seriesName: sampleSeries?.children.seriesName.getView(), + value: yAxisContextValue, + }; + if ( + comp.children.chartConfig.children.comp.children.hasOwnProperty("itemColor") && + !_.isEqual(colorContextVal, comp.lastColorContext) + ) { + comp.lastColorContext = colorContextVal; + resultComp = resultComp.setChild( + "chartConfig", + comp.children.chartConfig.reduce( + wrapChildAction( + "comp", + wrapChildAction("itemColor", ItemColorComp.changeContextDataAction(colorContextVal)) + ) + ) + ); + } + return resultComp; + } + + override reduce(action: CompAction): this { + const comp = super.reduce(action); + if (action.type === CompActionTypes.UPDATE_NODES_V2) { + const newData = comp.children.data.getView(); + // data changes + if (comp.children.data !== this.children.data) { + setTimeout(() => { + // update x-axis value + const keys = getDataKeys(newData); + if (keys.length > 0 && !keys.includes(comp.children.xAxisKey.getView())) { + comp.children.xAxisKey.dispatch(changeValueAction(keys[0] || "")); + } + // pass to child series comp + comp.children.series.dispatchDataChanged(newData); + }, 0); + } + return this.updateContext(comp); + } + return comp; + } + + override autoHeight(): boolean { + return false; + } +}; + +let BarChartComp = withExposingConfigs(BarChartTmpComp, [ + depsConfig({ + name: "selectedPoints", + desc: trans("chart.selectedPointsDesc"), + depKeys: ["selectedPoints"], + func: (input) => { + return input.selectedPoints; + }, + }), + depsConfig({ + name: "lastInteractionData", + desc: trans("chart.lastInteractionDataDesc"), + depKeys: ["lastInteractionData"], + func: (input) => { + return input.lastInteractionData; + }, + }), + depsConfig({ + name: "data", + desc: trans("chart.dataDesc"), + depKeys: ["data", "mode"], + func: (input) =>[] , + }), + new NameConfig("title", trans("chart.titleDesc")), +]); + + +export const BarChartCompWithDefault = withDefault(BarChartComp, { + xAxisKey: "date", + series: [ + { + dataIndex: genRandomKey(), + seriesName: trans("chart.spending"), + columnName: "spending", + }, + { + dataIndex: genRandomKey(), + seriesName: trans("chart.budget"), + columnName: "budget", + }, + ], +}); diff --git a/client/packages/lowcoder-comps/src/comps/barChartComp/barChartConstants.tsx b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartConstants.tsx new file mode 100644 index 000000000..668b569be --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartConstants.tsx @@ -0,0 +1,323 @@ +import { + jsonControl, + JSONObject, + stateComp, + toJSONObjectArray, + toObject, + BoolControl, + withDefault, + StringControl, + NumberControl, + FunctionControl, + dropdownControl, + eventHandlerControl, + valueComp, + withType, + uiChildren, + clickEvent, + styleControl, + EchartDefaultTextStyle, + EchartDefaultChartStyle, + toArray +} from "lowcoder-sdk"; +import { RecordConstructorToComp, RecordConstructorToView } from "lowcoder-core"; +import { BarChartConfig } from "../basicChartComp/chartConfigs/barChartConfig"; +import { XAxisConfig, YAxisConfig } from "../basicChartComp/chartConfigs/cartesianAxisConfig"; +import { LegendConfig } from "../basicChartComp/chartConfigs/legendConfig"; +import { EchartsLegendConfig } from "../basicChartComp/chartConfigs/echartsLegendConfig"; +import { EchartsLabelConfig } from "../basicChartComp/chartConfigs/echartsLabelConfig"; +import { LineChartConfig } from "../basicChartComp/chartConfigs/lineChartConfig"; +import { PieChartConfig } from "../basicChartComp/chartConfigs/pieChartConfig"; +import { ScatterChartConfig } from "../basicChartComp/chartConfigs/scatterChartConfig"; +import { SeriesListComp } from "./seriesComp"; +import { EChartsOption } from "echarts"; +import { i18nObjs, trans } from "i18n/comps"; +import { GaugeChartConfig } from "../basicChartComp/chartConfigs/gaugeChartConfig"; +import { FunnelChartConfig } from "../basicChartComp/chartConfigs/funnelChartConfig"; +import {EchartsTitleVerticalConfig} from "../chartComp/chartConfigs/echartsTitleVerticalConfig"; +import {EchartsTitleConfig} from "../basicChartComp/chartConfigs/echartsTitleConfig"; + +export const ChartTypeOptions = [ + { + label: trans("chart.bar"), + value: "bar", + }, + { + label: trans("chart.line"), + value: "line", + }, + { + label: trans("chart.scatter"), + value: "scatter", + }, + { + label: trans("chart.pie"), + value: "pie", + }, +] as const; + +export const UIEventOptions = [ + { + label: trans("chart.select"), + value: "select", + description: trans("chart.selectDesc"), + }, + { + label: trans("chart.unSelect"), + value: "unselect", + description: trans("chart.unselectDesc"), + }, +] as const; + +export const MapEventOptions = [ + { + label: trans("chart.mapReady"), + value: "mapReady", + description: trans("chart.mapReadyDesc"), + }, + { + label: trans("chart.zoomLevelChange"), + value: "zoomLevelChange", + description: trans("chart.zoomLevelChangeDesc"), + }, + { + label: trans("chart.centerPositionChange"), + value: "centerPositionChange", + description: trans("chart.centerPositionChangeDesc"), + }, +] as const; + +export const XAxisDirectionOptions = [ + { + label: trans("chart.horizontal"), + value: "horizontal", + }, + { + label: trans("chart.vertical"), + value: "vertical", + }, +] as const; + +export type XAxisDirectionType = ValueFromOption; + +export const noDataAxisConfig = { + animation: false, + xAxis: { + type: "category", + name: trans("chart.noData"), + nameLocation: "middle", + data: [], + axisLine: { + lineStyle: { + color: "#8B8FA3", + }, + }, + }, + yAxis: { + type: "value", + axisLabel: { + color: "#8B8FA3", + }, + splitLine: { + lineStyle: { + color: "#F0F0F0", + }, + }, + }, + tooltip: { + show: false, + }, + series: [ + { + data: [700], + type: "line", + itemStyle: { + opacity: 0, + }, + }, + ], +} as EChartsOption; + +export const noDataPieChartConfig = { + animation: false, + tooltip: { + show: false, + }, + legend: { + formatter: trans("chart.unknown"), + top: "bottom", + selectedMode: false, + }, + color: ["#B8BBCC", "#CED0D9", "#DCDEE6", "#E6E6EB"], + series: [ + { + type: "pie", + radius: "35%", + center: ["25%", "50%"], + silent: true, + label: { + show: false, + }, + data: [ + { + name: "1", + value: 70, + }, + { + name: "2", + value: 68, + }, + { + name: "3", + value: 48, + }, + { + name: "4", + value: 40, + }, + ], + }, + { + type: "pie", + radius: "35%", + center: ["75%", "50%"], + silent: true, + label: { + show: false, + }, + data: [ + { + name: "1", + value: 70, + }, + { + name: "2", + value: 68, + }, + { + name: "3", + value: 48, + }, + { + name: "4", + value: 40, + }, + ], + }, + ], +} as EChartsOption; + +export type ChartSize = { w: number; h: number }; + +export const getDataKeys = (data: Array) => { + if (!data) { + return []; + } + const dataKeys: Array = []; + data.slice(0, 50).forEach((d) => { + Object.keys(d).forEach((key) => { + if (!dataKeys.includes(key)) { + dataKeys.push(key); + } + }); + }); + return dataKeys; +}; + +const ChartOptionMap = { + bar: BarChartConfig, + line: LineChartConfig, + pie: PieChartConfig, + scatter: ScatterChartConfig, +}; + +const EchartsOptionMap = { + funnel: FunnelChartConfig, + gauge: GaugeChartConfig, +}; + +const ChartOptionComp = withType(ChartOptionMap, "bar"); +const EchartsOptionComp = withType(EchartsOptionMap, "funnel"); +export type CharOptionCompType = keyof typeof ChartOptionMap; + +export const chartUiModeChildren = { + title: withDefault(StringControl, trans("echarts.defaultTitle")), + data: jsonControl(toJSONObjectArray, i18nObjs.defaultDataSource), + xAxisKey: valueComp(""), // x-axis, key from data + xAxisDirection: dropdownControl(XAxisDirectionOptions, "horizontal"), + xAxisData: jsonControl(toArray, []), + series: SeriesListComp, + xConfig: XAxisConfig, + yConfig: YAxisConfig, + legendConfig: LegendConfig, + chartConfig: ChartOptionComp, + onUIEvent: eventHandlerControl(UIEventOptions), +}; + +let chartJsonModeChildren: any = { + echartsOption: jsonControl(toObject, i18nObjs.defaultEchartsJsonOption), + echartsTitle: withDefault(StringControl, trans("echarts.defaultTitle")), + echartsLegendConfig: EchartsLegendConfig, + echartsLabelConfig: EchartsLabelConfig, + echartsConfig: EchartsOptionComp, + echartsTitleVerticalConfig: EchartsTitleVerticalConfig, + echartsTitleConfig:EchartsTitleConfig, + + left:withDefault(NumberControl,trans('chart.defaultLeft')), + right:withDefault(NumberControl,trans('chart.defaultRight')), + top:withDefault(NumberControl,trans('chart.defaultTop')), + bottom:withDefault(NumberControl,trans('chart.defaultBottom')), + + tooltip: withDefault(BoolControl, true), + legendVisibility: withDefault(BoolControl, true), +} +if (EchartDefaultChartStyle && EchartDefaultTextStyle) { + chartJsonModeChildren = { + ...chartJsonModeChildren, + chartStyle: styleControl(EchartDefaultChartStyle, 'chartStyle'), + titleStyle: styleControl(EchartDefaultTextStyle, 'titleStyle'), + xAxisStyle: styleControl(EchartDefaultTextStyle, 'xAxis'), + yAxisStyle: styleControl(EchartDefaultTextStyle, 'yAxisStyle'), + legendStyle: styleControl(EchartDefaultTextStyle, 'legendStyle'), + } +} + +const chartMapModeChildren = { + mapInstance: stateComp(), + getMapInstance: FunctionControl, + mapApiKey: withDefault(StringControl, ''), + mapZoomLevel: withDefault(NumberControl, 3), + mapCenterLng: withDefault(NumberControl, 15.932644), + mapCenterLat: withDefault(NumberControl, 50.942063), + mapOptions: jsonControl(toObject, i18nObjs.defaultMapJsonOption), + onMapEvent: eventHandlerControl(MapEventOptions), + showCharts: withDefault(BoolControl, true), +} + +export type UIChartDataType = { + seriesName: string; + // coordinate chart + x?: any; + y?: any; + // pie or funnel + itemName?: any; + value?: any; +}; + +export type NonUIChartDataType = { + name: string; + value: any; +} + +export const barChartChildrenMap = { + selectedPoints: stateComp>([]), + lastInteractionData: stateComp | NonUIChartDataType>({}), + onEvent: eventHandlerControl([clickEvent] as const), + ...chartUiModeChildren, + ...chartJsonModeChildren, + ...chartMapModeChildren, +}; + +const chartUiChildrenMap = uiChildren(barChartChildrenMap); +export type ChartCompPropsType = RecordConstructorToView; +export type ChartCompChildrenType = RecordConstructorToComp; diff --git a/client/packages/lowcoder-comps/src/comps/barChartComp/barChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartPropertyView.tsx new file mode 100644 index 000000000..5f3d41879 --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartPropertyView.tsx @@ -0,0 +1,150 @@ +import { changeChildAction, CompAction } from "lowcoder-core"; +import { ChartCompChildrenType, ChartTypeOptions,getDataKeys } from "./barChartConstants"; +import { newSeries } from "./seriesComp"; +import { + CustomModal, + Dropdown, + hiddenPropertyView, + Option, + RedButton, + Section, + sectionNames, + controlItem, +} from "lowcoder-sdk"; +import { trans } from "i18n/comps"; + +export function barChartPropertyView( + children: ChartCompChildrenType, + dispatch: (action: CompAction) => void +) { + const series = children.series.getView(); + const columnOptions = getDataKeys(children.data.getView()).map((key) => ({ + label: key, + value: key, + })); + + const uiModePropertyView = ( + <> +
+ {children.chartConfig.getPropertyView()} + { + dispatch(changeChildAction("xAxisKey", value)); + }} + /> + {children.chartConfig.getView().subtype === "waterfall" && children.xAxisData.propertyView({ + label: "X-Label-Data" + })} +
+
+
+ {children.onUIEvent.propertyView({title: trans("chart.chartEventHandlers")})} +
+
+ {children.onEvent.propertyView()} +
+
+
+ {children.echartsTitleConfig.getPropertyView()} + {children.echartsTitleVerticalConfig.getPropertyView()} + {children.legendConfig.getPropertyView()} + {children.title.propertyView({ label: trans("chart.title") })} + {children.left.propertyView({ label: trans("chart.left"), tooltip: trans("echarts.leftTooltip") })} + {children.right.propertyView({ label: trans("chart.right"), tooltip: trans("echarts.rightTooltip") })} + {children.top.propertyView({ label: trans("chart.top"), tooltip: trans("echarts.topTooltip") })} + {children.bottom.propertyView({ label: trans("chart.bottom"), tooltip: trans("echarts.bottomTooltip") })} + {children.chartConfig.children.compType.getView() !== "pie" && ( + <> + {children.xAxisDirection.propertyView({ + label: trans("chart.xAxisDirection"), + radioButton: true, + })} + {children.xConfig.getPropertyView()} + {children.yConfig.getPropertyView()} + + )} + {hiddenPropertyView(children)} + {children.tooltip.propertyView({label: trans("echarts.tooltip"), tooltip: trans("echarts.tooltipTooltip")})} +
+
+ {children.chartStyle?.getPropertyView()} +
+
+ {children.titleStyle?.getPropertyView()} +
+
+ {children.xAxisStyle?.getPropertyView()} +
+
+ {children.yAxisStyle?.getPropertyView()} +
+
+ {children.legendStyle?.getPropertyView()} +
+
+ {children.data.propertyView({ + label: trans("chart.data"), + })} +
+ + ); + + const getChatConfigByMode = (mode: string) => { + switch(mode) { + case "ui": + return uiModePropertyView; + } + } + return ( + <> + {getChatConfigByMode(children.mode.getView())} + + ); +} diff --git a/client/packages/lowcoder-comps/src/comps/barChartComp/barChartUtils.ts b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartUtils.ts new file mode 100644 index 000000000..72abe79f7 --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartUtils.ts @@ -0,0 +1,396 @@ +import { + CharOptionCompType, + ChartCompPropsType, + ChartSize, + noDataAxisConfig, + noDataPieChartConfig, +} from "comps/barChartComp/barChartConstants"; +import { getPieRadiusAndCenter } from "comps/basicChartComp/chartConfigs/pieChartConfig"; +import { EChartsOptionWithMap } from "../basicChartComp/reactEcharts/types"; +import _ from "lodash"; +import { chartColorPalette, isNumeric, JSONObject, loadScript } from "lowcoder-sdk"; +import { calcXYConfig } from "comps/basicChartComp/chartConfigs/cartesianAxisConfig"; +import Big from "big.js"; +import { googleMapsApiUrl } from "../basicChartComp/chartConfigs/chartUrls"; +import opacityToHex from "../../util/opacityToHex"; +import parseBackground from "../../util/gradientBackgroundColor"; +import {ba} from "@fullcalendar/core/internal-common"; +import {chartStyleWrapper, styleWrapper} from "../../util/styleWrapper"; + +export function transformData( + originData: JSONObject[], + xAxis: string, + seriesColumnNames: string[] +) { + // aggregate data by x-axis + const transformedData: JSONObject[] = []; + originData.reduce((prev, cur) => { + if (cur === null || cur === undefined) { + return prev; + } + const groupValue = cur[xAxis] as string; + if (!prev[groupValue]) { + // init as 0 + const initValue: any = {}; + seriesColumnNames.forEach((name) => { + initValue[name] = 0; + }); + prev[groupValue] = initValue; + transformedData.push(prev[groupValue]); + } + // remain the x-axis data + prev[groupValue][xAxis] = groupValue; + seriesColumnNames.forEach((key) => { + if (key === xAxis) { + return; + } else if (isNumeric(cur[key])) { + const bigNum = Big(cur[key]); + prev[groupValue][key] = bigNum.add(prev[groupValue][key]).toNumber(); + } else { + prev[groupValue][key] += 1; + } + }); + return prev; + }, {} as any); + return transformedData; +} + +const notAxisChartSet: Set = new Set(["pie"] as const); +const notAxisChartSubtypeSet: Set = new Set(["polar"] as const); +export const echartsConfigOmitChildren = [ + "hidden", + "selectedPoints", + "onUIEvent", + "mapInstance" +] as const; +type EchartsConfigProps = Omit; + + +export function isAxisChart(type: CharOptionCompType, subtype: string) { + return !notAxisChartSet.has(type) && !notAxisChartSubtypeSet.has(subtype); +} + +export function getSeriesConfig(props: EchartsConfigProps) { + let visibleSeries = props.series.filter((s) => !s.getView().hide); + if(props.chartConfig.subtype === "waterfall") { + const seriesOn = visibleSeries[0]; + const seriesPlaceholder = visibleSeries[0]; + visibleSeries = [seriesPlaceholder, seriesOn]; + } + const seriesLength = visibleSeries.length; + return visibleSeries.map((s, index) => { + if (isAxisChart(props.chartConfig.type, props.chartConfig.subtype)) { + let encodeX: string, encodeY: string; + const horizontalX = props.xAxisDirection === "horizontal"; + let itemStyle = props.chartConfig.itemStyle; + // FIXME: need refactor... chartConfig returns a function with paramters + if (props.chartConfig.type === "bar") { + // barChart's border radius, depend on x-axis direction and stack state + const borderRadius = horizontalX ? [2, 2, 0, 0] : [0, 2, 2, 0]; + if (props.chartConfig.stack && index === visibleSeries.length - 1) { + itemStyle = { ...itemStyle, borderRadius: borderRadius }; + } else if (!props.chartConfig.stack) { + itemStyle = { ...itemStyle, borderRadius: borderRadius }; + } + + if(props.chartConfig.subtype === "waterfall" && index === 0) { + itemStyle = { + borderColor: 'transparent', + color: 'transparent' + } + } + } + if (horizontalX) { + encodeX = props.xAxisKey; + encodeY = s.getView().columnName; + } else { + encodeX = s.getView().columnName; + encodeY = props.xAxisKey; + } + return { + name: props.chartConfig.subtype === "waterfall" && index === 0?" ":s.getView().seriesName, + columnName: props.chartConfig.subtype === "waterfall" && index === 0?" ":s.getView().columnName, + selectedMode: "single", + select: { + itemStyle: { + borderColor: "#000", + }, + }, + encode: { + x: encodeX, + y: encodeY, + }, + // each type of chart's config + ...props.chartConfig, + itemStyle: itemStyle, + label: { + ...props.chartConfig.label, + ...(!horizontalX && { position: "outside" }), + }, + }; + } else { + const radiusAndCenter = getPieRadiusAndCenter(seriesLength, index, props.chartConfig); + return { + ...props.chartConfig, + columnName: s.getView().columnName, + radius: radiusAndCenter.radius, + center: radiusAndCenter.center, + name: s.getView().seriesName, + selectedMode: "single", + encode: { + itemName: props.xAxisKey, + value: s.getView().columnName, + }, + }; + } + }); +} + +// https://echarts.apache.org/en/option.html +export function getEchartsConfig( + props: EchartsConfigProps, + chartSize?: ChartSize, + theme?: any, +): EChartsOptionWithMap { + // axisChart + const axisChart = isAxisChart(props.chartConfig.type, props.chartConfig.subtype); + const gridPos = { + left: `${props?.left}%`, + right: `${props?.right}%`, + bottom: `${props?.bottom}%`, + top: `${props?.top}%`, + }; + let config: any = { + title: { + text: props.title, + top: props.echartsTitleVerticalConfig.top, + left:props.echartsTitleConfig.top, + textStyle: { + ...styleWrapper(props?.titleStyle, theme?.titleStyle) + } + }, + backgroundColor: parseBackground( props?.chartStyle?.background || theme?.chartStyle?.backgroundColor || "#FFFFFF"), + legend: { + ...props.legendConfig, + textStyle: { + ...styleWrapper(props?.legendStyle, theme?.legendStyle, 15) + } + }, + tooltip: props.tooltip && { + trigger: "axis", + axisPointer: { + type: "line", + lineStyle: { + color: "rgba(0,0,0,0.2)", + width: 2, + type: "solid" + } + } + }, + grid: { + ...gridPos, + containLabel: true, + }, + }; + if(props.chartConfig.race) { + config = { + ...config, + // Disable init animation. + animationDuration: 0, + animationDurationUpdate: 2000, + animationEasing: 'linear', + animationEasingUpdate: 'linear', + } + } + if (props.data.length <= 0) { + // no data + return { + ...config, + ...(axisChart ? noDataAxisConfig : noDataPieChartConfig), + }; + } + const yAxisConfig = props.yConfig(); + const seriesColumnNames = props.series + .filter((s) => !s.getView().hide) + .map((s) => s.getView().columnName); + // y-axis is category and time, data doesn't need to aggregate + let transformedData = + yAxisConfig.type === "category" || yAxisConfig.type === "time" ? props.echartsOption.length && props.echartsOption || props.data : transformData(props.echartsOption.length && props.echartsOption || props.data, props.xAxisKey, seriesColumnNames); + + if(props.chartConfig.subtype === "waterfall") { + config.legend = undefined; + let sum = transformedData.reduce((acc, item) => { + if(typeof item[seriesColumnNames[0]] === 'number') return acc + item[seriesColumnNames[0]]; + else return acc; + }, 0) + const total = sum; + transformedData.map(d => { + d[` `] = sum - d[seriesColumnNames[0]]; + sum = d[` `]; + }) + transformedData = [{[" "]: 0, [seriesColumnNames[0]]: total, [props.xAxisKey]: "Total"}, ...transformedData] + } + + if(props.chartConfig.subtype === "polar") { + config = { + ...config, + polar: { + radius: [props.chartConfig.polarData.polarRadiusStart, props.chartConfig.polarData.polarRadiusEnd], + }, + radiusAxis: { + type: props.chartConfig.polarData.polarIsTangent?'category':undefined, + data: props.chartConfig.polarData.polarIsTangent && props.chartConfig.polarData.labelData.length!==0?props.chartConfig.polarData.labelData:undefined, + max: props.chartConfig.polarData.polarIsTangent?undefined:props.chartConfig.polarData.radiusAxisMax || undefined, + }, + angleAxis: { + type: props.chartConfig.polarData.polarIsTangent?undefined:'category', + data: !props.chartConfig.polarData.polarIsTangent && props.chartConfig.polarData.labelData.length!==0?props.chartConfig.polarData.labelData:undefined, + max: props.chartConfig.polarData.polarIsTangent?props.chartConfig.polarData.radiusAxisMax || undefined:undefined, + startAngle: props.chartConfig.polarData.polarStartAngle, + endAngle: props.chartConfig.polarData.polarEndAngle, + }, + } + } + + config = { + ...config, + dataset: [ + { + source: transformedData, + sourceHeader: false, + }, + ], + series: getSeriesConfig(props).map(series => ({ + ...series, + encode: { + ...series.encode, + y: series.name, + }, + itemStyle: { + ...series.itemStyle, + ...chartStyleWrapper(props?.chartStyle, theme?.chartStyle) + }, + lineStyle: { + ...chartStyleWrapper(props?.chartStyle, theme?.chartStyle) + }, + data: transformedData.map((i: any) => i[series.columnName]) + })), + }; + if (axisChart) { + // pure chart's size except the margin around + let chartRealSize; + if (chartSize) { + const rightSize = + typeof gridPos.right === "number" + ? gridPos.right + : (chartSize.w * parseFloat(gridPos.right)) / 100.0; + chartRealSize = { + // actually it's self-adaptive with the x-axis label on the left, not that accurate but work + w: chartSize.w - gridPos.left - rightSize, + // also self-adaptive on the bottom + h: chartSize.h - gridPos.top - gridPos.bottom, + right: rightSize, + }; + } + const finalXyConfig = calcXYConfig( + props.xConfig, + yAxisConfig, + props.xAxisDirection, + transformedData.map((d) => d[props.xAxisKey]), + chartRealSize + ); + config = { + ...config, + // @ts-ignore + xAxis: { + ...finalXyConfig.xConfig, + axisLabel: { + ...styleWrapper(props?.xAxisStyle, theme?.xAxisStyle, 11) + }, + data: finalXyConfig.xConfig.type === "category" && (props.xAxisData as []).length!==0?props?.xAxisData:transformedData.map((i: any) => i[props.xAxisKey]), + }, + // @ts-ignore + yAxis: { + ...finalXyConfig.yConfig, + axisLabel: { + ...styleWrapper(props?.yAxisStyle, theme?.yAxisStyle, 11) + }, + data: finalXyConfig.yConfig.type === "category" && (props.xAxisData as []).length!==0?props?.xAxisData:transformedData.map((i: any) => i[props.xAxisKey]), + }, + }; + + if(props.chartConfig.race) { + config = { + ...config, + xAxis: { + ...config.xAxis, + animationDuration: 300, + animationDurationUpdate: 300 + }, + yAxis: { + ...config.yAxis, + animationDuration: 300, + animationDurationUpdate: 300 + }, + } + } + } + // console.log("Echarts transformedData and config", transformedData, config); + return config; +} + +export function getSelectedPoints(param: any, option: any) { + const series = option.series; + const dataSource = _.isArray(option.dataset) && option.dataset[0]?.source; + if (series && dataSource) { + return param.selected.flatMap((selectInfo: any) => { + const seriesInfo = series[selectInfo.seriesIndex]; + if (!seriesInfo || !seriesInfo.encode) { + return []; + } + return selectInfo.dataIndex.map((index: any) => { + const commonResult = { + seriesName: seriesInfo.name, + }; + if (seriesInfo.encode.itemName && seriesInfo.encode.value) { + return { + ...commonResult, + itemName: dataSource[index][seriesInfo.encode.itemName], + value: dataSource[index][seriesInfo.encode.value], + }; + } else { + return { + ...commonResult, + x: dataSource[index][seriesInfo.encode.x], + y: dataSource[index][seriesInfo.encode.y], + }; + } + }); + }); + } + return []; +} + +export function loadGoogleMapsScript(apiKey: string) { + const mapsUrl = `${googleMapsApiUrl}?key=${apiKey}`; + const scripts = document.getElementsByTagName('script'); + // is script already loaded + let scriptIndex = _.findIndex(scripts, (script) => script.src.endsWith(mapsUrl)); + if(scriptIndex > -1) { + return scripts[scriptIndex]; + } + // is script loaded with diff api_key, remove the script and load again + scriptIndex = _.findIndex(scripts, (script) => script.src.startsWith(googleMapsApiUrl)); + if(scriptIndex > -1) { + scripts[scriptIndex].remove(); + } + + const script = document.createElement("script"); + script.type = "text/javascript"; + script.src = mapsUrl; + script.async = true; + script.defer = true; + window.document.body.appendChild(script); + + return script; +} diff --git a/client/packages/lowcoder-comps/src/comps/barChartComp/seriesComp.tsx b/client/packages/lowcoder-comps/src/comps/barChartComp/seriesComp.tsx new file mode 100644 index 000000000..9ded885b5 --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/barChartComp/seriesComp.tsx @@ -0,0 +1,119 @@ +import { + BoolControl, + StringControl, + list, + JSONObject, + isNumeric, + genRandomKey, + Dropdown, + OptionsType, + MultiCompBuilder, + valueComp, +} from "lowcoder-sdk"; +import { trans } from "i18n/comps"; + +import { ConstructorToComp, ConstructorToDataType, ConstructorToView } from "lowcoder-core"; +import { CompAction, CustomAction, customAction, isMyCustomAction } from "lowcoder-core"; + +export type SeriesCompType = ConstructorToComp; +export type RawSeriesCompType = ConstructorToView; +type SeriesDataType = ConstructorToDataType; + +type ActionDataType = { + type: "chartDataChanged"; + chartData: Array; +}; + +export function newSeries(name: string, columnName: string): SeriesDataType { + return { + seriesName: name, + columnName: columnName, + dataIndex: genRandomKey(), + }; +} + +const seriesChildrenMap = { + columnName: StringControl, + seriesName: StringControl, + hide: BoolControl, + // unique key, for sort + dataIndex: valueComp(""), +}; + +const SeriesTmpComp = new MultiCompBuilder(seriesChildrenMap, (props) => { + return props; +}) + .setPropertyViewFn(() => { + return <>; + }) + .build(); + +class SeriesComp extends SeriesTmpComp { + getPropertyViewWithData(columnOptions: OptionsType): React.ReactNode { + return ( + <> + {this.children.seriesName.propertyView({ + label: trans("chart.seriesName"), + })} + { + this.children.columnName.dispatchChangeValueAction(value); + }} + /> + + ); + } +} + +const SeriesListTmpComp = list(SeriesComp); + +export class SeriesListComp extends SeriesListTmpComp { + override reduce(action: CompAction): this { + if (isMyCustomAction(action, "chartDataChanged")) { + // auto generate series + const actions = this.genExampleSeriesActions(action.value.chartData); + return this.reduce(this.multiAction(actions)); + } + return super.reduce(action); + } + + private genExampleSeriesActions(chartData: Array) { + const actions: CustomAction[] = []; + if (!chartData || chartData.length <= 0 || !chartData[0]) { + return actions; + } + let delCnt = 0; + const existColumns = this.getView().map((s) => s.getView().columnName); + // delete series not in data + existColumns.forEach((columnName) => { + if (chartData[0]?.[columnName] === undefined) { + actions.push(this.deleteAction(0)); + delCnt++; + } + }); + if (existColumns.length > delCnt) { + // don't generate example if exists + return actions; + } + // generate example series + const exampleKeys = Object.keys(chartData[0]) + .filter((key) => { + return !existColumns.includes(key) && isNumeric(chartData[0][key]); + }) + .slice(0, 3); + exampleKeys.forEach((key) => actions.push(this.pushAction(newSeries(key, key)))); + return actions; + } + + dispatchDataChanged(chartData: Array): void { + this.dispatch( + customAction({ + type: "chartDataChanged", + chartData: chartData, + }) + ); + } +} diff --git a/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/barChartConfig.tsx b/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/barChartConfig.tsx index 6c91fe252..ee1188335 100644 --- a/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/barChartConfig.tsx +++ b/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/barChartConfig.tsx @@ -1,11 +1,19 @@ import { BoolControl, + NumberControl, + StringControl, + withDefault, dropdownControl, MultiCompBuilder, showLabelPropertyView, + ColorControl, + Dropdown, + toArray, + jsonControl, } from "lowcoder-sdk"; +import { changeChildAction, CompAction } from "lowcoder-core"; import { BarSeriesOption } from "echarts"; -import { trans } from "i18n/comps"; +import { i18nObjs, trans } from "i18n/comps"; const BarTypeOptions = [ { @@ -13,8 +21,12 @@ const BarTypeOptions = [ value: "basicBar", }, { - label: trans("chart.stackedBar"), - value: "stackedBar", + label: trans("chart.waterfallBar"), + value: "waterfall", + }, + { + label: trans("chart.polar"), + value: "polar", }, ] as const; @@ -23,27 +35,105 @@ export const BarChartConfig = (function () { { showLabel: BoolControl, type: dropdownControl(BarTypeOptions, "basicBar"), + barWidth: withDefault(NumberControl, i18nObjs.defaultBarChartOption.barWidth), + showBackground: BoolControl, + backgroundColor: withDefault(ColorControl, i18nObjs.defaultBarChartOption.barBg), + radiusAxisMax: NumberControl, + polarRadiusStart: withDefault(StringControl, '30'), + polarRadiusEnd: withDefault(StringControl, '80%'), + polarStartAngle: withDefault(NumberControl, 90), + polarEndAngle: withDefault(NumberControl, -180), + polarIsTangent: withDefault(BoolControl, false), + stack: withDefault(BoolControl, false), + race: withDefault(BoolControl, false), + labelData: jsonControl(toArray, []), }, (props): BarSeriesOption => { const config: BarSeriesOption = { type: "bar", + subtype: props.type, + realtimeSort: props.race, + seriesLayoutBy: props.race?'column':undefined, label: { show: props.showLabel, position: "top", + valueAnimation: props.race, + }, + barWidth: `${props.barWidth}%`, + showBackground: props.showBackground, + backgroundStyle: { + color: props.backgroundColor, }, + polarData: { + radiusAxisMax: props.radiusAxisMax, + polarRadiusStart: props.polarRadiusStart, + polarRadiusEnd: props.polarRadiusEnd, + polarStartAngle: props.polarStartAngle, + polarEndAngle: props.polarEndAngle, + labelData: props.labelData, + polarIsTangent: props.polarIsTangent, + }, + race: props.race, }; - if (props.type === "stackedBar") { + if (props.stack) { config.stack = "stackValue"; } + if (props.type === "waterfall") { + config.label = undefined; + config.stack = "stackValue"; + } + if (props.type === "polar") { + config.coordinateSystem = 'polar'; + } return config; } ) - .setPropertyViewFn((children) => ( + .setPropertyViewFn((children, dispatch: (action: CompAction) => void) => ( <> + { + dispatch(changeChildAction("type", value)); + }} + /> {showLabelPropertyView(children)} - {children.type.propertyView({ - label: trans("chart.barType"), - radioButton: true, + {children.barWidth.propertyView({ + label: trans("barChart.barWidth"), + })} + {children.type.getView() !== "waterfall" && children.race.propertyView({ + label: trans("barChart.race"), + })} + {children.type.getView() !== "waterfall" && children.stack.propertyView({ + label: trans("barChart.stack"), + })} + {children.showBackground.propertyView({ + label: trans("barChart.showBg"), + })} + {children.showBackground.getView() && children.backgroundColor.propertyView({ + label: trans("barChart.bgColor"), + })} + {children.type.getView() === "polar" && children.polarIsTangent.propertyView({ + label: trans("barChart.polarIsTangent"), + })} + {children.type.getView() === "polar" && children.polarStartAngle.propertyView({ + label: trans("barChart.polarStartAngle"), + })} + {children.type.getView() === "polar" && children.polarEndAngle.propertyView({ + label: trans("barChart.polarEndAngle"), + })} + {children.type.getView() === "polar" && children.radiusAxisMax.propertyView({ + label: trans("barChart.radiusAxisMax"), + })} + {children.type.getView() === "polar" && children.polarRadiusStart.propertyView({ + label: trans("barChart.polarRadiusStart"), + })} + {children.type.getView() === "polar" && children.polarRadiusEnd.propertyView({ + label: trans("barChart.polarRadiusEnd"), + })} + {children.type.getView() === "polar" && children.labelData.propertyView({ + label: trans("barChart.polarLabelData"), })} )) diff --git a/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/lineChartConfig.tsx b/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/lineChartConfig.tsx index 266e5fbf7..1b88d4a06 100644 --- a/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/lineChartConfig.tsx +++ b/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/lineChartConfig.tsx @@ -3,28 +3,18 @@ import { MultiCompBuilder, BoolControl, dropdownControl, + jsonControl, + toArray, showLabelPropertyView, withContext, + ColorControl, StringControl, + NumberControl, + withDefault, ColorOrBoolCodeControl, } from "lowcoder-sdk"; import { trans } from "i18n/comps"; -const BarTypeOptions = [ - { - label: trans("chart.basicLine"), - value: "basicLine", - }, - { - label: trans("chart.stackedLine"), - value: "stackedLine", - }, - { - label: trans("chart.areaLine"), - value: "areaLine", - }, -] as const; - export const ItemColorComp = withContext( new MultiCompBuilder({ value: ColorOrBoolCodeControl }, (props) => props.value) .setPropertyViewFn((children) => @@ -38,13 +28,83 @@ export const ItemColorComp = withContext( ["seriesName", "value"] as const ); +export const SymbolOptions = [ + { + label: trans("chart.rect"), + value: "rect", + }, + { + label: trans("chart.circle"), + value: "circle", + }, + { + label: trans("chart.roundRect"), + value: "roundRect", + }, + { + label: trans("chart.triangle"), + value: "triangle", + }, + { + label: trans("chart.diamond"), + value: "diamond", + }, + { + label: trans("chart.pin"), + value: "pin", + }, + { + label: trans("chart.arrow"), + value: "arrow", + }, + { + label: trans("chart.none"), + value: "none", + }, + { + label: trans("chart.emptyCircle"), + value: "emptyCircle", + }, +] as const; + +export const BorderTypeOptions = [ + { + label: trans("lineChart.solid"), + value: "solid", + }, + { + label: trans("lineChart.dashed"), + value: "dashed", + }, + { + label: trans("lineChart.dotted"), + value: "dotted", + }, +] as const; + export const LineChartConfig = (function () { return new MultiCompBuilder( { showLabel: BoolControl, - type: dropdownControl(BarTypeOptions, "basicLine"), + showEndLabel: BoolControl, + stacked: BoolControl, + area: BoolControl, smooth: BoolControl, + polar: BoolControl, itemColor: ItemColorComp, + symbol: dropdownControl(SymbolOptions, "emptyCircle"), + symbolSize: withDefault(NumberControl, 4), + radiusAxisMax: NumberControl, + polarRadiusStart: withDefault(StringControl, '30'), + polarRadiusEnd: withDefault(StringControl, '80%'), + polarStartAngle: withDefault(NumberControl, 90), + polarEndAngle: withDefault(NumberControl, -180), + polarIsTangent: withDefault(BoolControl, false), + labelData: jsonControl(toArray, []), + //series-line.itemStyle + borderColor: ColorControl, + borderWidth: NumberControl, + borderType: dropdownControl(BorderTypeOptions, 'solid'), }, (props): LineSeriesOption => { const config: LineSeriesOption = { @@ -52,15 +112,13 @@ export const LineChartConfig = (function () { label: { show: props.showLabel, }, + symbol: props.symbol, + symbolSize: props.symbolSize, itemStyle: { color: (params) => { - if (!params.encode || !params.dimensionNames) { - return params.color; - } - const dataKey = params.dimensionNames[params.encode["y"][0]]; const color = (props.itemColor as any)({ seriesName: params.seriesName, - value: (params.data as any)[dataKey], + value: params.data, }); if (color === "true") { return "red"; @@ -69,27 +127,96 @@ export const LineChartConfig = (function () { } return color; }, + borderColor: props.borderColor, + borderWidth: props.borderWidth, + borderType: props.borderType, + }, + polarData: { + polar: props.polar, + radiusAxisMax: props.radiusAxisMax, + polarRadiusStart: props.polarRadiusStart, + polarRadiusEnd: props.polarRadiusEnd, + polarStartAngle: props.polarStartAngle, + polarEndAngle: props.polarEndAngle, + labelData: props.labelData, + polarIsTangent: props.polarIsTangent, }, }; - if (props.type === "stackedLine") { + if (props.stacked) { config.stack = "stackValue"; - } else if (props.type === "areaLine") { + } + if (props.area) { config.areaStyle = {}; } if (props.smooth) { config.smooth = true; } + if (props.showEndLabel) { + config.endLabel = { + show: true, + formatter: '{a}', + distance: 20 + } + } + if (props.polar) { + config.coordinateSystem = 'polar'; + } return config; } ) .setPropertyViewFn((children) => ( <> - {children.type.propertyView({ - label: trans("chart.lineType"), + {children.stacked.propertyView({ + label: trans("lineChart.stacked"), + })} + {children.area.propertyView({ + label: trans("lineChart.area"), + })} + {children.polar.propertyView({ + label: trans("lineChart.polar"), + })} + {children.polar.getView() && children.polarIsTangent.propertyView({ + label: trans("barChart.polarIsTangent"), + })} + {children.polar.getView() && children.polarStartAngle.propertyView({ + label: trans("barChart.polarStartAngle"), + })} + {children.polar.getView() && children.polarEndAngle.propertyView({ + label: trans("barChart.polarEndAngle"), + })} + {children.polar.getView() && children.radiusAxisMax.propertyView({ + label: trans("barChart.radiusAxisMax"), + })} + {children.polar.getView() && children.polarRadiusStart.propertyView({ + label: trans("barChart.polarRadiusStart"), + })} + {children.polar.getView() && children.polarRadiusEnd.propertyView({ + label: trans("barChart.polarRadiusEnd"), + })} + {children.polar.getView() && children.labelData.propertyView({ + label: trans("barChart.polarLabelData"), })} {showLabelPropertyView(children)} + {children.showEndLabel.propertyView({ + label: trans("lineChart.showEndLabel"), + })} {children.smooth.propertyView({ label: trans("chart.smooth") })} + {children.symbol.propertyView({ + label: trans("lineChart.symbol"), + })} + {children.symbolSize.propertyView({ + label: trans("lineChart.symbolSize"), + })} {children.itemColor.getPropertyView()} + {children.borderColor.propertyView({ + label: trans("lineChart.borderColor"), + })} + {children.borderWidth.propertyView({ + label: trans("lineChart.borderWidth"), + })} + {children.borderType.propertyView({ + label: trans("lineChart.borderType"), + })} )) .build(); diff --git a/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/pieChartConfig.tsx b/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/pieChartConfig.tsx index 0861fb6ba..e8781d5c3 100644 --- a/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/pieChartConfig.tsx +++ b/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/pieChartConfig.tsx @@ -1,6 +1,11 @@ import { MultiCompBuilder } from "lowcoder-sdk"; import { PieSeriesOption } from "echarts"; -import { dropdownControl } from "lowcoder-sdk"; +import { + dropdownControl, + NumberControl, + StringControl, + withDefault, + } from "lowcoder-sdk"; import { ConstructorToView } from "lowcoder-core"; import { trans } from "i18n/comps"; @@ -17,6 +22,14 @@ const BarTypeOptions = [ label: trans("chart.rosePie"), value: "rosePie", }, + { + label: trans("chart.calendarPie"), + value: "calendarPie", + }, + { + label: trans("chart.geoPie"), + value: "geoPie", + }, ] as const; // radius percent for each pie chart when one line has [1, 2, 3] pie charts @@ -28,20 +41,37 @@ export const PieChartConfig = (function () { return new MultiCompBuilder( { type: dropdownControl(BarTypeOptions, "basicPie"), + cellSize: withDefault(NumberControl, 40), + range: withDefault(StringControl, "2021-09"), + mapUrl: withDefault(StringControl, "https://echarts.apache.org/examples/data/asset/geo/USA.json"), }, (props): PieSeriesOption => { const config: PieSeriesOption = { type: "pie", + subtype: props.type, label: { show: true, formatter: "{d}%", }, + range: props.range, }; if (props.type === "rosePie") { config.roseType = "area"; - } else if (props.type === "doughnutPie") { + } + if (props.type === "doughnutPie") { config.radius = ["40%", "60%"]; } + if (props.type === "calendarPie") { + config.coordinateSystem = 'calendar'; + config.cellSize = [props.cellSize, props.cellSize]; + config.label = { + ...config.label, + position: 'inside' + }; + } + if (props.type === "geoPie") { + config.mapUrl = props.mapUrl; + } return config; } ) @@ -50,6 +80,15 @@ export const PieChartConfig = (function () { {children.type.propertyView({ label: trans("chart.pieType"), })} + {children.type.getView() === "calendarPie" && children.cellSize.propertyView({ + label: trans("lineChart.cellSize"), + })} + {children.type.getView() === "calendarPie" && children.range.propertyView({ + label: trans("lineChart.range"), + })} + {children.type.getView() === "geoPie" && children.mapUrl.propertyView({ + label: trans("pieChart.mapUrl"), + })} )) .build(); diff --git a/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/scatterChartConfig.tsx b/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/scatterChartConfig.tsx index edb339bdb..34b5f2cb6 100644 --- a/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/scatterChartConfig.tsx +++ b/client/packages/lowcoder-comps/src/comps/basicChartComp/chartConfigs/scatterChartConfig.tsx @@ -2,6 +2,10 @@ import { MultiCompBuilder, dropdownControl, BoolControl, + StringControl, + NumberControl, + ColorControl, + withDefault, showLabelPropertyView, } from "lowcoder-sdk"; import { ScatterSeriesOption } from "echarts"; @@ -38,7 +42,19 @@ export const ScatterChartConfig = (function () { return new MultiCompBuilder( { showLabel: BoolControl, + labelIndex: withDefault(NumberControl, 2), shape: dropdownControl(ScatterShapeOptions, "circle"), + singleAxis: BoolControl, + boundaryGap: withDefault(BoolControl, true), + visualMap: BoolControl, + visualMapMin: NumberControl, + visualMapMax: NumberControl, + visualMapDimension: NumberControl, + visualMapColorMin: ColorControl, + visualMapColorMax: ColorControl, + polar: BoolControl, + heatmap: BoolControl, + heatmapMonth: withDefault(StringControl, "2021-09"), }, (props): ScatterSeriesOption => { return { @@ -46,16 +62,82 @@ export const ScatterChartConfig = (function () { symbol: props.shape, label: { show: props.showLabel, + position: 'right', + formatter: function (param) { + return param.data[props.labelIndex]; + }, }, + labelLayout: function () { + return { + x: '88%', + moveOverlap: 'shiftY' + }; + }, + labelLine: { + show: true, + length2: 5, + lineStyle: { + color: '#bbb' + } + }, + singleAxis: props.singleAxis, + boundaryGap: props.boundaryGap, + visualMapData: { + visualMap: props.visualMap, + visualMapMin: props.visualMapMin, + visualMapMax: props.visualMapMax, + visualMapDimension: props.visualMapDimension, + visualMapColorMin: props.visualMapColorMin, + visualMapColorMax: props.visualMapColorMax, + }, + polar: props.polar, + heatmap: props.heatmap, + heatmapMonth: props.heatmapMonth, }; } ) .setPropertyViewFn((children) => ( <> {showLabelPropertyView(children)} + {children.showLabel.getView() && children.labelIndex.propertyView({ + label: trans("scatterChart.labelIndex"), + })} + {children.boundaryGap.propertyView({ + label: trans("scatterChart.boundaryGap"), + })} {children.shape.propertyView({ label: trans("chart.scatterShape"), })} + {children.singleAxis.propertyView({ + label: trans("scatterChart.singleAxis"), + })} + {children.visualMap.propertyView({ + label: trans("scatterChart.visualMap"), + })} + {children.visualMap.getView() && children.visualMapMin.propertyView({ + label: trans("scatterChart.visualMapMin"), + })} + {children.visualMap.getView() && children.visualMapMax.propertyView({ + label: trans("scatterChart.visualMapMax"), + })} + {children.visualMap.getView() && children.visualMapDimension.propertyView({ + label: trans("scatterChart.visualMapDimension"), + })} + {children.visualMap.getView() && children.visualMapColorMin.propertyView({ + label: trans("scatterChart.visualMapColorMin"), + })} + {children.visualMap.getView() && children.visualMapColorMax.propertyView({ + label: trans("scatterChart.visualMapColorMax"), + })} + {children.visualMap.getView() && children.heatmap.propertyView({ + label: trans("scatterChart.heatmap"), + })} + {children.visualMap.getView() && children.heatmapMonth.propertyView({ + label: trans("scatterChart.heatmapMonth"), + })} + {children.polar.propertyView({ + label: trans("scatterChart.polar"), + })} )) .build(); diff --git a/client/packages/lowcoder-comps/src/comps/basicChartComp/chartUtils.ts b/client/packages/lowcoder-comps/src/comps/basicChartComp/chartUtils.ts index 402011e6c..6c5020690 100644 --- a/client/packages/lowcoder-comps/src/comps/basicChartComp/chartUtils.ts +++ b/client/packages/lowcoder-comps/src/comps/basicChartComp/chartUtils.ts @@ -276,7 +276,7 @@ export function getEchartsConfig( }, }; } - // log.log("Echarts transformedData and config", transformedData, config); + // console.log("Echarts transformedData and config", transformedData, config); return config; } diff --git a/client/packages/lowcoder-comps/src/comps/basicChartComp/reactEcharts/index.ts b/client/packages/lowcoder-comps/src/comps/basicChartComp/reactEcharts/index.ts index dcb57f0f9..da1f165a1 100644 --- a/client/packages/lowcoder-comps/src/comps/basicChartComp/reactEcharts/index.ts +++ b/client/packages/lowcoder-comps/src/comps/basicChartComp/reactEcharts/index.ts @@ -1,4 +1,5 @@ import * as echarts from "echarts"; +import "echarts-gl"; import "echarts-wordcloud"; import { EChartsReactProps, EChartsInstance, EChartsOptionWithMap } from "./types"; import EChartsReactCore from "./core"; diff --git a/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartComp.tsx b/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartComp.tsx new file mode 100644 index 000000000..8cd1910b1 --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartComp.tsx @@ -0,0 +1,282 @@ +import { + changeChildAction, + changeValueAction, + CompAction, + CompActionTypes, + wrapChildAction, +} from "lowcoder-core"; +import { AxisFormatterComp, EchartsAxisType } from "../basicChartComp/chartConfigs/cartesianAxisConfig"; +import { boxplotChartChildrenMap, ChartSize, getDataKeys } from "./boxplotChartConstants"; +import { boxplotChartPropertyView } from "./boxplotChartPropertyView"; +import _ from "lodash"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; +import ReactResizeDetector from "react-resize-detector"; +import ReactECharts from "../basicChartComp/reactEcharts"; +import * as echarts from "echarts"; +import { + childrenToProps, + depsConfig, + genRandomKey, + NameConfig, + UICompBuilder, + withDefault, + withExposingConfigs, + withViewFn, + ThemeContext, + chartColorPalette, + getPromiseAfterDispatch, + dropdownControl, +} from "lowcoder-sdk"; +import { getEchartsLocale, i18nObjs, trans } from "i18n/comps"; +import { + echartsConfigOmitChildren, + getEchartsConfig, + getSelectedPoints, +} from "./boxplotChartUtils"; +import 'echarts-extension-gmap'; +import log from "loglevel"; + +let clickEventCallback = () => {}; + +const chartModeOptions = [ + { + label: "UI", + value: "ui", + } +] as const; + +let BoxplotChartTmpComp = (function () { + return new UICompBuilder({mode:dropdownControl(chartModeOptions,'ui'),...boxplotChartChildrenMap}, () => null) + .setPropertyViewFn(boxplotChartPropertyView) + .build(); +})(); + +BoxplotChartTmpComp = withViewFn(BoxplotChartTmpComp, (comp) => { + const mode = comp.children.mode.getView(); + const onUIEvent = comp.children.onUIEvent.getView(); + const onEvent = comp.children.onEvent.getView(); + const echartsCompRef = useRef(); + const [chartSize, setChartSize] = useState(); + const firstResize = useRef(true); + const theme = useContext(ThemeContext); + const defaultChartTheme = { + color: chartColorPalette, + backgroundColor: "#fff", + }; + + let themeConfig = defaultChartTheme; + try { + themeConfig = theme?.theme.chart ? JSON.parse(theme?.theme.chart) : defaultChartTheme; + } catch (error) { + log.error('theme chart error: ', error); + } + + const triggerClickEvent = async (dispatch: any, action: CompAction) => { + await getPromiseAfterDispatch( + dispatch, + action, + { autoHandleAfterReduce: true } + ); + onEvent('click'); + } + + useEffect(() => { + const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance(); + if (!echartsCompInstance) { + return _.noop; + } + echartsCompInstance?.on("click", (param: any) => { + document.dispatchEvent(new CustomEvent("clickEvent", { + bubbles: true, + detail: { + action: 'click', + data: param.data, + } + })); + triggerClickEvent( + comp.dispatch, + changeChildAction("lastInteractionData", param.data, false) + ); + }); + return () => { + echartsCompInstance?.off("click"); + document.removeEventListener('clickEvent', clickEventCallback) + }; + }, []); + + useEffect(() => { + // bind events + const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance(); + if (!echartsCompInstance) { + return _.noop; + } + echartsCompInstance?.on("selectchanged", (param: any) => { + const option: any = echartsCompInstance?.getOption(); + document.dispatchEvent(new CustomEvent("clickEvent", { + bubbles: true, + detail: { + action: param.fromAction, + data: getSelectedPoints(param, option) + } + })); + + if (param.fromAction === "select") { + comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false)); + onUIEvent("select"); + } else if (param.fromAction === "unselect") { + comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false)); + onUIEvent("unselect"); + } + + triggerClickEvent( + comp.dispatch, + changeChildAction("lastInteractionData", getSelectedPoints(param, option), false) + ); + }); + // unbind + return () => { + echartsCompInstance?.off("selectchanged"); + document.removeEventListener('clickEvent', clickEventCallback) + }; + }, [onUIEvent]); + + const echartsConfigChildren = _.omit(comp.children, echartsConfigOmitChildren); + const childrenProps = childrenToProps(echartsConfigChildren); + + const option = useMemo(() => { + return getEchartsConfig( + childrenProps as ToViewReturn, + chartSize, + themeConfig + ); + }, [theme, childrenProps, chartSize, ...Object.values(echartsConfigChildren)]); + + return ( + { + if (w && h) { + setChartSize({ w: w, h: h }); + } + if (!firstResize.current) { + // ignore the first resize, which will impact the loading animation + echartsCompRef.current?.getEchartsInstance().resize(); + } else { + firstResize.current = false; + } + }} + > + (echartsCompRef.current = e)} + style={{ height: "100%" }} + notMerge + lazyUpdate + opts={{ locale: getEchartsLocale() }} + option={option} + mode={mode} + /> + + ); +}); + +function getYAxisFormatContextValue( + data: Array, + yAxisType: EchartsAxisType, + yAxisName?: string +) { + const dataSample = yAxisName && data.length > 0 && data[0][yAxisName]; + let contextValue = dataSample; + if (yAxisType === "time") { + // to timestamp + const time = + typeof dataSample === "number" || typeof dataSample === "string" + ? new Date(dataSample).getTime() + : null; + if (time) contextValue = time; + } + return contextValue; +} + +BoxplotChartTmpComp = class extends BoxplotChartTmpComp { + private lastYAxisFormatContextVal?: JSONValue; + private lastColorContext?: JSONObject; + + updateContext(comp: this) { + // the context value of axis format + let resultComp = comp; + const data = comp.children.data.getView(); + const yAxisContextValue = getYAxisFormatContextValue( + data, + comp.children.yConfig.children.yAxisType.getView(), + ); + if (yAxisContextValue !== comp.lastYAxisFormatContextVal) { + comp.lastYAxisFormatContextVal = yAxisContextValue; + resultComp = comp.setChild( + "yConfig", + comp.children.yConfig.reduce( + wrapChildAction( + "formatter", + AxisFormatterComp.changeContextDataAction({ value: yAxisContextValue }) + ) + ) + ); + } + return resultComp; + } + + override reduce(action: CompAction): this { + const comp = super.reduce(action); + if (action.type === CompActionTypes.UPDATE_NODES_V2) { + const newData = comp.children.data.getView(); + // data changes + if (comp.children.data !== this.children.data) { + setTimeout(() => { + // update x-axis value + const keys = getDataKeys(newData); + if (keys.length > 0 && !keys.includes(comp.children.xAxisKey.getView())) { + comp.children.xAxisKey.dispatch(changeValueAction(keys[0] || "")); + } + if (keys.length > 0 && !keys.includes(comp.children.yAxisKey.getView())) { + comp.children.yAxisKey.dispatch(changeValueAction(keys[1] || "")); + } + }, 0); + } + return this.updateContext(comp); + } + return comp; + } + + override autoHeight(): boolean { + return false; + } +}; + +let BoxplotChartComp = withExposingConfigs(BoxplotChartTmpComp, [ + depsConfig({ + name: "selectedPoints", + desc: trans("chart.selectedPointsDesc"), + depKeys: ["selectedPoints"], + func: (input) => { + return input.selectedPoints; + }, + }), + depsConfig({ + name: "lastInteractionData", + desc: trans("chart.lastInteractionDataDesc"), + depKeys: ["lastInteractionData"], + func: (input) => { + return input.lastInteractionData; + }, + }), + depsConfig({ + name: "data", + desc: trans("chart.dataDesc"), + depKeys: ["data", "mode"], + func: (input) =>[] , + }), + new NameConfig("title", trans("chart.titleDesc")), +]); + + +export const BoxplotChartCompWithDefault = withDefault(BoxplotChartComp, { + xAxisKey: "date", +}); diff --git a/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartConstants.tsx b/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartConstants.tsx new file mode 100644 index 000000000..ffec6b31e --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartConstants.tsx @@ -0,0 +1,248 @@ +import { + jsonControl, + stateComp, + toJSONObjectArray, + toObject, + BoolControl, + ColorControl, + withDefault, + StringControl, + NumberControl, + dropdownControl, + list, + eventHandlerControl, + valueComp, + withType, + uiChildren, + clickEvent, + toArray, + styleControl, + EchartDefaultTextStyle, + EchartDefaultChartStyle, + MultiCompBuilder, +} from "lowcoder-sdk"; +import { RecordConstructorToComp, RecordConstructorToView } from "lowcoder-core"; +import { XAxisConfig, YAxisConfig } from "../basicChartComp/chartConfigs/cartesianAxisConfig"; +import { LegendConfig } from "../basicChartComp/chartConfigs/legendConfig"; +import { EchartsLegendConfig } from "../basicChartComp/chartConfigs/echartsLegendConfig"; +import { EchartsLabelConfig } from "../basicChartComp/chartConfigs/echartsLabelConfig"; +import { EChartsOption } from "echarts"; +import { i18nObjs, trans } from "i18n/comps"; +import {EchartsTitleVerticalConfig} from "../chartComp/chartConfigs/echartsTitleVerticalConfig"; +import {EchartsTitleConfig} from "../basicChartComp/chartConfigs/echartsTitleConfig"; + +export const UIEventOptions = [ + { + label: trans("chart.select"), + value: "select", + description: trans("chart.selectDesc"), + }, + { + label: trans("chart.unSelect"), + value: "unselect", + description: trans("chart.unselectDesc"), + }, +] as const; + +export const XAxisDirectionOptions = [ + { + label: trans("chart.horizontal"), + value: "horizontal", + }, + { + label: trans("chart.vertical"), + value: "vertical", + }, +] as const; + +export type XAxisDirectionType = ValueFromOption; + +export const noDataAxisConfig = { + animation: false, + xAxis: { + type: "category", + name: trans("chart.noData"), + nameLocation: "middle", + data: [], + axisLine: { + lineStyle: { + color: "#8B8FA3", + }, + }, + }, + yAxis: { + type: "value", + axisLabel: { + color: "#8B8FA3", + }, + splitLine: { + lineStyle: { + color: "#F0F0F0", + }, + }, + }, + tooltip: { + show: false, + }, + series: [ + { + data: [700], + type: "line", + itemStyle: { + opacity: 0, + }, + }, + ], +} as EChartsOption; + +export const noDataBoxplotChartConfig = { + animation: false, + tooltip: { + show: false, + }, + legend: { + formatter: trans("chart.unknown"), + top: "bottom", + selectedMode: false, + }, + color: ["#B8BBCC", "#CED0D9", "#DCDEE6", "#E6E6EB"], + series: [ + { + type: "boxplot", + radius: "35%", + center: ["25%", "50%"], + silent: true, + label: { + show: false, + }, + data: [ + { + name: "1", + value: 70, + }, + { + name: "2", + value: 68, + }, + { + name: "3", + value: 48, + }, + { + name: "4", + value: 40, + }, + ], + }, + { + type: "boxplot", + radius: "35%", + center: ["75%", "50%"], + silent: true, + label: { + show: false, + }, + data: [ + { + name: "1", + value: 70, + }, + { + name: "2", + value: 68, + }, + { + name: "3", + value: 48, + }, + { + name: "4", + value: 40, + }, + ], + }, + ], +} as EChartsOption; + +export type ChartSize = { w: number; h: number }; + +export const getDataKeys = (data: Array) => { + if (!data) { + return []; + } + const dataKeys: Array = []; + data[0].forEach((key) => { + if (!dataKeys.includes(key)) { + dataKeys.push(key); + } + }); + return dataKeys; +}; + +export const chartUiModeChildren = { + title: withDefault(StringControl, trans("echarts.defaultTitle")), + data: jsonControl(toArray, i18nObjs.defaultDatasourceBoxplot), + xAxisKey: valueComp(""), // x-axis, key from data + xAxisDirection: dropdownControl(XAxisDirectionOptions, "horizontal"), + xAxisData: jsonControl(toArray, []), + yAxisKey: valueComp(""), // x-axis, key from data + xConfig: XAxisConfig, + yConfig: YAxisConfig, + legendConfig: LegendConfig, + onUIEvent: eventHandlerControl(UIEventOptions), +}; + +let chartJsonModeChildren: any = { + echartsOption: jsonControl(toObject, i18nObjs.defaultEchartsJsonOption), + echartsTitle: withDefault(StringControl, trans("echarts.defaultTitle")), + echartsLegendConfig: EchartsLegendConfig, + echartsLabelConfig: EchartsLabelConfig, + echartsTitleVerticalConfig: EchartsTitleVerticalConfig, + echartsTitleConfig:EchartsTitleConfig, + + left:withDefault(NumberControl,trans('chart.defaultLeft')), + right:withDefault(NumberControl,trans('chart.defaultRight')), + top:withDefault(NumberControl,trans('chart.defaultTop')), + bottom:withDefault(NumberControl,trans('chart.defaultBottom')), + + tooltip: withDefault(BoolControl, true), + legendVisibility: withDefault(BoolControl, true), +} + +if (EchartDefaultChartStyle && EchartDefaultTextStyle) { + chartJsonModeChildren = { + ...chartJsonModeChildren, + chartStyle: styleControl(EchartDefaultChartStyle, 'chartStyle'), + titleStyle: styleControl(EchartDefaultTextStyle, 'titleStyle'), + xAxisStyle: styleControl(EchartDefaultTextStyle, 'xAxis'), + yAxisStyle: styleControl(EchartDefaultTextStyle, 'yAxisStyle'), + legendStyle: styleControl(EchartDefaultTextStyle, 'legendStyle'), + } +} + +export type UIChartDataType = { + seriesName: string; + // coordinate chart + x?: any; + y?: any; + // boxplot or funnel + itemName?: any; + value?: any; +}; + +export type NonUIChartDataType = { + name: string; + value: any; +} + +export const boxplotChartChildrenMap = { + selectedPoints: stateComp>([]), + lastInteractionData: stateComp | NonUIChartDataType>({}), + onEvent: eventHandlerControl([clickEvent] as const), + ...chartUiModeChildren, + ...chartJsonModeChildren, +}; + +const chartUiChildrenMap = uiChildren(boxplotChartChildrenMap); +export type ChartCompPropsType = RecordConstructorToView; +export type ChartCompChildrenType = RecordConstructorToComp; diff --git a/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartPropertyView.tsx new file mode 100644 index 000000000..b6694e910 --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartPropertyView.tsx @@ -0,0 +1,95 @@ +import { changeChildAction, CompAction } from "lowcoder-core"; +import { ChartCompChildrenType, getDataKeys } from "./boxplotChartConstants"; +import { + CustomModal, + Dropdown, + hiddenPropertyView, + Option, + RedButton, + Section, + sectionNames, + controlItem, +} from "lowcoder-sdk"; +import { trans } from "i18n/comps"; + +export function boxplotChartPropertyView( + children: ChartCompChildrenType, + dispatch: (action: CompAction) => void +) { + const columnOptions = getDataKeys(children.data.getView()).map((key) => ({ + label: key, + value: key, + })); + + const uiModePropertyView = ( + <> +
+ { + dispatch(changeChildAction("xAxisKey", value)); + }} + /> + { + dispatch(changeChildAction("yAxisKey", value)); + }} + /> +
+
+
+ {children.onUIEvent.propertyView({title: trans("chart.chartEventHandlers")})} +
+
+ {children.onEvent.propertyView()} +
+
+
+ {children.echartsTitleConfig.getPropertyView()} + {children.echartsTitleVerticalConfig.getPropertyView()} + {children.legendConfig.getPropertyView()} + {children.title.propertyView({ label: trans("chart.title") })} + {children.left.propertyView({ label: trans("chart.left"), tooltip: trans("echarts.leftTooltip") })} + {children.right.propertyView({ label: trans("chart.right"), tooltip: trans("echarts.rightTooltip") })} + {children.top.propertyView({ label: trans("chart.top"), tooltip: trans("echarts.topTooltip") })} + {children.bottom.propertyView({ label: trans("chart.bottom"), tooltip: trans("echarts.bottomTooltip") })} + {hiddenPropertyView(children)} + {children.tooltip.propertyView({label: trans("echarts.tooltip"), tooltip: trans("echarts.tooltipTooltip")})} +
+
+ {children.chartStyle?.getPropertyView()} +
+
+ {children.titleStyle?.getPropertyView()} +
+
+ {children.xAxisStyle?.getPropertyView()} +
+
+ {children.yAxisStyle?.getPropertyView()} +
+
+ {children.data.propertyView({ + label: trans("chart.data"), + })} +
+ + ); + + const getChatConfigByMode = (mode: string) => { + switch(mode) { + case "ui": + return uiModePropertyView; + } + } + return ( + <> + {getChatConfigByMode(children.mode.getView())} + + ); +} diff --git a/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartUtils.ts b/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartUtils.ts new file mode 100644 index 000000000..2bc1904d4 --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartUtils.ts @@ -0,0 +1,293 @@ +import { + ChartCompPropsType, + ChartSize, + noDataBoxplotChartConfig, +} from "comps/boxplotChartComp/boxplotChartConstants"; +import { EChartsOptionWithMap } from "../basicChartComp/reactEcharts/types"; +import _ from "lodash"; +import { googleMapsApiUrl } from "../basicChartComp/chartConfigs/chartUrls"; +import parseBackground from "../../util/gradientBackgroundColor"; +import {chartStyleWrapper, styleWrapper} from "../../util/styleWrapper"; +// Define the configuration interface to match the original transform + +interface AggregateConfig { + resultDimensions: Array<{ + name: string; + from: string; + method?: string; // e.g., 'min', 'Q1', 'median', 'Q3', 'max' + }>; + groupBy: string; +} + +// Custom transform function +function customAggregateTransform(params: { + upstream: { source: any[] }; + config: AggregateConfig; +}): any[] { + const { upstream, config } = params; + const data = upstream.source; + + // Assume data is an array of arrays, with the first row as headers + const headers = data[0]; + const rows = data.slice(1); + + // Find the index of the groupBy column + const groupByIndex = headers.indexOf(config.groupBy); + if (groupByIndex === -1) { + return []; + } + + // Group rows by the groupBy column + const groups: { [key: string]: any[][] } = {}; + rows.forEach(row => { + const key = row[groupByIndex]; + if (!groups[key]) { + groups[key] = []; + } + groups[key].push(row); + }); + + // Define aggregation functions + const aggregators: { + [method: string]: (values: number[]) => number; + } = { + min: values => Math.min(...values), + max: values => Math.max(...values), + Q1: values => percentile(values, 25), + median: values => percentile(values, 50), + Q3: values => percentile(values, 75), + }; + + // Helper function to calculate percentiles (Q1, median, Q3) + function percentile(arr: number[], p: number): number { + const sorted = arr.slice().sort((a, b) => a - b); + const index = (p / 100) * (sorted.length - 1); + const i = Math.floor(index); + const f = index - i; + if (i === sorted.length - 1) { + return sorted[i]; + } + return sorted[i] + f * (sorted[i + 1] - sorted[i]); + } + + // Prepare output headers from resultDimensions + const outputHeaders = config.resultDimensions.map(dim => dim.name); + + // Compute aggregated data for each group + const aggregatedData: any[][] = []; + for (const key in groups) { + const groupRows = groups[key]; + const row: any[] = []; + + config.resultDimensions.forEach(dim => { + if (dim.from === config.groupBy) { + // Include the group key directly + row.push(key); + } else { + // Find the index of the 'from' column + const fromIndex = headers.indexOf(dim.from); + if (fromIndex === -1) { + return; + } + // Extract values for the 'from' column in this group + const values = groupRows + .map(r => parseFloat(r[fromIndex])) + .filter(v => !isNaN(v)); + if (dim.method && aggregators[dim.method]) { + // Apply the aggregation method + row.push(aggregators[dim.method](values)); + } else { + return; + } + } + }); + + aggregatedData.push(row); + } + + // Return the transformed data with headers + return [outputHeaders, ...aggregatedData]; +} + +export const echartsConfigOmitChildren = [ + "hidden", + "selectedPoints", + "onUIEvent", + "mapInstance" +] as const; +type EchartsConfigProps = Omit; + +// https://echarts.apache.org/en/option.html +export function getEchartsConfig( + props: EchartsConfigProps, + chartSize?: ChartSize, + theme?: any, +): EChartsOptionWithMap { + const gridPos = { + left: `${props?.left}%`, + right: `${props?.right}%`, + bottom: `${props?.bottom}%`, + top: `${props?.top}%`, + }; + + let config: any = { + title: { + text: props.title, + top: props.echartsTitleVerticalConfig.top, + left:props.echartsTitleConfig.top, + textStyle: { + ...styleWrapper(props?.titleStyle, theme?.titleStyle) + } + }, + backgroundColor: parseBackground( props?.chartStyle?.background || theme?.chartStyle?.backgroundColor || "#FFFFFF"), + tooltip: props.tooltip && { + trigger: "axis", + axisPointer: { + type: "line", + lineStyle: { + color: "rgba(0,0,0,0.2)", + width: 2, + type: "solid" + } + } + }, + grid: { + ...gridPos, + containLabel: true, + }, + xAxis: { + name: props.xAxisKey, + nameLocation: 'middle', + nameGap: 30, + scale: true, + axisLabel: { + ...styleWrapper(props?.xAxisStyle, theme?.xAxisStyle, 11) + } + }, + yAxis: { + type: "category", + axisLabel: { + ...styleWrapper(props?.yAxisStyle, theme?.yAxisStyle, 11) + } + }, + dataset: [ + { + id: 'raw', + source: customAggregateTransform({upstream: {source: props.data as any[]}, config:{ + resultDimensions: [ + { name: 'min', from: props.xAxisKey, method: 'min' }, + { name: 'Q1', from: props.xAxisKey, method: 'Q1' }, + { name: 'median', from: props.xAxisKey, method: 'median' }, + { name: 'Q3', from: props.xAxisKey, method: 'Q3' }, + { name: 'max', from: props.xAxisKey, method: 'max' }, + { name: props.yAxisKey, from: props.yAxisKey } + ], + groupBy: props.yAxisKey + }}), + }, + { + id: 'finaldataset', + fromDatasetId: 'raw', + transform: [ + { + type: 'sort', + config: { + dimension: 'Q3', + order: 'asc' + } + } + ] + } + ], + }; + + if (props.data.length <= 0) { + // no data + return { + ...config, + ...noDataBoxplotChartConfig, + }; + } + const yAxisConfig = props.yConfig(); + // y-axis is category and time, data doesn't need to aggregate + let transformedData = props.data; + + config = { + ...config, + series: [{ + name: props.xAxisKey, + type: 'boxplot', + datasetId: 'finaldataset', + encode: { + x: ['min', 'Q1', 'median', 'Q3', 'max'], + y: props.yAxisKey, + itemName: [props.yAxisKey], + tooltip: ['min', 'Q1', 'median', 'Q3', 'max'] + }, + itemStyle: { + color: '#b8c5f2', + ...chartStyleWrapper(props?.chartStyle, theme?.chartStyle) + }, + }], + }; + if(config.series[0].itemStyle.borderWidth === 0) config.series[0].itemStyle.borderWidth = 1; + + // console.log("Echarts transformedData and config", transformedData, config); + return config; +} + +export function getSelectedPoints(param: any, option: any) { + const series = option.series; + const dataSource = _.isArray(option.dataset) && option.dataset[0]?.source; + if (series && dataSource) { + return param.selected.flatMap((selectInfo: any) => { + const seriesInfo = series[selectInfo.seriesIndex]; + if (!seriesInfo || !seriesInfo.encode) { + return []; + } + return selectInfo.dataIndex.map((index: any) => { + const commonResult = { + seriesName: seriesInfo.name, + }; + if (seriesInfo.encode.itemName && seriesInfo.encode.value) { + return { + ...commonResult, + itemName: dataSource[index][seriesInfo.encode.itemName], + value: dataSource[index][seriesInfo.encode.value], + }; + } else { + return { + ...commonResult, + x: dataSource[index][seriesInfo.encode.x], + y: dataSource[index][seriesInfo.encode.y], + }; + } + }); + }); + } + return []; +} + +export function loadGoogleMapsScript(apiKey: string) { + const mapsUrl = `${googleMapsApiUrl}?key=${apiKey}`; + const scripts = document.getElementsByTagName('script'); + // is script already loaded + let scriptIndex = _.findIndex(scripts, (script) => script.src.endsWith(mapsUrl)); + if(scriptIndex > -1) { + return scripts[scriptIndex]; + } + // is script loaded with diff api_key, remove the script and load again + scriptIndex = _.findIndex(scripts, (script) => script.src.startsWith(googleMapsApiUrl)); + if(scriptIndex > -1) { + scripts[scriptIndex].remove(); + } + + const script = document.createElement("script"); + script.type = "text/javascript"; + script.src = mapsUrl; + script.async = true; + script.defer = true; + window.document.body.appendChild(script); + + return script; +} diff --git a/client/packages/lowcoder-comps/src/comps/chartComp/chartConfigs/barChartConfig.tsx b/client/packages/lowcoder-comps/src/comps/chartComp/chartConfigs/barChartConfig.tsx index 6c91fe252..707b16170 100644 --- a/client/packages/lowcoder-comps/src/comps/chartComp/chartConfigs/barChartConfig.tsx +++ b/client/packages/lowcoder-comps/src/comps/chartComp/chartConfigs/barChartConfig.tsx @@ -5,7 +5,7 @@ import { showLabelPropertyView, } from "lowcoder-sdk"; import { BarSeriesOption } from "echarts"; -import { trans } from "i18n/comps"; +import { i18nObjs, trans } from "i18n/comps"; const BarTypeOptions = [ { diff --git a/client/packages/lowcoder-comps/src/comps/line3dChartComp/images/default_ambient_cubemap_texture.hdr b/client/packages/lowcoder-comps/src/comps/line3dChartComp/images/default_ambient_cubemap_texture.hdr new file mode 100644 index 000000000..4d53b3609 Binary files /dev/null and b/client/packages/lowcoder-comps/src/comps/line3dChartComp/images/default_ambient_cubemap_texture.hdr differ diff --git a/client/packages/lowcoder-comps/src/comps/line3dChartComp/images/default_base_texture.jpg b/client/packages/lowcoder-comps/src/comps/line3dChartComp/images/default_base_texture.jpg new file mode 100644 index 000000000..c4a5d335c Binary files /dev/null and b/client/packages/lowcoder-comps/src/comps/line3dChartComp/images/default_base_texture.jpg differ diff --git a/client/packages/lowcoder-comps/src/comps/line3dChartComp/images/default_environment.jpg b/client/packages/lowcoder-comps/src/comps/line3dChartComp/images/default_environment.jpg new file mode 100644 index 000000000..314999840 Binary files /dev/null and b/client/packages/lowcoder-comps/src/comps/line3dChartComp/images/default_environment.jpg differ diff --git a/client/packages/lowcoder-comps/src/comps/line3dChartComp/images/default_height_texture.jpg b/client/packages/lowcoder-comps/src/comps/line3dChartComp/images/default_height_texture.jpg new file mode 100644 index 000000000..9f8dcdf31 Binary files /dev/null and b/client/packages/lowcoder-comps/src/comps/line3dChartComp/images/default_height_texture.jpg differ diff --git a/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartComp.tsx b/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartComp.tsx new file mode 100644 index 000000000..712e224b2 --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartComp.tsx @@ -0,0 +1,282 @@ +import { + changeChildAction, + changeValueAction, + CompAction, + CompActionTypes, + wrapChildAction, +} from "lowcoder-core"; +import { AxisFormatterComp, EchartsAxisType } from "../basicChartComp/chartConfigs/cartesianAxisConfig"; +import { line3dChartChildrenMap, ChartSize, getDataKeys } from "./line3dChartConstants"; +import { line3dChartPropertyView } from "./line3dChartPropertyView"; +import _ from "lodash"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; +import ReactResizeDetector from "react-resize-detector"; +import ReactECharts from "../basicChartComp/reactEcharts"; +import * as echarts from "echarts"; +import { + childrenToProps, + depsConfig, + genRandomKey, + NameConfig, + UICompBuilder, + withDefault, + withExposingConfigs, + withViewFn, + ThemeContext, + chartColorPalette, + getPromiseAfterDispatch, + dropdownControl, +} from "lowcoder-sdk"; +import { getEchartsLocale, i18nObjs, trans } from "i18n/comps"; +import { + echartsConfigOmitChildren, + getEchartsConfig, + getSelectedPoints, +} from "./line3dChartUtils"; +import 'echarts-extension-gmap'; +import log from "loglevel"; + +let clickEventCallback = () => {}; + +const chartModeOptions = [ + { + label: "UI", + value: "ui", + } +] as const; + +let Line3DChartTmpComp = (function () { + return new UICompBuilder({mode:dropdownControl(chartModeOptions,'ui'),...line3dChartChildrenMap}, () => null) + .setPropertyViewFn(line3dChartPropertyView) + .build(); +})(); + +Line3DChartTmpComp = withViewFn(Line3DChartTmpComp, (comp) => { + const mode = comp.children.mode.getView(); + const onUIEvent = comp.children.onUIEvent.getView(); + const onEvent = comp.children.onEvent.getView(); + const echartsCompRef = useRef(); + const [chartSize, setChartSize] = useState(); + const firstResize = useRef(true); + const theme = useContext(ThemeContext); + const defaultChartTheme = { + color: chartColorPalette, + backgroundColor: "#fff", + }; + + let themeConfig = defaultChartTheme; + try { + themeConfig = theme?.theme.chart ? JSON.parse(theme?.theme.chart) : defaultChartTheme; + } catch (error) { + log.error('theme chart error: ', error); + } + + const triggerClickEvent = async (dispatch: any, action: CompAction) => { + await getPromiseAfterDispatch( + dispatch, + action, + { autoHandleAfterReduce: true } + ); + onEvent('click'); + } + + useEffect(() => { + const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance(); + if (!echartsCompInstance) { + return _.noop; + } + echartsCompInstance?.on("click", (param: any) => { + document.dispatchEvent(new CustomEvent("clickEvent", { + bubbles: true, + detail: { + action: 'click', + data: param.data, + } + })); + triggerClickEvent( + comp.dispatch, + changeChildAction("lastInteractionData", param.data, false) + ); + }); + return () => { + echartsCompInstance?.off("click"); + document.removeEventListener('clickEvent', clickEventCallback) + }; + }, []); + + useEffect(() => { + // bind events + const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance(); + if (!echartsCompInstance) { + return _.noop; + } + echartsCompInstance?.on("selectchanged", (param: any) => { + const option: any = echartsCompInstance?.getOption(); + document.dispatchEvent(new CustomEvent("clickEvent", { + bubbles: true, + detail: { + action: param.fromAction, + data: getSelectedPoints(param, option) + } + })); + + if (param.fromAction === "select") { + comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false)); + onUIEvent("select"); + } else if (param.fromAction === "unselect") { + comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false)); + onUIEvent("unselect"); + } + + triggerClickEvent( + comp.dispatch, + changeChildAction("lastInteractionData", getSelectedPoints(param, option), false) + ); + }); + // unbind + return () => { + echartsCompInstance?.off("selectchanged"); + document.removeEventListener('clickEvent', clickEventCallback) + }; + }, [onUIEvent]); + + const echartsConfigChildren = _.omit(comp.children, echartsConfigOmitChildren); + const childrenProps = childrenToProps(echartsConfigChildren); + + const option = useMemo(() => { + return getEchartsConfig( + childrenProps as ToViewReturn, + chartSize, + themeConfig + ); + }, [theme, childrenProps, chartSize, ...Object.values(echartsConfigChildren)]); + + return ( + { + if (w && h) { + setChartSize({ w: w, h: h }); + } + if (!firstResize.current) { + // ignore the first resize, which will impact the loading animation + echartsCompRef.current?.getEchartsInstance().resize(); + } else { + firstResize.current = false; + } + }} + > + (echartsCompRef.current = e)} + style={{ height: "100%" }} + notMerge + lazyUpdate + opts={{ locale: getEchartsLocale() }} + option={option} + mode={mode} + /> + + ); +}); + +function getYAxisFormatContextValue( + data: Array, + yAxisType: EchartsAxisType, + yAxisName?: string +) { + const dataSample = yAxisName && data.length > 0 && data[0][yAxisName]; + let contextValue = dataSample; + if (yAxisType === "time") { + // to timestamp + const time = + typeof dataSample === "number" || typeof dataSample === "string" + ? new Date(dataSample).getTime() + : null; + if (time) contextValue = time; + } + return contextValue; +} + +Line3DChartTmpComp = class extends Line3DChartTmpComp { + private lastYAxisFormatContextVal?: JSONValue; + private lastColorContext?: JSONObject; + + updateContext(comp: this) { + // the context value of axis format + let resultComp = comp; + const data = comp.children.data.getView(); + const yAxisContextValue = getYAxisFormatContextValue( + data, + comp.children.yConfig.children.yAxisType.getView(), + ); + if (yAxisContextValue !== comp.lastYAxisFormatContextVal) { + comp.lastYAxisFormatContextVal = yAxisContextValue; + resultComp = comp.setChild( + "yConfig", + comp.children.yConfig.reduce( + wrapChildAction( + "formatter", + AxisFormatterComp.changeContextDataAction({ value: yAxisContextValue }) + ) + ) + ); + } + return resultComp; + } + + override reduce(action: CompAction): this { + const comp = super.reduce(action); + if (action.type === CompActionTypes.UPDATE_NODES_V2) { + const newData = comp.children.data.getView(); + // data changes + if (comp.children.data !== this.children.data) { + setTimeout(() => { + // update x-axis value + const keys = getDataKeys(newData); + if (keys.length > 0 && !keys.includes(comp.children.xAxisKey.getView())) { + comp.children.xAxisKey.dispatch(changeValueAction(keys[0] || "")); + } + if (keys.length > 0 && !keys.includes(comp.children.yAxisKey.getView())) { + comp.children.yAxisKey.dispatch(changeValueAction(keys[1] || "")); + } + }, 0); + } + return this.updateContext(comp); + } + return comp; + } + + override autoHeight(): boolean { + return false; + } +}; + +let Line3DChartComp = withExposingConfigs(Line3DChartTmpComp, [ + depsConfig({ + name: "selectedPoints", + desc: trans("chart.selectedPointsDesc"), + depKeys: ["selectedPoints"], + func: (input) => { + return input.selectedPoints; + }, + }), + depsConfig({ + name: "lastInteractionData", + desc: trans("chart.lastInteractionDataDesc"), + depKeys: ["lastInteractionData"], + func: (input) => { + return input.lastInteractionData; + }, + }), + depsConfig({ + name: "data", + desc: trans("chart.dataDesc"), + depKeys: ["data", "mode"], + func: (input) =>[] , + }), + new NameConfig("title", trans("chart.titleDesc")), +]); + + +export const Line3DChartCompWithDefault = withDefault(Line3DChartComp, { + xAxisKey: "date", +}); diff --git a/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartConstants.tsx b/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartConstants.tsx new file mode 100644 index 000000000..41a405c55 --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartConstants.tsx @@ -0,0 +1,176 @@ +import { + jsonControl, + stateComp, + toJSONObjectArray, + toObject, + BoolControl, + ColorControl, + withDefault, + StringControl, + NumberControl, + dropdownControl, + list, + eventHandlerControl, + valueComp, + withType, + uiChildren, + clickEvent, + toArray, + styleControl, + EchartDefaultTextStyle, + EchartDefaultChartStyle, + MultiCompBuilder, +} from "lowcoder-sdk"; +import { RecordConstructorToComp, RecordConstructorToView } from "lowcoder-core"; +import { XAxisConfig, YAxisConfig } from "../basicChartComp/chartConfigs/cartesianAxisConfig"; +import { LegendConfig } from "../basicChartComp/chartConfigs/legendConfig"; +import { EchartsLegendConfig } from "../basicChartComp/chartConfigs/echartsLegendConfig"; +import { EchartsLabelConfig } from "../basicChartComp/chartConfigs/echartsLabelConfig"; +import { EChartsOption } from "echarts"; +import { i18nObjs, trans } from "i18n/comps"; +import {EchartsTitleVerticalConfig} from "../chartComp/chartConfigs/echartsTitleVerticalConfig"; +import {EchartsTitleConfig} from "../basicChartComp/chartConfigs/echartsTitleConfig"; + +export const UIEventOptions = [ + { + label: trans("chart.select"), + value: "select", + description: trans("chart.selectDesc"), + }, + { + label: trans("chart.unSelect"), + value: "unselect", + description: trans("chart.unselectDesc"), + }, +] as const; + +export const XAxisDirectionOptions = [ + { + label: trans("chart.horizontal"), + value: "horizontal", + }, + { + label: trans("chart.vertical"), + value: "vertical", + }, +] as const; + +export type XAxisDirectionType = ValueFromOption; + +export const noDataAxisConfig = { + animation: false, + xAxis: { + type: "category", + name: trans("chart.noData"), + nameLocation: "middle", + data: [], + axisLine: { + lineStyle: { + color: "#8B8FA3", + }, + }, + }, + yAxis: { + type: "value", + axisLabel: { + color: "#8B8FA3", + }, + splitLine: { + lineStyle: { + color: "#F0F0F0", + }, + }, + }, + tooltip: { + show: false, + }, + series: [ + { + data: [700], + type: "line", + itemStyle: { + opacity: 0, + }, + }, + ], +} as EChartsOption; + +export const noDataLine3DChartConfig = { + animation: false, + tooltip: { + show: false, + }, + legend: { + formatter: trans("chart.unknown"), + top: "bottom", + selectedMode: false, + }, + color: ["#B8BBCC", "#CED0D9", "#DCDEE6", "#E6E6EB"], + series: [], +} as EChartsOption; + +export type ChartSize = { w: number; h: number }; + +export const getDataKeys = (data: Array) => { + if (!data) { + return []; + } + const dataKeys: Array = []; + data[0].forEach((key) => { + if (!dataKeys.includes(key)) { + dataKeys.push(key); + } + }); + return dataKeys; +}; + +export const chartUiModeChildren = { + title: withDefault(StringControl, trans("echarts.defaultTitle")), + data: jsonControl(toArray, i18nObjs.defaultDatasource3DGlobe), + xAxisKey: valueComp(""), // x-axis, key from data + xAxisDirection: dropdownControl(XAxisDirectionOptions, "horizontal"), + xAxisData: jsonControl(toArray, []), + yAxisKey: valueComp(""), // x-axis, key from data + xConfig: XAxisConfig, + yConfig: YAxisConfig, + legendConfig: LegendConfig, + environment: withDefault(StringControl, trans("line3dchart.defaultEnvironment")), + baseTexture: withDefault(StringControl, trans("line3dchart.defaultBaseTexture")), + heightTexture: withDefault(StringControl, trans("line3dchart.defaultHeightTexture")), + background: withDefault(ColorControl, "black"), + lineStyleWidth: withDefault(NumberControl, 1), + lineStyleColor: withDefault(ColorControl, "rgb(50, 50, 150)"), + lineStyleOpacity: withDefault(NumberControl, 0.1), + effectShow: withDefault(BoolControl, true), + effectWidth: withDefault(NumberControl, 2), + effectLength: withDefault(NumberControl, 0.15), + effectOpacity: withDefault(NumberControl, 1), + effectColor: withDefault(ColorControl, 'rgb(30, 30, 60)'), + onUIEvent: eventHandlerControl(UIEventOptions), +}; + +export type UIChartDataType = { + seriesName: string; + // coordinate chart + x?: any; + y?: any; + // line3d or funnel + itemName?: any; + value?: any; +}; + +export type NonUIChartDataType = { + name: string; + value: any; +} + +export const line3dChartChildrenMap = { + selectedPoints: stateComp>([]), + lastInteractionData: stateComp | NonUIChartDataType>({}), + onEvent: eventHandlerControl([clickEvent] as const), + ...chartUiModeChildren, +}; + +const chartUiChildrenMap = uiChildren(line3dChartChildrenMap); +export type ChartCompPropsType = RecordConstructorToView; +export type ChartCompChildrenType = RecordConstructorToComp; diff --git a/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartPropertyView.tsx new file mode 100644 index 000000000..bbcebf358 --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartPropertyView.tsx @@ -0,0 +1,62 @@ +import { changeChildAction, CompAction } from "lowcoder-core"; +import { ChartCompChildrenType, getDataKeys } from "./line3dChartConstants"; +import { + CustomModal, + Dropdown, + hiddenPropertyView, + Option, + RedButton, + Section, + sectionNames, + controlItem, +} from "lowcoder-sdk"; +import { trans } from "i18n/comps"; + +export function line3dChartPropertyView( + children: ChartCompChildrenType, + dispatch: (action: CompAction) => void +) { + const uiModePropertyView = ( + <> +
+ {children.environment.propertyView({label: trans("line3dchart.environment")})} + {children.baseTexture.propertyView({label: trans("line3dchart.baseTexture")})} + {children.heightTexture.propertyView({label: trans("line3dchart.heightTexture")})} + {children.background.propertyView({label: trans("line3dchart.background")})} + {children.lineStyleWidth.propertyView({label: trans("line3dchart.lineStyleWidth")})} + {children.lineStyleColor.propertyView({label: trans("line3dchart.lineStyleColor")})} + {children.lineStyleOpacity.propertyView({label: trans("line3dchart.lineStyleOpacity")})} + {children.effectShow.propertyView({label: trans("line3dchart.effectShow")})} + {children.effectShow.getView() && children.effectWidth.propertyView({label: trans("line3dchart.effectTrailWidth")})} + {children.effectShow.getView() && children.effectLength.propertyView({label: trans("line3dchart.effectTrailLength")})} + {children.effectShow.getView() && children.effectOpacity.propertyView({label: trans("line3dchart.effectTrailOpacity")})} + {children.effectShow.getView() && children.effectColor.propertyView({label: trans("line3dchart.effectTrailColor")})} +
+
+
+ {children.onUIEvent.propertyView({title: trans("chart.chartEventHandlers")})} +
+
+ {children.onEvent.propertyView()} +
+
+
+ {children.data.propertyView({ + label: trans("chart.data"), + })} +
+ + ); + + const getChatConfigByMode = (mode: string) => { + switch(mode) { + case "ui": + return uiModePropertyView; + } + } + return ( + <> + {getChatConfigByMode(children.mode.getView())} + + ); +} diff --git a/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartUtils.ts b/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartUtils.ts new file mode 100644 index 000000000..d1be05edf --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartUtils.ts @@ -0,0 +1,239 @@ +import { + ChartCompPropsType, + ChartSize, + noDataLine3DChartConfig, +} from "comps/line3dChartComp/line3dChartConstants"; +import { EChartsOptionWithMap } from "../basicChartComp/reactEcharts/types"; +import _ from "lodash"; +import { googleMapsApiUrl } from "../basicChartComp/chartConfigs/chartUrls"; +import parseBackground from "../../util/gradientBackgroundColor"; +import {chartStyleWrapper, styleWrapper} from "../../util/styleWrapper"; +// Define the configuration interface to match the original transform + +interface AggregateConfig { + resultDimensions: Array<{ + name: string; + from: string; + method?: string; // e.g., 'min', 'Q1', 'median', 'Q3', 'max' + }>; + groupBy: string; +} + +// Custom transform function +function customAggregateTransform(params: { + upstream: { source: any[] }; + config: AggregateConfig; +}): any[] { + const { upstream, config } = params; + const data = upstream.source; + + // Assume data is an array of arrays, with the first row as headers + const headers = data[0]; + const rows = data.slice(1); + + // Find the index of the groupBy column + const groupByIndex = headers.indexOf(config.groupBy); + if (groupByIndex === -1) { + return []; + } + + // Group rows by the groupBy column + const groups: { [key: string]: any[][] } = {}; + rows.forEach(row => { + const key = row[groupByIndex]; + if (!groups[key]) { + groups[key] = []; + } + groups[key].push(row); + }); + + // Define aggregation functions + const aggregators: { + [method: string]: (values: number[]) => number; + } = { + min: values => Math.min(...values), + max: values => Math.max(...values), + Q1: values => percentile(values, 25), + median: values => percentile(values, 50), + Q3: values => percentile(values, 75), + }; + + // Helper function to calculate percentiles (Q1, median, Q3) + function percentile(arr: number[], p: number): number { + const sorted = arr.slice().sort((a, b) => a - b); + const index = (p / 100) * (sorted.length - 1); + const i = Math.floor(index); + const f = index - i; + if (i === sorted.length - 1) { + return sorted[i]; + } + return sorted[i] + f * (sorted[i + 1] - sorted[i]); + } + + // Prepare output headers from resultDimensions + const outputHeaders = config.resultDimensions.map(dim => dim.name); + + // Compute aggregated data for each group + const aggregatedData: any[][] = []; + for (const key in groups) { + const groupRows = groups[key]; + const row: any[] = []; + + config.resultDimensions.forEach(dim => { + if (dim.from === config.groupBy) { + // Include the group key directly + row.push(key); + } else { + // Find the index of the 'from' column + const fromIndex = headers.indexOf(dim.from); + if (fromIndex === -1) { + return; + } + // Extract values for the 'from' column in this group + const values = groupRows + .map(r => parseFloat(r[fromIndex])) + .filter(v => !isNaN(v)); + if (dim.method && aggregators[dim.method]) { + // Apply the aggregation method + row.push(aggregators[dim.method](values)); + } else { + return; + } + } + }); + + aggregatedData.push(row); + } + + // Return the transformed data with headers + return [outputHeaders, ...aggregatedData]; +} + +export const echartsConfigOmitChildren = [ + "hidden", + "selectedPoints", + "onUIEvent", + "mapInstance" +] as const; +type EchartsConfigProps = Omit; + +// https://echarts.apache.org/en/option.html +export function getEchartsConfig( + props: EchartsConfigProps, + chartSize?: ChartSize, + theme?: any, +): EChartsOptionWithMap { + let config: any = { + backgroundColor: props.background, + globe: { + environment: props.environment, + baseTexture: props.baseTexture, + heightTexture: props.heightTexture, + shading: 'realistic', + realisticMaterial: { + roughness: 0.2, + metalness: 0 + }, + postEffect: { + enable: true, + depthOfField: { + enable: false, + focalDistance: 150 + } + }, + displacementScale: 0.1, + displacementQuality: 'high', + temporalSuperSampling: { + enable: true + }, + light: { + ambient: { + intensity: 0.4 + }, + main: { + intensity: 0.4 + }, + }, + viewControl: { + autoRotate: false + }, + silent: true + }, + series: { + type: 'lines3D', + coordinateSystem: 'globe', + blendMode: 'lighter', + lineStyle: { + width: props.lineStyleWidth, + color: props.lineStyleColor, + opacity: props.lineStyleOpacity + }, + data: props.data, + effect: { + show: props.effectShow, + trailWidth: props.effectWidth, + trailLength: props.effectLength, + trailOpacity: props.effectOpacity, + trailColor: props.effectColor + }, + } + }; + console.log(config); + return config; +} + +export function getSelectedPoints(param: any, option: any) { + const series = option.series; + const dataSource = _.isArray(option.dataset) && option.dataset[0]?.source; + if (series && dataSource) { + return param.selected.flatMap((selectInfo: any) => { + const seriesInfo = series[selectInfo.seriesIndex]; + if (!seriesInfo || !seriesInfo.encode) { + return []; + } + return selectInfo.dataIndex.map((index: any) => { + const commonResult = { + seriesName: seriesInfo.name, + }; + if (seriesInfo.encode.itemName && seriesInfo.encode.value) { + return { + ...commonResult, + itemName: dataSource[index][seriesInfo.encode.itemName], + value: dataSource[index][seriesInfo.encode.value], + }; + } else { + return { + ...commonResult, + x: dataSource[index][seriesInfo.encode.x], + y: dataSource[index][seriesInfo.encode.y], + }; + } + }); + }); + } + return []; +} + +export function loadGoogleMapsScript(apiKey: string) { + const mapsUrl = `${googleMapsApiUrl}?key=${apiKey}`; + const scripts = document.getElementsByTagName('script'); + // is script already loaded + let scriptIndex = _.findIndex(scripts, (script) => script.src.endsWith(mapsUrl)); + if(scriptIndex > -1) { + return scripts[scriptIndex]; + } + // is script loaded with diff api_key, remove the script and load again + scriptIndex = _.findIndex(scripts, (script) => script.src.startsWith(googleMapsApiUrl)); + if(scriptIndex > -1) { + scripts[scriptIndex].remove(); + } + + const script = document.createElement("script"); + script.type = "text/javascript"; + script.src = mapsUrl; + script.async = true; + script.defer = true; + window.document.body.appendChild(script); + + return script; +} diff --git a/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartComp.tsx b/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartComp.tsx new file mode 100644 index 000000000..be3e5bf65 --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartComp.tsx @@ -0,0 +1,314 @@ +import { + changeChildAction, + changeValueAction, + CompAction, + CompActionTypes, + wrapChildAction, +} from "lowcoder-core"; +import { AxisFormatterComp, EchartsAxisType } from "../basicChartComp/chartConfigs/cartesianAxisConfig"; +import { lineChartChildrenMap, ChartSize, getDataKeys } from "./lineChartConstants"; +import { lineChartPropertyView } from "./lineChartPropertyView"; +import _ from "lodash"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; +import ReactResizeDetector from "react-resize-detector"; +import ReactECharts from "../basicChartComp/reactEcharts"; +import { + childrenToProps, + depsConfig, + genRandomKey, + NameConfig, + UICompBuilder, + withDefault, + withExposingConfigs, + withViewFn, + ThemeContext, + chartColorPalette, + getPromiseAfterDispatch, + dropdownControl, +} from "lowcoder-sdk"; +import { getEchartsLocale, trans } from "i18n/comps"; +import { ItemColorComp } from "comps/basicChartComp/chartConfigs/lineChartConfig"; +import { + echartsConfigOmitChildren, + getEchartsConfig, + getSelectedPoints, +} from "./lineChartUtils"; +import 'echarts-extension-gmap'; +import log from "loglevel"; + +let clickEventCallback = () => {}; + +const chartModeOptions = [ + { + label: "ECharts JSON", + value: "json", + } +] as const; + +let LineChartTmpComp = (function () { + return new UICompBuilder({mode:dropdownControl(chartModeOptions,'ui'),...lineChartChildrenMap}, () => null) + .setPropertyViewFn(lineChartPropertyView) + .build(); +})(); + +LineChartTmpComp = withViewFn(LineChartTmpComp, (comp) => { + const mode = comp.children.mode.getView(); + const onUIEvent = comp.children.onUIEvent.getView(); + const onEvent = comp.children.onEvent.getView(); + const echartsCompRef = useRef(); + const [chartSize, setChartSize] = useState(); + const firstResize = useRef(true); + const theme = useContext(ThemeContext); + const defaultChartTheme = { + color: chartColorPalette, + backgroundColor: "#fff", + }; + + let themeConfig = defaultChartTheme; + try { + themeConfig = theme?.theme.chart ? JSON.parse(theme?.theme.chart) : defaultChartTheme; + } catch (error) { + log.error('theme chart error: ', error); + } + + const triggerClickEvent = async (dispatch: any, action: CompAction) => { + await getPromiseAfterDispatch( + dispatch, + action, + { autoHandleAfterReduce: true } + ); + onEvent('click'); + } + + useEffect(() => { + const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance(); + if (!echartsCompInstance) { + return _.noop; + } + echartsCompInstance?.on("click", (param: any) => { + document.dispatchEvent(new CustomEvent("clickEvent", { + bubbles: true, + detail: { + action: 'click', + data: param.data, + } + })); + triggerClickEvent( + comp.dispatch, + changeChildAction("lastInteractionData", param.data, false) + ); + }); + return () => { + echartsCompInstance?.off("click"); + document.removeEventListener('clickEvent', clickEventCallback) + }; + }, []); + + useEffect(() => { + // bind events + const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance(); + if (!echartsCompInstance) { + return _.noop; + } + echartsCompInstance?.on("selectchanged", (param: any) => { + const option: any = echartsCompInstance?.getOption(); + document.dispatchEvent(new CustomEvent("clickEvent", { + bubbles: true, + detail: { + action: param.fromAction, + data: getSelectedPoints(param, option) + } + })); + + if (param.fromAction === "select") { + comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false)); + onUIEvent("select"); + } else if (param.fromAction === "unselect") { + comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false)); + onUIEvent("unselect"); + } + + triggerClickEvent( + comp.dispatch, + changeChildAction("lastInteractionData", getSelectedPoints(param, option), false) + ); + }); + // unbind + return () => { + echartsCompInstance?.off("selectchanged"); + document.removeEventListener('clickEvent', clickEventCallback) + }; + }, [onUIEvent]); + + const echartsConfigChildren = _.omit(comp.children, echartsConfigOmitChildren); + const childrenProps = childrenToProps(echartsConfigChildren); + const option = useMemo(() => { + return getEchartsConfig( + childrenProps as ToViewReturn, + chartSize, + themeConfig + ); + }, [theme, childrenProps, chartSize, ...Object.values(echartsConfigChildren)]); + + return ( + { + if (w && h) { + setChartSize({ w: w, h: h }); + } + if (!firstResize.current) { + // ignore the first resize, which will impact the loading animation + echartsCompRef.current?.getEchartsInstance().resize(); + } else { + firstResize.current = false; + } + }} + > + (echartsCompRef.current = e)} + style={{ height: "100%" }} + notMerge + lazyUpdate + opts={{ locale: getEchartsLocale() }} + option={option} + mode={mode} + /> + + ); +}); + +function getYAxisFormatContextValue( + data: Array, + yAxisType: EchartsAxisType, + yAxisName?: string +) { + const dataSample = yAxisName && data.length > 0 && data[0][yAxisName]; + let contextValue = dataSample; + if (yAxisType === "time") { + // to timestamp + const time = + typeof dataSample === "number" || typeof dataSample === "string" + ? new Date(dataSample).getTime() + : null; + if (time) contextValue = time; + } + return contextValue; +} + +LineChartTmpComp = class extends LineChartTmpComp { + private lastYAxisFormatContextVal?: JSONValue; + private lastColorContext?: JSONObject; + + updateContext(comp: this) { + // the context value of axis format + let resultComp = comp; + const data = comp.children.data.getView(); + const sampleSeries = comp.children.series.getView().find((s) => !s.getView().hide); + const yAxisContextValue = getYAxisFormatContextValue( + data, + comp.children.yConfig.children.yAxisType.getView(), + sampleSeries?.children.columnName.getView() + ); + if (yAxisContextValue !== comp.lastYAxisFormatContextVal) { + comp.lastYAxisFormatContextVal = yAxisContextValue; + resultComp = comp.setChild( + "yConfig", + comp.children.yConfig.reduce( + wrapChildAction( + "formatter", + AxisFormatterComp.changeContextDataAction({ value: yAxisContextValue }) + ) + ) + ); + } + // item color context + const colorContextVal = { + seriesName: sampleSeries?.children.seriesName.getView(), + value: yAxisContextValue, + }; + if ( + comp.children.chartConfig.children.comp.children.hasOwnProperty("itemColor") && + !_.isEqual(colorContextVal, comp.lastColorContext) + ) { + comp.lastColorContext = colorContextVal; + resultComp = resultComp.setChild( + "chartConfig", + comp.children.chartConfig.reduce( + wrapChildAction( + "comp", + wrapChildAction("itemColor", ItemColorComp.changeContextDataAction(colorContextVal)) + ) + ) + ); + } + return resultComp; + } + + override reduce(action: CompAction): this { + const comp = super.reduce(action); + if (action.type === CompActionTypes.UPDATE_NODES_V2) { + const newData = comp.children.data.getView(); + // data changes + if (comp.children.data !== this.children.data) { + setTimeout(() => { + // update x-axis value + const keys = getDataKeys(newData); + if (keys.length > 0 && !keys.includes(comp.children.xAxisKey.getView())) { + comp.children.xAxisKey.dispatch(changeValueAction(keys[0] || "")); + } + // pass to child series comp + comp.children.series.dispatchDataChanged(newData); + }, 0); + } + return this.updateContext(comp); + } + return comp; + } + + override autoHeight(): boolean { + return false; + } +}; + +let LineChartComp = withExposingConfigs(LineChartTmpComp, [ + depsConfig({ + name: "selectedPoints", + desc: trans("chart.selectedPointsDesc"), + depKeys: ["selectedPoints"], + func: (input) => { + return input.selectedPoints; + }, + }), + depsConfig({ + name: "lastInteractionData", + desc: trans("chart.lastInteractionDataDesc"), + depKeys: ["lastInteractionData"], + func: (input) => { + return input.lastInteractionData; + }, + }), + depsConfig({ + name: "data", + desc: trans("chart.dataDesc"), + depKeys: ["data", "mode"], + func: (input) =>[] , + }), + new NameConfig("title", trans("chart.titleDesc")), +]); + + +export const LineChartCompWithDefault = withDefault(LineChartComp, { + xAxisKey: "date", + series: [ + { + dataIndex: genRandomKey(), + seriesName: trans("chart.spending"), + columnName: "spending", + }, + { + dataIndex: genRandomKey(), + seriesName: trans("chart.budget"), + columnName: "budget", + }, + ], +}); diff --git a/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartConstants.tsx b/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartConstants.tsx new file mode 100644 index 000000000..2685f1972 --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartConstants.tsx @@ -0,0 +1,315 @@ +import { + jsonControl, + stateComp, + toJSONObjectArray, + toObject, + BoolControl, + ColorControl, + withDefault, + StringControl, + NumberControl, + dropdownControl, + list, + eventHandlerControl, + valueComp, + withType, + uiChildren, + clickEvent, + toArray, + styleControl, + EchartDefaultTextStyle, + EchartDefaultChartStyle, + MultiCompBuilder, +} from "lowcoder-sdk"; +import { RecordConstructorToComp, RecordConstructorToView } from "lowcoder-core"; +import { BarChartConfig } from "../basicChartComp/chartConfigs/barChartConfig"; +import { XAxisConfig, YAxisConfig } from "../basicChartComp/chartConfigs/cartesianAxisConfig"; +import { LegendConfig } from "../basicChartComp/chartConfigs/legendConfig"; +import { EchartsLegendConfig } from "../basicChartComp/chartConfigs/echartsLegendConfig"; +import { EchartsLabelConfig } from "../basicChartComp/chartConfigs/echartsLabelConfig"; +import { LineChartConfig } from "../basicChartComp/chartConfigs/lineChartConfig"; +import { PieChartConfig } from "../basicChartComp/chartConfigs/pieChartConfig"; +import { ScatterChartConfig } from "../basicChartComp/chartConfigs/scatterChartConfig"; +import { SeriesListComp } from "./seriesComp"; +import { EChartsOption } from "echarts"; +import { i18nObjs, trans } from "i18n/comps"; +import { GaugeChartConfig } from "../basicChartComp/chartConfigs/gaugeChartConfig"; +import { FunnelChartConfig } from "../basicChartComp/chartConfigs/funnelChartConfig"; +import {EchartsTitleVerticalConfig} from "../chartComp/chartConfigs/echartsTitleVerticalConfig"; +import {EchartsTitleConfig} from "../basicChartComp/chartConfigs/echartsTitleConfig"; + +export const ChartTypeOptions = [ + { + label: trans("chart.bar"), + value: "bar", + }, + { + label: trans("chart.line"), + value: "line", + }, + { + label: trans("chart.scatter"), + value: "scatter", + }, + { + label: trans("chart.pie"), + value: "pie", + }, +] as const; + +export const UIEventOptions = [ + { + label: trans("chart.select"), + value: "select", + description: trans("chart.selectDesc"), + }, + { + label: trans("chart.unSelect"), + value: "unselect", + description: trans("chart.unselectDesc"), + }, +] as const; + +export const XAxisDirectionOptions = [ + { + label: trans("chart.horizontal"), + value: "horizontal", + }, + { + label: trans("chart.vertical"), + value: "vertical", + }, +] as const; + +export type XAxisDirectionType = ValueFromOption; + +export const noDataAxisConfig = { + animation: false, + xAxis: { + type: "category", + name: trans("chart.noData"), + nameLocation: "middle", + data: [], + axisLine: { + lineStyle: { + color: "#8B8FA3", + }, + }, + }, + yAxis: { + type: "value", + axisLabel: { + color: "#8B8FA3", + }, + splitLine: { + lineStyle: { + color: "#F0F0F0", + }, + }, + }, + tooltip: { + show: false, + }, + series: [ + { + data: [700], + type: "line", + itemStyle: { + opacity: 0, + }, + }, + ], +} as EChartsOption; + +export const noDataPieChartConfig = { + animation: false, + tooltip: { + show: false, + }, + legend: { + formatter: trans("chart.unknown"), + top: "bottom", + selectedMode: false, + }, + color: ["#B8BBCC", "#CED0D9", "#DCDEE6", "#E6E6EB"], + series: [ + { + type: "pie", + radius: "35%", + center: ["25%", "50%"], + silent: true, + label: { + show: false, + }, + data: [ + { + name: "1", + value: 70, + }, + { + name: "2", + value: 68, + }, + { + name: "3", + value: 48, + }, + { + name: "4", + value: 40, + }, + ], + }, + { + type: "pie", + radius: "35%", + center: ["75%", "50%"], + silent: true, + label: { + show: false, + }, + data: [ + { + name: "1", + value: 70, + }, + { + name: "2", + value: 68, + }, + { + name: "3", + value: 48, + }, + { + name: "4", + value: 40, + }, + ], + }, + ], +} as EChartsOption; + +const areaPiecesChildrenMap = { + color: ColorControl, + from: StringControl, + to: StringControl, + // unique key, for sort + dataIndex: valueComp(""), +}; +const AreaPiecesTmpComp = new MultiCompBuilder(areaPiecesChildrenMap, (props) => { + return props; +}) + .setPropertyViewFn((children: any) => + (<> + {children.color.propertyView({label: trans("lineChart.color")})} + {children.from.propertyView({label: trans("lineChart.from")})} + {children.to.propertyView({label: trans("lineChart.to")})} + ) + ) + .build(); + +export type ChartSize = { w: number; h: number }; + +export const getDataKeys = (data: Array) => { + if (!data) { + return []; + } + const dataKeys: Array = []; + data.slice(0, 50).forEach((d) => { + Object.keys(d).forEach((key) => { + if (!dataKeys.includes(key)) { + dataKeys.push(key); + } + }); + }); + return dataKeys; +}; + +const ChartOptionMap = { + bar: BarChartConfig, + line: LineChartConfig, + pie: PieChartConfig, + scatter: ScatterChartConfig, +}; + +const EchartsOptionMap = { + funnel: FunnelChartConfig, + gauge: GaugeChartConfig, +}; + +const ChartOptionComp = withType(ChartOptionMap, "line"); +const EchartsOptionComp = withType(EchartsOptionMap, "funnel"); +export type CharOptionCompType = keyof typeof ChartOptionMap; + +export const chartUiModeChildren = { + title: withDefault(StringControl, trans("echarts.defaultTitle")), + data: jsonControl(toJSONObjectArray, i18nObjs.defaultDataSource), + xAxisKey: valueComp(""), // x-axis, key from data + xAxisDirection: dropdownControl(XAxisDirectionOptions, "horizontal"), + xAxisData: jsonControl(toArray, []), + series: SeriesListComp, + xConfig: XAxisConfig, + yConfig: YAxisConfig, + legendConfig: LegendConfig, + chartConfig: ChartOptionComp, + areaPieces: list(AreaPiecesTmpComp), + animationDuration: withDefault(NumberControl, 1000), + onUIEvent: eventHandlerControl(UIEventOptions), +}; + +let chartJsonModeChildren: any = { + echartsOption: jsonControl(toObject, i18nObjs.defaultEchartsJsonOption), + echartsTitle: withDefault(StringControl, trans("echarts.defaultTitle")), + echartsLegendConfig: EchartsLegendConfig, + echartsLabelConfig: EchartsLabelConfig, + echartsConfig: EchartsOptionComp, + echartsTitleVerticalConfig: EchartsTitleVerticalConfig, + echartsTitleConfig:EchartsTitleConfig, + + left:withDefault(NumberControl,trans('chart.defaultLeft')), + right:withDefault(NumberControl,trans('chart.defaultRight')), + top:withDefault(NumberControl,trans('chart.defaultTop')), + bottom:withDefault(NumberControl,trans('chart.defaultBottom')), + + tooltip: withDefault(BoolControl, true), + legendVisibility: withDefault(BoolControl, true), +} + +if (EchartDefaultChartStyle && EchartDefaultTextStyle) { + chartJsonModeChildren = { + ...chartJsonModeChildren, + chartStyle: styleControl(EchartDefaultChartStyle, 'chartStyle'), + titleStyle: styleControl(EchartDefaultTextStyle, 'titleStyle'), + xAxisStyle: styleControl(EchartDefaultTextStyle, 'xAxis'), + yAxisStyle: styleControl(EchartDefaultTextStyle, 'yAxisStyle'), + legendStyle: styleControl(EchartDefaultTextStyle, 'legendStyle'), + } +} + +export type UIChartDataType = { + seriesName: string; + // coordinate chart + x?: any; + y?: any; + // pie or funnel + itemName?: any; + value?: any; +}; + +export type NonUIChartDataType = { + name: string; + value: any; +} + +export const lineChartChildrenMap = { + selectedPoints: stateComp>([]), + lastInteractionData: stateComp | NonUIChartDataType>({}), + onEvent: eventHandlerControl([clickEvent] as const), + ...chartUiModeChildren, + ...chartJsonModeChildren, +}; + +const chartUiChildrenMap = uiChildren(lineChartChildrenMap); +export type ChartCompPropsType = RecordConstructorToView; +export type ChartCompChildrenType = RecordConstructorToComp; diff --git a/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartPropertyView.tsx new file mode 100644 index 000000000..5a67d8ecf --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartPropertyView.tsx @@ -0,0 +1,187 @@ +import { changeChildAction, CompAction } from "lowcoder-core"; +import { ChartCompChildrenType, ChartTypeOptions,getDataKeys } from "./lineChartConstants"; +import { newSeries } from "./seriesComp"; +import { + CustomModal, + Dropdown, + hiddenPropertyView, + Option, + RedButton, + Section, + sectionNames, + controlItem, +} from "lowcoder-sdk"; +import { trans } from "i18n/comps"; + +export function lineChartPropertyView( + children: ChartCompChildrenType, + dispatch: (action: CompAction) => void +) { + const series = children.series.getView(); + const columnOptions = getDataKeys(children.data.getView()).map((key) => ({ + label: key, + value: key, + })); + + const uiModePropertyView = ( + <> +
+ {children.chartConfig.getPropertyView()} + {children.animationDuration.propertyView({label: trans("lineChart.animationDuration")})} + { + dispatch(changeChildAction("xAxisKey", value)); + }} + /> + {children.chartConfig.getView().subtype === "waterfall" && children.xAxisData.propertyView({ + label: "X-Label-Data" + })} +
+
+
+ {children.onUIEvent.propertyView({title: trans("chart.chartEventHandlers")})} +
+
+ {children.onEvent.propertyView()} +
+
+
+ {children.echartsTitleConfig.getPropertyView()} + {children.echartsTitleVerticalConfig.getPropertyView()} + {children.legendConfig.getPropertyView()} + {children.title.propertyView({ label: trans("chart.title") })} + {children.left.propertyView({ label: trans("chart.left"), tooltip: trans("echarts.leftTooltip") })} + {children.right.propertyView({ label: trans("chart.right"), tooltip: trans("echarts.rightTooltip") })} + {children.top.propertyView({ label: trans("chart.top"), tooltip: trans("echarts.topTooltip") })} + {children.bottom.propertyView({ label: trans("chart.bottom"), tooltip: trans("echarts.bottomTooltip") })} + {children.chartConfig.children.compType.getView() !== "pie" && ( + <> + {children.xAxisDirection.propertyView({ + label: trans("chart.xAxisDirection"), + radioButton: true, + })} + {children.xConfig.getPropertyView()} + {children.yConfig.getPropertyView()} + + )} + {hiddenPropertyView(children)} + {children.tooltip.propertyView({label: trans("echarts.tooltip"), tooltip: trans("echarts.tooltipTooltip")})} +
+
+ {children.chartStyle?.getPropertyView()} +
+
+ {children.titleStyle?.getPropertyView()} +
+
+ {children.xAxisStyle?.getPropertyView()} +
+
+ {children.yAxisStyle?.getPropertyView()} +
+
+ {children.legendStyle?.getPropertyView()} +
+
+ {children.data.propertyView({ + label: trans("chart.data"), + })} +
+ + ); + + const getChatConfigByMode = (mode: string) => { + switch(mode) { + case "ui": + return uiModePropertyView; + } + } + return ( + <> + {getChatConfigByMode(children.mode.getView())} + + ); +} diff --git a/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartUtils.ts b/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartUtils.ts new file mode 100644 index 000000000..3dfb3769e --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartUtils.ts @@ -0,0 +1,398 @@ +import { + CharOptionCompType, + ChartCompPropsType, + ChartSize, + noDataAxisConfig, + noDataPieChartConfig, +} from "comps/lineChartComp/lineChartConstants"; +import { getPieRadiusAndCenter } from "comps/basicChartComp/chartConfigs/pieChartConfig"; +import { EChartsOptionWithMap } from "../basicChartComp/reactEcharts/types"; +import _ from "lodash"; +import { chartColorPalette, isNumeric, JSONObject, loadScript } from "lowcoder-sdk"; +import { calcXYConfig } from "comps/basicChartComp/chartConfigs/cartesianAxisConfig"; +import Big from "big.js"; +import { googleMapsApiUrl } from "../basicChartComp/chartConfigs/chartUrls"; +import opacityToHex from "../../util/opacityToHex"; +import parseBackground from "../../util/gradientBackgroundColor"; +import {ba, s} from "@fullcalendar/core/internal-common"; +import {chartStyleWrapper, styleWrapper} from "../../util/styleWrapper"; + +export function transformData( + originData: JSONObject[], + xAxis: string, + seriesColumnNames: string[] +) { + // aggregate data by x-axis + const transformedData: JSONObject[] = []; + originData.reduce((prev, cur) => { + if (cur === null || cur === undefined) { + return prev; + } + const groupValue = cur[xAxis] as string; + if (!prev[groupValue]) { + // init as 0 + const initValue: any = {}; + seriesColumnNames.forEach((name) => { + initValue[name] = 0; + }); + prev[groupValue] = initValue; + transformedData.push(prev[groupValue]); + } + // remain the x-axis data + prev[groupValue][xAxis] = groupValue; + seriesColumnNames.forEach((key) => { + if (key === xAxis) { + return; + } else if (isNumeric(cur[key])) { + const bigNum = Big(cur[key]); + prev[groupValue][key] = bigNum.add(prev[groupValue][key]).toNumber(); + } else { + prev[groupValue][key] += 1; + } + }); + return prev; + }, {} as any); + return transformedData; +} + +const notAxisChartSet: Set = new Set(["pie"] as const); +const notAxisChartSubtypeSet: Set = new Set(["polar"] as const); +export const echartsConfigOmitChildren = [ + "hidden", + "selectedPoints", + "onUIEvent", + "mapInstance" +] as const; +type EchartsConfigProps = Omit; + + +export function isAxisChart(type: CharOptionCompType, polar: boolean) { + return !notAxisChartSet.has(type) && !polar; +} + +export function getSeriesConfig(props: EchartsConfigProps) { + let visibleSeries = props.series.filter((s) => !s.getView().hide); + if(props.chartConfig.subtype === "waterfall") { + const seriesOn = visibleSeries[0]; + const seriesPlaceholder = visibleSeries[0]; + visibleSeries = [seriesPlaceholder, seriesOn]; + } + const seriesLength = visibleSeries.length; + return visibleSeries.map((s, index) => { + if (isAxisChart(props.chartConfig.type, props.chartConfig.polarData.polar)) { + let encodeX: string, encodeY: string; + const horizontalX = props.xAxisDirection === "horizontal"; + let itemStyle = props.chartConfig.itemStyle; + + if (horizontalX) { + encodeX = props.xAxisKey; + encodeY = s.getView().columnName; + } else { + encodeX = s.getView().columnName; + encodeY = props.xAxisKey; + } + const markLineData = s.getView().markLines.map(line => ({type: line.getView().type})); + const markAreaData = s.getView().markAreas.map(area => ([{name: area.getView().name, [horizontalX?"xAxis":"yAxis"]: area.getView().from, label: { + position: horizontalX?"top":"right", + }}, {[horizontalX?"xAxis":"yAxis"]: area.getView().to}])); + return { + name: s.getView().seriesName, + columnName: s.getView().columnName, + selectedMode: "single", + select: { + itemStyle: { + borderColor: "#000", + }, + }, + step: s.getView().step, + encode: { + x: encodeX, + y: encodeY, + }, + markLine: { + data: markLineData, + }, + markArea: { + itemStyle: { + color: 'rgba(255, 173, 177, 0.4)', + }, + data: markAreaData, + }, + // each type of chart's config + ...props.chartConfig, + itemStyle: itemStyle, + label: { + ...props.chartConfig.label, + ...(!horizontalX && { position: "outside" }), + }, + }; + } else { + const radiusAndCenter = getPieRadiusAndCenter(seriesLength, index, props.chartConfig); + return { + ...props.chartConfig, + columnName: s.getView().columnName, + radius: radiusAndCenter.radius, + center: radiusAndCenter.center, + name: s.getView().seriesName, + selectedMode: "single", + encode: { + itemName: props.xAxisKey, + value: s.getView().columnName, + }, + }; + } + }); +} + +// https://echarts.apache.org/en/option.html +export function getEchartsConfig( + props: EchartsConfigProps, + chartSize?: ChartSize, + theme?: any, +): EChartsOptionWithMap { + // axisChart + const axisChart = isAxisChart(props.chartConfig.type, props.chartConfig.polarData.polar); + const gridPos = { + left: `${props?.left}%`, + right: `${props?.right}%`, + bottom: `${props?.bottom}%`, + top: `${props?.top}%`, + }; + + let config: any = { + title: { + text: props.title, + top: props.echartsTitleVerticalConfig.top, + left:props.echartsTitleConfig.top, + textStyle: { + ...styleWrapper(props?.titleStyle, theme?.titleStyle) + } + }, + backgroundColor: parseBackground( props?.chartStyle?.background || theme?.chartStyle?.backgroundColor || "#FFFFFF"), + legend: { + ...props.legendConfig, + textStyle: { + ...styleWrapper(props?.legendStyle, theme?.legendStyle, 15) + } + }, + tooltip: props.tooltip && { + trigger: "axis", + axisPointer: { + type: "line", + lineStyle: { + color: "rgba(0,0,0,0.2)", + width: 2, + type: "solid" + } + } + }, + grid: { + ...gridPos, + containLabel: true, + }, + animationDuration: props.animationDuration, + }; + if (props.areaPieces.length > 0) { + config.visualMap = { + type: 'piecewise', + show: false, + dimension: 0, + seriesIndex: 0, + pieces: props.areaPieces?.filter(p => p.getView().from && p.getView().to && p.getView().color)?.map(p => ( + { + ...(p.getView().from?{min: parseInt(p.getView().from)}:{}), + ...(p.getView().to?{max: parseInt(p.getView().to)}:{}), + ...(p.getView().color?{color: p.getView().color}:{}), + } + )) + } + } + if(props.chartConfig.race) { + config = { + ...config, + // Disable init animation. + animationDuration: 0, + animationDurationUpdate: 2000, + animationEasing: 'linear', + animationEasingUpdate: 'linear', + } + } + if (props.data.length <= 0) { + // no data + return { + ...config, + ...(axisChart ? noDataAxisConfig : noDataPieChartConfig), + }; + } + const yAxisConfig = props.yConfig(); + const seriesColumnNames = props.series + .filter((s) => !s.getView().hide) + .map((s) => s.getView().columnName); + // y-axis is category and time, data doesn't need to aggregate + let transformedData = + yAxisConfig.type === "category" || yAxisConfig.type === "time" ? props.data : transformData(props.data, props.xAxisKey, seriesColumnNames); + + if(props.chartConfig.polarData.polar) { + config = { + ...config, + polar: { + radius: [props.chartConfig.polarData.polarRadiusStart, props.chartConfig.polarData.polarRadiusEnd], + }, + radiusAxis: { + type: props.chartConfig.polarData.polarIsTangent?'category':undefined, + data: props.chartConfig.polarData.polarIsTangent && props.chartConfig.polarData.labelData.length!==0?props.chartConfig.polarData.labelData:undefined, + max: props.chartConfig.polarData.polarIsTangent?undefined:props.chartConfig.polarData.radiusAxisMax || undefined, + }, + angleAxis: { + type: props.chartConfig.polarData.polarIsTangent?undefined:'category', + data: !props.chartConfig.polarData.polarIsTangent && props.chartConfig.polarData.labelData.length!==0?props.chartConfig.polarData.labelData:undefined, + max: props.chartConfig.polarData.polarIsTangent?props.chartConfig.polarData.radiusAxisMax || undefined:undefined, + startAngle: props.chartConfig.polarData.polarStartAngle, + endAngle: props.chartConfig.polarData.polarEndAngle, + }, + } + } + + config = { + ...config, + dataset: [ + { + source: transformedData, + sourceHeader: false, + }, + ], + series: getSeriesConfig(props).map(series => ({ + ...series, + encode: { + ...series.encode, + y: series.columnName, + }, + itemStyle: { + ...series.itemStyle, + // ...chartStyleWrapper(props?.chartStyle, theme?.chartStyle) + }, + lineStyle: { + ...chartStyleWrapper(props?.chartStyle, theme?.chartStyle) + }, + data: transformedData.map((i: any) => i[series.columnName]) + })), + }; + if (axisChart) { + // pure chart's size except the margin around + let chartRealSize; + if (chartSize) { + const rightSize = + typeof gridPos.right === "number" + ? gridPos.right + : (chartSize.w * parseFloat(gridPos.right)) / 100.0; + chartRealSize = { + // actually it's self-adaptive with the x-axis label on the left, not that accurate but work + w: chartSize.w - gridPos.left - rightSize, + // also self-adaptive on the bottom + h: chartSize.h - gridPos.top - gridPos.bottom, + right: rightSize, + }; + } + const finalXyConfig = calcXYConfig( + props.xConfig, + yAxisConfig, + props.xAxisDirection, + transformedData.map((d) => d[props.xAxisKey]), + chartRealSize + ); + config = { + ...config, + // @ts-ignore + xAxis: { + ...finalXyConfig.xConfig, + axisLabel: { + ...styleWrapper(props?.xAxisStyle, theme?.xAxisStyle, 11) + }, + data: finalXyConfig.xConfig.type === "category" && (props.xAxisData as []).length!==0?props?.xAxisData:transformedData.map((i: any) => i[props.xAxisKey]), + }, + // @ts-ignore + yAxis: { + ...finalXyConfig.yConfig, + axisLabel: { + ...styleWrapper(props?.yAxisStyle, theme?.yAxisStyle, 11) + }, + data: finalXyConfig.yConfig.type === "category" && (props.xAxisData as []).length!==0?props?.xAxisData:transformedData.map((i: any) => i[props.xAxisKey]), + }, + }; + + if(props.chartConfig.race) { + config = { + ...config, + xAxis: { + ...config.xAxis, + animationDuration: 300, + animationDurationUpdate: 300 + }, + yAxis: { + ...config.yAxis, + animationDuration: 300, + animationDurationUpdate: 300 + }, + } + } + } + + // console.log("Echarts transformedData and config", transformedData, config); + return config; +} + +export function getSelectedPoints(param: any, option: any) { + const series = option.series; + const dataSource = _.isArray(option.dataset) && option.dataset[0]?.source; + if (series && dataSource) { + return param.selected.flatMap((selectInfo: any) => { + const seriesInfo = series[selectInfo.seriesIndex]; + if (!seriesInfo || !seriesInfo.encode) { + return []; + } + return selectInfo.dataIndex.map((index: any) => { + const commonResult = { + seriesName: seriesInfo.name, + }; + if (seriesInfo.encode.itemName && seriesInfo.encode.value) { + return { + ...commonResult, + itemName: dataSource[index][seriesInfo.encode.itemName], + value: dataSource[index][seriesInfo.encode.value], + }; + } else { + return { + ...commonResult, + x: dataSource[index][seriesInfo.encode.x], + y: dataSource[index][seriesInfo.encode.y], + }; + } + }); + }); + } + return []; +} + +export function loadGoogleMapsScript(apiKey: string) { + const mapsUrl = `${googleMapsApiUrl}?key=${apiKey}`; + const scripts = document.getElementsByTagName('script'); + // is script already loaded + let scriptIndex = _.findIndex(scripts, (script) => script.src.endsWith(mapsUrl)); + if(scriptIndex > -1) { + return scripts[scriptIndex]; + } + // is script loaded with diff api_key, remove the script and load again + scriptIndex = _.findIndex(scripts, (script) => script.src.startsWith(googleMapsApiUrl)); + if(scriptIndex > -1) { + scripts[scriptIndex].remove(); + } + + const script = document.createElement("script"); + script.type = "text/javascript"; + script.src = mapsUrl; + script.async = true; + script.defer = true; + window.document.body.appendChild(script); + + return script; +} diff --git a/client/packages/lowcoder-comps/src/comps/lineChartComp/seriesComp.tsx b/client/packages/lowcoder-comps/src/comps/lineChartComp/seriesComp.tsx new file mode 100644 index 000000000..5a61774f5 --- /dev/null +++ b/client/packages/lowcoder-comps/src/comps/lineChartComp/seriesComp.tsx @@ -0,0 +1,280 @@ +import { + BoolControl, + StringControl, + list, + isNumeric, + genRandomKey, + Dropdown, + Option, + RedButton, + CustomModal, + MultiCompBuilder, + valueComp, + dropdownControl, +} from "lowcoder-sdk"; +import { trans } from "i18n/comps"; + +import { ConstructorToComp, ConstructorToDataType, ConstructorToView } from "lowcoder-core"; +import { CompAction, CustomAction, customAction, isMyCustomAction } from "lowcoder-core"; + +export type SeriesCompType = ConstructorToComp; +export type RawSeriesCompType = ConstructorToView; +type SeriesDataType = ConstructorToDataType; +type MarkLineDataType = ConstructorToDataType; + +type ActionDataType = { + type: "chartDataChanged"; + chartData: Array; +}; + +export function newSeries(name: string, columnName: string): SeriesDataType { + return { + seriesName: name, + columnName: columnName, + dataIndex: genRandomKey(), + }; +} + +export function newMarkLine(type: string): MarkLineDataType { + return { + type, + dataIndex: genRandomKey(), + }; +} + +export const MarkLineTypeOptions = [ + { + label: trans("lineChart.max"), + value: "max", + }, + { + label: trans("lineChart.average"), + value: "average", + }, + { + label: trans("lineChart.min"), + value: "min", + }, +] as const; + +export const StepOptions = [ + { + label: trans("lineChart.none"), + value: "", + }, + { + label: trans("lineChart.start"), + value: "start", + }, + { + label: trans("lineChart.middle"), + value: "middle", + }, + { + label: trans("lineChart.end"), + value: "end", + }, +] as const; + +const valToLabel = (val) => MarkLineTypeOptions.find(o => o.value === val)?.label || ""; +const markLinesChildrenMap = { + type: dropdownControl(MarkLineTypeOptions, "max"), + // unique key, for sort + dataIndex: valueComp(""), +}; +const MarkLinesTmpComp = new MultiCompBuilder(markLinesChildrenMap, (props) => { + return props; +}) + .setPropertyViewFn((children: any) => { + return <>{children.type.propertyView({label: trans("lineChart.type")})}; + }) + .build(); +const markAreasChildrenMap = { + name: StringControl, + from: StringControl, + to: StringControl, + // unique key, for sort + dataIndex: valueComp(""), +}; +const MarkAreasTmpComp = new MultiCompBuilder(markAreasChildrenMap, (props) => { + return props; +}) + .setPropertyViewFn((children: any) => + (<> + {children.name.propertyView({label: trans("lineChart.name")})} + {children.from.propertyView({label: trans("lineChart.from")})} + {children.to.propertyView({label: trans("lineChart.to")})} + ) + ) + .build(); + + +export function newMarkArea(): MarkLineDataType { + return { + dataIndex: genRandomKey(), + }; +} + +const seriesChildrenMap = { + columnName: StringControl, + seriesName: StringControl, + markLines: list(MarkLinesTmpComp), + markAreas: list(MarkAreasTmpComp), + hide: BoolControl, + // unique key, for sort + dataIndex: valueComp(""), + step: dropdownControl(StepOptions, ""), +}; + +const SeriesTmpComp = new MultiCompBuilder(seriesChildrenMap, (props) => { + return props; +}) + .setPropertyViewFn(() => { + return <>; + }) + .build(); + +class SeriesComp extends SeriesTmpComp { + getPropertyViewWithData(columnOptions: OptionsType): React.ReactNode { + return ( + <> + {this.children.seriesName.propertyView({ + label: trans("chart.seriesName"), + })} + { + this.children.columnName.dispatchChangeValueAction(value); + }} + /> + {this.children.step.propertyView({ + label: trans("lineChart.step"), + })} +