|
| 1 | +import { |
| 2 | + CharOptionCompType, |
| 3 | + ChartCompPropsType, |
| 4 | + ChartSize, |
| 5 | + noDataAxisConfig, |
| 6 | + noDataPieChartConfig, |
| 7 | +} from "comps/chartComp/chartConstants"; |
| 8 | +import { getPieRadiusAndCenter } from "comps/chartComp/chartConfigs/pieChartConfig"; |
| 9 | +import { EChartsOptionWithMap } from "../chartComp/reactEcharts/types"; |
| 10 | +import _ from "lodash"; |
| 11 | +import { chartColorPalette, isNumeric, JSONObject, loadScript } from "lowcoder-sdk"; |
| 12 | +import { calcXYConfig } from "comps/chartComp/chartConfigs/cartesianAxisConfig"; |
| 13 | +import Big from "big.js"; |
| 14 | +import { googleMapsApiUrl } from "../chartComp/chartConfigs/chartUrls"; |
| 15 | + |
| 16 | +export function transformData( |
| 17 | + originData: JSONObject[], |
| 18 | + xAxis: string, |
| 19 | + seriesColumnNames: string[] |
| 20 | +) { |
| 21 | + // aggregate data by x-axis |
| 22 | + const transformedData: JSONObject[] = []; |
| 23 | + originData.reduce((prev, cur) => { |
| 24 | + if (cur === null || cur === undefined) { |
| 25 | + return prev; |
| 26 | + } |
| 27 | + const groupValue = cur[xAxis] as string; |
| 28 | + if (!prev[groupValue]) { |
| 29 | + // init as 0 |
| 30 | + const initValue: any = {}; |
| 31 | + seriesColumnNames.forEach((name) => { |
| 32 | + initValue[name] = 0; |
| 33 | + }); |
| 34 | + prev[groupValue] = initValue; |
| 35 | + transformedData.push(prev[groupValue]); |
| 36 | + } |
| 37 | + // remain the x-axis data |
| 38 | + prev[groupValue][xAxis] = groupValue; |
| 39 | + seriesColumnNames.forEach((key) => { |
| 40 | + if (key === xAxis) { |
| 41 | + return; |
| 42 | + } else if (isNumeric(cur[key])) { |
| 43 | + const bigNum = Big(cur[key]); |
| 44 | + prev[groupValue][key] = bigNum.add(prev[groupValue][key]).toNumber(); |
| 45 | + } else { |
| 46 | + prev[groupValue][key] += 1; |
| 47 | + } |
| 48 | + }); |
| 49 | + return prev; |
| 50 | + }, {} as any); |
| 51 | + return transformedData; |
| 52 | +} |
| 53 | + |
| 54 | +const notAxisChartSet: Set<CharOptionCompType> = new Set(["pie"] as const); |
| 55 | +export const echartsConfigOmitChildren = [ |
| 56 | + "hidden", |
| 57 | + "selectedPoints", |
| 58 | + "onUIEvent", |
| 59 | + "mapInstance" |
| 60 | +] as const; |
| 61 | +type EchartsConfigProps = Omit<ChartCompPropsType, typeof echartsConfigOmitChildren[number]>; |
| 62 | + |
| 63 | +export function isAxisChart(type: CharOptionCompType) { |
| 64 | + return !notAxisChartSet.has(type); |
| 65 | +} |
| 66 | + |
| 67 | +export function getSeriesConfig(props: EchartsConfigProps) { |
| 68 | + const visibleSeries = props.series.filter((s) => !s.getView().hide); |
| 69 | + const seriesLength = visibleSeries.length; |
| 70 | + return visibleSeries.map((s, index) => { |
| 71 | + if (isAxisChart(props.chartConfig.type)) { |
| 72 | + let encodeX: string, encodeY: string; |
| 73 | + const horizontalX = props.xAxisDirection === "horizontal"; |
| 74 | + let itemStyle = props.chartConfig.itemStyle; |
| 75 | + // FIXME: need refactor... chartConfig returns a function with paramters |
| 76 | + if (props.chartConfig.type === "bar") { |
| 77 | + // barChart's border radius, depend on x-axis direction and stack state |
| 78 | + const borderRadius = horizontalX ? [2, 2, 0, 0] : [0, 2, 2, 0]; |
| 79 | + if (props.chartConfig.stack && index === visibleSeries.length - 1) { |
| 80 | + itemStyle = { ...itemStyle, borderRadius: borderRadius }; |
| 81 | + } else if (!props.chartConfig.stack) { |
| 82 | + itemStyle = { ...itemStyle, borderRadius: borderRadius }; |
| 83 | + } |
| 84 | + } |
| 85 | + if (horizontalX) { |
| 86 | + encodeX = props.xAxisKey; |
| 87 | + encodeY = s.getView().columnName; |
| 88 | + } else { |
| 89 | + encodeX = s.getView().columnName; |
| 90 | + encodeY = props.xAxisKey; |
| 91 | + } |
| 92 | + return { |
| 93 | + name: s.getView().seriesName, |
| 94 | + selectedMode: "single", |
| 95 | + select: { |
| 96 | + itemStyle: { |
| 97 | + borderColor: "#000", |
| 98 | + }, |
| 99 | + }, |
| 100 | + encode: { |
| 101 | + x: encodeX, |
| 102 | + y: encodeY, |
| 103 | + }, |
| 104 | + // each type of chart's config |
| 105 | + ...props.chartConfig, |
| 106 | + itemStyle: itemStyle, |
| 107 | + label: { |
| 108 | + ...props.chartConfig.label, |
| 109 | + ...(!horizontalX && { position: "outside" }), |
| 110 | + }, |
| 111 | + }; |
| 112 | + } else { |
| 113 | + // pie |
| 114 | + const radiusAndCenter = getPieRadiusAndCenter(seriesLength, index, props.chartConfig); |
| 115 | + return { |
| 116 | + ...props.chartConfig, |
| 117 | + radius: radiusAndCenter.radius, |
| 118 | + center: radiusAndCenter.center, |
| 119 | + name: s.getView().seriesName, |
| 120 | + selectedMode: "single", |
| 121 | + encode: { |
| 122 | + itemName: props.xAxisKey, |
| 123 | + value: s.getView().columnName, |
| 124 | + }, |
| 125 | + }; |
| 126 | + } |
| 127 | + }); |
| 128 | +} |
| 129 | + |
| 130 | +// https://echarts.apache.org/en/option.html |
| 131 | +export function getEchartsConfig(props: EchartsConfigProps, chartSize?: ChartSize): EChartsOptionWithMap { |
| 132 | + if (props.mode === "json") { |
| 133 | + let opt={ |
| 134 | + "title": { |
| 135 | + "text": props.echartsTitle, |
| 136 | + 'top': props.echartsLegendConfig.top === 'bottom' ?'top':'bottom', |
| 137 | + "left":"center" |
| 138 | + }, |
| 139 | + "backgroundColor": props?.style?.background, |
| 140 | + "color": props.echartsOption.data?.map(data => data.color), |
| 141 | + "tooltip": props.tooltip&& { |
| 142 | + "trigger": "item" |
| 143 | + }, |
| 144 | + 'series': [ |
| 145 | + { |
| 146 | + "type": "graph", |
| 147 | + "layout": "force", |
| 148 | + "force": { |
| 149 | + "repulsion": 100, |
| 150 | + "gravity": 0.1, |
| 151 | + "edgeLength": 100 |
| 152 | + }, |
| 153 | + 'categories': props.echartsOption.categories, |
| 154 | + 'links': props.echartsOption.links, |
| 155 | + 'nodes': props.echartsOption.nodes, |
| 156 | + } |
| 157 | + ] |
| 158 | +} |
| 159 | + return props.echartsOption ? opt : {}; |
| 160 | + |
| 161 | + } |
| 162 | + |
| 163 | + if(props.mode === "map") { |
| 164 | + const { |
| 165 | + mapZoomLevel, |
| 166 | + mapCenterLat, |
| 167 | + mapCenterLng, |
| 168 | + mapOptions, |
| 169 | + showCharts, |
| 170 | + } = props; |
| 171 | + |
| 172 | + const echartsOption = mapOptions && showCharts ? mapOptions : {}; |
| 173 | + return { |
| 174 | + gmap: { |
| 175 | + center: [mapCenterLng, mapCenterLat], |
| 176 | + zoom: mapZoomLevel, |
| 177 | + renderOnMoving: true, |
| 178 | + echartsLayerZIndex: showCharts ? 2019 : 0, |
| 179 | + roam: true |
| 180 | + }, |
| 181 | + ...echartsOption, |
| 182 | + } |
| 183 | + } |
| 184 | + // axisChart |
| 185 | + const axisChart = isAxisChart(props.chartConfig.type); |
| 186 | + const gridPos = { |
| 187 | + left: 20, |
| 188 | + right: props.legendConfig.left === "right" ? "10%" : 20, |
| 189 | + top: 50, |
| 190 | + bottom: 35, |
| 191 | + }; |
| 192 | + let config: EChartsOptionWithMap = { |
| 193 | + title: { text: props.title, left: "center" }, |
| 194 | + tooltip: { |
| 195 | + confine: true, |
| 196 | + trigger: axisChart ? "axis" : "item", |
| 197 | + }, |
| 198 | + legend: props.legendConfig, |
| 199 | + grid: { |
| 200 | + ...gridPos, |
| 201 | + containLabel: true, |
| 202 | + }, |
| 203 | + }; |
| 204 | + if (props.data.length <= 0) { |
| 205 | + // no data |
| 206 | + return { |
| 207 | + ...config, |
| 208 | + ...(axisChart ? noDataAxisConfig : noDataPieChartConfig), |
| 209 | + }; |
| 210 | + } |
| 211 | + const yAxisConfig = props.yConfig(); |
| 212 | + const seriesColumnNames = props.series |
| 213 | + .filter((s) => !s.getView().hide) |
| 214 | + .map((s) => s.getView().columnName); |
| 215 | + // y-axis is category and time, data doesn't need to aggregate |
| 216 | + const transformedData = |
| 217 | + yAxisConfig.type === "category" || yAxisConfig.type === "time" |
| 218 | + ? props.data |
| 219 | + : transformData(props.data, props.xAxisKey, seriesColumnNames); |
| 220 | + config = { |
| 221 | + ...config, |
| 222 | + dataset: [ |
| 223 | + { |
| 224 | + source: transformedData, |
| 225 | + sourceHeader: false, |
| 226 | + }, |
| 227 | + ], |
| 228 | + series: getSeriesConfig(props), |
| 229 | + }; |
| 230 | + if (axisChart) { |
| 231 | + // pure chart's size except the margin around |
| 232 | + let chartRealSize; |
| 233 | + if (chartSize) { |
| 234 | + const rightSize = |
| 235 | + typeof gridPos.right === "number" |
| 236 | + ? gridPos.right |
| 237 | + : (chartSize.w * parseFloat(gridPos.right)) / 100.0; |
| 238 | + chartRealSize = { |
| 239 | + // actually it's self-adaptive with the x-axis label on the left, not that accurate but work |
| 240 | + w: chartSize.w - gridPos.left - rightSize, |
| 241 | + // also self-adaptive on the bottom |
| 242 | + h: chartSize.h - gridPos.top - gridPos.bottom, |
| 243 | + right: rightSize, |
| 244 | + }; |
| 245 | + } |
| 246 | + const finalXyConfig = calcXYConfig( |
| 247 | + props.xConfig, |
| 248 | + yAxisConfig, |
| 249 | + props.xAxisDirection, |
| 250 | + transformedData.map((d) => d[props.xAxisKey]), |
| 251 | + chartRealSize |
| 252 | + ); |
| 253 | + config = { |
| 254 | + ...config, |
| 255 | + // @ts-ignore |
| 256 | + xAxis: finalXyConfig.xConfig, |
| 257 | + // @ts-ignore |
| 258 | + yAxis: finalXyConfig.yConfig, |
| 259 | + }; |
| 260 | + } |
| 261 | + // log.log("Echarts transformedData and config", transformedData, config); |
| 262 | + return config; |
| 263 | +} |
| 264 | + |
| 265 | +export function getSelectedPoints(param: any, option: any) { |
| 266 | + const series = option.series; |
| 267 | + const dataSource = _.isArray(option.dataset) && option.dataset[0]?.source; |
| 268 | + if (series && dataSource) { |
| 269 | + return param.selected.flatMap((selectInfo: any) => { |
| 270 | + const seriesInfo = series[selectInfo.seriesIndex]; |
| 271 | + if (!seriesInfo || !seriesInfo.encode) { |
| 272 | + return []; |
| 273 | + } |
| 274 | + return selectInfo.dataIndex.map((index: any) => { |
| 275 | + const commonResult = { |
| 276 | + seriesName: seriesInfo.name, |
| 277 | + }; |
| 278 | + if (seriesInfo.encode.itemName && seriesInfo.encode.value) { |
| 279 | + return { |
| 280 | + ...commonResult, |
| 281 | + itemName: dataSource[index][seriesInfo.encode.itemName], |
| 282 | + value: dataSource[index][seriesInfo.encode.value], |
| 283 | + }; |
| 284 | + } else { |
| 285 | + return { |
| 286 | + ...commonResult, |
| 287 | + x: dataSource[index][seriesInfo.encode.x], |
| 288 | + y: dataSource[index][seriesInfo.encode.y], |
| 289 | + }; |
| 290 | + } |
| 291 | + }); |
| 292 | + }); |
| 293 | + } |
| 294 | + return []; |
| 295 | +} |
| 296 | + |
| 297 | +export function loadGoogleMapsScript(apiKey: string) { |
| 298 | + const mapsUrl = `${googleMapsApiUrl}?key=${apiKey}`; |
| 299 | + const scripts = document.getElementsByTagName('script'); |
| 300 | + // is script already loaded |
| 301 | + let scriptIndex = _.findIndex(scripts, (script) => script.src.endsWith(mapsUrl)); |
| 302 | + if(scriptIndex > -1) { |
| 303 | + return scripts[scriptIndex]; |
| 304 | + } |
| 305 | + // is script loaded with diff api_key, remove the script and load again |
| 306 | + scriptIndex = _.findIndex(scripts, (script) => script.src.startsWith(googleMapsApiUrl)); |
| 307 | + if(scriptIndex > -1) { |
| 308 | + scripts[scriptIndex].remove(); |
| 309 | + } |
| 310 | + |
| 311 | + const script = document.createElement("script"); |
| 312 | + script.type = "text/javascript"; |
| 313 | + script.src = mapsUrl; |
| 314 | + script.async = true; |
| 315 | + script.defer = true; |
| 316 | + window.document.body.appendChild(script); |
| 317 | + |
| 318 | + return script; |
| 319 | +} |
0 commit comments