bito-code-review[bot] commented on code in PR #37868:
URL: https://github.com/apache/superset/pull/37868#discussion_r2879878095
##########
superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx:
##########
@@ -251,6 +251,7 @@ const config: ControlPanelConfig = {
},
},
],
+ ['echart_options'],
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Broken custom ECharts options control</b></div>
<div id="fix">
The added 'echart_options' control allows users to input custom ECharts
options for Area charts, matching the implementation in other timeseries chart
types. However, due to a bug in transformProps.ts where the destructuring
accesses 'echartOptions' instead of 'echart_options', the custom options will
not be applied. This affects all charts using this control, not just Area.
</div>
</div>
<small><i>Code Review Run #048e53</i></small>
</div>
---
Should Bito avoid suggestions like this for future reviews? (<a
href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
- [ ] Yes, avoid them
##########
superset-frontend/plugins/plugin-chart-echarts/src/utils/mergeCustomEChartOptions.ts:
##########
@@ -0,0 +1,79 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CustomEChartOptions } from './eChartOptionsTypes';
+
+type PlainObject = Record<string, unknown>;
+
+function isPlainObject(value: unknown): value is PlainObject {
+ return (
+ typeof value === 'object' &&
+ value !== null &&
+ !Array.isArray(value) &&
+ Object.prototype.toString.call(value) === '[object Object]'
+ );
+}
+
+/**
+ * Deep merges custom EChart options into base options.
+ * Arrays are replaced entirely, objects are merged recursively.
+ *
+ * @param baseOptions - The base ECharts options object
+ * @param customOptions - Custom options to merge (from safeParseEChartOptions)
+ * @returns Merged ECharts options
+ */
+export function mergeCustomEChartOptions<T extends EChartsCoreOption>(
+ baseOptions: T,
+ customOptions: CustomEChartOptions | undefined,
+): T & Partial<CustomEChartOptions> {
+ type MergedResult = T & Partial<CustomEChartOptions>;
+
+ if (!customOptions) {
+ return baseOptions as MergedResult;
+ }
+
+ const result = { ...baseOptions } as MergedResult;
+
+ for (const key of Object.keys(customOptions) as Array<
+ keyof typeof customOptions
+ >) {
+ const customValue = customOptions[key];
+ const baseValue = result[key as keyof T];
+
+ if (customValue === undefined) {
+ continue;
+ }
+
+ if (isPlainObject(customValue) && isPlainObject(baseValue)) {
+ // Recursively merge nested objects
+ (result as PlainObject)[key] = mergeCustomEChartOptions(
+ baseValue as EChartsCoreOption,
+ customValue as CustomEChartOptions,
+ );
+ } else {
+ // Replace arrays and primitive values directly
+ (result as PlainObject)[key] = customValue;
+ }
+ }
+
+ return result;
+}
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Type Safety Violation in Recursive Merge</b></div>
<div id="fix">
Consider constraining the generic type parameter T to PlainObject and
updating the recursive merge call to cast both `baseValue` and `customValue` to
PlainObject. This will avoid incorrect casts to EChartsCoreOption in nested
merges and improve type safety.
</div>
</div>
<small><i>Code Review Run #048e53</i></small>
</div>
---
Should Bito avoid suggestions like this for future reviews? (<a
href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
- [ ] Yes, avoid them
##########
superset-frontend/src/explore/components/controls/JSEditorControl.test.tsx:
##########
@@ -0,0 +1,125 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { ReactNode } from 'react';
+import { render, screen, userEvent } from 'spec/helpers/testing-library';
+import JSEditorControl from 'src/explore/components/controls/JSEditorControl';
+
+jest.mock('react-virtualized-auto-sizer', () => ({
+ __esModule: true,
+ default: ({
+ children,
+ }: {
+ children: (params: { width: number; height: number }) => ReactNode;
+ }) => children({ width: 500, height: 250 }),
+}));
+
+jest.mock('src/core/editors', () => ({
+ EditorHost: ({
+ value,
+ onChange,
+ }: {
+ value: string;
+ onChange: (v: string) => void;
+ }) => (
+ <textarea
+ data-test="js-editor"
+ defaultValue={value}
+ onChange={e => onChange?.(e.target.value)}
+ />
+ ),
+}));
+
+jest.mock('src/hooks/useDebounceValue', () => ({
+ useDebounceValue: (value: string) => value,
+}));
+
+const defaultProps = {
+ name: 'echartOptions',
+ label: 'EChart Options',
+ onChange: jest.fn(),
+ value: '',
+};
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+test('renders the control with label', () => {
+ render(<JSEditorControl {...defaultProps} />);
+ expect(screen.getByText('EChart Options')).toBeInTheDocument();
+});
+
+test('renders the editor', () => {
+ render(<JSEditorControl {...defaultProps} />);
+ expect(screen.getByTestId('js-editor')).toBeInTheDocument();
+});
+
+test('renders with initial value', () => {
+ const value = "{ title: { text: 'Test' } }";
+ render(<JSEditorControl {...defaultProps} value={value} />);
+ const editor = screen.getByTestId('js-editor');
+ expect(editor).toHaveValue(value);
+});
+
+test('calls onChange when editor content changes', async () => {
+ render(<JSEditorControl {...defaultProps} />);
+ const editor = screen.getByTestId('js-editor');
+ await userEvent.type(editor, '{ }');
+ expect(defaultProps.onChange).toHaveBeenCalled();
+});
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Insufficient onChange test verification</b></div>
<div id="fix">
The test checks that onChange is called when editor content changes, but
doesn't verify the argument passed to it. Per BITO rule [6262], tests should
verify actual business logic behavior, not just that functions are called.
Since userEvent.type simulates typing '{ }' into an empty textarea, onChange
should be called with '{ }'.
</div>
<details>
<summary>
<b>Code suggestion</b>
</summary>
<blockquote>Check the AI-generated fix before applying</blockquote>
<div id="code">
````suggestion
test('calls onChange when editor content changes', async () => {
render(<JSEditorControl {...defaultProps} />);
const editor = screen.getByTestId('js-editor');
await userEvent.type(editor, '{ }');
expect(defaultProps.onChange).toHaveBeenCalled();
expect(defaultProps.onChange).toHaveBeenCalledWith('{ }');
});
````
</div>
</details>
</div>
<small><i>Code Review Run #048e53</i></small>
</div>
---
Should Bito avoid suggestions like this for future reviews? (<a
href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
- [ ] Yes, avoid them
##########
superset-frontend/plugins/plugin-chart-echarts/src/utils/eChartOptionsSchema.ts:
##########
@@ -0,0 +1,827 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Unified ECharts Options Schema
+ *
+ * This file serves as the single source of truth for:
+ * 1. Runtime validation (Zod schema)
+ * 2. TypeScript types (inferred from Zod)
+ *
+ * Reference: https://echarts.apache.org/en/option.html
+ */
+
+import { z } from 'zod';
+
+//
=============================================================================
+// Common Schemas
+//
=============================================================================
+
+/** Color value - hex, rgb, rgba, or named color */
+const colorSchema = z.string();
+
+/** Numeric or percentage string (e.g., '50%') */
+const numberOrPercentSchema = z.union([z.number(), z.string()]);
+
+/** Line type */
+const lineTypeSchema = z.union([
+ z.enum(['solid', 'dashed', 'dotted']),
+ z.array(z.number()),
+]);
+
+/** Font weight */
+const fontWeightSchema = z.union([
+ z.enum(['normal', 'bold', 'bolder', 'lighter']),
+ z.number().min(100).max(900),
+]);
+
+/** Font style */
+const fontStyleSchema = z.enum(['normal', 'italic', 'oblique']);
+
+/** Symbol type */
+const symbolTypeSchema = z.string();
+
+//
=============================================================================
+// Text Style Schema
+//
=============================================================================
+
+export const textStyleSchema = z.object({
+ color: colorSchema.optional(),
+ fontStyle: fontStyleSchema.optional(),
+ fontWeight: fontWeightSchema.optional(),
+ fontFamily: z.string().optional(),
+ fontSize: z.number().optional(),
+ lineHeight: z.number().optional(),
+ width: numberOrPercentSchema.optional(),
+ height: numberOrPercentSchema.optional(),
+ textBorderColor: colorSchema.optional(),
+ textBorderWidth: z.number().optional(),
+ textBorderType: lineTypeSchema.optional(),
+ textBorderDashOffset: z.number().optional(),
+ textShadowColor: colorSchema.optional(),
+ textShadowBlur: z.number().optional(),
+ textShadowOffsetX: z.number().optional(),
+ textShadowOffsetY: z.number().optional(),
+ overflow: z.enum(['none', 'truncate', 'break', 'breakAll']).optional(),
+ ellipsis: z.string().optional(),
+});
+
+//
=============================================================================
+// Style Schemas
+//
=============================================================================
+
+export const lineStyleSchema = z.object({
+ color: colorSchema.optional(),
+ width: z.number().optional(),
+ type: lineTypeSchema.optional(),
+ dashOffset: z.number().optional(),
+ cap: z.enum(['butt', 'round', 'square']).optional(),
+ join: z.enum(['bevel', 'round', 'miter']).optional(),
+ miterLimit: z.number().optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+ opacity: z.number().min(0).max(1).optional(),
+});
+
+export const areaStyleSchema = z.object({
+ color: z.union([colorSchema, z.array(colorSchema)]).optional(),
+ origin: z.union([z.enum(['auto', 'start', 'end']), z.number()]).optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+ opacity: z.number().min(0).max(1).optional(),
+});
+
+export const itemStyleSchema = z.object({
+ color: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderWidth: z.number().optional(),
+ borderType: lineTypeSchema.optional(),
+ borderRadius: z.union([z.number(), z.array(z.number())]).optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+ opacity: z.number().min(0).max(1).optional(),
+});
+
+//
=============================================================================
+// Label Schema
+//
=============================================================================
+
+export const labelSchema = z.object({
+ show: z.boolean().optional(),
+ position: z
+ .enum([
+ 'top',
+ 'left',
+ 'right',
+ 'bottom',
+ 'inside',
+ 'insideLeft',
+ 'insideRight',
+ 'insideTop',
+ 'insideBottom',
+ 'insideTopLeft',
+ 'insideBottomLeft',
+ 'insideTopRight',
+ 'insideBottomRight',
+ 'outside',
+ ])
+ .optional(),
+ distance: z.number().optional(),
+ rotate: z.number().optional(),
+ offset: z.array(z.number()).optional(),
+ formatter: z.string().optional(), // Only string formatters allowed, not
functions
+ color: colorSchema.optional(),
+ fontStyle: fontStyleSchema.optional(),
+ fontWeight: fontWeightSchema.optional(),
+ fontFamily: z.string().optional(),
+ fontSize: z.number().optional(),
+ lineHeight: z.number().optional(),
+});
+
+//
=============================================================================
+// Title Schema
+//
=============================================================================
+
+export const titleSchema = z.object({
+ id: z.string().optional(),
+ show: z.boolean().optional(),
+ text: z.string().optional(),
+ link: z.string().optional(),
+ target: z.enum(['self', 'blank']).optional(),
+ textStyle: textStyleSchema.optional(),
+ subtext: z.string().optional(),
+ sublink: z.string().optional(),
+ subtarget: z.enum(['self', 'blank']).optional(),
+ subtextStyle: textStyleSchema.optional(),
+ textAlign: z.enum(['left', 'center', 'right']).optional(),
+ textVerticalAlign: z.enum(['top', 'middle', 'bottom']).optional(),
+ triggerEvent: z.boolean().optional(),
+ padding: z.union([z.number(), z.array(z.number())]).optional(),
+ itemGap: z.number().optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ backgroundColor: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderWidth: z.number().optional(),
+ borderRadius: z.union([z.number(), z.array(z.number())]).optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+});
+
+//
=============================================================================
+// Legend Schema
+//
=============================================================================
+
+export const legendSchema = z.object({
+ id: z.string().optional(),
+ type: z.enum(['plain', 'scroll']).optional(),
+ show: z.boolean().optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ width: numberOrPercentSchema.optional(),
+ height: numberOrPercentSchema.optional(),
+ orient: z.enum(['horizontal', 'vertical']).optional(),
+ align: z.enum(['auto', 'left', 'right']).optional(),
+ padding: z.union([z.number(), z.array(z.number())]).optional(),
+ itemGap: z.number().optional(),
+ itemWidth: z.number().optional(),
+ itemHeight: z.number().optional(),
+ itemStyle: itemStyleSchema.optional(),
+ lineStyle: lineStyleSchema.optional(),
+ textStyle: textStyleSchema.optional(),
+ icon: symbolTypeSchema.optional(),
+ selectedMode: z
+ .union([z.boolean(), z.enum(['single', 'multiple', 'series'])])
+ .optional(),
+ inactiveColor: colorSchema.optional(),
+ inactiveBorderColor: colorSchema.optional(),
+ inactiveBorderWidth: z.number().optional(),
+ backgroundColor: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderWidth: z.number().optional(),
+ borderRadius: z.union([z.number(), z.array(z.number())]).optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+ pageButtonItemGap: z.number().optional(),
+ pageButtonGap: z.number().optional(),
+ pageButtonPosition: z.enum(['start', 'end']).optional(),
+ pageIconColor: colorSchema.optional(),
+ pageIconInactiveColor: colorSchema.optional(),
+ pageIconSize: z.union([z.number(), z.array(z.number())]).optional(),
+ pageTextStyle: textStyleSchema.optional(),
+});
+
+//
=============================================================================
+// Grid Schema
+//
=============================================================================
+
+export const gridSchema = z.object({
+ id: z.string().optional(),
+ show: z.boolean().optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ width: numberOrPercentSchema.optional(),
+ height: numberOrPercentSchema.optional(),
+ containLabel: z.boolean().optional(),
+ backgroundColor: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderWidth: z.number().optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+});
+
+//
=============================================================================
+// Axis Schemas
+//
=============================================================================
+
+const axisLineSchema = z.object({
+ show: z.boolean().optional(),
+ onZero: z.boolean().optional(),
+ onZeroAxisIndex: z.number().optional(),
+ symbol: z.union([z.string(), z.array(z.string())]).optional(),
+ symbolSize: z.array(z.number()).optional(),
+ symbolOffset: z.union([z.number(), z.array(z.number())]).optional(),
+ lineStyle: lineStyleSchema.optional(),
+});
+
+const axisTickSchema = z.object({
+ show: z.boolean().optional(),
+ alignWithLabel: z.boolean().optional(),
+ interval: z.union([z.number(), z.literal('auto')]).optional(),
+ inside: z.boolean().optional(),
+ length: z.number().optional(),
+ lineStyle: lineStyleSchema.optional(),
+});
+
+const axisLabelSchema = z.object({
+ show: z.boolean().optional(),
+ interval: z.union([z.number(), z.literal('auto')]).optional(),
+ inside: z.boolean().optional(),
+ rotate: z.number().optional(),
+ margin: z.number().optional(),
+ formatter: z.string().optional(), // Only string formatters
+ showMinLabel: z.boolean().optional(),
+ showMaxLabel: z.boolean().optional(),
+ hideOverlap: z.boolean().optional(),
+ color: colorSchema.optional(),
+ fontStyle: fontStyleSchema.optional(),
+ fontWeight: fontWeightSchema.optional(),
+ fontFamily: z.string().optional(),
+ fontSize: z.number().optional(),
+ align: z.enum(['left', 'center', 'right']).optional(),
+ verticalAlign: z.enum(['top', 'middle', 'bottom']).optional(),
+});
+
+const splitLineSchema = z.object({
+ show: z.boolean().optional(),
+ interval: z.union([z.number(), z.literal('auto')]).optional(),
+ lineStyle: lineStyleSchema.optional(),
+});
+
+const splitAreaSchema = z.object({
+ show: z.boolean().optional(),
+ interval: z.union([z.number(), z.literal('auto')]).optional(),
+ areaStyle: areaStyleSchema.optional(),
+});
+
+export const axisSchema = z.object({
+ id: z.string().optional(),
+ show: z.boolean().optional(),
+ gridIndex: z.number().optional(),
+ alignTicks: z.boolean().optional(),
+ position: z.enum(['top', 'bottom', 'left', 'right']).optional(),
+ offset: z.number().optional(),
+ type: z.enum(['value', 'category', 'time', 'log']).optional(),
+ name: z.string().optional(),
+ nameLocation: z.enum(['start', 'middle', 'center', 'end']).optional(),
+ nameTextStyle: textStyleSchema.optional(),
+ nameGap: z.number().optional(),
+ nameRotate: z.number().optional(),
+ inverse: z.boolean().optional(),
+ boundaryGap: z
+ .union([z.boolean(), z.array(z.union([z.string(), z.number()]))])
+ .optional(),
+ min: z.union([z.number(), z.string(), z.literal('dataMin')]).optional(),
+ max: z.union([z.number(), z.string(), z.literal('dataMax')]).optional(),
+ scale: z.boolean().optional(),
+ splitNumber: z.number().optional(),
+ minInterval: z.number().optional(),
+ maxInterval: z.number().optional(),
+ interval: z.number().optional(),
+ logBase: z.number().optional(),
+ silent: z.boolean().optional(),
+ triggerEvent: z.boolean().optional(),
+ axisLine: axisLineSchema.optional(),
+ axisTick: axisTickSchema.optional(),
+ minorTick: axisTickSchema.optional(),
+ axisLabel: axisLabelSchema.optional(),
+ splitLine: splitLineSchema.optional(),
+ minorSplitLine: splitLineSchema.optional(),
+ splitArea: splitAreaSchema.optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+});
+
+//
=============================================================================
+// Tooltip Schema
+//
=============================================================================
+
+export const tooltipSchema = z.object({
+ show: z.boolean().optional(),
+ trigger: z.enum(['item', 'axis', 'none']).optional(),
+ triggerOn: z
+ .enum(['mousemove', 'click', 'mousemove|click', 'none'])
+ .optional(),
+ alwaysShowContent: z.boolean().optional(),
+ showDelay: z.number().optional(),
+ hideDelay: z.number().optional(),
+ enterable: z.boolean().optional(),
+ renderMode: z.enum(['html', 'richText']).optional(),
+ confine: z.boolean().optional(),
+ appendToBody: z.boolean().optional(),
+ transitionDuration: z.number().optional(),
+ position: z
+ .union([
+ z.enum(['inside', 'top', 'left', 'right', 'bottom']),
+ z.array(z.union([z.number(), z.string()])),
+ ])
+ .optional(),
+ formatter: z.string().optional(), // Only string formatters
+ padding: z.union([z.number(), z.array(z.number())]).optional(),
+ backgroundColor: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderWidth: z.number().optional(),
+ borderRadius: z.number().optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+ textStyle: textStyleSchema.optional(),
+ extraCssText: z.string().optional(),
+ order: z
+ .enum(['seriesAsc', 'seriesDesc', 'valueAsc', 'valueDesc'])
+ .optional(),
+});
+
+//
=============================================================================
+// DataZoom Schema
+//
=============================================================================
+
+export const dataZoomSchema = z.object({
+ type: z.enum(['slider', 'inside']).optional(),
+ id: z.string().optional(),
+ show: z.boolean().optional(),
+ disabled: z.boolean().optional(),
+ xAxisIndex: z.union([z.number(), z.array(z.number())]).optional(),
+ yAxisIndex: z.union([z.number(), z.array(z.number())]).optional(),
+ filterMode: z.enum(['filter', 'weakFilter', 'empty', 'none']).optional(),
+ start: z.number().optional(),
+ end: z.number().optional(),
+ startValue: z.union([z.number(), z.string()]).optional(),
+ endValue: z.union([z.number(), z.string()]).optional(),
+ minSpan: z.number().optional(),
+ maxSpan: z.number().optional(),
+ minValueSpan: z.union([z.number(), z.string()]).optional(),
+ maxValueSpan: z.union([z.number(), z.string()]).optional(),
+ orient: z.enum(['horizontal', 'vertical']).optional(),
+ zoomLock: z.boolean().optional(),
+ throttle: z.number().optional(),
+ rangeMode: z.array(z.enum(['value', 'percent'])).optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ width: numberOrPercentSchema.optional(),
+ height: numberOrPercentSchema.optional(),
+ backgroundColor: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderRadius: z.number().optional(),
+ fillerColor: colorSchema.optional(),
+ handleSize: numberOrPercentSchema.optional(),
+ handleStyle: itemStyleSchema.optional(),
+ moveHandleSize: z.number().optional(),
+ moveHandleStyle: itemStyleSchema.optional(),
+ labelPrecision: z.union([z.number(), z.literal('auto')]).optional(),
+ textStyle: textStyleSchema.optional(),
+ realtime: z.boolean().optional(),
+ showDetail: z.boolean().optional(),
+ showDataShadow: z.union([z.boolean(), z.literal('auto')]).optional(),
+ zoomOnMouseWheel: z
+ .union([z.boolean(), z.enum(['shift', 'ctrl', 'alt'])])
+ .optional(),
+ moveOnMouseMove: z
+ .union([z.boolean(), z.enum(['shift', 'ctrl', 'alt'])])
+ .optional(),
+ moveOnMouseWheel: z
+ .union([z.boolean(), z.enum(['shift', 'ctrl', 'alt'])])
+ .optional(),
+ preventDefaultMouseMove: z.boolean().optional(),
+});
+
+//
=============================================================================
+// Toolbox Schema
+//
=============================================================================
+
+export const toolboxSchema = z.object({
+ id: z.string().optional(),
+ show: z.boolean().optional(),
+ orient: z.enum(['horizontal', 'vertical']).optional(),
+ itemSize: z.number().optional(),
+ itemGap: z.number().optional(),
+ showTitle: z.boolean().optional(),
+ feature: z.record(z.string(), z.unknown()).optional(),
+ iconStyle: itemStyleSchema.optional(),
+ emphasis: z
+ .object({
+ iconStyle: itemStyleSchema.optional(),
+ })
+ .optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ width: numberOrPercentSchema.optional(),
+ height: numberOrPercentSchema.optional(),
+});
+
+//
=============================================================================
+// VisualMap Schema
+//
=============================================================================
+
+export const visualMapSchema = z.object({
+ type: z.enum(['continuous', 'piecewise']).optional(),
+ id: z.string().optional(),
+ min: z.number().optional(),
+ max: z.number().optional(),
+ range: z.array(z.number()).optional(),
+ calculable: z.boolean().optional(),
+ realtime: z.boolean().optional(),
+ inverse: z.boolean().optional(),
+ precision: z.number().optional(),
+ itemWidth: z.number().optional(),
+ itemHeight: z.number().optional(),
+ align: z.enum(['auto', 'left', 'right', 'top', 'bottom']).optional(),
+ text: z.array(z.string()).optional(),
+ textGap: z.number().optional(),
+ show: z.boolean().optional(),
+ dimension: z.union([z.number(), z.string()]).optional(),
+ seriesIndex: z.union([z.number(), z.array(z.number())]).optional(),
+ hoverLink: z.boolean().optional(),
+ inRange: z.record(z.string(), z.unknown()).optional(),
+ outOfRange: z.record(z.string(), z.unknown()).optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ orient: z.enum(['horizontal', 'vertical']).optional(),
+ padding: z.union([z.number(), z.array(z.number())]).optional(),
+ backgroundColor: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderWidth: z.number().optional(),
+ color: z.array(colorSchema).optional(),
+ textStyle: textStyleSchema.optional(),
+ splitNumber: z.number().optional(),
+ pieces: z.array(z.record(z.string(), z.unknown())).optional(),
+ categories: z.array(z.string()).optional(),
+ minOpen: z.boolean().optional(),
+ maxOpen: z.boolean().optional(),
+ selectedMode: z
+ .union([z.boolean(), z.enum(['single', 'multiple'])])
+ .optional(),
+ showLabel: z.boolean().optional(),
+ itemGap: z.number().optional(),
+ itemSymbol: symbolTypeSchema.optional(),
+});
+
+//
=============================================================================
+// Series Schema
+//
=============================================================================
+
+const emphasisSchema = z.object({
+ disabled: z.boolean().optional(),
+ focus: z
+ .enum(['none', 'self', 'series', 'ancestor', 'descendant', 'relative'])
+ .optional(),
+ blurScope: z.enum(['coordinateSystem', 'series', 'global']).optional(),
+ scale: z.union([z.boolean(), z.number()]).optional(),
+ label: labelSchema.optional(),
+ itemStyle: itemStyleSchema.optional(),
+ lineStyle: lineStyleSchema.optional(),
+ areaStyle: areaStyleSchema.optional(),
+});
+
+const stateSchema = z.object({
+ label: labelSchema.optional(),
+ itemStyle: itemStyleSchema.optional(),
+ lineStyle: lineStyleSchema.optional(),
+ areaStyle: areaStyleSchema.optional(),
+});
+
+export const seriesSchema = z.object({
+ type: z.string().optional(),
+ id: z.string().optional(),
+ name: z.string().optional(),
+ colorBy: z.enum(['series', 'data']).optional(),
+ legendHoverLink: z.boolean().optional(),
+ coordinateSystem: z.string().optional(),
+ xAxisIndex: z.number().optional(),
+ yAxisIndex: z.number().optional(),
+ polarIndex: z.number().optional(),
+ geoIndex: z.number().optional(),
+ calendarIndex: z.number().optional(),
+ label: labelSchema.optional(),
+ labelLine: z
+ .object({
+ show: z.boolean().optional(),
+ showAbove: z.boolean().optional(),
+ length: z.number().optional(),
+ length2: z.number().optional(),
+ smooth: z.union([z.boolean(), z.number()]).optional(),
+ minTurnAngle: z.number().optional(),
+ lineStyle: lineStyleSchema.optional(),
+ })
+ .optional(),
+ labelLayout: z
+ .object({
+ hideOverlap: z.boolean().optional(),
+ moveOverlap: z.enum(['shiftX', 'shiftY']).optional(),
+ })
+ .optional(),
+ itemStyle: itemStyleSchema.optional(),
+ lineStyle: lineStyleSchema.optional(),
+ areaStyle: areaStyleSchema.optional(),
+ emphasis: emphasisSchema.optional(),
+ blur: stateSchema.optional(),
+ select: stateSchema.optional(),
+ selectedMode: z
+ .union([z.boolean(), z.enum(['single', 'multiple', 'series'])])
+ .optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ silent: z.boolean().optional(),
+ cursor: z.string().optional(),
+ animation: z.boolean().optional(),
+ animationThreshold: z.number().optional(),
+ animationDuration: z.number().optional(),
+ animationEasing: z.string().optional(),
+ animationDelay: z.number().optional(),
+ animationDurationUpdate: z.number().optional(),
+ animationEasingUpdate: z.string().optional(),
+ animationDelayUpdate: z.number().optional(),
+});
+
+//
=============================================================================
+// Graphic Schema
+//
=============================================================================
+
+export const graphicElementSchema = z.object({
+ type: z
+ .enum([
+ 'group',
+ 'image',
+ 'text',
+ 'rect',
+ 'circle',
+ 'ring',
+ 'sector',
+ 'arc',
+ 'polygon',
+ 'polyline',
+ 'line',
+ 'bezierCurve',
+ ])
+ .optional(),
+ id: z.string().optional(),
+ $action: z.enum(['merge', 'replace', 'remove']).optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ bounding: z.enum(['all', 'raw']).optional(),
+ z: z.number().optional(),
+ zlevel: z.number().optional(),
+ silent: z.boolean().optional(),
+ invisible: z.boolean().optional(),
+ cursor: z.string().optional(),
+ draggable: z
+ .union([z.boolean(), z.enum(['horizontal', 'vertical'])])
+ .optional(),
+ progressive: z.boolean().optional(),
+ width: z.number().optional(),
+ height: z.number().optional(),
+ shape: z.record(z.string(), z.unknown()).optional(),
+ style: z.record(z.string(), z.unknown()).optional(),
+ rotation: z.number().optional(),
+ scaleX: z.number().optional(),
+ scaleY: z.number().optional(),
+ originX: z.number().optional(),
+ originY: z.number().optional(),
+ children: z.array(z.record(z.string(), z.unknown())).optional(),
+});
+
+//
=============================================================================
+// AxisPointer Schema
+//
=============================================================================
+
+export const axisPointerSchema = z.object({
+ id: z.string().optional(),
+ show: z.boolean().optional(),
+ type: z.enum(['line', 'shadow', 'none']).optional(),
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Incorrect axisPointer type enum</b></div>
<div id="fix">
The axisPointer.type enum currently includes 'none', but ECharts
documentation specifies 'line', 'shadow', or 'cross' as valid values. 'none'
appears to be incorrect and should be replaced with 'cross' to match the
ECharts API.
</div>
<details>
<summary>
<b>Code suggestion</b>
</summary>
<blockquote>Check the AI-generated fix before applying</blockquote>
<div id="code">
````suggestion
type: z.enum(['line', 'shadow', 'cross']).optional(),
````
</div>
</details>
</div>
<small><i>Code Review Run #048e53</i></small>
</div>
---
Should Bito avoid suggestions like this for future reviews? (<a
href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
- [ ] Yes, avoid them
##########
superset-frontend/plugins/plugin-chart-echarts/src/utils/eChartOptionsSchema.ts:
##########
@@ -0,0 +1,827 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Unified ECharts Options Schema
+ *
+ * This file serves as the single source of truth for:
+ * 1. Runtime validation (Zod schema)
+ * 2. TypeScript types (inferred from Zod)
+ *
+ * Reference: https://echarts.apache.org/en/option.html
+ */
+
+import { z } from 'zod';
+
+//
=============================================================================
+// Common Schemas
+//
=============================================================================
+
+/** Color value - hex, rgb, rgba, or named color */
+const colorSchema = z.string();
+
+/** Numeric or percentage string (e.g., '50%') */
+const numberOrPercentSchema = z.union([z.number(), z.string()]);
+
+/** Line type */
+const lineTypeSchema = z.union([
+ z.enum(['solid', 'dashed', 'dotted']),
+ z.array(z.number()),
+]);
+
+/** Font weight */
+const fontWeightSchema = z.union([
+ z.enum(['normal', 'bold', 'bolder', 'lighter']),
+ z.number().min(100).max(900),
+]);
+
+/** Font style */
+const fontStyleSchema = z.enum(['normal', 'italic', 'oblique']);
+
+/** Symbol type */
+const symbolTypeSchema = z.string();
+
+//
=============================================================================
+// Text Style Schema
+//
=============================================================================
+
+export const textStyleSchema = z.object({
+ color: colorSchema.optional(),
+ fontStyle: fontStyleSchema.optional(),
+ fontWeight: fontWeightSchema.optional(),
+ fontFamily: z.string().optional(),
+ fontSize: z.number().optional(),
+ lineHeight: z.number().optional(),
+ width: numberOrPercentSchema.optional(),
+ height: numberOrPercentSchema.optional(),
+ textBorderColor: colorSchema.optional(),
+ textBorderWidth: z.number().optional(),
+ textBorderType: lineTypeSchema.optional(),
+ textBorderDashOffset: z.number().optional(),
+ textShadowColor: colorSchema.optional(),
+ textShadowBlur: z.number().optional(),
+ textShadowOffsetX: z.number().optional(),
+ textShadowOffsetY: z.number().optional(),
+ overflow: z.enum(['none', 'truncate', 'break', 'breakAll']).optional(),
+ ellipsis: z.string().optional(),
+});
+
+//
=============================================================================
+// Style Schemas
+//
=============================================================================
+
+export const lineStyleSchema = z.object({
+ color: colorSchema.optional(),
+ width: z.number().optional(),
+ type: lineTypeSchema.optional(),
+ dashOffset: z.number().optional(),
+ cap: z.enum(['butt', 'round', 'square']).optional(),
+ join: z.enum(['bevel', 'round', 'miter']).optional(),
+ miterLimit: z.number().optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+ opacity: z.number().min(0).max(1).optional(),
+});
+
+export const areaStyleSchema = z.object({
+ color: z.union([colorSchema, z.array(colorSchema)]).optional(),
+ origin: z.union([z.enum(['auto', 'start', 'end']), z.number()]).optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+ opacity: z.number().min(0).max(1).optional(),
+});
+
+export const itemStyleSchema = z.object({
+ color: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderWidth: z.number().optional(),
+ borderType: lineTypeSchema.optional(),
+ borderRadius: z.union([z.number(), z.array(z.number())]).optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+ opacity: z.number().min(0).max(1).optional(),
+});
+
+//
=============================================================================
+// Label Schema
+//
=============================================================================
+
+export const labelSchema = z.object({
+ show: z.boolean().optional(),
+ position: z
+ .enum([
+ 'top',
+ 'left',
+ 'right',
+ 'bottom',
+ 'inside',
+ 'insideLeft',
+ 'insideRight',
+ 'insideTop',
+ 'insideBottom',
+ 'insideTopLeft',
+ 'insideBottomLeft',
+ 'insideTopRight',
+ 'insideBottomRight',
+ 'outside',
+ ])
+ .optional(),
+ distance: z.number().optional(),
+ rotate: z.number().optional(),
+ offset: z.array(z.number()).optional(),
+ formatter: z.string().optional(), // Only string formatters allowed, not
functions
+ color: colorSchema.optional(),
+ fontStyle: fontStyleSchema.optional(),
+ fontWeight: fontWeightSchema.optional(),
+ fontFamily: z.string().optional(),
+ fontSize: z.number().optional(),
+ lineHeight: z.number().optional(),
+});
+
+//
=============================================================================
+// Title Schema
+//
=============================================================================
+
+export const titleSchema = z.object({
+ id: z.string().optional(),
+ show: z.boolean().optional(),
+ text: z.string().optional(),
+ link: z.string().optional(),
+ target: z.enum(['self', 'blank']).optional(),
+ textStyle: textStyleSchema.optional(),
+ subtext: z.string().optional(),
+ sublink: z.string().optional(),
+ subtarget: z.enum(['self', 'blank']).optional(),
+ subtextStyle: textStyleSchema.optional(),
+ textAlign: z.enum(['left', 'center', 'right']).optional(),
+ textVerticalAlign: z.enum(['top', 'middle', 'bottom']).optional(),
+ triggerEvent: z.boolean().optional(),
+ padding: z.union([z.number(), z.array(z.number())]).optional(),
+ itemGap: z.number().optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ backgroundColor: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderWidth: z.number().optional(),
+ borderRadius: z.union([z.number(), z.array(z.number())]).optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+});
+
+//
=============================================================================
+// Legend Schema
+//
=============================================================================
+
+export const legendSchema = z.object({
+ id: z.string().optional(),
+ type: z.enum(['plain', 'scroll']).optional(),
+ show: z.boolean().optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ width: numberOrPercentSchema.optional(),
+ height: numberOrPercentSchema.optional(),
+ orient: z.enum(['horizontal', 'vertical']).optional(),
+ align: z.enum(['auto', 'left', 'right']).optional(),
+ padding: z.union([z.number(), z.array(z.number())]).optional(),
+ itemGap: z.number().optional(),
+ itemWidth: z.number().optional(),
+ itemHeight: z.number().optional(),
+ itemStyle: itemStyleSchema.optional(),
+ lineStyle: lineStyleSchema.optional(),
+ textStyle: textStyleSchema.optional(),
+ icon: symbolTypeSchema.optional(),
+ selectedMode: z
+ .union([z.boolean(), z.enum(['single', 'multiple', 'series'])])
+ .optional(),
+ inactiveColor: colorSchema.optional(),
+ inactiveBorderColor: colorSchema.optional(),
+ inactiveBorderWidth: z.number().optional(),
+ backgroundColor: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderWidth: z.number().optional(),
+ borderRadius: z.union([z.number(), z.array(z.number())]).optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+ pageButtonItemGap: z.number().optional(),
+ pageButtonGap: z.number().optional(),
+ pageButtonPosition: z.enum(['start', 'end']).optional(),
+ pageIconColor: colorSchema.optional(),
+ pageIconInactiveColor: colorSchema.optional(),
+ pageIconSize: z.union([z.number(), z.array(z.number())]).optional(),
+ pageTextStyle: textStyleSchema.optional(),
+});
+
+//
=============================================================================
+// Grid Schema
+//
=============================================================================
+
+export const gridSchema = z.object({
+ id: z.string().optional(),
+ show: z.boolean().optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ width: numberOrPercentSchema.optional(),
+ height: numberOrPercentSchema.optional(),
+ containLabel: z.boolean().optional(),
+ backgroundColor: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderWidth: z.number().optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+});
+
+//
=============================================================================
+// Axis Schemas
+//
=============================================================================
+
+const axisLineSchema = z.object({
+ show: z.boolean().optional(),
+ onZero: z.boolean().optional(),
+ onZeroAxisIndex: z.number().optional(),
+ symbol: z.union([z.string(), z.array(z.string())]).optional(),
+ symbolSize: z.array(z.number()).optional(),
+ symbolOffset: z.union([z.number(), z.array(z.number())]).optional(),
+ lineStyle: lineStyleSchema.optional(),
+});
+
+const axisTickSchema = z.object({
+ show: z.boolean().optional(),
+ alignWithLabel: z.boolean().optional(),
+ interval: z.union([z.number(), z.literal('auto')]).optional(),
+ inside: z.boolean().optional(),
+ length: z.number().optional(),
+ lineStyle: lineStyleSchema.optional(),
+});
+
+const axisLabelSchema = z.object({
+ show: z.boolean().optional(),
+ interval: z.union([z.number(), z.literal('auto')]).optional(),
+ inside: z.boolean().optional(),
+ rotate: z.number().optional(),
+ margin: z.number().optional(),
+ formatter: z.string().optional(), // Only string formatters
+ showMinLabel: z.boolean().optional(),
+ showMaxLabel: z.boolean().optional(),
+ hideOverlap: z.boolean().optional(),
+ color: colorSchema.optional(),
+ fontStyle: fontStyleSchema.optional(),
+ fontWeight: fontWeightSchema.optional(),
+ fontFamily: z.string().optional(),
+ fontSize: z.number().optional(),
+ align: z.enum(['left', 'center', 'right']).optional(),
+ verticalAlign: z.enum(['top', 'middle', 'bottom']).optional(),
+});
+
+const splitLineSchema = z.object({
+ show: z.boolean().optional(),
+ interval: z.union([z.number(), z.literal('auto')]).optional(),
+ lineStyle: lineStyleSchema.optional(),
+});
+
+const splitAreaSchema = z.object({
+ show: z.boolean().optional(),
+ interval: z.union([z.number(), z.literal('auto')]).optional(),
+ areaStyle: areaStyleSchema.optional(),
+});
+
+export const axisSchema = z.object({
+ id: z.string().optional(),
+ show: z.boolean().optional(),
+ gridIndex: z.number().optional(),
+ alignTicks: z.boolean().optional(),
+ position: z.enum(['top', 'bottom', 'left', 'right']).optional(),
+ offset: z.number().optional(),
+ type: z.enum(['value', 'category', 'time', 'log']).optional(),
+ name: z.string().optional(),
+ nameLocation: z.enum(['start', 'middle', 'center', 'end']).optional(),
+ nameTextStyle: textStyleSchema.optional(),
+ nameGap: z.number().optional(),
+ nameRotate: z.number().optional(),
+ inverse: z.boolean().optional(),
+ boundaryGap: z
+ .union([z.boolean(), z.array(z.union([z.string(), z.number()]))])
+ .optional(),
+ min: z.union([z.number(), z.string(), z.literal('dataMin')]).optional(),
+ max: z.union([z.number(), z.string(), z.literal('dataMax')]).optional(),
+ scale: z.boolean().optional(),
+ splitNumber: z.number().optional(),
+ minInterval: z.number().optional(),
+ maxInterval: z.number().optional(),
+ interval: z.number().optional(),
+ logBase: z.number().optional(),
+ silent: z.boolean().optional(),
+ triggerEvent: z.boolean().optional(),
+ axisLine: axisLineSchema.optional(),
+ axisTick: axisTickSchema.optional(),
+ minorTick: axisTickSchema.optional(),
+ axisLabel: axisLabelSchema.optional(),
+ splitLine: splitLineSchema.optional(),
+ minorSplitLine: splitLineSchema.optional(),
+ splitArea: splitAreaSchema.optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+});
+
+//
=============================================================================
+// Tooltip Schema
+//
=============================================================================
+
+export const tooltipSchema = z.object({
+ show: z.boolean().optional(),
+ trigger: z.enum(['item', 'axis', 'none']).optional(),
+ triggerOn: z
+ .enum(['mousemove', 'click', 'mousemove|click', 'none'])
+ .optional(),
+ alwaysShowContent: z.boolean().optional(),
+ showDelay: z.number().optional(),
+ hideDelay: z.number().optional(),
+ enterable: z.boolean().optional(),
+ renderMode: z.enum(['html', 'richText']).optional(),
+ confine: z.boolean().optional(),
+ appendToBody: z.boolean().optional(),
+ transitionDuration: z.number().optional(),
+ position: z
+ .union([
+ z.enum(['inside', 'top', 'left', 'right', 'bottom']),
+ z.array(z.union([z.number(), z.string()])),
+ ])
+ .optional(),
+ formatter: z.string().optional(), // Only string formatters
+ padding: z.union([z.number(), z.array(z.number())]).optional(),
+ backgroundColor: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderWidth: z.number().optional(),
+ borderRadius: z.number().optional(),
+ shadowBlur: z.number().optional(),
+ shadowColor: colorSchema.optional(),
+ shadowOffsetX: z.number().optional(),
+ shadowOffsetY: z.number().optional(),
+ textStyle: textStyleSchema.optional(),
+ extraCssText: z.string().optional(),
+ order: z
+ .enum(['seriesAsc', 'seriesDesc', 'valueAsc', 'valueDesc'])
+ .optional(),
+});
+
+//
=============================================================================
+// DataZoom Schema
+//
=============================================================================
+
+export const dataZoomSchema = z.object({
+ type: z.enum(['slider', 'inside']).optional(),
+ id: z.string().optional(),
+ show: z.boolean().optional(),
+ disabled: z.boolean().optional(),
+ xAxisIndex: z.union([z.number(), z.array(z.number())]).optional(),
+ yAxisIndex: z.union([z.number(), z.array(z.number())]).optional(),
+ filterMode: z.enum(['filter', 'weakFilter', 'empty', 'none']).optional(),
+ start: z.number().optional(),
+ end: z.number().optional(),
+ startValue: z.union([z.number(), z.string()]).optional(),
+ endValue: z.union([z.number(), z.string()]).optional(),
+ minSpan: z.number().optional(),
+ maxSpan: z.number().optional(),
+ minValueSpan: z.union([z.number(), z.string()]).optional(),
+ maxValueSpan: z.union([z.number(), z.string()]).optional(),
+ orient: z.enum(['horizontal', 'vertical']).optional(),
+ zoomLock: z.boolean().optional(),
+ throttle: z.number().optional(),
+ rangeMode: z.array(z.enum(['value', 'percent'])).optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ width: numberOrPercentSchema.optional(),
+ height: numberOrPercentSchema.optional(),
+ backgroundColor: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderRadius: z.number().optional(),
+ fillerColor: colorSchema.optional(),
+ handleSize: numberOrPercentSchema.optional(),
+ handleStyle: itemStyleSchema.optional(),
+ moveHandleSize: z.number().optional(),
+ moveHandleStyle: itemStyleSchema.optional(),
+ labelPrecision: z.union([z.number(), z.literal('auto')]).optional(),
+ textStyle: textStyleSchema.optional(),
+ realtime: z.boolean().optional(),
+ showDetail: z.boolean().optional(),
+ showDataShadow: z.union([z.boolean(), z.literal('auto')]).optional(),
+ zoomOnMouseWheel: z
+ .union([z.boolean(), z.enum(['shift', 'ctrl', 'alt'])])
+ .optional(),
+ moveOnMouseMove: z
+ .union([z.boolean(), z.enum(['shift', 'ctrl', 'alt'])])
+ .optional(),
+ moveOnMouseWheel: z
+ .union([z.boolean(), z.enum(['shift', 'ctrl', 'alt'])])
+ .optional(),
+ preventDefaultMouseMove: z.boolean().optional(),
+});
+
+//
=============================================================================
+// Toolbox Schema
+//
=============================================================================
+
+export const toolboxSchema = z.object({
+ id: z.string().optional(),
+ show: z.boolean().optional(),
+ orient: z.enum(['horizontal', 'vertical']).optional(),
+ itemSize: z.number().optional(),
+ itemGap: z.number().optional(),
+ showTitle: z.boolean().optional(),
+ feature: z.record(z.string(), z.unknown()).optional(),
+ iconStyle: itemStyleSchema.optional(),
+ emphasis: z
+ .object({
+ iconStyle: itemStyleSchema.optional(),
+ })
+ .optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ width: numberOrPercentSchema.optional(),
+ height: numberOrPercentSchema.optional(),
+});
+
+//
=============================================================================
+// VisualMap Schema
+//
=============================================================================
+
+export const visualMapSchema = z.object({
+ type: z.enum(['continuous', 'piecewise']).optional(),
+ id: z.string().optional(),
+ min: z.number().optional(),
+ max: z.number().optional(),
+ range: z.array(z.number()).optional(),
+ calculable: z.boolean().optional(),
+ realtime: z.boolean().optional(),
+ inverse: z.boolean().optional(),
+ precision: z.number().optional(),
+ itemWidth: z.number().optional(),
+ itemHeight: z.number().optional(),
+ align: z.enum(['auto', 'left', 'right', 'top', 'bottom']).optional(),
+ text: z.array(z.string()).optional(),
+ textGap: z.number().optional(),
+ show: z.boolean().optional(),
+ dimension: z.union([z.number(), z.string()]).optional(),
+ seriesIndex: z.union([z.number(), z.array(z.number())]).optional(),
+ hoverLink: z.boolean().optional(),
+ inRange: z.record(z.string(), z.unknown()).optional(),
+ outOfRange: z.record(z.string(), z.unknown()).optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ orient: z.enum(['horizontal', 'vertical']).optional(),
+ padding: z.union([z.number(), z.array(z.number())]).optional(),
+ backgroundColor: colorSchema.optional(),
+ borderColor: colorSchema.optional(),
+ borderWidth: z.number().optional(),
+ color: z.array(colorSchema).optional(),
+ textStyle: textStyleSchema.optional(),
+ splitNumber: z.number().optional(),
+ pieces: z.array(z.record(z.string(), z.unknown())).optional(),
+ categories: z.array(z.string()).optional(),
+ minOpen: z.boolean().optional(),
+ maxOpen: z.boolean().optional(),
+ selectedMode: z
+ .union([z.boolean(), z.enum(['single', 'multiple'])])
+ .optional(),
+ showLabel: z.boolean().optional(),
+ itemGap: z.number().optional(),
+ itemSymbol: symbolTypeSchema.optional(),
+});
+
+//
=============================================================================
+// Series Schema
+//
=============================================================================
+
+const emphasisSchema = z.object({
+ disabled: z.boolean().optional(),
+ focus: z
+ .enum(['none', 'self', 'series', 'ancestor', 'descendant', 'relative'])
+ .optional(),
+ blurScope: z.enum(['coordinateSystem', 'series', 'global']).optional(),
+ scale: z.union([z.boolean(), z.number()]).optional(),
+ label: labelSchema.optional(),
+ itemStyle: itemStyleSchema.optional(),
+ lineStyle: lineStyleSchema.optional(),
+ areaStyle: areaStyleSchema.optional(),
+});
+
+const stateSchema = z.object({
+ label: labelSchema.optional(),
+ itemStyle: itemStyleSchema.optional(),
+ lineStyle: lineStyleSchema.optional(),
+ areaStyle: areaStyleSchema.optional(),
+});
+
+export const seriesSchema = z.object({
+ type: z.string().optional(),
+ id: z.string().optional(),
+ name: z.string().optional(),
+ colorBy: z.enum(['series', 'data']).optional(),
+ legendHoverLink: z.boolean().optional(),
+ coordinateSystem: z.string().optional(),
+ xAxisIndex: z.number().optional(),
+ yAxisIndex: z.number().optional(),
+ polarIndex: z.number().optional(),
+ geoIndex: z.number().optional(),
+ calendarIndex: z.number().optional(),
+ label: labelSchema.optional(),
+ labelLine: z
+ .object({
+ show: z.boolean().optional(),
+ showAbove: z.boolean().optional(),
+ length: z.number().optional(),
+ length2: z.number().optional(),
+ smooth: z.union([z.boolean(), z.number()]).optional(),
+ minTurnAngle: z.number().optional(),
+ lineStyle: lineStyleSchema.optional(),
+ })
+ .optional(),
+ labelLayout: z
+ .object({
+ hideOverlap: z.boolean().optional(),
+ moveOverlap: z.enum(['shiftX', 'shiftY']).optional(),
+ })
+ .optional(),
+ itemStyle: itemStyleSchema.optional(),
+ lineStyle: lineStyleSchema.optional(),
+ areaStyle: areaStyleSchema.optional(),
+ emphasis: emphasisSchema.optional(),
+ blur: stateSchema.optional(),
+ select: stateSchema.optional(),
+ selectedMode: z
+ .union([z.boolean(), z.enum(['single', 'multiple', 'series'])])
+ .optional(),
+ zlevel: z.number().optional(),
+ z: z.number().optional(),
+ silent: z.boolean().optional(),
+ cursor: z.string().optional(),
+ animation: z.boolean().optional(),
+ animationThreshold: z.number().optional(),
+ animationDuration: z.number().optional(),
+ animationEasing: z.string().optional(),
+ animationDelay: z.number().optional(),
+ animationDurationUpdate: z.number().optional(),
+ animationEasingUpdate: z.string().optional(),
+ animationDelayUpdate: z.number().optional(),
+});
+
+//
=============================================================================
+// Graphic Schema
+//
=============================================================================
+
+export const graphicElementSchema = z.object({
+ type: z
+ .enum([
+ 'group',
+ 'image',
+ 'text',
+ 'rect',
+ 'circle',
+ 'ring',
+ 'sector',
+ 'arc',
+ 'polygon',
+ 'polyline',
+ 'line',
+ 'bezierCurve',
+ ])
+ .optional(),
+ id: z.string().optional(),
+ $action: z.enum(['merge', 'replace', 'remove']).optional(),
+ left: numberOrPercentSchema.optional(),
+ top: numberOrPercentSchema.optional(),
+ right: numberOrPercentSchema.optional(),
+ bottom: numberOrPercentSchema.optional(),
+ bounding: z.enum(['all', 'raw']).optional(),
+ z: z.number().optional(),
+ zlevel: z.number().optional(),
+ silent: z.boolean().optional(),
+ invisible: z.boolean().optional(),
+ cursor: z.string().optional(),
+ draggable: z
+ .union([z.boolean(), z.enum(['horizontal', 'vertical'])])
+ .optional(),
+ progressive: z.boolean().optional(),
+ width: z.number().optional(),
+ height: z.number().optional(),
+ shape: z.record(z.string(), z.unknown()).optional(),
+ style: z.record(z.string(), z.unknown()).optional(),
+ rotation: z.number().optional(),
+ scaleX: z.number().optional(),
+ scaleY: z.number().optional(),
+ originX: z.number().optional(),
+ originY: z.number().optional(),
+ children: z.array(z.record(z.string(), z.unknown())).optional(),
+});
+
+//
=============================================================================
+// AxisPointer Schema
+//
=============================================================================
+
+export const axisPointerSchema = z.object({
+ id: z.string().optional(),
+ show: z.boolean().optional(),
+ type: z.enum(['line', 'shadow', 'none']).optional(),
+ axis: z.enum(['x', 'y']).optional(),
+ snap: z.boolean().optional(),
+ z: z.number().optional(),
+ label: z
+ .object({
+ show: z.boolean().optional(),
+ precision: z.union([z.number(), z.literal('auto')]).optional(),
+ margin: z.number().optional(),
+ color: colorSchema.optional(),
+ fontStyle: fontStyleSchema.optional(),
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Missing formatter in axisPointer label</b></div>
<div id="fix">
The axisPointer.label schema is missing the 'formatter' field, which ECharts
supports for string-based formatting. Other label schemas in this file include
formatter as z.string().optional() for consistency and to allow string
formatters while excluding functions for security.
</div>
<details>
<summary>
<b>Code suggestion</b>
</summary>
<blockquote>Check the AI-generated fix before applying</blockquote>
<div id="code">
````suggestion
margin: z.number().optional(),
color: colorSchema.optional(),
formatter: z.string().optional(),
fontStyle: fontStyleSchema.optional(),
````
</div>
</details>
</div>
<small><i>Code Review Run #048e53</i></small>
</div>
---
Should Bito avoid suggestions like this for future reviews? (<a
href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
- [ ] Yes, avoid them
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]