You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@superset.apache.org by "SBIN2010 (via GitHub)" <gi...@apache.org> on 2026/09/04 13:18:48 UTC
[PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
SBIN2010 opened a new pull request, #43863:
URL: https://github.com/apache/superset/pull/43863
<!---
Please write the PR title following the conventions at https://www.conventionalcommits.org/en/v1.0.0/
Example:
fix(dashboard): load charts correctly
-->
### SUMMARY
<!--- Describe the change below, including rationale and design decisions -->
A new visualization type, the Candlestick Chart
implements the idea #20254
The chart maps each data row to a candlestick in the format: `[open, close, low, high]`. An optional **Series** parameter allows the data to be split into multiple candlestick series. Optional **moving averages** (MA5, MA10, MA15, etc., including custom periods) are displayed as smoothed lines based on closing prices (following the official ECharts candlestick chart example).
Settings added for this chart include: titles, a legend, time and number formatting in tooltips, data zooming, X and Y axis visibility and formatting, colors for upward and downward movement, and `echart_options`.
### BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
<!--- Skip this if not applicable -->
<img width="2390" height="1030" alt="Снимок экрана от 2026-09-04 16-05-21" src="https://github.com/user-attachments/assets/400ec4a6-6635-4e0d-90bc-fbadba6abdf5" />
### TESTING INSTRUCTIONS
<!--- Required! What steps can be taken to manually verify the changes? -->
run test
npm run test superset-frontend/plugins/plugin-chart-echarts/test/Butterfly
### ADDITIONAL INFORMATION
<!--- Check any relevant boxes with "x" -->
<!--- HINT: Include "Fixes #nnn" if you are fixing an existing issue -->
- [ ] Has associated issue:
- [ ] Required feature flags:
- [X] Changes UI
- [ ] Includes DB Migration (follow approval process in [SIP-59](https://github.com/apache/superset/issues/13351))
- [ ] Migration is atomic, supports rollback & is backwards-compatible
- [ ] Confirm DB migration upgrade and downgrade tested
- [ ] Runtime estimates and downtime expectations provided
- [ ] Introduces new feature or API
- [ ] Removes existing feature or API
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5546103486
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4094346374
##########
superset/translations/ca/LC_MESSAGES/messages.po:
##########
@@ -19577,6 +19693,15 @@ msgstr ""
"Quan està marcat, el mapa farà zoom a les teves dades després de cada "
"consulta"
+msgid ""
+"When enabled, increasing candles use the increase color and decreasing "
+"candles use the decrease color. Turn off to use one series color, with a "
+"filled body for increases and a hollow body for decreases."
+msgstr ""
+"Si està activat, les veles alcistes usen el color de pujada i les "
+"baixistes el de baixada. Desactiveu-ho per usar un sol color de sèrie, amb "
Review Comment:
fix in [c0dcb9c](https://github.com/apache/superset/pull/43863/commits/c0dcb9cf604c5fe18d1c4a4ff3920af65c95ae2b)
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3936592329
##########
superset-frontend/plugins/plugin-chart-echarts/test/Candlestick/buildQuery.test.ts:
##########
@@ -0,0 +1,57 @@
+/**
+ * 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 { QueryFormData, VizType } from '@superset-ui/core';
+import buildQuery from '../../src/Candlestick/buildQuery';
+
+const formData = {
+ datasource: '5__table',
+ viz_type: VizType.Candlestick,
+ x_axis: 'date',
+ open: 'open',
+ close: 'close',
+ high: 'high',
+ low: 'low',
+} as QueryFormData;
+
+test('builds query fields from OHLC metrics and x-axis', () => {
Review Comment:
fix
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5850809616
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4094341768
##########
superset/translations/cs/LC_MESSAGES/messages.po:
##########
@@ -9043,6 +9095,12 @@ msgstr "Jednotka velikosti popisku"
msgid "Label threshold"
msgstr "Prahová hodnota popisku"
+msgid "Label used for decreasing candles in the tooltip."
+msgstr "Popisek klesajících svíček v popisku."
Review Comment:
fix in [c0dcb9c](https://github.com/apache/superset/pull/43863/commits/c0dcb9cf604c5fe18d1c4a4ff3920af65c95ae2b)
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "sadpandajoe (via GitHub)" <gi...@apache.org>.
sadpandajoe commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4109311842
##########
superset-frontend/packages/superset-ui-core/src/query/extractQueryFields.ts:
##########
@@ -49,6 +49,10 @@ export default function extractQueryFields(
secondary_metric: 'metrics',
left_metric: 'metrics',
right_metric: 'metrics',
+ open: 'metrics',
Review Comment:
This registers `open`/`close`/`high`/`low` as metrics for every chart's query building, but the separate list that `getStandardizedControls` reads when a user switches chart types wasn't updated to include them — so switching from Candlestick to another chart type drops all four OHLC metrics instead of carrying them over, unlike how other charts register their standardized fields in both places. Should these four keys be added there too?
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,767 @@
+/**
+ * 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 {
+ AxisType,
+ CategoricalColorNamespace,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ getXAxisLabel,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import { t } from '@apache-superset/core/translation';
+import type { CustomSeriesOption, CustomSeriesRenderItem } from 'echarts';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+ OHLC_TICK_WIDTH_RATIO,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { getPadding } from '../Timeseries/transformers';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+type AxisTooltipParams = CallbackDataParams & {
+ axisValue?: string | number;
+ axisValueLabel?: string;
+};
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(ohlc: OhlcValue | null): CandlestickDatum {
+ return ohlc ?? [];
+}
+
+function toOhlcBarDatum(
+ ohlc: OhlcValue | null,
+ categoryIndex: number,
+ color: string,
+) {
+ if (!ohlc) {
+ return [];
+ }
+ return {
+ value: [categoryIndex, ...ohlc],
+ itemStyle: {
+ color,
+ },
+ };
+}
+
+function getDirectionItemStyle(increaseHex: string, decreaseHex: string) {
+ return {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ };
+}
+
+function getSeriesItemStyle(seriesColor: string, hollowFill: string) {
+ return {
+ color: seriesColor,
+ color0: hollowFill,
+ borderColor: seriesColor,
+ borderColor0: seriesColor,
+ };
+}
+
+const renderOhlcItem: CustomSeriesRenderItem = (_params, item) => {
+ const x = toNumber(item.value(0));
+ const open = toNumber(item.value(1));
+ const close = toNumber(item.value(2));
+ const low = toNumber(item.value(3));
+ const high = toNumber(item.value(4));
+ if (
+ x === null ||
+ open === null ||
+ close === null ||
+ low === null ||
+ high === null
+ ) {
+ return null;
+ }
+
+ const openPoint = item.coord([x, open]);
+ const closePoint = item.coord([x, close]);
+ const lowPoint = item.coord([x, low]);
+ const highPoint = item.coord([x, high]);
+ const categorySize = item.size?.([1, 0]);
+ const categoryWidth = Array.isArray(categorySize)
+ ? categorySize[0]
+ : categorySize;
+ if (categoryWidth == null || !Number.isFinite(categoryWidth)) {
+ return null;
+ }
+ const halfWidth = categoryWidth * OHLC_TICK_WIDTH_RATIO;
+ const style = item.style({
+ stroke: item.visual('color'),
+ });
+
+ return {
+ type: 'group',
+ children: [
+ {
+ type: 'line',
+ shape: {
+ x1: lowPoint[0],
+ y1: lowPoint[1],
+ x2: highPoint[0],
+ y2: highPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: openPoint[0],
+ y1: openPoint[1],
+ x2: openPoint[0] - halfWidth,
+ y2: openPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: closePoint[0],
+ y1: closePoint[1],
+ x2: closePoint[0] + halfWidth,
+ y2: closePoint[1],
+ },
+ style,
+ },
+ ],
+ };
+};
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function extractLineValue(item: CallbackDataParams): number | null {
+ const raw = item.value ?? item.data;
Review Comment:
On a date where a series has no candle, its moving-average line is padded with `null` further down, but `raw` resolves to that `null` here and `Number(null)` is `0` and finite — so the tooltip shows a fabricated `0` for that point instead of omitting it, unlike the `'-'` warm-up placeholder which is correctly skipped. Should a `null` raw value be treated as "no data" the same way?
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/controlPanel.tsx:
##########
@@ -0,0 +1,417 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t } from '@apache-superset/core/translation';
+import {
+ ensureIsArray,
+ getColumnLabel,
+ QueryFormColumn,
+ validateNonEmpty,
+} from '@superset-ui/core';
+import {
+ ControlPanelConfig,
+ ControlPanelsContainerProps,
+ ControlSubSectionHeader,
+ D3_TIME_FORMAT_DOCS,
+ DEFAULT_TIME_FORMAT,
+ getStandardizedControls,
+ sections,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import {
+ legendSection,
+ tooltipTimeFormatControl,
+ tooltipValuesFormatControl,
+ xAxisLabelInterval,
+ xAxisLabelRotation,
+} from '../controls';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_INCREASE_COLOR,
+ DEFAULT_SERIES_STYLE,
+} from './constants';
+import { MOVING_AVERAGE_PERIODS } from './utils';
+
+function hasSeriesDimension({
+ controls,
+}: ControlPanelsContainerProps): boolean {
+ return ensureIsArray(controls?.series?.value).length > 0;
+}
+
+type QueryRow = Record<string, unknown>;
+type ChartQueryResponse = { data?: QueryRow[] };
+
+const uniqueSeriesCountByResponse = new WeakMap<
+ ChartQueryResponse,
+ Map<string, number>
+>();
+
+function getSeriesColumnLabel(
+ props: ControlPanelsContainerProps,
+): string | undefined {
+ const [series] = ensureIsArray(props.controls?.series?.value);
+ if (series == null) {
+ return undefined;
+ }
+ return getColumnLabel(series as QueryFormColumn);
+}
+
+function getChartQueryResponse(
+ props: ControlPanelsContainerProps,
+): ChartQueryResponse | undefined {
+ return (props as { chart?: { queriesResponse?: ChartQueryResponse[] } }).chart
+ ?.queriesResponse?.[0];
+}
+
+function countUniqueSeriesValues(
+ props: ControlPanelsContainerProps,
+): number | undefined {
+ const seriesColumn = getSeriesColumnLabel(props);
+ if (!seriesColumn) {
+ return undefined;
+ }
+ const queryResponse = getChartQueryResponse(props);
+ const rows = queryResponse?.data;
+ if (!queryResponse || !rows) {
+ return undefined;
+ }
+ let cachedByColumn = uniqueSeriesCountByResponse.get(queryResponse);
+ if (!cachedByColumn) {
+ cachedByColumn = new Map<string, number>();
+ uniqueSeriesCountByResponse.set(queryResponse, cachedByColumn);
+ }
+ const cachedCount = cachedByColumn.get(seriesColumn);
+ if (cachedCount !== undefined) {
+ return cachedCount;
+ }
+ const count = new Set(
+ rows.map(row =>
+ row[seriesColumn] == null ? null : String(row[seriesColumn]),
+ ),
+ ).size;
+ cachedByColumn.set(seriesColumn, count);
+ return count;
+}
+
+function hasMultipleSeries(props: ControlPanelsContainerProps): boolean {
+ return (countUniqueSeriesValues(props) ?? 0) > 1;
+}
+
+function showColorByDirection(props: ControlPanelsContainerProps): boolean {
+ return !hasMultipleSeries(props);
+}
+
+function showDirectionColors(props: ControlPanelsContainerProps): boolean {
+ return (
+ showColorByDirection(props) &&
+ props.controls?.color_by_direction?.value !== false
+ );
+}
+
+function showColorScheme(props: ControlPanelsContainerProps): boolean {
+ return (
+ hasMultipleSeries(props) ||
+ props.controls?.color_by_direction?.value === false
+ );
+}
+
+const config: ControlPanelConfig = {
+ controlPanelSections: [
+ {
+ label: t('Query'),
+ expanded: true,
+ controlSetRows: [
+ ['x_axis'],
+ ['time_grain_sqla'],
+ [
+ {
+ name: 'open',
+ config: {
+ ...sharedControls.metric,
+ label: t('Open'),
+ description: t('Opening value for each period.'),
+ validators: [validateNonEmpty],
+ },
+ },
+ ],
+ [
+ {
+ name: 'close',
+ config: {
+ ...sharedControls.metric,
+ label: t('Close'),
+ description: t('Closing value for each period.'),
+ validators: [validateNonEmpty],
+ },
+ },
+ ],
+ [
+ {
+ name: 'high',
+ config: {
+ ...sharedControls.metric,
+ label: t('High'),
+ description: t('Highest value for each period.'),
+ validators: [validateNonEmpty],
+ },
+ },
+ ],
+ [
+ {
+ name: 'low',
+ config: {
+ ...sharedControls.metric,
+ label: t('Low'),
+ description: t('Lowest value for each period.'),
+ validators: [validateNonEmpty],
+ },
+ },
+ ],
+ ['series'],
+ ['adhoc_filters'],
+ ['row_limit'],
+ ],
+ },
+ sections.titleControls,
+ {
+ label: t('Chart Options'),
+ expanded: true,
+ controlSetRows: [
+ ['zoomable'],
+ [
+ {
+ name: 'series_style',
+ config: {
+ type: 'SelectControl',
+ label: t('Series style'),
+ renderTrigger: true,
+ default: DEFAULT_SERIES_STYLE,
+ clearable: false,
+ choices: [
+ ['candlestick', t('Candlestick')],
+ ['ohlc', t('OHLC')],
+ ],
+ description: t(
+ 'Candlestick draws a filled body between open and close. OHLC draws ticks for open and close on a high-low stem.',
+ ),
+ },
+ },
+ ],
+ [
+ {
+ name: 'candlestick_series_name',
+ config: {
+ type: 'TextControl',
+ label: t('Series name'),
+ default: CANDLESTICK_SERIES_NAME,
Review Comment:
This translated string is evaluated once in the author's browser locale and used as the control's saved default, so an untouched control persists that translation into the chart's saved params — a dashboard viewer in a different locale then sees the original author's word for "Candlestick," not their own. Since the transform already falls back to this same constant at render time when the value is empty, should the control have no baked-in default (or use it only as a placeholder) so the translation resolves per viewer instead?
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/EchartsCandlestick.tsx:
##########
@@ -0,0 +1,176 @@
+/**
+ * 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 { useRef } from 'react';
+import {
+ BinaryQueryObjectFilterClause,
+ TimeGranularity,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import Echart from '../components/Echart';
+import { EchartsHandler, EventHandlers } from '../types';
+import { CandlestickChartTransformedProps } from './types';
+
+type ContextMenuEvent = {
+ event?: { stop?: () => void; event?: PointerEvent };
+ dataIndex?: number;
+ seriesName?: string;
+ seriesType?: string;
+};
+
+function toFilterValue(value: unknown): string | number | boolean | null {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof Date) {
+ return value.valueOf();
+ }
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+ return String(value);
+}
+
+function toFilterClause(
+ col: string,
+ value: unknown,
+ formattedVal: string,
+ grain?: TimeGranularity,
+): BinaryQueryObjectFilterClause {
+ const val = toFilterValue(value);
+ if (val == null) {
+ return {
+ col,
+ op: 'IS NULL' as BinaryQueryObjectFilterClause['op'],
+ val: null,
+ formattedVal,
+ };
+ }
+ return {
+ col,
+ op: '==',
+ val,
+ formattedVal,
+ ...(grain ? { grain } : {}),
+ };
+}
+
+export default function EchartsCandlestick(
+ props: CandlestickChartTransformedProps,
+) {
+ const {
+ height,
+ width,
+ echartOptions,
+ refs,
+ onLegendStateChanged,
+ onContextMenu,
+ formData,
+ coltypeMapping,
+ xAxisColumn,
+ seriesColumn,
+ xValues,
+ xLabels,
+ seriesValues,
+ } = props;
+ const echartRef = useRef<EchartsHandler | null>(null);
+ // eslint-disable-next-line no-param-reassign
+ refs.echartRef = echartRef;
+
+ const hideTooltip = () => {
+ echartRef.current?.getEchartInstance()?.dispatchAction({ type: 'hideTip' });
+ };
+
+ const eventHandlers: EventHandlers = {
+ legendselectchanged: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ legendselectall: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ legendinverseselect: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ contextmenu: (eventParams: ContextMenuEvent) => {
+ if (!onContextMenu) {
+ return;
+ }
+ eventParams.event?.stop?.();
+ hideTooltip();
+ const pointerEvent = eventParams.event?.event;
+ if (!pointerEvent) {
+ return;
+ }
+ const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
+ const categoryIndex = eventParams.dataIndex;
+ if (
+ xAxisColumn &&
+ categoryIndex != null &&
+ categoryIndex >= 0 &&
+ categoryIndex < xValues.length
+ ) {
+ const xValue = xValues[categoryIndex];
+ const isTemporal =
+ coltypeMapping?.[xAxisColumn] === GenericDataType.Temporal;
+ drillToDetailFilters.push(
+ toFilterClause(
+ xAxisColumn,
+ xValue,
+ xLabels[categoryIndex] ?? String(xValue ?? ''),
+ isTemporal
+ ? (formData.timeGrainSqla as TimeGranularity | undefined)
+ : undefined,
+ ),
+ );
+ }
+ if (seriesColumn && eventParams.seriesType !== 'line') {
+ const seriesValue = seriesValues.find(
Review Comment:
This looks up the series value by its display name, but a real `null` series value and a literal "<NULL>" string value in the same column can both display as the same null placeholder — so right-clicking the second one finds the first matching entry and drills with the wrong series' value/filter. Should this look up by series index instead of display name?
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "rusackas (via GitHub)" <gi...@apache.org>.
rusackas commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5840331380
Pushed in d8b0c19852: `seriesValues` now carries the raw datum value via a `seriesRecords` array alongside `seriesKeys`, `xAxisName` resolves through `getXAxisLabel` (the same helper `Timeseries/transformProps.ts` uses for the same fallback), and moving averages are computed from each series' own compact close array, then scattered back onto the shared category axis. Added a regression test for each: a numeric series filter value, an unset-`x_axis`-with-`granularity_sqla` chart, and a two-series case where one series' dates fall between the other's. All green locally, CI running now.
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5740984054
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5547751294
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5543631183
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4094413421
##########
superset/translations/zh_TW/LC_MESSAGES/messages.po:
##########
@@ -1046,6 +1046,12 @@ msgid ""
" bars that extend left and right from a central axis."
msgstr ""
+msgid ""
+"A candlestick chart shows opening, closing, high, and low values for each"
+" period. It is commonly used for financial and other time-series range "
+"analysis."
+msgstr "K線圖顯示每個週期的開盤、收盤、最高和最低值。常用於金融及其他時間序列區間分析。"
Review Comment:
fix in [c0dcb9c](https://github.com/apache/superset/pull/43863/commits/c0dcb9cf604c5fe18d1c4a4ff3920af65c95ae2b)
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4109737555
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,767 @@
+/**
+ * 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 {
+ AxisType,
+ CategoricalColorNamespace,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ getXAxisLabel,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import { t } from '@apache-superset/core/translation';
+import type { CustomSeriesOption, CustomSeriesRenderItem } from 'echarts';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+ OHLC_TICK_WIDTH_RATIO,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { getPadding } from '../Timeseries/transformers';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+type AxisTooltipParams = CallbackDataParams & {
+ axisValue?: string | number;
+ axisValueLabel?: string;
+};
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(ohlc: OhlcValue | null): CandlestickDatum {
+ return ohlc ?? [];
+}
+
+function toOhlcBarDatum(
+ ohlc: OhlcValue | null,
+ categoryIndex: number,
+ color: string,
+) {
+ if (!ohlc) {
+ return [];
+ }
+ return {
+ value: [categoryIndex, ...ohlc],
+ itemStyle: {
+ color,
+ },
+ };
+}
+
+function getDirectionItemStyle(increaseHex: string, decreaseHex: string) {
+ return {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ };
+}
+
+function getSeriesItemStyle(seriesColor: string, hollowFill: string) {
+ return {
+ color: seriesColor,
+ color0: hollowFill,
+ borderColor: seriesColor,
+ borderColor0: seriesColor,
+ };
+}
+
+const renderOhlcItem: CustomSeriesRenderItem = (_params, item) => {
+ const x = toNumber(item.value(0));
+ const open = toNumber(item.value(1));
+ const close = toNumber(item.value(2));
+ const low = toNumber(item.value(3));
+ const high = toNumber(item.value(4));
+ if (
+ x === null ||
+ open === null ||
+ close === null ||
+ low === null ||
+ high === null
+ ) {
+ return null;
+ }
+
+ const openPoint = item.coord([x, open]);
+ const closePoint = item.coord([x, close]);
+ const lowPoint = item.coord([x, low]);
+ const highPoint = item.coord([x, high]);
+ const categorySize = item.size?.([1, 0]);
+ const categoryWidth = Array.isArray(categorySize)
+ ? categorySize[0]
+ : categorySize;
+ if (categoryWidth == null || !Number.isFinite(categoryWidth)) {
+ return null;
+ }
+ const halfWidth = categoryWidth * OHLC_TICK_WIDTH_RATIO;
+ const style = item.style({
+ stroke: item.visual('color'),
+ });
+
+ return {
+ type: 'group',
+ children: [
+ {
+ type: 'line',
+ shape: {
+ x1: lowPoint[0],
+ y1: lowPoint[1],
+ x2: highPoint[0],
+ y2: highPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: openPoint[0],
+ y1: openPoint[1],
+ x2: openPoint[0] - halfWidth,
+ y2: openPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: closePoint[0],
+ y1: closePoint[1],
+ x2: closePoint[0] + halfWidth,
+ y2: closePoint[1],
+ },
+ style,
+ },
+ ],
+ };
+};
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function extractLineValue(item: CallbackDataParams): number | null {
+ const raw = item.value ?? item.data;
+ if (Array.isArray(raw)) {
+ const y = Number(raw[raw.length - 1]);
+ return Number.isFinite(y) ? y : null;
+ }
+ const numeric = Number(raw);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function appendOhlcRows(
+ rows: string[][],
+ ohlc: OhlcValue,
+ numberFormatter: NumberFormatter | CurrencyFormatter,
+) {
+ const [open, close, low, high] = ohlc;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ const candles = params.flatMap(item => {
+ const ohlc = extractOhlc(item.value ?? item.data);
+ return ohlc ? [{ item, ohlc }] : [];
+ });
+
+ candles.forEach(({ item, ohlc }) => {
+ const [open, close] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ const seriesLabel = String(item.seriesName ?? '');
+ rows.push([seriesLabel ? `${seriesLabel} (${direction})` : direction]);
+ appendOhlcRows(rows, ohlc, numberFormatter);
+ });
+
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = extractLineValue(item);
+ if (value === null) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, title);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: _echartOptions, ...formData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged, onContextMenu } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ candlestickSeriesName,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ seriesStyle,
+ colorScheme,
+ sliceId,
+ colorByDirection,
+ }: EchartsCandlestickFormData = { ...DEFAULT_FORM_DATA, ...formData };
+
+ // Matches buildQuery.ts's getXAxisColumn: an unset x_axis with
+ // granularity_sqla present still queries DTTM_ALIAS, so the transform must
+ // resolve the same column or every row collapses into one empty category.
+ const xAxisName = (getXAxisLabel(chartProps.rawFormData) as string) ?? '';
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Redundant type assertion</b></div>
<div id="fix">
`getXAxisLabel` already returns `Optional<string>` (superset-ui-core `getXAxis.ts:45`), so `as string` is a redundant assertion that hides the declared optionality one token before `?? ''` handles it. Dropping the cast keeps the type honest and lets the nullish coalescing do the narrowing; runtime behavior is unchanged.
</div>
</div>
<small><i>Code Review Run #e7894c</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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "rusackas (via GitHub)" <gi...@apache.org>.
rusackas merged PR #43863:
URL: https://github.com/apache/superset/pull/43863
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5737539414
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4051010390
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/controlPanel.tsx:
##########
@@ -0,0 +1,353 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t } from '@apache-superset/core/translation';
+import { ensureIsArray, validateNonEmpty } from '@superset-ui/core';
+import {
+ ControlPanelConfig,
+ ControlPanelsContainerProps,
+ ControlSubSectionHeader,
+ D3_TIME_FORMAT_DOCS,
+ DEFAULT_TIME_FORMAT,
+ getStandardizedControls,
+ sections,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import {
+ legendSection,
+ tooltipTimeFormatControl,
+ tooltipValuesFormatControl,
+ xAxisLabelInterval,
+ xAxisLabelRotation,
+} from '../controls';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_INCREASE_COLOR,
+ DEFAULT_SERIES_STYLE,
+} from './constants';
+import { MOVING_AVERAGE_PERIODS } from './utils';
+
+function hasSeriesDimension({
+ controls,
+}: ControlPanelsContainerProps): boolean {
+ return ensureIsArray(controls?.series?.value).length > 0;
+}
+
+function showColorByDirection(props: ControlPanelsContainerProps): boolean {
+ return !hasSeriesDimension(props);
+}
Review Comment:
fix in [1459e67](https://github.com/apache/superset/pull/43863/commits/1459e67e91fd4370a0b6457e7207e59ae5b82b4f)
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4052915925
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/controlPanel.tsx:
##########
@@ -0,0 +1,427 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t } from '@apache-superset/core/translation';
+import {
+ ensureIsArray,
+ getColumnLabel,
+ QueryFormColumn,
+ validateNonEmpty,
+} from '@superset-ui/core';
+import {
+ ControlPanelConfig,
+ ControlPanelsContainerProps,
+ ControlSubSectionHeader,
+ D3_TIME_FORMAT_DOCS,
+ DEFAULT_TIME_FORMAT,
+ getStandardizedControls,
+ sections,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import {
+ legendSection,
+ tooltipTimeFormatControl,
+ tooltipValuesFormatControl,
+ xAxisLabelInterval,
+ xAxisLabelRotation,
+} from '../controls';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_INCREASE_COLOR,
+ DEFAULT_SERIES_STYLE,
+} from './constants';
+import { MOVING_AVERAGE_PERIODS } from './utils';
+
+function hasSeriesDimension({
+ controls,
+}: ControlPanelsContainerProps): boolean {
+ return ensureIsArray(controls?.series?.value).length > 0;
+}
+
+type QueryRow = Record<string, unknown>;
+type ChartQueryResponse = { data?: QueryRow[] };
+
+const uniqueSeriesCountByResponse = new WeakMap<
+ ChartQueryResponse,
+ Map<string, number>
+>();
+const hasMultipleSeriesByProps = new WeakMap<
+ ControlPanelsContainerProps,
+ boolean
+>();
+
+function getSeriesColumnLabel(
+ props: ControlPanelsContainerProps,
+): string | undefined {
+ const [series] = ensureIsArray(props.controls?.series?.value);
+ if (series == null) {
+ return undefined;
+ }
+ return getColumnLabel(series as QueryFormColumn);
+}
+
+function getChartQueryResponse(
+ props: ControlPanelsContainerProps,
+): ChartQueryResponse | undefined {
+ return (props as { chart?: { queriesResponse?: ChartQueryResponse[] } }).chart
+ ?.queriesResponse?.[0];
+}
+
+function countUniqueSeriesValues(
+ props: ControlPanelsContainerProps,
+): number | undefined {
+ const seriesColumn = getSeriesColumnLabel(props);
+ if (!seriesColumn) {
+ return undefined;
+ }
+ const queryResponse = getChartQueryResponse(props);
+ const rows = queryResponse?.data;
+ if (!queryResponse || !rows) {
+ return undefined;
+ }
+ let cachedByColumn = uniqueSeriesCountByResponse.get(queryResponse);
+ if (!cachedByColumn) {
+ cachedByColumn = new Map<string, number>();
+ uniqueSeriesCountByResponse.set(queryResponse, cachedByColumn);
+ }
+ const cachedCount = cachedByColumn.get(seriesColumn);
+ if (cachedCount !== undefined) {
+ return cachedCount;
+ }
+ const count = new Set(
+ rows.map(row =>
+ row[seriesColumn] == null ? null : String(row[seriesColumn]),
+ ),
+ ).size;
+ cachedByColumn.set(seriesColumn, count);
+ return count;
+}
+
+function hasMultipleSeries(props: ControlPanelsContainerProps): boolean {
+ const cached = hasMultipleSeriesByProps.get(props);
+ if (cached !== undefined) {
+ return cached;
+ }
+ const result = (countUniqueSeriesValues(props) ?? 0) > 1;
+ hasMultipleSeriesByProps.set(props, result);
+ return result;
Review Comment:
<!-- Bito Reply -->
The suggestion to remove the `hasMultipleSeriesByProps` cache is correct. Since `countUniqueSeriesValues` already memoizes the result using `uniqueSeriesCountByResponse`, the outer cache is redundant and introduces unnecessary module-level state that relies on prop identity. Removing the outer cache simplifies the logic while maintaining identical behavior.
**superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/controlPanel.tsx**
```
function hasMultipleSeries(props: ControlPanelsContainerProps): boolean {
return (countUniqueSeriesValues(props) ?? 0) > 1;
}
```
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4050874735
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,729 @@
+/**
+ * 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 {
+ AxisType,
+ CategoricalColorNamespace,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { CustomSeriesOption, CustomSeriesRenderItem } from 'echarts';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+ OHLC_TICK_WIDTH_RATIO,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+type AxisTooltipParams = CallbackDataParams & {
+ axisValue?: string | number;
+ axisValueLabel?: string;
+};
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(ohlc: OhlcValue | null): CandlestickDatum {
+ return ohlc ?? [];
+}
+
+function toOhlcBarDatum(
+ ohlc: OhlcValue | null,
+ categoryIndex: number,
+ color: string,
+) {
+ if (!ohlc) {
+ return [];
+ }
+ return {
+ value: [categoryIndex, ...ohlc],
+ itemStyle: {
+ color,
+ },
+ };
+}
+
+function getDirectionItemStyle(increaseHex: string, decreaseHex: string) {
+ return {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ };
+}
+
+function getSeriesItemStyle(seriesColor: string, hollowFill: string) {
+ return {
+ color: seriesColor,
+ color0: hollowFill,
+ borderColor: seriesColor,
+ borderColor0: seriesColor,
+ };
+}
+
+const renderOhlcItem: CustomSeriesRenderItem = (_params, item) => {
+ const x = toNumber(item.value(0));
+ const open = toNumber(item.value(1));
+ const close = toNumber(item.value(2));
+ const low = toNumber(item.value(3));
+ const high = toNumber(item.value(4));
+ if (
+ x === null ||
+ open === null ||
+ close === null ||
+ low === null ||
+ high === null
+ ) {
+ return null;
+ }
+
+ const openPoint = item.coord([x, open]);
+ const closePoint = item.coord([x, close]);
+ const lowPoint = item.coord([x, low]);
+ const highPoint = item.coord([x, high]);
+ const categorySize = item.size?.([1, 0]);
+ const categoryWidth = Array.isArray(categorySize)
+ ? categorySize[0]
+ : categorySize;
+ if (categoryWidth == null || !Number.isFinite(categoryWidth)) {
+ return null;
+ }
+ const halfWidth = categoryWidth * OHLC_TICK_WIDTH_RATIO;
+ const style = item.style({
+ stroke: item.visual('color'),
+ });
+
+ return {
+ type: 'group',
+ children: [
+ {
+ type: 'line',
+ shape: {
+ x1: lowPoint[0],
+ y1: lowPoint[1],
+ x2: highPoint[0],
+ y2: highPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: openPoint[0],
+ y1: openPoint[1],
+ x2: openPoint[0] - halfWidth,
+ y2: openPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: closePoint[0],
+ y1: closePoint[1],
+ x2: closePoint[0] + halfWidth,
+ y2: closePoint[1],
+ },
+ style,
+ },
+ ],
+ };
+};
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function extractLineValue(item: CallbackDataParams): number | null {
+ const raw = item.value ?? item.data;
+ if (Array.isArray(raw)) {
+ const y = Number(raw[raw.length - 1]);
+ return Number.isFinite(y) ? y : null;
+ }
+ const numeric = Number(raw);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function appendOhlcRows(
+ rows: string[][],
+ ohlc: OhlcValue,
+ numberFormatter: NumberFormatter | CurrencyFormatter,
+) {
+ const [open, close, low, high] = ohlc;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+ showSeriesName,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+ showSeriesName: boolean;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candles = params.flatMap(item => {
+ const ohlc = extractOhlc(item.value ?? item.data);
+ return ohlc ? [{ item, ohlc }] : [];
+ });
+
+ candles.forEach(({ item, ohlc }) => {
+ const [open, close] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ if (showSeriesName) {
+ const seriesLabel = String(item.seriesName ?? '');
+ if (seriesLabel) {
+ rows.push([`${seriesLabel} (${direction})`]);
+ }
+ } else {
+ heading = title ? `${title} (${direction})` : direction;
+ }
+ appendOhlcRows(rows, ohlc, numberFormatter);
+ });
+
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = extractLineValue(item);
+ if (value === null) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: _echartOptions, ...formData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ candlestickSeriesName,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ seriesStyle,
+ colorScheme,
+ sliceId,
+ colorByDirection,
+ }: EchartsCandlestickFormData = { ...DEFAULT_FORM_DATA, ...formData };
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const defaultSeriesLabel =
+ candlestickSeriesName?.trim() || CANDLESTICK_SERIES_NAME;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const colorScale = CategoricalColorNamespace.getScale(colorScheme);
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: LookupKey[] = [];
+ const xLabels: string[] = [];
+ const xRecords: DataRecord[] = [];
+ const xKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, xAxisName));
+ if (xKeySet.has(key)) {
+ return;
+ }
+ xKeySet.add(key);
+ xKeys.push(key);
+ xRecords.push(datum);
+ xLabels.push(
+ getOwnValue(coltypeMapping, xAxisName) === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter: axisTimeFormatter,
+ })
+ : extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ }),
+ );
+ });
+
+ const seriesKeys: LookupKey[] = [];
+ const seriesNames: string[] = [];
+ if (seriesName) {
+ const seriesKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, seriesName));
+ if (seriesKeySet.has(key)) {
+ return;
+ }
+ seriesKeySet.add(key);
+ seriesKeys.push(key);
+ seriesNames.push(
+ extractGroupbyLabel({
+ datum,
+ groupby: [seriesName],
+ coltypeMapping,
+ }),
+ );
+ });
+ } else {
+ seriesKeys.push(CANDLESTICK_SERIES_NAME);
+ seriesNames.push(defaultSeriesLabel);
+ }
+
+ const useSeriesColors = seriesNames.length > 1 || colorByDirection === false;
+
+ const recordsBySeriesAndX = new Map<LookupKey, Map<LookupKey, DataRecord>>();
+ data.forEach(datum => {
+ const xKey = toLookupKey(getOwnValue(datum, xAxisName));
+ const seriesKey = seriesName
+ ? toLookupKey(getOwnValue(datum, seriesName))
+ : CANDLESTICK_SERIES_NAME;
+ let byX = recordsBySeriesAndX.get(seriesKey);
+ if (!byX) {
+ byX = new Map();
+ recordsBySeriesAndX.set(seriesKey, byX);
+ }
+ byX.set(xKey, datum);
+ });
+
+ const ohlcBySeries: (OhlcValue | null)[][] = seriesKeys.map(key =>
+ xKeys.map(xKey => {
+ const datum = recordsBySeriesAndX.get(key)?.get(xKey);
+ return datum
+ ? getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel)
+ : null;
+ }),
+ );
+
+ const priceSeries: (CandlestickSeriesOption | CustomSeriesOption)[] =
+ ohlcBySeries.map((ohlcData, index) => {
+ const name = seriesNames[index];
+ const seriesColor = useSeriesColors
+ ? colorScale(name, sliceId)
+ : increaseHex;
+ if (seriesStyle === 'ohlc') {
+ return {
+ name,
+ type: 'custom',
+ renderItem: renderOhlcItem,
+ dimensions: ['-', 'open', 'close', 'low', 'high'],
+ encode: {
+ x: 0,
+ y: [1, 2, 3, 4],
+ tooltip: [1, 2, 3, 4],
+ },
+ itemStyle: {
+ color: seriesColor,
+ },
+ data: ohlcData.map((ohlc, categoryIndex) => {
+ if (!ohlc) {
+ return [];
+ }
+ const [openValue, closeValue] = ohlc;
+ const color = useSeriesColors
+ ? seriesColor
+ : closeValue >= openValue
+ ? increaseHex
+ : decreaseHex;
+ return toOhlcBarDatum(ohlc, categoryIndex, color);
+ }),
+ };
+ }
+ return {
+ name,
+ type: 'candlestick',
+ data: ohlcData.map(toCandlestickDatum),
+ itemStyle: useSeriesColors
+ ? getSeriesItemStyle(seriesColor, theme.colorBgContainer)
+ : getDirectionItemStyle(increaseHex, decreaseHex),
+ };
+ });
+
+ const periods = parseMovingAveragePeriods(movingAverages);
+ const qualifyMaNames = seriesNames.length > 1;
+ const movingAverageSeries: LineSeriesOption[] = ohlcBySeries.flatMap(
+ (ohlcData, index) => {
+ const closes = ohlcData.map(ohlc => ohlc?.[1] ?? null);
+ const seriesLabel = qualifyMaNames ? seriesNames[index] : undefined;
+ const seriesColor = useSeriesColors
+ ? colorScale(seriesNames[index], sliceId)
+ : undefined;
+ return periods.map(period => ({
+ name: movingAverageName(period, seriesLabel),
+ type: 'line' as const,
+ data: calculateMA(closes, period),
+ smooth: true,
+ showSymbol: false,
+ itemStyle: seriesColor ? { color: seriesColor } : undefined,
+ lineStyle: {
+ opacity: MA_LINE_OPACITY,
+ ...(seriesColor ? { color: seriesColor } : {}),
+ },
+ }));
+ },
+ );
+
+ const legendData = [
+ ...seriesNames,
+ ...movingAverageSeries.map(series => String(series.name)),
+ ].sort((a, b) => {
+ if (!legendSort) {
+ return 0;
+ }
+ return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
+ });
+
+ const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({
+ chartHeight: height,
+ chartWidth: width,
+ legendItems: legendData,
+ legendMargin,
+ orientation: legendOrientation,
+ show: showLegend,
+ theme,
+ type: legendType,
+ });
+ const legendPadding = getChartPadding(
+ showLegend,
+ legendOrientation,
+ effectiveLegendMargin,
+ );
+
+ const dataZoom = zoomable
+ ? [
+ {
+ type: 'inside',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ },
+ {
+ type: 'slider',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ bottom: TIMESERIES_CONSTANTS.zoomBottom,
+ },
+ ]
+ : [];
+
+ const echartOptions: EChartsCoreOption = {
+ grid: {
+ ...defaultGrid,
+ top: theme.sizeUnit * 5 + legendPadding.top,
+ bottom:
+ theme.sizeUnit * (showXAxis ? 5 : 3) +
+ legendPadding.bottom +
+ convertInteger(xAxisTitleMargin) +
+ (zoomable ? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable : 0),
+ left:
+ theme.sizeUnit * (showYAxis ? 5 : 2) +
+ legendPadding.left +
+ convertInteger(yAxisTitleMargin),
+ right: theme.sizeUnit * 5 + legendPadding.right,
+ },
+ legend: {
+ ...getLegendProps(
+ effectiveLegendType,
+ legendOrientation,
+ showLegend,
+ theme,
+ zoomable,
+ legendState,
+ ),
+ data: legendData,
+ },
+ xAxis: {
+ show: showXAxis,
+ type: AxisType.Category,
+ data: xLabels,
+ name: xAxisTitle,
+ nameGap: convertInteger(xAxisTitleMargin),
+ nameLocation: 'middle',
+ axisLabel: {
+ rotate: xAxisLabelRotation,
+ interval: xAxisLabelInterval === 'auto' ? 'auto' : 0,
+ hideOverlap: true,
+ },
+ },
+ yAxis: {
+ ...defaultYAxis,
+ show: showYAxis,
+ type: AxisType.Value,
+ name: yAxisTitle,
+ nameGap: convertInteger(yAxisTitleMargin),
+ nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
+ axisLabel: { formatter: yAxisFormatter },
+ },
+ tooltip: {
+ ...getDefaultTooltip(refs),
+ trigger: 'axis',
+ axisPointer: { type: 'cross' },
+ show: !inContextMenu,
+ formatter: (params: CallbackDataParams | CallbackDataParams[]) => {
+ const items = ensureIsArray(params) as AxisTooltipParams[];
+ const [item] = items;
+ if (!item) {
+ return '';
+ }
+ const categoryIndex = item.dataIndex;
+ const categoryDatum =
+ Number.isInteger(categoryIndex) &&
+ categoryIndex >= 0 &&
+ categoryIndex < xRecords.length
+ ? xRecords[categoryIndex]
+ : {};
+ const categoryLabel = Number.isInteger(categoryIndex)
+ ? xLabels[categoryIndex]
+ : undefined;
+ const title =
+ getOwnValue(coltypeMapping, xAxisName) === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum: categoryDatum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter,
+ })
+ : String(
+ item.axisValueLabel ??
+ item.axisValue ??
+ item.name ??
+ categoryLabel ??
+ '',
+ );
+ return formatTooltip({
+ params: items,
+ numberFormatter,
+ title,
+ increaseLabel: upLabel,
+ decreaseLabel: downLabel,
+ showSeriesName: seriesNames.length > 1,
Review Comment:
fix in [0249394](https://github.com/apache/superset/pull/43863/commits/0249394adc95a0a410f1e12c0a27e463a6ce0626)
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4050837588
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/controlPanel.tsx:
##########
@@ -0,0 +1,353 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t } from '@apache-superset/core/translation';
+import { ensureIsArray, validateNonEmpty } from '@superset-ui/core';
+import {
+ ControlPanelConfig,
+ ControlPanelsContainerProps,
+ ControlSubSectionHeader,
+ D3_TIME_FORMAT_DOCS,
+ DEFAULT_TIME_FORMAT,
+ getStandardizedControls,
+ sections,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import {
+ legendSection,
+ tooltipTimeFormatControl,
+ tooltipValuesFormatControl,
+ xAxisLabelInterval,
+ xAxisLabelRotation,
+} from '../controls';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_INCREASE_COLOR,
+ DEFAULT_SERIES_STYLE,
+} from './constants';
+import { MOVING_AVERAGE_PERIODS } from './utils';
+
+function hasSeriesDimension({
+ controls,
+}: ControlPanelsContainerProps): boolean {
+ return ensureIsArray(controls?.series?.value).length > 0;
+}
+
+function showColorByDirection(props: ControlPanelsContainerProps): boolean {
+ return !hasSeriesDimension(props);
+}
Review Comment:
This visibility check depends on whether a Series dimension is configured, not on how many values it returns. For a dimension containing one ticker, `transformProps` still uses direction coloring, but the checkbox and direction colors are hidden, so the user cannot turn direction coloring off as promised in the PR’s single-ticker behavior. Since cardinality is unavailable to the control panel, keep this control visible; multiple-series rendering already ignores its value.
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,729 @@
+/**
+ * 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 {
+ AxisType,
+ CategoricalColorNamespace,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { CustomSeriesOption, CustomSeriesRenderItem } from 'echarts';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+ OHLC_TICK_WIDTH_RATIO,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+type AxisTooltipParams = CallbackDataParams & {
+ axisValue?: string | number;
+ axisValueLabel?: string;
+};
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(ohlc: OhlcValue | null): CandlestickDatum {
+ return ohlc ?? [];
+}
+
+function toOhlcBarDatum(
+ ohlc: OhlcValue | null,
+ categoryIndex: number,
+ color: string,
+) {
+ if (!ohlc) {
+ return [];
+ }
+ return {
+ value: [categoryIndex, ...ohlc],
+ itemStyle: {
+ color,
+ },
+ };
+}
+
+function getDirectionItemStyle(increaseHex: string, decreaseHex: string) {
+ return {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ };
+}
+
+function getSeriesItemStyle(seriesColor: string, hollowFill: string) {
+ return {
+ color: seriesColor,
+ color0: hollowFill,
+ borderColor: seriesColor,
+ borderColor0: seriesColor,
+ };
+}
+
+const renderOhlcItem: CustomSeriesRenderItem = (_params, item) => {
+ const x = toNumber(item.value(0));
+ const open = toNumber(item.value(1));
+ const close = toNumber(item.value(2));
+ const low = toNumber(item.value(3));
+ const high = toNumber(item.value(4));
+ if (
+ x === null ||
+ open === null ||
+ close === null ||
+ low === null ||
+ high === null
+ ) {
+ return null;
+ }
+
+ const openPoint = item.coord([x, open]);
+ const closePoint = item.coord([x, close]);
+ const lowPoint = item.coord([x, low]);
+ const highPoint = item.coord([x, high]);
+ const categorySize = item.size?.([1, 0]);
+ const categoryWidth = Array.isArray(categorySize)
+ ? categorySize[0]
+ : categorySize;
+ if (categoryWidth == null || !Number.isFinite(categoryWidth)) {
+ return null;
+ }
+ const halfWidth = categoryWidth * OHLC_TICK_WIDTH_RATIO;
+ const style = item.style({
+ stroke: item.visual('color'),
+ });
+
+ return {
+ type: 'group',
+ children: [
+ {
+ type: 'line',
+ shape: {
+ x1: lowPoint[0],
+ y1: lowPoint[1],
+ x2: highPoint[0],
+ y2: highPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: openPoint[0],
+ y1: openPoint[1],
+ x2: openPoint[0] - halfWidth,
+ y2: openPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: closePoint[0],
+ y1: closePoint[1],
+ x2: closePoint[0] + halfWidth,
+ y2: closePoint[1],
+ },
+ style,
+ },
+ ],
+ };
+};
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function extractLineValue(item: CallbackDataParams): number | null {
+ const raw = item.value ?? item.data;
+ if (Array.isArray(raw)) {
+ const y = Number(raw[raw.length - 1]);
+ return Number.isFinite(y) ? y : null;
+ }
+ const numeric = Number(raw);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function appendOhlcRows(
+ rows: string[][],
+ ohlc: OhlcValue,
+ numberFormatter: NumberFormatter | CurrencyFormatter,
+) {
+ const [open, close, low, high] = ohlc;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+ showSeriesName,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+ showSeriesName: boolean;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candles = params.flatMap(item => {
+ const ohlc = extractOhlc(item.value ?? item.data);
+ return ohlc ? [{ item, ohlc }] : [];
+ });
+
+ candles.forEach(({ item, ohlc }) => {
+ const [open, close] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ if (showSeriesName) {
+ const seriesLabel = String(item.seriesName ?? '');
+ if (seriesLabel) {
+ rows.push([`${seriesLabel} (${direction})`]);
+ }
+ } else {
+ heading = title ? `${title} (${direction})` : direction;
+ }
+ appendOhlcRows(rows, ohlc, numberFormatter);
+ });
+
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = extractLineValue(item);
+ if (value === null) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: _echartOptions, ...formData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ candlestickSeriesName,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ seriesStyle,
+ colorScheme,
+ sliceId,
+ colorByDirection,
+ }: EchartsCandlestickFormData = { ...DEFAULT_FORM_DATA, ...formData };
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const defaultSeriesLabel =
+ candlestickSeriesName?.trim() || CANDLESTICK_SERIES_NAME;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const colorScale = CategoricalColorNamespace.getScale(colorScheme);
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: LookupKey[] = [];
+ const xLabels: string[] = [];
+ const xRecords: DataRecord[] = [];
+ const xKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, xAxisName));
+ if (xKeySet.has(key)) {
+ return;
+ }
+ xKeySet.add(key);
+ xKeys.push(key);
+ xRecords.push(datum);
+ xLabels.push(
+ getOwnValue(coltypeMapping, xAxisName) === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter: axisTimeFormatter,
+ })
+ : extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ }),
+ );
+ });
+
+ const seriesKeys: LookupKey[] = [];
+ const seriesNames: string[] = [];
+ if (seriesName) {
+ const seriesKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, seriesName));
+ if (seriesKeySet.has(key)) {
+ return;
+ }
+ seriesKeySet.add(key);
+ seriesKeys.push(key);
+ seriesNames.push(
+ extractGroupbyLabel({
+ datum,
+ groupby: [seriesName],
+ coltypeMapping,
+ }),
+ );
+ });
+ } else {
+ seriesKeys.push(CANDLESTICK_SERIES_NAME);
+ seriesNames.push(defaultSeriesLabel);
+ }
+
+ const useSeriesColors = seriesNames.length > 1 || colorByDirection === false;
+
+ const recordsBySeriesAndX = new Map<LookupKey, Map<LookupKey, DataRecord>>();
+ data.forEach(datum => {
+ const xKey = toLookupKey(getOwnValue(datum, xAxisName));
+ const seriesKey = seriesName
+ ? toLookupKey(getOwnValue(datum, seriesName))
+ : CANDLESTICK_SERIES_NAME;
+ let byX = recordsBySeriesAndX.get(seriesKey);
+ if (!byX) {
+ byX = new Map();
+ recordsBySeriesAndX.set(seriesKey, byX);
+ }
+ byX.set(xKey, datum);
+ });
+
+ const ohlcBySeries: (OhlcValue | null)[][] = seriesKeys.map(key =>
+ xKeys.map(xKey => {
+ const datum = recordsBySeriesAndX.get(key)?.get(xKey);
+ return datum
+ ? getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel)
+ : null;
+ }),
+ );
+
+ const priceSeries: (CandlestickSeriesOption | CustomSeriesOption)[] =
+ ohlcBySeries.map((ohlcData, index) => {
+ const name = seriesNames[index];
+ const seriesColor = useSeriesColors
+ ? colorScale(name, sliceId)
+ : increaseHex;
+ if (seriesStyle === 'ohlc') {
+ return {
+ name,
+ type: 'custom',
+ renderItem: renderOhlcItem,
+ dimensions: ['-', 'open', 'close', 'low', 'high'],
+ encode: {
+ x: 0,
+ y: [1, 2, 3, 4],
+ tooltip: [1, 2, 3, 4],
+ },
+ itemStyle: {
+ color: seriesColor,
+ },
+ data: ohlcData.map((ohlc, categoryIndex) => {
+ if (!ohlc) {
+ return [];
+ }
+ const [openValue, closeValue] = ohlc;
+ const color = useSeriesColors
+ ? seriesColor
+ : closeValue >= openValue
+ ? increaseHex
+ : decreaseHex;
+ return toOhlcBarDatum(ohlc, categoryIndex, color);
+ }),
+ };
+ }
+ return {
+ name,
+ type: 'candlestick',
+ data: ohlcData.map(toCandlestickDatum),
+ itemStyle: useSeriesColors
+ ? getSeriesItemStyle(seriesColor, theme.colorBgContainer)
+ : getDirectionItemStyle(increaseHex, decreaseHex),
+ };
+ });
+
+ const periods = parseMovingAveragePeriods(movingAverages);
+ const qualifyMaNames = seriesNames.length > 1;
+ const movingAverageSeries: LineSeriesOption[] = ohlcBySeries.flatMap(
+ (ohlcData, index) => {
+ const closes = ohlcData.map(ohlc => ohlc?.[1] ?? null);
+ const seriesLabel = qualifyMaNames ? seriesNames[index] : undefined;
+ const seriesColor = useSeriesColors
+ ? colorScale(seriesNames[index], sliceId)
+ : undefined;
+ return periods.map(period => ({
+ name: movingAverageName(period, seriesLabel),
+ type: 'line' as const,
+ data: calculateMA(closes, period),
+ smooth: true,
+ showSymbol: false,
+ itemStyle: seriesColor ? { color: seriesColor } : undefined,
+ lineStyle: {
+ opacity: MA_LINE_OPACITY,
+ ...(seriesColor ? { color: seriesColor } : {}),
+ },
+ }));
+ },
+ );
+
+ const legendData = [
+ ...seriesNames,
+ ...movingAverageSeries.map(series => String(series.name)),
+ ].sort((a, b) => {
+ if (!legendSort) {
+ return 0;
+ }
+ return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
+ });
+
+ const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({
+ chartHeight: height,
+ chartWidth: width,
+ legendItems: legendData,
+ legendMargin,
+ orientation: legendOrientation,
+ show: showLegend,
+ theme,
+ type: legendType,
+ });
+ const legendPadding = getChartPadding(
+ showLegend,
+ legendOrientation,
+ effectiveLegendMargin,
+ );
+
+ const dataZoom = zoomable
+ ? [
+ {
+ type: 'inside',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ },
+ {
+ type: 'slider',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ bottom: TIMESERIES_CONSTANTS.zoomBottom,
+ },
+ ]
+ : [];
+
+ const echartOptions: EChartsCoreOption = {
+ grid: {
+ ...defaultGrid,
+ top: theme.sizeUnit * 5 + legendPadding.top,
+ bottom:
+ theme.sizeUnit * (showXAxis ? 5 : 3) +
+ legendPadding.bottom +
+ convertInteger(xAxisTitleMargin) +
+ (zoomable ? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable : 0),
+ left:
+ theme.sizeUnit * (showYAxis ? 5 : 2) +
+ legendPadding.left +
+ convertInteger(yAxisTitleMargin),
+ right: theme.sizeUnit * 5 + legendPadding.right,
+ },
Review Comment:
Axis-title margins are applied even when the axis/title is hidden (defaults reserve 40px bottom and 50px left for empty titles), and the Y margin is always put on the left even when the title position is `Top`. This unnecessarily shrinks default charts and leaves top-positioned Y titles without their intended top offset. Gate margins on visible, non-empty titles and apply the Y margin to the selected edge, consistent with `Timeseries/transformers.ts:982-996`.
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,729 @@
+/**
+ * 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 {
+ AxisType,
+ CategoricalColorNamespace,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { CustomSeriesOption, CustomSeriesRenderItem } from 'echarts';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+ OHLC_TICK_WIDTH_RATIO,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+type AxisTooltipParams = CallbackDataParams & {
+ axisValue?: string | number;
+ axisValueLabel?: string;
+};
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(ohlc: OhlcValue | null): CandlestickDatum {
+ return ohlc ?? [];
+}
+
+function toOhlcBarDatum(
+ ohlc: OhlcValue | null,
+ categoryIndex: number,
+ color: string,
+) {
+ if (!ohlc) {
+ return [];
+ }
+ return {
+ value: [categoryIndex, ...ohlc],
+ itemStyle: {
+ color,
+ },
+ };
+}
+
+function getDirectionItemStyle(increaseHex: string, decreaseHex: string) {
+ return {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ };
+}
+
+function getSeriesItemStyle(seriesColor: string, hollowFill: string) {
+ return {
+ color: seriesColor,
+ color0: hollowFill,
+ borderColor: seriesColor,
+ borderColor0: seriesColor,
+ };
+}
+
+const renderOhlcItem: CustomSeriesRenderItem = (_params, item) => {
+ const x = toNumber(item.value(0));
+ const open = toNumber(item.value(1));
+ const close = toNumber(item.value(2));
+ const low = toNumber(item.value(3));
+ const high = toNumber(item.value(4));
+ if (
+ x === null ||
+ open === null ||
+ close === null ||
+ low === null ||
+ high === null
+ ) {
+ return null;
+ }
+
+ const openPoint = item.coord([x, open]);
+ const closePoint = item.coord([x, close]);
+ const lowPoint = item.coord([x, low]);
+ const highPoint = item.coord([x, high]);
+ const categorySize = item.size?.([1, 0]);
+ const categoryWidth = Array.isArray(categorySize)
+ ? categorySize[0]
+ : categorySize;
+ if (categoryWidth == null || !Number.isFinite(categoryWidth)) {
+ return null;
+ }
+ const halfWidth = categoryWidth * OHLC_TICK_WIDTH_RATIO;
+ const style = item.style({
+ stroke: item.visual('color'),
+ });
+
+ return {
+ type: 'group',
+ children: [
+ {
+ type: 'line',
+ shape: {
+ x1: lowPoint[0],
+ y1: lowPoint[1],
+ x2: highPoint[0],
+ y2: highPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: openPoint[0],
+ y1: openPoint[1],
+ x2: openPoint[0] - halfWidth,
+ y2: openPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: closePoint[0],
+ y1: closePoint[1],
+ x2: closePoint[0] + halfWidth,
+ y2: closePoint[1],
+ },
+ style,
+ },
+ ],
+ };
+};
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function extractLineValue(item: CallbackDataParams): number | null {
+ const raw = item.value ?? item.data;
+ if (Array.isArray(raw)) {
+ const y = Number(raw[raw.length - 1]);
+ return Number.isFinite(y) ? y : null;
+ }
+ const numeric = Number(raw);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function appendOhlcRows(
+ rows: string[][],
+ ohlc: OhlcValue,
+ numberFormatter: NumberFormatter | CurrencyFormatter,
+) {
+ const [open, close, low, high] = ohlc;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+ showSeriesName,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+ showSeriesName: boolean;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candles = params.flatMap(item => {
+ const ohlc = extractOhlc(item.value ?? item.data);
+ return ohlc ? [{ item, ohlc }] : [];
+ });
+
+ candles.forEach(({ item, ohlc }) => {
+ const [open, close] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ if (showSeriesName) {
+ const seriesLabel = String(item.seriesName ?? '');
+ if (seriesLabel) {
+ rows.push([`${seriesLabel} (${direction})`]);
+ }
+ } else {
+ heading = title ? `${title} (${direction})` : direction;
+ }
+ appendOhlcRows(rows, ohlc, numberFormatter);
+ });
+
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = extractLineValue(item);
+ if (value === null) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: _echartOptions, ...formData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ candlestickSeriesName,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ seriesStyle,
+ colorScheme,
+ sliceId,
+ colorByDirection,
+ }: EchartsCandlestickFormData = { ...DEFAULT_FORM_DATA, ...formData };
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const defaultSeriesLabel =
+ candlestickSeriesName?.trim() || CANDLESTICK_SERIES_NAME;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const colorScale = CategoricalColorNamespace.getScale(colorScheme);
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: LookupKey[] = [];
+ const xLabels: string[] = [];
+ const xRecords: DataRecord[] = [];
+ const xKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, xAxisName));
+ if (xKeySet.has(key)) {
+ return;
+ }
+ xKeySet.add(key);
+ xKeys.push(key);
+ xRecords.push(datum);
+ xLabels.push(
+ getOwnValue(coltypeMapping, xAxisName) === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter: axisTimeFormatter,
+ })
+ : extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ }),
+ );
+ });
+
+ const seriesKeys: LookupKey[] = [];
+ const seriesNames: string[] = [];
+ if (seriesName) {
+ const seriesKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, seriesName));
+ if (seriesKeySet.has(key)) {
+ return;
+ }
+ seriesKeySet.add(key);
+ seriesKeys.push(key);
+ seriesNames.push(
+ extractGroupbyLabel({
+ datum,
+ groupby: [seriesName],
+ coltypeMapping,
+ }),
+ );
+ });
+ } else {
+ seriesKeys.push(CANDLESTICK_SERIES_NAME);
+ seriesNames.push(defaultSeriesLabel);
+ }
+
+ const useSeriesColors = seriesNames.length > 1 || colorByDirection === false;
+
+ const recordsBySeriesAndX = new Map<LookupKey, Map<LookupKey, DataRecord>>();
+ data.forEach(datum => {
+ const xKey = toLookupKey(getOwnValue(datum, xAxisName));
+ const seriesKey = seriesName
+ ? toLookupKey(getOwnValue(datum, seriesName))
+ : CANDLESTICK_SERIES_NAME;
+ let byX = recordsBySeriesAndX.get(seriesKey);
+ if (!byX) {
+ byX = new Map();
+ recordsBySeriesAndX.set(seriesKey, byX);
+ }
+ byX.set(xKey, datum);
+ });
+
+ const ohlcBySeries: (OhlcValue | null)[][] = seriesKeys.map(key =>
+ xKeys.map(xKey => {
+ const datum = recordsBySeriesAndX.get(key)?.get(xKey);
+ return datum
+ ? getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel)
+ : null;
+ }),
+ );
+
+ const priceSeries: (CandlestickSeriesOption | CustomSeriesOption)[] =
+ ohlcBySeries.map((ohlcData, index) => {
+ const name = seriesNames[index];
+ const seriesColor = useSeriesColors
+ ? colorScale(name, sliceId)
+ : increaseHex;
+ if (seriesStyle === 'ohlc') {
+ return {
+ name,
+ type: 'custom',
+ renderItem: renderOhlcItem,
+ dimensions: ['-', 'open', 'close', 'low', 'high'],
+ encode: {
+ x: 0,
+ y: [1, 2, 3, 4],
+ tooltip: [1, 2, 3, 4],
+ },
+ itemStyle: {
+ color: seriesColor,
+ },
+ data: ohlcData.map((ohlc, categoryIndex) => {
+ if (!ohlc) {
+ return [];
+ }
+ const [openValue, closeValue] = ohlc;
+ const color = useSeriesColors
+ ? seriesColor
+ : closeValue >= openValue
+ ? increaseHex
+ : decreaseHex;
+ return toOhlcBarDatum(ohlc, categoryIndex, color);
+ }),
+ };
+ }
+ return {
+ name,
+ type: 'candlestick',
+ data: ohlcData.map(toCandlestickDatum),
+ itemStyle: useSeriesColors
+ ? getSeriesItemStyle(seriesColor, theme.colorBgContainer)
+ : getDirectionItemStyle(increaseHex, decreaseHex),
+ };
+ });
+
+ const periods = parseMovingAveragePeriods(movingAverages);
+ const qualifyMaNames = seriesNames.length > 1;
+ const movingAverageSeries: LineSeriesOption[] = ohlcBySeries.flatMap(
+ (ohlcData, index) => {
+ const closes = ohlcData.map(ohlc => ohlc?.[1] ?? null);
+ const seriesLabel = qualifyMaNames ? seriesNames[index] : undefined;
+ const seriesColor = useSeriesColors
+ ? colorScale(seriesNames[index], sliceId)
+ : undefined;
+ return periods.map(period => ({
+ name: movingAverageName(period, seriesLabel),
+ type: 'line' as const,
+ data: calculateMA(closes, period),
+ smooth: true,
+ showSymbol: false,
+ itemStyle: seriesColor ? { color: seriesColor } : undefined,
+ lineStyle: {
+ opacity: MA_LINE_OPACITY,
+ ...(seriesColor ? { color: seriesColor } : {}),
+ },
+ }));
+ },
+ );
+
+ const legendData = [
+ ...seriesNames,
+ ...movingAverageSeries.map(series => String(series.name)),
+ ].sort((a, b) => {
+ if (!legendSort) {
+ return 0;
+ }
+ return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
+ });
+
+ const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({
+ chartHeight: height,
+ chartWidth: width,
+ legendItems: legendData,
+ legendMargin,
+ orientation: legendOrientation,
+ show: showLegend,
+ theme,
+ type: legendType,
+ });
+ const legendPadding = getChartPadding(
+ showLegend,
+ legendOrientation,
+ effectiveLegendMargin,
+ );
+
+ const dataZoom = zoomable
+ ? [
+ {
+ type: 'inside',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ },
+ {
+ type: 'slider',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ bottom: TIMESERIES_CONSTANTS.zoomBottom,
+ },
+ ]
+ : [];
+
+ const echartOptions: EChartsCoreOption = {
+ grid: {
+ ...defaultGrid,
+ top: theme.sizeUnit * 5 + legendPadding.top,
+ bottom:
+ theme.sizeUnit * (showXAxis ? 5 : 3) +
+ legendPadding.bottom +
+ convertInteger(xAxisTitleMargin) +
+ (zoomable ? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable : 0),
+ left:
+ theme.sizeUnit * (showYAxis ? 5 : 2) +
+ legendPadding.left +
+ convertInteger(yAxisTitleMargin),
+ right: theme.sizeUnit * 5 + legendPadding.right,
+ },
+ legend: {
+ ...getLegendProps(
+ effectiveLegendType,
+ legendOrientation,
+ showLegend,
+ theme,
+ zoomable,
+ legendState,
+ ),
+ data: legendData,
+ },
+ xAxis: {
+ show: showXAxis,
+ type: AxisType.Category,
+ data: xLabels,
+ name: xAxisTitle,
+ nameGap: convertInteger(xAxisTitleMargin),
+ nameLocation: 'middle',
+ axisLabel: {
+ rotate: xAxisLabelRotation,
+ interval: xAxisLabelInterval === 'auto' ? 'auto' : 0,
+ hideOverlap: true,
+ },
+ },
+ yAxis: {
+ ...defaultYAxis,
+ show: showYAxis,
+ type: AxisType.Value,
+ name: yAxisTitle,
+ nameGap: convertInteger(yAxisTitleMargin),
+ nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
+ axisLabel: { formatter: yAxisFormatter },
+ },
+ tooltip: {
+ ...getDefaultTooltip(refs),
+ trigger: 'axis',
+ axisPointer: { type: 'cross' },
+ show: !inContextMenu,
Review Comment:
`inContextMenu` never becomes true for this plugin in normal dashboard rendering. `ChartRenderer.tsx:501-507` only forwards that state to charts declaring `Behavior.DrillToDetail`, while the new Candlestick metadata declares no behaviors; the fallback context menu can therefore open while this tooltip remains shown, contrary to testing step 9. Add the appropriate metadata/event integration or explicitly hide the ECharts tooltip on context-menu events.
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,729 @@
+/**
+ * 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 {
+ AxisType,
+ CategoricalColorNamespace,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { CustomSeriesOption, CustomSeriesRenderItem } from 'echarts';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+ OHLC_TICK_WIDTH_RATIO,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+type AxisTooltipParams = CallbackDataParams & {
+ axisValue?: string | number;
+ axisValueLabel?: string;
+};
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(ohlc: OhlcValue | null): CandlestickDatum {
+ return ohlc ?? [];
+}
+
+function toOhlcBarDatum(
+ ohlc: OhlcValue | null,
+ categoryIndex: number,
+ color: string,
+) {
+ if (!ohlc) {
+ return [];
+ }
+ return {
+ value: [categoryIndex, ...ohlc],
+ itemStyle: {
+ color,
+ },
+ };
+}
+
+function getDirectionItemStyle(increaseHex: string, decreaseHex: string) {
+ return {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ };
+}
+
+function getSeriesItemStyle(seriesColor: string, hollowFill: string) {
+ return {
+ color: seriesColor,
+ color0: hollowFill,
+ borderColor: seriesColor,
+ borderColor0: seriesColor,
+ };
+}
+
+const renderOhlcItem: CustomSeriesRenderItem = (_params, item) => {
+ const x = toNumber(item.value(0));
+ const open = toNumber(item.value(1));
+ const close = toNumber(item.value(2));
+ const low = toNumber(item.value(3));
+ const high = toNumber(item.value(4));
+ if (
+ x === null ||
+ open === null ||
+ close === null ||
+ low === null ||
+ high === null
+ ) {
+ return null;
+ }
+
+ const openPoint = item.coord([x, open]);
+ const closePoint = item.coord([x, close]);
+ const lowPoint = item.coord([x, low]);
+ const highPoint = item.coord([x, high]);
+ const categorySize = item.size?.([1, 0]);
+ const categoryWidth = Array.isArray(categorySize)
+ ? categorySize[0]
+ : categorySize;
+ if (categoryWidth == null || !Number.isFinite(categoryWidth)) {
+ return null;
+ }
+ const halfWidth = categoryWidth * OHLC_TICK_WIDTH_RATIO;
+ const style = item.style({
+ stroke: item.visual('color'),
+ });
+
+ return {
+ type: 'group',
+ children: [
+ {
+ type: 'line',
+ shape: {
+ x1: lowPoint[0],
+ y1: lowPoint[1],
+ x2: highPoint[0],
+ y2: highPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: openPoint[0],
+ y1: openPoint[1],
+ x2: openPoint[0] - halfWidth,
+ y2: openPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: closePoint[0],
+ y1: closePoint[1],
+ x2: closePoint[0] + halfWidth,
+ y2: closePoint[1],
+ },
+ style,
+ },
+ ],
+ };
+};
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function extractLineValue(item: CallbackDataParams): number | null {
+ const raw = item.value ?? item.data;
+ if (Array.isArray(raw)) {
+ const y = Number(raw[raw.length - 1]);
+ return Number.isFinite(y) ? y : null;
+ }
+ const numeric = Number(raw);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function appendOhlcRows(
+ rows: string[][],
+ ohlc: OhlcValue,
+ numberFormatter: NumberFormatter | CurrencyFormatter,
+) {
+ const [open, close, low, high] = ohlc;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+ showSeriesName,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+ showSeriesName: boolean;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candles = params.flatMap(item => {
+ const ohlc = extractOhlc(item.value ?? item.data);
+ return ohlc ? [{ item, ohlc }] : [];
+ });
+
+ candles.forEach(({ item, ohlc }) => {
+ const [open, close] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ if (showSeriesName) {
+ const seriesLabel = String(item.seriesName ?? '');
+ if (seriesLabel) {
+ rows.push([`${seriesLabel} (${direction})`]);
+ }
+ } else {
+ heading = title ? `${title} (${direction})` : direction;
+ }
+ appendOhlcRows(rows, ohlc, numberFormatter);
+ });
+
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = extractLineValue(item);
+ if (value === null) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: _echartOptions, ...formData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ candlestickSeriesName,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ seriesStyle,
+ colorScheme,
+ sliceId,
+ colorByDirection,
+ }: EchartsCandlestickFormData = { ...DEFAULT_FORM_DATA, ...formData };
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const defaultSeriesLabel =
+ candlestickSeriesName?.trim() || CANDLESTICK_SERIES_NAME;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const colorScale = CategoricalColorNamespace.getScale(colorScheme);
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: LookupKey[] = [];
+ const xLabels: string[] = [];
+ const xRecords: DataRecord[] = [];
+ const xKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, xAxisName));
+ if (xKeySet.has(key)) {
+ return;
+ }
+ xKeySet.add(key);
+ xKeys.push(key);
+ xRecords.push(datum);
+ xLabels.push(
+ getOwnValue(coltypeMapping, xAxisName) === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter: axisTimeFormatter,
+ })
+ : extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ }),
+ );
+ });
+
+ const seriesKeys: LookupKey[] = [];
+ const seriesNames: string[] = [];
+ if (seriesName) {
+ const seriesKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, seriesName));
+ if (seriesKeySet.has(key)) {
+ return;
+ }
+ seriesKeySet.add(key);
+ seriesKeys.push(key);
+ seriesNames.push(
+ extractGroupbyLabel({
+ datum,
+ groupby: [seriesName],
+ coltypeMapping,
+ }),
+ );
+ });
+ } else {
+ seriesKeys.push(CANDLESTICK_SERIES_NAME);
+ seriesNames.push(defaultSeriesLabel);
+ }
+
+ const useSeriesColors = seriesNames.length > 1 || colorByDirection === false;
+
+ const recordsBySeriesAndX = new Map<LookupKey, Map<LookupKey, DataRecord>>();
+ data.forEach(datum => {
+ const xKey = toLookupKey(getOwnValue(datum, xAxisName));
+ const seriesKey = seriesName
+ ? toLookupKey(getOwnValue(datum, seriesName))
+ : CANDLESTICK_SERIES_NAME;
+ let byX = recordsBySeriesAndX.get(seriesKey);
+ if (!byX) {
+ byX = new Map();
+ recordsBySeriesAndX.set(seriesKey, byX);
+ }
+ byX.set(xKey, datum);
+ });
+
+ const ohlcBySeries: (OhlcValue | null)[][] = seriesKeys.map(key =>
+ xKeys.map(xKey => {
+ const datum = recordsBySeriesAndX.get(key)?.get(xKey);
+ return datum
+ ? getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel)
+ : null;
+ }),
+ );
+
+ const priceSeries: (CandlestickSeriesOption | CustomSeriesOption)[] =
+ ohlcBySeries.map((ohlcData, index) => {
+ const name = seriesNames[index];
+ const seriesColor = useSeriesColors
+ ? colorScale(name, sliceId)
+ : increaseHex;
+ if (seriesStyle === 'ohlc') {
+ return {
+ name,
+ type: 'custom',
+ renderItem: renderOhlcItem,
+ dimensions: ['-', 'open', 'close', 'low', 'high'],
+ encode: {
+ x: 0,
+ y: [1, 2, 3, 4],
+ tooltip: [1, 2, 3, 4],
+ },
+ itemStyle: {
+ color: seriesColor,
+ },
+ data: ohlcData.map((ohlc, categoryIndex) => {
+ if (!ohlc) {
+ return [];
+ }
+ const [openValue, closeValue] = ohlc;
+ const color = useSeriesColors
+ ? seriesColor
+ : closeValue >= openValue
+ ? increaseHex
+ : decreaseHex;
+ return toOhlcBarDatum(ohlc, categoryIndex, color);
+ }),
+ };
+ }
+ return {
+ name,
+ type: 'candlestick',
+ data: ohlcData.map(toCandlestickDatum),
+ itemStyle: useSeriesColors
+ ? getSeriesItemStyle(seriesColor, theme.colorBgContainer)
+ : getDirectionItemStyle(increaseHex, decreaseHex),
+ };
+ });
+
+ const periods = parseMovingAveragePeriods(movingAverages);
+ const qualifyMaNames = seriesNames.length > 1;
+ const movingAverageSeries: LineSeriesOption[] = ohlcBySeries.flatMap(
+ (ohlcData, index) => {
+ const closes = ohlcData.map(ohlc => ohlc?.[1] ?? null);
+ const seriesLabel = qualifyMaNames ? seriesNames[index] : undefined;
+ const seriesColor = useSeriesColors
+ ? colorScale(seriesNames[index], sliceId)
+ : undefined;
+ return periods.map(period => ({
+ name: movingAverageName(period, seriesLabel),
+ type: 'line' as const,
+ data: calculateMA(closes, period),
+ smooth: true,
+ showSymbol: false,
+ itemStyle: seriesColor ? { color: seriesColor } : undefined,
+ lineStyle: {
+ opacity: MA_LINE_OPACITY,
+ ...(seriesColor ? { color: seriesColor } : {}),
+ },
+ }));
+ },
+ );
+
+ const legendData = [
+ ...seriesNames,
+ ...movingAverageSeries.map(series => String(series.name)),
+ ].sort((a, b) => {
+ if (!legendSort) {
+ return 0;
+ }
+ return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
+ });
+
+ const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({
+ chartHeight: height,
+ chartWidth: width,
+ legendItems: legendData,
+ legendMargin,
+ orientation: legendOrientation,
+ show: showLegend,
+ theme,
+ type: legendType,
+ });
+ const legendPadding = getChartPadding(
+ showLegend,
+ legendOrientation,
+ effectiveLegendMargin,
+ );
+
+ const dataZoom = zoomable
+ ? [
+ {
+ type: 'inside',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ },
+ {
+ type: 'slider',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ bottom: TIMESERIES_CONSTANTS.zoomBottom,
+ },
+ ]
+ : [];
+
+ const echartOptions: EChartsCoreOption = {
+ grid: {
+ ...defaultGrid,
+ top: theme.sizeUnit * 5 + legendPadding.top,
+ bottom:
+ theme.sizeUnit * (showXAxis ? 5 : 3) +
+ legendPadding.bottom +
+ convertInteger(xAxisTitleMargin) +
+ (zoomable ? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable : 0),
+ left:
+ theme.sizeUnit * (showYAxis ? 5 : 2) +
+ legendPadding.left +
+ convertInteger(yAxisTitleMargin),
+ right: theme.sizeUnit * 5 + legendPadding.right,
+ },
+ legend: {
+ ...getLegendProps(
+ effectiveLegendType,
+ legendOrientation,
+ showLegend,
+ theme,
+ zoomable,
+ legendState,
+ ),
+ data: legendData,
+ },
+ xAxis: {
+ show: showXAxis,
+ type: AxisType.Category,
+ data: xLabels,
+ name: xAxisTitle,
+ nameGap: convertInteger(xAxisTitleMargin),
+ nameLocation: 'middle',
+ axisLabel: {
+ rotate: xAxisLabelRotation,
+ interval: xAxisLabelInterval === 'auto' ? 'auto' : 0,
+ hideOverlap: true,
+ },
+ },
+ yAxis: {
+ ...defaultYAxis,
+ show: showYAxis,
+ type: AxisType.Value,
+ name: yAxisTitle,
+ nameGap: convertInteger(yAxisTitleMargin),
+ nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
+ axisLabel: { formatter: yAxisFormatter },
+ },
+ tooltip: {
+ ...getDefaultTooltip(refs),
+ trigger: 'axis',
+ axisPointer: { type: 'cross' },
+ show: !inContextMenu,
+ formatter: (params: CallbackDataParams | CallbackDataParams[]) => {
+ const items = ensureIsArray(params) as AxisTooltipParams[];
+ const [item] = items;
+ if (!item) {
+ return '';
+ }
+ const categoryIndex = item.dataIndex;
+ const categoryDatum =
+ Number.isInteger(categoryIndex) &&
+ categoryIndex >= 0 &&
+ categoryIndex < xRecords.length
+ ? xRecords[categoryIndex]
+ : {};
+ const categoryLabel = Number.isInteger(categoryIndex)
+ ? xLabels[categoryIndex]
+ : undefined;
+ const title =
+ getOwnValue(coltypeMapping, xAxisName) === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum: categoryDatum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter,
+ })
+ : String(
+ item.axisValueLabel ??
+ item.axisValue ??
+ item.name ??
+ categoryLabel ??
+ '',
+ );
+ return formatTooltip({
+ params: items,
+ numberFormatter,
+ title,
+ increaseLabel: upLabel,
+ decreaseLabel: downLabel,
+ showSeriesName: seriesNames.length > 1,
+ });
+ },
+ },
+ series: [...priceSeries, ...movingAverageSeries],
+ toolbox: {
+ show: zoomable,
+ feature: {
+ dataZoom: {
+ yAxisIndex: false,
+ title: {
+ zoom: 'zoom area',
+ back: 'restore zoom',
+ },
Review Comment:
These new toolbox labels bypass translation, so localized Superset deployments will always display them in English. Wrap them with `t(...)`, matching the same controls in `Timeseries/transformProps.ts:1704-1706`, and add the translation import.
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,729 @@
+/**
+ * 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 {
+ AxisType,
+ CategoricalColorNamespace,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { CustomSeriesOption, CustomSeriesRenderItem } from 'echarts';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+ OHLC_TICK_WIDTH_RATIO,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+type AxisTooltipParams = CallbackDataParams & {
+ axisValue?: string | number;
+ axisValueLabel?: string;
+};
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(ohlc: OhlcValue | null): CandlestickDatum {
+ return ohlc ?? [];
+}
+
+function toOhlcBarDatum(
+ ohlc: OhlcValue | null,
+ categoryIndex: number,
+ color: string,
+) {
+ if (!ohlc) {
+ return [];
+ }
+ return {
+ value: [categoryIndex, ...ohlc],
+ itemStyle: {
+ color,
+ },
+ };
+}
+
+function getDirectionItemStyle(increaseHex: string, decreaseHex: string) {
+ return {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ };
+}
+
+function getSeriesItemStyle(seriesColor: string, hollowFill: string) {
+ return {
+ color: seriesColor,
+ color0: hollowFill,
+ borderColor: seriesColor,
+ borderColor0: seriesColor,
+ };
+}
+
+const renderOhlcItem: CustomSeriesRenderItem = (_params, item) => {
+ const x = toNumber(item.value(0));
+ const open = toNumber(item.value(1));
+ const close = toNumber(item.value(2));
+ const low = toNumber(item.value(3));
+ const high = toNumber(item.value(4));
+ if (
+ x === null ||
+ open === null ||
+ close === null ||
+ low === null ||
+ high === null
+ ) {
+ return null;
+ }
+
+ const openPoint = item.coord([x, open]);
+ const closePoint = item.coord([x, close]);
+ const lowPoint = item.coord([x, low]);
+ const highPoint = item.coord([x, high]);
+ const categorySize = item.size?.([1, 0]);
+ const categoryWidth = Array.isArray(categorySize)
+ ? categorySize[0]
+ : categorySize;
+ if (categoryWidth == null || !Number.isFinite(categoryWidth)) {
+ return null;
+ }
+ const halfWidth = categoryWidth * OHLC_TICK_WIDTH_RATIO;
+ const style = item.style({
+ stroke: item.visual('color'),
+ });
+
+ return {
+ type: 'group',
+ children: [
+ {
+ type: 'line',
+ shape: {
+ x1: lowPoint[0],
+ y1: lowPoint[1],
+ x2: highPoint[0],
+ y2: highPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: openPoint[0],
+ y1: openPoint[1],
+ x2: openPoint[0] - halfWidth,
+ y2: openPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: closePoint[0],
+ y1: closePoint[1],
+ x2: closePoint[0] + halfWidth,
+ y2: closePoint[1],
+ },
+ style,
+ },
+ ],
+ };
+};
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function extractLineValue(item: CallbackDataParams): number | null {
+ const raw = item.value ?? item.data;
+ if (Array.isArray(raw)) {
+ const y = Number(raw[raw.length - 1]);
+ return Number.isFinite(y) ? y : null;
+ }
+ const numeric = Number(raw);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function appendOhlcRows(
+ rows: string[][],
+ ohlc: OhlcValue,
+ numberFormatter: NumberFormatter | CurrencyFormatter,
+) {
+ const [open, close, low, high] = ohlc;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+ showSeriesName,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+ showSeriesName: boolean;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candles = params.flatMap(item => {
+ const ohlc = extractOhlc(item.value ?? item.data);
+ return ohlc ? [{ item, ohlc }] : [];
+ });
+
+ candles.forEach(({ item, ohlc }) => {
+ const [open, close] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ if (showSeriesName) {
+ const seriesLabel = String(item.seriesName ?? '');
+ if (seriesLabel) {
+ rows.push([`${seriesLabel} (${direction})`]);
+ }
+ } else {
+ heading = title ? `${title} (${direction})` : direction;
+ }
+ appendOhlcRows(rows, ohlc, numberFormatter);
+ });
+
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = extractLineValue(item);
+ if (value === null) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: _echartOptions, ...formData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ candlestickSeriesName,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ seriesStyle,
+ colorScheme,
+ sliceId,
+ colorByDirection,
+ }: EchartsCandlestickFormData = { ...DEFAULT_FORM_DATA, ...formData };
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const defaultSeriesLabel =
+ candlestickSeriesName?.trim() || CANDLESTICK_SERIES_NAME;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const colorScale = CategoricalColorNamespace.getScale(colorScheme);
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: LookupKey[] = [];
+ const xLabels: string[] = [];
+ const xRecords: DataRecord[] = [];
+ const xKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, xAxisName));
+ if (xKeySet.has(key)) {
+ return;
+ }
+ xKeySet.add(key);
+ xKeys.push(key);
+ xRecords.push(datum);
+ xLabels.push(
+ getOwnValue(coltypeMapping, xAxisName) === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter: axisTimeFormatter,
+ })
+ : extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ }),
+ );
+ });
+
+ const seriesKeys: LookupKey[] = [];
+ const seriesNames: string[] = [];
+ if (seriesName) {
+ const seriesKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, seriesName));
+ if (seriesKeySet.has(key)) {
+ return;
+ }
+ seriesKeySet.add(key);
+ seriesKeys.push(key);
+ seriesNames.push(
+ extractGroupbyLabel({
+ datum,
+ groupby: [seriesName],
+ coltypeMapping,
+ }),
+ );
+ });
+ } else {
+ seriesKeys.push(CANDLESTICK_SERIES_NAME);
+ seriesNames.push(defaultSeriesLabel);
+ }
+
+ const useSeriesColors = seriesNames.length > 1 || colorByDirection === false;
+
+ const recordsBySeriesAndX = new Map<LookupKey, Map<LookupKey, DataRecord>>();
+ data.forEach(datum => {
+ const xKey = toLookupKey(getOwnValue(datum, xAxisName));
+ const seriesKey = seriesName
+ ? toLookupKey(getOwnValue(datum, seriesName))
+ : CANDLESTICK_SERIES_NAME;
+ let byX = recordsBySeriesAndX.get(seriesKey);
+ if (!byX) {
+ byX = new Map();
+ recordsBySeriesAndX.set(seriesKey, byX);
+ }
+ byX.set(xKey, datum);
+ });
+
+ const ohlcBySeries: (OhlcValue | null)[][] = seriesKeys.map(key =>
+ xKeys.map(xKey => {
+ const datum = recordsBySeriesAndX.get(key)?.get(xKey);
+ return datum
+ ? getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel)
+ : null;
+ }),
+ );
+
+ const priceSeries: (CandlestickSeriesOption | CustomSeriesOption)[] =
+ ohlcBySeries.map((ohlcData, index) => {
+ const name = seriesNames[index];
+ const seriesColor = useSeriesColors
+ ? colorScale(name, sliceId)
+ : increaseHex;
+ if (seriesStyle === 'ohlc') {
+ return {
+ name,
+ type: 'custom',
+ renderItem: renderOhlcItem,
+ dimensions: ['-', 'open', 'close', 'low', 'high'],
+ encode: {
+ x: 0,
+ y: [1, 2, 3, 4],
+ tooltip: [1, 2, 3, 4],
+ },
+ itemStyle: {
+ color: seriesColor,
+ },
+ data: ohlcData.map((ohlc, categoryIndex) => {
+ if (!ohlc) {
+ return [];
+ }
+ const [openValue, closeValue] = ohlc;
+ const color = useSeriesColors
+ ? seriesColor
+ : closeValue >= openValue
+ ? increaseHex
+ : decreaseHex;
+ return toOhlcBarDatum(ohlc, categoryIndex, color);
+ }),
+ };
+ }
+ return {
+ name,
+ type: 'candlestick',
+ data: ohlcData.map(toCandlestickDatum),
+ itemStyle: useSeriesColors
+ ? getSeriesItemStyle(seriesColor, theme.colorBgContainer)
+ : getDirectionItemStyle(increaseHex, decreaseHex),
+ };
+ });
+
+ const periods = parseMovingAveragePeriods(movingAverages);
+ const qualifyMaNames = seriesNames.length > 1;
+ const movingAverageSeries: LineSeriesOption[] = ohlcBySeries.flatMap(
+ (ohlcData, index) => {
+ const closes = ohlcData.map(ohlc => ohlc?.[1] ?? null);
+ const seriesLabel = qualifyMaNames ? seriesNames[index] : undefined;
+ const seriesColor = useSeriesColors
+ ? colorScale(seriesNames[index], sliceId)
+ : undefined;
+ return periods.map(period => ({
+ name: movingAverageName(period, seriesLabel),
+ type: 'line' as const,
+ data: calculateMA(closes, period),
+ smooth: true,
+ showSymbol: false,
+ itemStyle: seriesColor ? { color: seriesColor } : undefined,
+ lineStyle: {
+ opacity: MA_LINE_OPACITY,
+ ...(seriesColor ? { color: seriesColor } : {}),
+ },
+ }));
+ },
+ );
+
+ const legendData = [
+ ...seriesNames,
+ ...movingAverageSeries.map(series => String(series.name)),
+ ].sort((a, b) => {
+ if (!legendSort) {
+ return 0;
+ }
+ return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
+ });
+
+ const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({
+ chartHeight: height,
+ chartWidth: width,
+ legendItems: legendData,
+ legendMargin,
+ orientation: legendOrientation,
+ show: showLegend,
+ theme,
+ type: legendType,
+ });
+ const legendPadding = getChartPadding(
+ showLegend,
+ legendOrientation,
+ effectiveLegendMargin,
+ );
+
+ const dataZoom = zoomable
+ ? [
+ {
+ type: 'inside',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ },
+ {
+ type: 'slider',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ bottom: TIMESERIES_CONSTANTS.zoomBottom,
+ },
+ ]
+ : [];
+
+ const echartOptions: EChartsCoreOption = {
+ grid: {
+ ...defaultGrid,
+ top: theme.sizeUnit * 5 + legendPadding.top,
+ bottom:
+ theme.sizeUnit * (showXAxis ? 5 : 3) +
+ legendPadding.bottom +
+ convertInteger(xAxisTitleMargin) +
+ (zoomable ? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable : 0),
+ left:
+ theme.sizeUnit * (showYAxis ? 5 : 2) +
+ legendPadding.left +
+ convertInteger(yAxisTitleMargin),
+ right: theme.sizeUnit * 5 + legendPadding.right,
+ },
+ legend: {
+ ...getLegendProps(
+ effectiveLegendType,
+ legendOrientation,
+ showLegend,
+ theme,
+ zoomable,
+ legendState,
+ ),
+ data: legendData,
+ },
+ xAxis: {
+ show: showXAxis,
+ type: AxisType.Category,
+ data: xLabels,
+ name: xAxisTitle,
+ nameGap: convertInteger(xAxisTitleMargin),
+ nameLocation: 'middle',
+ axisLabel: {
+ rotate: xAxisLabelRotation,
+ interval: xAxisLabelInterval === 'auto' ? 'auto' : 0,
+ hideOverlap: true,
+ },
+ },
+ yAxis: {
+ ...defaultYAxis,
+ show: showYAxis,
+ type: AxisType.Value,
+ name: yAxisTitle,
+ nameGap: convertInteger(yAxisTitleMargin),
+ nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
+ axisLabel: { formatter: yAxisFormatter },
+ },
+ tooltip: {
+ ...getDefaultTooltip(refs),
+ trigger: 'axis',
+ axisPointer: { type: 'cross' },
+ show: !inContextMenu,
+ formatter: (params: CallbackDataParams | CallbackDataParams[]) => {
+ const items = ensureIsArray(params) as AxisTooltipParams[];
+ const [item] = items;
+ if (!item) {
+ return '';
+ }
+ const categoryIndex = item.dataIndex;
+ const categoryDatum =
+ Number.isInteger(categoryIndex) &&
+ categoryIndex >= 0 &&
+ categoryIndex < xRecords.length
+ ? xRecords[categoryIndex]
+ : {};
+ const categoryLabel = Number.isInteger(categoryIndex)
+ ? xLabels[categoryIndex]
+ : undefined;
+ const title =
+ getOwnValue(coltypeMapping, xAxisName) === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum: categoryDatum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter,
+ })
+ : String(
+ item.axisValueLabel ??
+ item.axisValue ??
+ item.name ??
+ categoryLabel ??
+ '',
+ );
+ return formatTooltip({
+ params: items,
+ numberFormatter,
+ title,
+ increaseLabel: upLabel,
+ decreaseLabel: downLabel,
+ showSeriesName: seriesNames.length > 1,
Review Comment:
The tooltip suppresses the series name whenever there is only one rendered series. That also covers the no-dimension case, so the configurable “Series name” never appears in the tooltip despite this control explicitly promising legend and tooltip usage; a Series dimension with one value similarly loses its data-derived name. Always include the price-series label (the date can remain the tooltip heading).
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5841846247
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3934401893
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/utils.ts:
##########
@@ -0,0 +1,70 @@
+/**
+ * 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 { ensureIsArray } from "@superset-ui/core";
+
+export const MOVING_AVERAGE_PERIODS = [5, 10, 15, 20, 30, 60];
+export const MA_LINE_OPACITY = 0.5;
+
+export function movingAverageName(period: number, seriesName?: string): string {
+ const label = `MA${period}`;
+ return seriesName ? `${seriesName} ${label}` : label;
+}
+
+export function parseMovingAveragePeriods(value: unknown): number[] {
+ const periods = ensureIsArray(value)
+ .map((item) => {
+ if (typeof item === "number") {
+ return item;
+ }
+ const match = String(item).match(/(\d+)/);
+ return match ? Number(match[1]) : Number.NaN;
+ })
+ .filter((period) => Number.isInteger(period) && period > 1);
+ return [...new Set(periods)].sort((left, right) => left - right);
+}
Review Comment:
**Suggestion:** Malformed custom values such as `MAfoo15`, `-5`, or string `2.5` are accepted as periods, silently creating moving-average lines for unintended values. [logic error]
**Assessment:** 🟠 `Major` · 🔁 `Occurrence: Sometimes`
[](https://docs.codeant.ai/cli/resolve-pr-comments-skill) [](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=bdfee59cbdc849d29b3734951cd6dfed&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset) [](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=bdfee59cbdc849d29b3734951cd6dfed&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/utils.ts
**Line:** 29:40
**Comment:**
*Logic Error: Malformed custom values such as `MAfoo15`, `-5`, or string `2.5` are accepted as periods, silently creating moving-average lines for unintended values.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
```
</details>
<a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F43863&comment_hash=6086c5fbc973de7faeec906161a4883e4ec19b8b4145c9222b75a808093c9b07&reaction=like'>👍</a> | <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F43863&comment_hash=6086c5fbc973de7faeec906161a4883e4ec19b8b4145c9222b75a808093c9b07&reaction=dislike'>👎</a>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5541009197
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3935301602
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/utils.ts:
##########
@@ -0,0 +1,78 @@
+/**
+ * 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 { ensureIsArray } from '@superset-ui/core';
+
+export const MOVING_AVERAGE_PERIODS = [5, 10, 15, 20, 30, 60];
+export const MA_LINE_OPACITY = 0.5;
+
+export function movingAverageName(period: number, seriesName?: string): string {
+ const label = `MA${period}`;
+ return seriesName ? `${seriesName} ${label}` : label;
+}
+
+function parseMovingAveragePeriod(item: unknown): number | null {
+ if (typeof item === 'number') {
+ return Number.isInteger(item) && item > 1 ? item : null;
+ }
+ const match = String(item)
+ .trim()
+ .match(/^(?:MA)?(\d+)$/i);
+ if (!match) {
+ return null;
+ }
+ const period = Number(match[1]);
+ return Number.isInteger(period) && period > 1 ? period : null;
+}
+
+export function parseMovingAveragePeriods(value: unknown): number[] {
+ const periods = ensureIsArray(value)
+ .map(parseMovingAveragePeriod)
+ .filter((period): period is number => period !== null);
+ return [...new Set(periods)].sort((left, right) => left - right);
+}
+
+/**
+ * Simple moving average of close prices, matching the ECharts candlestick
+ * example: the first `period` points are omitted, then each value is the
+ * mean of the current close and the previous `period - 1` closes.
+ */
+export function calculateMA(
+ closes: Array<number | null>,
+ period: number,
+): Array<number | '-'> {
+ const result: Array<number | '-'> = [];
+ for (let i = 0; i < closes.length; i += 1) {
+ if (i < period) {
Review Comment:
fix in be6d168
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3934963730
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,519 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(datum[openLabel]);
+ const close = toNumber(datum[closeLabel]);
+ const low = toNumber(datum[lowLabel]);
+ const high = toNumber(datum[highLabel]);
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: customEchartOptionsInput, ...rawFormData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+ const formData = {
+ ...DEFAULT_FORM_DATA,
+ ...rawFormData,
+ };
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ } = formData;
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: string[] = [];
+ const xLabels: string[] = [];
+ const xRecords: DataRecord[] = [];
+ const xKeySet = new Set<string>();
+ data.forEach(datum => {
+ const raw = datum[xAxisName];
+ const key = raw == null ? NULL_STRING : String(raw);
Review Comment:
Using the display placeholder as the identity key collapses a real null x value with a literal `"<NULL>"` value. The second category is skipped by `xKeySet`, so one candle is silently lost; retain the raw value or a unique null sentinel for lookup and format only the displayed label.
This issue also appears on line 295 of the same file.
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/utils.ts:
##########
@@ -0,0 +1,78 @@
+/**
+ * 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 { ensureIsArray } from '@superset-ui/core';
+
+export const MOVING_AVERAGE_PERIODS = [5, 10, 15, 20, 30, 60];
+export const MA_LINE_OPACITY = 0.5;
+
+export function movingAverageName(period: number, seriesName?: string): string {
+ const label = `MA${period}`;
+ return seriesName ? `${seriesName} ${label}` : label;
+}
+
+function parseMovingAveragePeriod(item: unknown): number | null {
+ if (typeof item === 'number') {
+ return Number.isInteger(item) && item > 1 ? item : null;
+ }
+ const match = String(item)
+ .trim()
+ .match(/^(?:MA)?(\d+)$/i);
+ if (!match) {
+ return null;
+ }
+ const period = Number(match[1]);
+ return Number.isInteger(period) && period > 1 ? period : null;
+}
+
+export function parseMovingAveragePeriods(value: unknown): number[] {
+ const periods = ensureIsArray(value)
+ .map(parseMovingAveragePeriod)
+ .filter((period): period is number => period !== null);
+ return [...new Set(periods)].sort((left, right) => left - right);
+}
+
+/**
+ * Simple moving average of close prices, matching the ECharts candlestick
+ * example: the first `period` points are omitted, then each value is the
+ * mean of the current close and the previous `period - 1` closes.
+ */
+export function calculateMA(
+ closes: Array<number | null>,
+ period: number,
+): Array<number | '-'> {
+ const result: Array<number | '-'> = [];
+ for (let i = 0; i < closes.length; i += 1) {
+ if (i < period) {
Review Comment:
A period-N moving average should become available after N observations, so only the first `period - 1` points should be omitted. This condition makes exactly N samples produce no MA and shifts every result by one category (for example, MA5 first averages rows 2–6 instead of 1–5). Change the boundary and update the test expectations accordingly.
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3935196846
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/utils.ts:
##########
@@ -0,0 +1,70 @@
+/**
+ * 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 { ensureIsArray } from "@superset-ui/core";
+
+export const MOVING_AVERAGE_PERIODS = [5, 10, 15, 20, 30, 60];
+export const MA_LINE_OPACITY = 0.5;
+
+export function movingAverageName(period: number, seriesName?: string): string {
+ const label = `MA${period}`;
+ return seriesName ? `${seriesName} ${label}` : label;
+}
+
+export function parseMovingAveragePeriods(value: unknown): number[] {
+ const periods = ensureIsArray(value)
+ .map((item) => {
+ if (typeof item === "number") {
+ return item;
+ }
+ const match = String(item).match(/(\d+)/);
+ return match ? Number(match[1]) : Number.NaN;
+ })
+ .filter((period) => Number.isInteger(period) && period > 1);
+ return [...new Set(periods)].sort((left, right) => left - right);
+}
Review Comment:
Good catch, fixed in 2b9cd3f
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3941130750
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/types.ts:
##########
@@ -0,0 +1,68 @@
+/**
+ * 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 {
+ ChartDataResponseResult,
+ ChartProps,
+ QueryFormColumn,
+ QueryFormData,
+ QueryFormMetric,
+ RgbaColor,
+} from '@superset-ui/core';
+import {
+ BaseChartProps,
+ BaseTransformedProps,
+ LegendFormData,
+ TitleFormData,
+} from '../types';
+
+export type EchartsCandlestickFormData = QueryFormData &
+ LegendFormData &
+ TitleFormData & {
+ xAxis: QueryFormColumn;
+ open: QueryFormMetric;
+ close: QueryFormMetric;
+ high: QueryFormMetric;
+ low: QueryFormMetric;
+ series?: QueryFormColumn | QueryFormColumn[];
+ increaseColor: RgbaColor;
+ decreaseColor: RgbaColor;
+ increaseLabel?: string;
+ decreaseLabel?: string;
+ showXAxis: boolean;
+ showYAxis: boolean;
+ xAxisTimeFormat?: string;
+ xAxisLabelRotation: number;
+ xAxisLabelInterval: string;
+ yAxisFormat: string;
+ tooltipTimeFormat?: string;
+ tooltipValuesFormat?: string;
+ zoomable: boolean;
+ movingAverages?: (number | string)[];
+ echartOptions?: string;
Review Comment:
The conversion is already taking place in superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts; I replaced it with formData in [134e138](https://github.com/apache/superset/pull/43863/commits/134e13832628b61a7136f027f1083b70a5293870):
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3941144403
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/EchartsCandlestick.tsx:
##########
@@ -0,0 +1,51 @@
+/**
+ * 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 Echart from '../components/Echart';
+import { EventHandlers } from '../types';
+import { CandlestickChartTransformedProps } from './types';
+
+export default function EchartsCandlestick(
+ props: CandlestickChartTransformedProps,
+) {
+ const { height, width, echartOptions, refs, onLegendStateChanged, formData } =
+ props;
+
+ const eventHandlers: EventHandlers = {
+ legendselectchanged: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ legendselectall: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ legendinverseselect: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ };
+
+ return (
+ <Echart
+ refs={refs}
+ height={height}
+ width={width}
+ echartOptions={echartOptions}
+ eventHandlers={eventHandlers}
+ vizType={formData.vizType}
Review Comment:
The conversion is already taking place in superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts;
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5614593718
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3937709240
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,533 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toCategoryKey(value: unknown): string {
+ return value == null ? NULL_STRING : String(value);
Review Comment:
In that case, I will separate the search key and the signature.
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4068993054
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/index.ts:
##########
@@ -0,0 +1,65 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t } from '@apache-superset/core/translation';
+import { Behavior } from '@superset-ui/core';
+import buildQuery from './buildQuery';
+import controlPanel from './controlPanel';
+import transformProps from './transformProps';
+import thumbnail from './images/thumbnail.png';
+import thumbnailDark from './images/thumbnail-dark.png';
+import example from './images/example1.png';
+import exampleDark from './images/example1-dark.png';
+import {
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+} from './types';
+import { EchartsChartPlugin } from '../types';
+
+export default class EchartsCandlestickChartPlugin extends EchartsChartPlugin<
+ EchartsCandlestickFormData,
+ EchartsCandlestickChartProps
+> {
+ constructor() {
+ super({
+ buildQuery,
+ controlPanel,
+ loadChart: () => import('./EchartsCandlestick'),
+ metadata: {
+ behaviors: [Behavior.DrillToDetail],
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>DrillToDetail declared but unhandled</b></div>
<div id="fix">
Declaring `Behavior.DrillToDetail` tells the framework this plugin handles the `contextmenu` event for dimensions (`useDrillDetailMenuItems` line 145-151). But `EchartsCandlestick.tsx` only registers legend events and `transformProps.ts` never passes `onContextMenu` from `hooks`. Result: the 'Drill to detail by value' submenu is enabled but always disabled with the `NO_FILTERS` message ('Right-click on a dimension value...') even though right-clicking a dimension never produces filters. Consider implementing `contextmenu` handling or dropping the behavior.
</div>
</div>
<small><i>Code Review Run #bcd054</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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4094416401
##########
superset/translations/zh_TW/LC_MESSAGES/messages.po:
##########
@@ -1046,6 +1046,12 @@ msgid ""
" bars that extend left and right from a central axis."
msgstr ""
+msgid ""
+"A candlestick chart shows opening, closing, high, and low values for each"
+" period. It is commonly used for financial and other time-series range "
+"analysis."
+msgstr "K線圖顯示每個週期的開盤、收盤、最高和最低值。常用於金融及其他時間序列區間分析。"
Review Comment:
<!-- Bito Reply -->
The suggestion to standardize the term for "candlestick chart" is appropriate. Using "K線" consistently throughout the translation file improves clarity and avoids confusion for users, as the current diff introduces both "K線" and "K 線" for the same concept. Applying "K線" to all instances, including the new string, is recommended to maintain consistency.
**superset/translations/zh_TW/LC_MESSAGES/messages.po**
```
+msgid ""
+"A candlestick chart shows opening, closing, high, and low values for each"
+" period. It is commonly used for financial and other time-series range "
+"analysis."
+msgstr "K線圖顯示每個週期的開盤、收盤、最高和最低值。常用於金融及其他時間序列區間分析。"
```
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5822082337
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5804177092
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "rusackas (via GitHub)" <gi...@apache.org>.
rusackas commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5801833209
Thanks for wiring up `onContextMenu` @SBIN2010. One thing before this merges: bito's point about the null case on the drill-to-detail filter seems legit... `toFilterValue` hands back `null`, so the clause becomes `== NULL` and never matches. The sibling charts (Timeseries, for one) still emit `IS NULL` there, so worth matching that.
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5772098853
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5818171735
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5740983125
/review
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4112696369
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,767 @@
+/**
+ * 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 {
+ AxisType,
+ CategoricalColorNamespace,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ getXAxisLabel,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import { t } from '@apache-superset/core/translation';
+import type { CustomSeriesOption, CustomSeriesRenderItem } from 'echarts';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+ OHLC_TICK_WIDTH_RATIO,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { getPadding } from '../Timeseries/transformers';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+type AxisTooltipParams = CallbackDataParams & {
+ axisValue?: string | number;
+ axisValueLabel?: string;
+};
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(ohlc: OhlcValue | null): CandlestickDatum {
+ return ohlc ?? [];
+}
+
+function toOhlcBarDatum(
+ ohlc: OhlcValue | null,
+ categoryIndex: number,
+ color: string,
+) {
+ if (!ohlc) {
+ return [];
+ }
+ return {
+ value: [categoryIndex, ...ohlc],
+ itemStyle: {
+ color,
+ },
+ };
+}
+
+function getDirectionItemStyle(increaseHex: string, decreaseHex: string) {
+ return {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ };
+}
+
+function getSeriesItemStyle(seriesColor: string, hollowFill: string) {
+ return {
+ color: seriesColor,
+ color0: hollowFill,
+ borderColor: seriesColor,
+ borderColor0: seriesColor,
+ };
+}
+
+const renderOhlcItem: CustomSeriesRenderItem = (_params, item) => {
+ const x = toNumber(item.value(0));
+ const open = toNumber(item.value(1));
+ const close = toNumber(item.value(2));
+ const low = toNumber(item.value(3));
+ const high = toNumber(item.value(4));
+ if (
+ x === null ||
+ open === null ||
+ close === null ||
+ low === null ||
+ high === null
+ ) {
+ return null;
+ }
+
+ const openPoint = item.coord([x, open]);
+ const closePoint = item.coord([x, close]);
+ const lowPoint = item.coord([x, low]);
+ const highPoint = item.coord([x, high]);
+ const categorySize = item.size?.([1, 0]);
+ const categoryWidth = Array.isArray(categorySize)
+ ? categorySize[0]
+ : categorySize;
+ if (categoryWidth == null || !Number.isFinite(categoryWidth)) {
+ return null;
+ }
+ const halfWidth = categoryWidth * OHLC_TICK_WIDTH_RATIO;
+ const style = item.style({
+ stroke: item.visual('color'),
+ });
+
+ return {
+ type: 'group',
+ children: [
+ {
+ type: 'line',
+ shape: {
+ x1: lowPoint[0],
+ y1: lowPoint[1],
+ x2: highPoint[0],
+ y2: highPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: openPoint[0],
+ y1: openPoint[1],
+ x2: openPoint[0] - halfWidth,
+ y2: openPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: closePoint[0],
+ y1: closePoint[1],
+ x2: closePoint[0] + halfWidth,
+ y2: closePoint[1],
+ },
+ style,
+ },
+ ],
+ };
+};
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function extractLineValue(item: CallbackDataParams): number | null {
+ const raw = item.value ?? item.data;
Review Comment:
fix in 1ec5fe3
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3935204934
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,512 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(datum[openLabel]);
+ const close = toNumber(datum[closeLabel]);
+ const low = toNumber(datum[lowLabel]);
+ const high = toNumber(datum[highLabel]);
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: customEchartOptionsInput, ...rawFormData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+ const formData = {
+ ...DEFAULT_FORM_DATA,
+ ...rawFormData,
+ };
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ } = formData;
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: string[] = [];
+ const xLabels: string[] = [];
+ const xKeySet = new Set<string>();
+ data.forEach(datum => {
+ const raw = datum[xAxisName];
+ const key = raw == null ? NULL_STRING : String(raw);
+ if (xKeySet.has(key)) {
+ return;
+ }
+ xKeySet.add(key);
+ xKeys.push(key);
+ xLabels.push(
+ coltypeMapping[xAxisName] === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter: axisTimeFormatter,
+ })
+ : extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ }),
+ );
+ });
+
+ const seriesNames = seriesName
+ ? [
+ ...new Set(
+ data.map(datum =>
+ datum[seriesName] == null ? NULL_STRING : String(datum[seriesName]),
+ ),
+ ),
+ ]
+ : [CANDLESTICK_SERIES_NAME];
+
+ const recordsBySeriesAndX = new Map<string, DataRecord>();
+ data.forEach(datum => {
+ const xKey =
+ datum[xAxisName] == null ? NULL_STRING : String(datum[xAxisName]);
+ const seriesKey = seriesName
+ ? datum[seriesName] == null
+ ? NULL_STRING
+ : String(datum[seriesName])
+ : seriesNames[0];
+ recordsBySeriesAndX.set(`${seriesKey}::${xKey}`, datum);
+ });
+
+ const candlestickSeries: CandlestickSeriesOption[] = seriesNames.map(
+ name => ({
+ name,
+ type: 'candlestick',
+ data: xKeys.map(xKey =>
+ toCandlestickDatum(
+ recordsBySeriesAndX.get(`${name}::${xKey}`),
+ openLabel,
+ closeLabel,
+ lowLabel,
+ highLabel,
+ ),
+ ),
+ itemStyle: {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ },
+ }),
+ );
+
+ const periods = parseMovingAveragePeriods(movingAverages);
+ const qualifyMaNames = seriesNames.length > 1;
+ const movingAverageSeries: LineSeriesOption[] = candlestickSeries.flatMap(
+ candle => {
+ const closes = (candle.data ?? []).map(item =>
+ Array.isArray(item) && Number.isFinite(Number(item[1]))
+ ? Number(item[1])
+ : null,
+ );
+ const seriesLabel = qualifyMaNames ? String(candle.name) : undefined;
+ return periods.map(period => ({
+ name: movingAverageName(period, seriesLabel),
+ type: 'line' as const,
+ data: calculateMA(closes, period),
+ smooth: true,
+ showSymbol: false,
+ lineStyle: {
+ opacity: MA_LINE_OPACITY,
+ },
+ }));
+ },
+ );
+
+ const legendData = [
+ ...seriesNames,
+ ...movingAverageSeries.map(series => String(series.name)),
+ ].sort((a, b) => {
+ if (!legendSort) {
+ return 0;
+ }
+ return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
+ });
+
+ const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({
+ chartHeight: height,
+ chartWidth: width,
+ legendItems: legendData,
+ legendMargin,
+ orientation: legendOrientation,
+ show: showLegend,
+ theme,
+ type: legendType,
+ });
+ const legendPadding = getChartPadding(
+ showLegend,
+ legendOrientation,
+ effectiveLegendMargin,
+ undefined,
+ true,
+ );
+
+ const dataZoom = zoomable
+ ? [
+ {
+ type: 'inside',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ },
+ {
+ type: 'slider',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ bottom: TIMESERIES_CONSTANTS.zoomBottom,
+ },
+ ]
+ : [];
+
+ const echartOptions: EChartsCoreOption = {
+ grid: {
+ ...defaultGrid,
+ top: theme.sizeUnit * 5 + legendPadding.top,
+ bottom:
+ theme.sizeUnit * (showXAxis ? 5 : 3) +
+ legendPadding.bottom +
+ convertInteger(xAxisTitleMargin) +
+ (zoomable ? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable : 0),
+ left:
+ theme.sizeUnit * (showYAxis ? 5 : 2) +
+ legendPadding.left +
+ convertInteger(yAxisTitleMargin),
+ right: theme.sizeUnit * 5 + legendPadding.right,
+ },
+ legend: {
+ ...getLegendProps(
+ effectiveLegendType,
+ legendOrientation,
+ showLegend,
+ theme,
+ zoomable,
+ legendState,
+ ),
+ data: legendData,
+ },
+ xAxis: {
+ show: showXAxis,
+ type: AxisType.Category,
+ data: xLabels,
+ name: xAxisTitle,
+ nameGap: convertInteger(xAxisTitleMargin),
+ nameLocation: 'middle',
+ axisLabel: {
+ rotate: xAxisLabelRotation,
+ interval: xAxisLabelInterval === 'auto' ? 'auto' : 0,
+ hideOverlap: true,
+ },
+ },
+ yAxis: {
+ ...defaultYAxis,
+ show: showYAxis,
+ type: AxisType.Value,
+ name: yAxisTitle,
+ nameGap: convertInteger(yAxisTitleMargin),
+ nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
+ axisLabel: { formatter: yAxisFormatter },
+ },
+ tooltip: {
+ ...getDefaultTooltip(refs),
+ trigger: 'axis',
+ axisPointer: { type: 'shadow' },
+ show: !inContextMenu,
+ formatter: (params: CallbackDataParams | CallbackDataParams[]) => {
+ const [item] = ensureIsArray(params);
+ if (!item) {
+ return '';
+ }
+ const title =
+ coltypeMapping[xAxisName] === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum: data[item.dataIndex] ?? {},
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter,
+ })
Review Comment:
also fixed in 2b9cd3f
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "sadpandajoe (via GitHub)" <gi...@apache.org>.
sadpandajoe commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3937629663
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,533 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toCategoryKey(value: unknown): string {
+ return value == null ? NULL_STRING : String(value);
Review Comment:
This uses the display placeholder as the lookup key, so a database NULL and a literal "<NULL>" category or series value collide. The Set/Map then discards one candle rather than rendering both. Could the internal key use a distinct null sentinel and only format the placeholder for display?
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3934406636
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,512 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(datum[openLabel]);
+ const close = toNumber(datum[closeLabel]);
+ const low = toNumber(datum[lowLabel]);
+ const high = toNumber(datum[highLabel]);
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: customEchartOptionsInput, ...rawFormData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+ const formData = {
+ ...DEFAULT_FORM_DATA,
+ ...rawFormData,
+ };
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ } = formData;
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: string[] = [];
+ const xLabels: string[] = [];
+ const xKeySet = new Set<string>();
+ data.forEach(datum => {
+ const raw = datum[xAxisName];
+ const key = raw == null ? NULL_STRING : String(raw);
+ if (xKeySet.has(key)) {
+ return;
+ }
+ xKeySet.add(key);
+ xKeys.push(key);
+ xLabels.push(
+ coltypeMapping[xAxisName] === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter: axisTimeFormatter,
+ })
+ : extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ }),
+ );
+ });
+
+ const seriesNames = seriesName
+ ? [
+ ...new Set(
+ data.map(datum =>
+ datum[seriesName] == null ? NULL_STRING : String(datum[seriesName]),
+ ),
+ ),
+ ]
+ : [CANDLESTICK_SERIES_NAME];
+
+ const recordsBySeriesAndX = new Map<string, DataRecord>();
+ data.forEach(datum => {
+ const xKey =
+ datum[xAxisName] == null ? NULL_STRING : String(datum[xAxisName]);
+ const seriesKey = seriesName
+ ? datum[seriesName] == null
+ ? NULL_STRING
+ : String(datum[seriesName])
+ : seriesNames[0];
+ recordsBySeriesAndX.set(`${seriesKey}::${xKey}`, datum);
Review Comment:
**Suggestion:** Joining series and x-axis values with `::` causes distinct values containing that separator to share one key, so one candle silently replaces another. [logic error]
**Assessment:** 🟠 `Major` · 🔁 `Occurrence: Sometimes`
[](https://docs.codeant.ai/cli/resolve-pr-comments-skill) [](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=f43b8dd417c844ccbdf24ff8541bddde&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset) [](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=f43b8dd417c844ccbdf24ff8541bddde&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts
**Line:** 300:309
**Comment:**
*Logic Error: Joining series and x-axis values with `::` causes distinct values containing that separator to share one key, so one candle silently replaces another.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
```
</details>
<a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F43863&comment_hash=683a3828c6e5f8e2a86b9e228999cd58d17a5df3678d62455ede1be33dd78ff3&reaction=like'>👍</a> | <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F43863&comment_hash=683a3828c6e5f8e2a86b9e228999cd58d17a5df3678d62455ede1be33dd78ff3&reaction=dislike'>👎</a>
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,512 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(datum[openLabel]);
+ const close = toNumber(datum[closeLabel]);
+ const low = toNumber(datum[lowLabel]);
+ const high = toNumber(datum[highLabel]);
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: customEchartOptionsInput, ...rawFormData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+ const formData = {
+ ...DEFAULT_FORM_DATA,
+ ...rawFormData,
+ };
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ } = formData;
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: string[] = [];
+ const xLabels: string[] = [];
+ const xKeySet = new Set<string>();
+ data.forEach(datum => {
+ const raw = datum[xAxisName];
+ const key = raw == null ? NULL_STRING : String(raw);
+ if (xKeySet.has(key)) {
+ return;
+ }
+ xKeySet.add(key);
+ xKeys.push(key);
+ xLabels.push(
+ coltypeMapping[xAxisName] === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter: axisTimeFormatter,
+ })
+ : extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ }),
+ );
+ });
+
+ const seriesNames = seriesName
+ ? [
+ ...new Set(
+ data.map(datum =>
+ datum[seriesName] == null ? NULL_STRING : String(datum[seriesName]),
+ ),
+ ),
+ ]
+ : [CANDLESTICK_SERIES_NAME];
+
+ const recordsBySeriesAndX = new Map<string, DataRecord>();
+ data.forEach(datum => {
+ const xKey =
+ datum[xAxisName] == null ? NULL_STRING : String(datum[xAxisName]);
+ const seriesKey = seriesName
+ ? datum[seriesName] == null
+ ? NULL_STRING
+ : String(datum[seriesName])
+ : seriesNames[0];
+ recordsBySeriesAndX.set(`${seriesKey}::${xKey}`, datum);
+ });
+
+ const candlestickSeries: CandlestickSeriesOption[] = seriesNames.map(
+ name => ({
+ name,
+ type: 'candlestick',
+ data: xKeys.map(xKey =>
+ toCandlestickDatum(
+ recordsBySeriesAndX.get(`${name}::${xKey}`),
+ openLabel,
+ closeLabel,
+ lowLabel,
+ highLabel,
+ ),
+ ),
+ itemStyle: {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ },
+ }),
+ );
+
+ const periods = parseMovingAveragePeriods(movingAverages);
+ const qualifyMaNames = seriesNames.length > 1;
+ const movingAverageSeries: LineSeriesOption[] = candlestickSeries.flatMap(
+ candle => {
+ const closes = (candle.data ?? []).map(item =>
+ Array.isArray(item) && Number.isFinite(Number(item[1]))
+ ? Number(item[1])
+ : null,
+ );
+ const seriesLabel = qualifyMaNames ? String(candle.name) : undefined;
+ return periods.map(period => ({
+ name: movingAverageName(period, seriesLabel),
+ type: 'line' as const,
+ data: calculateMA(closes, period),
+ smooth: true,
+ showSymbol: false,
+ lineStyle: {
+ opacity: MA_LINE_OPACITY,
+ },
+ }));
+ },
+ );
+
+ const legendData = [
+ ...seriesNames,
+ ...movingAverageSeries.map(series => String(series.name)),
+ ].sort((a, b) => {
+ if (!legendSort) {
+ return 0;
+ }
+ return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
+ });
+
+ const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({
+ chartHeight: height,
+ chartWidth: width,
+ legendItems: legendData,
+ legendMargin,
+ orientation: legendOrientation,
+ show: showLegend,
+ theme,
+ type: legendType,
+ });
+ const legendPadding = getChartPadding(
+ showLegend,
+ legendOrientation,
+ effectiveLegendMargin,
+ undefined,
+ true,
+ );
+
+ const dataZoom = zoomable
+ ? [
+ {
+ type: 'inside',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ },
+ {
+ type: 'slider',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ bottom: TIMESERIES_CONSTANTS.zoomBottom,
+ },
+ ]
+ : [];
+
+ const echartOptions: EChartsCoreOption = {
+ grid: {
+ ...defaultGrid,
+ top: theme.sizeUnit * 5 + legendPadding.top,
+ bottom:
+ theme.sizeUnit * (showXAxis ? 5 : 3) +
+ legendPadding.bottom +
+ convertInteger(xAxisTitleMargin) +
+ (zoomable ? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable : 0),
+ left:
+ theme.sizeUnit * (showYAxis ? 5 : 2) +
+ legendPadding.left +
+ convertInteger(yAxisTitleMargin),
+ right: theme.sizeUnit * 5 + legendPadding.right,
+ },
+ legend: {
+ ...getLegendProps(
+ effectiveLegendType,
+ legendOrientation,
+ showLegend,
+ theme,
+ zoomable,
+ legendState,
+ ),
+ data: legendData,
+ },
+ xAxis: {
+ show: showXAxis,
+ type: AxisType.Category,
+ data: xLabels,
+ name: xAxisTitle,
+ nameGap: convertInteger(xAxisTitleMargin),
+ nameLocation: 'middle',
+ axisLabel: {
+ rotate: xAxisLabelRotation,
+ interval: xAxisLabelInterval === 'auto' ? 'auto' : 0,
+ hideOverlap: true,
+ },
+ },
+ yAxis: {
+ ...defaultYAxis,
+ show: showYAxis,
+ type: AxisType.Value,
+ name: yAxisTitle,
+ nameGap: convertInteger(yAxisTitleMargin),
+ nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
+ axisLabel: { formatter: yAxisFormatter },
+ },
+ tooltip: {
+ ...getDefaultTooltip(refs),
+ trigger: 'axis',
+ axisPointer: { type: 'shadow' },
+ show: !inContextMenu,
+ formatter: (params: CallbackDataParams | CallbackDataParams[]) => {
+ const [item] = ensureIsArray(params);
+ if (!item) {
+ return '';
+ }
+ const title =
+ coltypeMapping[xAxisName] === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum: data[item.dataIndex] ?? {},
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter,
+ })
Review Comment:
**Suggestion:** Temporal tooltip dates use `dataIndex` against raw rows, but categories are deduplicated by x value; with multiple series, the tooltip can show another row's date. [incorrect variable usage]
**Assessment:** 🟠 `Major` · 🔁 `Occurrence: Sometimes`
[](https://docs.codeant.ai/cli/resolve-pr-comments-skill) [](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=527cc68516b8457e802b0753b02f0595&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset) [](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=527cc68516b8457e802b0753b02f0595&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts
**Line:** 459:466
**Comment:**
*Incorrect Variable Usage: Temporal tooltip dates use `dataIndex` against raw rows, but categories are deduplicated by x value; with multiple series, the tooltip can show another row's date.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
```
</details>
<a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F43863&comment_hash=7673412942ffa57e5ab60828c98416a1b56416a50e6b2deff7d11d645412c4a7&reaction=like'>👍</a> | <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F43863&comment_hash=7673412942ffa57e5ab60828c98416a1b56416a50e6b2deff7d11d645412c4a7&reaction=dislike'>👎</a>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "sadpandajoe (via GitHub)" <gi...@apache.org>.
sadpandajoe commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3939040037
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,533 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toCategoryKey(value: unknown): string {
+ return value == null ? NULL_STRING : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: customEchartOptionsInput, ...rawFormData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+ const formData = {
+ ...DEFAULT_FORM_DATA,
+ ...rawFormData,
Review Comment:
Agreed—the control panel persists `x_axis` while this transform reads `xAxis`, so the selected dimension is absent and every row collapses into the empty category. Could the transform and form-data type use the persisted snake_case keys?
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3941121770
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,533 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toCategoryKey(value: unknown): string {
+ return value == null ? NULL_STRING : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: customEchartOptionsInput, ...rawFormData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+ const formData = {
+ ...DEFAULT_FORM_DATA,
+ ...rawFormData,
+ };
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
Review Comment:
The conversion is already taking place in superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts; I replaced it with formData in [134e138](https://github.com/apache/superset/pull/43863/commits/134e13832628b61a7136f027f1083b70a5293870):
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "codecov[bot] (via GitHub)" <gi...@apache.org>.
codecov[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5541155336
## [Codecov](https://app.codecov.io/gh/apache/superset/pull/43863?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
:x: Patch coverage is `68.22430%` with `68 lines` in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 79.24%. Comparing base ([`1f1539f`](https://app.codecov.io/gh/apache/superset/commit/1f1539fe0ebbbf4602ebbb50f70e9f9893f8b71f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)) to head ([`f0e5e6e`](https://app.codecov.io/gh/apache/superset/commit/f0e5e6e16a759a77cee27359a1ef5062c1d26638?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
:warning: Report is 81 commits behind head on master.
| [Files with missing lines](https://app.codecov.io/gh/apache/superset/pull/43863?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Patch % | Lines |
|---|---|---|
| [...in-chart-echarts/src/Candlestick/transformProps.ts](https://app.codecov.io/gh/apache/superset/pull/43863?src=pr&el=tree&filepath=superset-frontend%2Fplugins%2Fplugin-chart-echarts%2Fsrc%2FCandlestick%2FtransformProps.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQtZnJvbnRlbmQvcGx1Z2lucy9wbHVnaW4tY2hhcnQtZWNoYXJ0cy9zcmMvQ2FuZGxlc3RpY2svdHJhbnNmb3JtUHJvcHMudHM=) | 64.28% | [55 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/43863?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...art-echarts/src/Candlestick/EchartsCandlestick.tsx](https://app.codecov.io/gh/apache/superset/pull/43863?src=pr&el=tree&filepath=superset-frontend%2Fplugins%2Fplugin-chart-echarts%2Fsrc%2FCandlestick%2FEchartsCandlestick.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQtZnJvbnRlbmQvcGx1Z2lucy9wbHVnaW4tY2hhcnQtZWNoYXJ0cy9zcmMvQ2FuZGxlc3RpY2svRWNoYXJ0c0NhbmRsZXN0aWNrLnRzeA==) | 0.00% | [10 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/43863?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...plugin-chart-echarts/src/Candlestick/buildQuery.ts](https://app.codecov.io/gh/apache/superset/pull/43863?src=pr&el=tree&filepath=superset-frontend%2Fplugins%2Fplugin-chart-echarts%2Fsrc%2FCandlestick%2FbuildQuery.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQtZnJvbnRlbmQvcGx1Z2lucy9wbHVnaW4tY2hhcnQtZWNoYXJ0cy9zcmMvQ2FuZGxlc3RpY2svYnVpbGRRdWVyeS50cw==) | 85.71% | [1 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/43863?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...gin-chart-echarts/src/Candlestick/controlPanel.tsx](https://app.codecov.io/gh/apache/superset/pull/43863?src=pr&el=tree&filepath=superset-frontend%2Fplugins%2Fplugin-chart-echarts%2Fsrc%2FCandlestick%2FcontrolPanel.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQtZnJvbnRlbmQvcGx1Z2lucy9wbHVnaW4tY2hhcnQtZWNoYXJ0cy9zcmMvQ2FuZGxlc3RpY2svY29udHJvbFBhbmVsLnRzeA==) | 75.00% | [1 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/43863?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...gins/plugin-chart-echarts/src/Candlestick/index.ts](https://app.codecov.io/gh/apache/superset/pull/43863?src=pr&el=tree&filepath=superset-frontend%2Fplugins%2Fplugin-chart-echarts%2Fsrc%2FCandlestick%2Findex.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQtZnJvbnRlbmQvcGx1Z2lucy9wbHVnaW4tY2hhcnQtZWNoYXJ0cy9zcmMvQ2FuZGxlc3RpY2svaW5kZXgudHM=) | 66.66% | [1 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/43863?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
<details><summary>Additional details and impacted files</summary>
```diff
@@ Coverage Diff @@
## master #43863 +/- ##
==========================================
- Coverage 79.26% 79.24% -0.02%
==========================================
Files 2888 2895 +7
Lines 166456 166670 +214
Branches 38532 38611 +79
==========================================
+ Hits 131938 132084 +146
- Misses 32030 32098 +68
Partials 2488 2488
```
| [Flag](https://app.codecov.io/gh/apache/superset/pull/43863/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
|---|---|---|
| [javascript](https://app.codecov.io/gh/apache/superset/pull/43863/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `74.72% <68.22%> (-0.02%)` | :arrow_down: |
Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#carryforward-flags-in-the-pull-request-comment) to find out more.
</details>
[:umbrella: View full report in Codecov by Harness](https://app.codecov.io/gh/apache/superset/pull/43863?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
<details><summary> :rocket: New features to boost your workflow: </summary>
- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "netlify[bot] (via GitHub)" <gi...@apache.org>.
netlify[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5541030509
### <span aria-hidden="true">✅</span> Deploy Preview for *superset-docs-preview* ready!
| Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | f0e5e6e16a759a77cee27359a1ef5062c1d26638 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/projects/superset-docs-preview/deploys/6a9ac539e323be0008232c6a |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-43863--superset-docs-preview.netlify.app](https://deploy-preview-43863--superset-docs-preview.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br /><br /><br />_Use your smartphone camera to open QR code link._</details> |
|<span aria-hidden="true">🤖</span> Make changes | [Run an agent on this branch](https://app.netlify.com/projects/superset-docs-preview/agent-runs#DcpBCoAgEADAv-w5SJQk-0GvkG13w8hUzEtEf8_bHOaFUvNVGiwAA2wVE4XuXbCNyOwJE0e520Gnp4C19cVSYn5W7s-iQ5qME6PNJkqpWRtNFuH7AQ) |
---
<!-- [superset-docs-preview Preview](https://deploy-preview-43863--superset-docs-preview.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify project configuration](https://app.netlify.com/projects/superset-docs-preview/configuration/notifications#deploy-notifications)._
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3941098473
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,533 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toCategoryKey(value: unknown): string {
+ return value == null ? NULL_STRING : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: customEchartOptionsInput, ...rawFormData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+ const formData = {
+ ...DEFAULT_FORM_DATA,
+ ...rawFormData,
Review Comment:
The conversion is already taking place in `superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts`; I replaced it with `formData` in [134e138](https://github.com/apache/superset/pull/43863/commits/134e13832628b61a7136f027f1083b70a5293870):
```
this.formData = convertKeysToCamelCase(formData);
this.rawFormData = formData;
```
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5742361975
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4094344390
##########
superset/translations/cs/LC_MESSAGES/messages.po:
##########
@@ -9043,6 +9095,12 @@ msgstr "Jednotka velikosti popisku"
msgid "Label threshold"
msgstr "Prahová hodnota popisku"
+msgid "Label used for decreasing candles in the tooltip."
+msgstr "Popisek klesajících svíček v popisku."
Review Comment:
fix in [c0dcb9c](https://github.com/apache/superset/pull/43863/commits/c0dcb9cf604c5fe18d1c4a4ff3920af65c95ae2b)
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5820639147
@rusackas @sadpandajoe
Bito's comment has been corrected, and translations have been added. Ready for review.
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4050873215
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,729 @@
+/**
+ * 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 {
+ AxisType,
+ CategoricalColorNamespace,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { CustomSeriesOption, CustomSeriesRenderItem } from 'echarts';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+ OHLC_TICK_WIDTH_RATIO,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+type AxisTooltipParams = CallbackDataParams & {
+ axisValue?: string | number;
+ axisValueLabel?: string;
+};
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(ohlc: OhlcValue | null): CandlestickDatum {
+ return ohlc ?? [];
+}
+
+function toOhlcBarDatum(
+ ohlc: OhlcValue | null,
+ categoryIndex: number,
+ color: string,
+) {
+ if (!ohlc) {
+ return [];
+ }
+ return {
+ value: [categoryIndex, ...ohlc],
+ itemStyle: {
+ color,
+ },
+ };
+}
+
+function getDirectionItemStyle(increaseHex: string, decreaseHex: string) {
+ return {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ };
+}
+
+function getSeriesItemStyle(seriesColor: string, hollowFill: string) {
+ return {
+ color: seriesColor,
+ color0: hollowFill,
+ borderColor: seriesColor,
+ borderColor0: seriesColor,
+ };
+}
+
+const renderOhlcItem: CustomSeriesRenderItem = (_params, item) => {
+ const x = toNumber(item.value(0));
+ const open = toNumber(item.value(1));
+ const close = toNumber(item.value(2));
+ const low = toNumber(item.value(3));
+ const high = toNumber(item.value(4));
+ if (
+ x === null ||
+ open === null ||
+ close === null ||
+ low === null ||
+ high === null
+ ) {
+ return null;
+ }
+
+ const openPoint = item.coord([x, open]);
+ const closePoint = item.coord([x, close]);
+ const lowPoint = item.coord([x, low]);
+ const highPoint = item.coord([x, high]);
+ const categorySize = item.size?.([1, 0]);
+ const categoryWidth = Array.isArray(categorySize)
+ ? categorySize[0]
+ : categorySize;
+ if (categoryWidth == null || !Number.isFinite(categoryWidth)) {
+ return null;
+ }
+ const halfWidth = categoryWidth * OHLC_TICK_WIDTH_RATIO;
+ const style = item.style({
+ stroke: item.visual('color'),
+ });
+
+ return {
+ type: 'group',
+ children: [
+ {
+ type: 'line',
+ shape: {
+ x1: lowPoint[0],
+ y1: lowPoint[1],
+ x2: highPoint[0],
+ y2: highPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: openPoint[0],
+ y1: openPoint[1],
+ x2: openPoint[0] - halfWidth,
+ y2: openPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: closePoint[0],
+ y1: closePoint[1],
+ x2: closePoint[0] + halfWidth,
+ y2: closePoint[1],
+ },
+ style,
+ },
+ ],
+ };
+};
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function extractLineValue(item: CallbackDataParams): number | null {
+ const raw = item.value ?? item.data;
+ if (Array.isArray(raw)) {
+ const y = Number(raw[raw.length - 1]);
+ return Number.isFinite(y) ? y : null;
+ }
+ const numeric = Number(raw);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function appendOhlcRows(
+ rows: string[][],
+ ohlc: OhlcValue,
+ numberFormatter: NumberFormatter | CurrencyFormatter,
+) {
+ const [open, close, low, high] = ohlc;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+ showSeriesName,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+ showSeriesName: boolean;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candles = params.flatMap(item => {
+ const ohlc = extractOhlc(item.value ?? item.data);
+ return ohlc ? [{ item, ohlc }] : [];
+ });
+
+ candles.forEach(({ item, ohlc }) => {
+ const [open, close] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ if (showSeriesName) {
+ const seriesLabel = String(item.seriesName ?? '');
+ if (seriesLabel) {
+ rows.push([`${seriesLabel} (${direction})`]);
+ }
+ } else {
+ heading = title ? `${title} (${direction})` : direction;
+ }
+ appendOhlcRows(rows, ohlc, numberFormatter);
+ });
+
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = extractLineValue(item);
+ if (value === null) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: _echartOptions, ...formData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ candlestickSeriesName,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ seriesStyle,
+ colorScheme,
+ sliceId,
+ colorByDirection,
+ }: EchartsCandlestickFormData = { ...DEFAULT_FORM_DATA, ...formData };
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const defaultSeriesLabel =
+ candlestickSeriesName?.trim() || CANDLESTICK_SERIES_NAME;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const colorScale = CategoricalColorNamespace.getScale(colorScheme);
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: LookupKey[] = [];
+ const xLabels: string[] = [];
+ const xRecords: DataRecord[] = [];
+ const xKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, xAxisName));
+ if (xKeySet.has(key)) {
+ return;
+ }
+ xKeySet.add(key);
+ xKeys.push(key);
+ xRecords.push(datum);
+ xLabels.push(
+ getOwnValue(coltypeMapping, xAxisName) === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter: axisTimeFormatter,
+ })
+ : extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ }),
+ );
+ });
+
+ const seriesKeys: LookupKey[] = [];
+ const seriesNames: string[] = [];
+ if (seriesName) {
+ const seriesKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, seriesName));
+ if (seriesKeySet.has(key)) {
+ return;
+ }
+ seriesKeySet.add(key);
+ seriesKeys.push(key);
+ seriesNames.push(
+ extractGroupbyLabel({
+ datum,
+ groupby: [seriesName],
+ coltypeMapping,
+ }),
+ );
+ });
+ } else {
+ seriesKeys.push(CANDLESTICK_SERIES_NAME);
+ seriesNames.push(defaultSeriesLabel);
+ }
+
+ const useSeriesColors = seriesNames.length > 1 || colorByDirection === false;
+
+ const recordsBySeriesAndX = new Map<LookupKey, Map<LookupKey, DataRecord>>();
+ data.forEach(datum => {
+ const xKey = toLookupKey(getOwnValue(datum, xAxisName));
+ const seriesKey = seriesName
+ ? toLookupKey(getOwnValue(datum, seriesName))
+ : CANDLESTICK_SERIES_NAME;
+ let byX = recordsBySeriesAndX.get(seriesKey);
+ if (!byX) {
+ byX = new Map();
+ recordsBySeriesAndX.set(seriesKey, byX);
+ }
+ byX.set(xKey, datum);
+ });
+
+ const ohlcBySeries: (OhlcValue | null)[][] = seriesKeys.map(key =>
+ xKeys.map(xKey => {
+ const datum = recordsBySeriesAndX.get(key)?.get(xKey);
+ return datum
+ ? getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel)
+ : null;
+ }),
+ );
+
+ const priceSeries: (CandlestickSeriesOption | CustomSeriesOption)[] =
+ ohlcBySeries.map((ohlcData, index) => {
+ const name = seriesNames[index];
+ const seriesColor = useSeriesColors
+ ? colorScale(name, sliceId)
+ : increaseHex;
+ if (seriesStyle === 'ohlc') {
+ return {
+ name,
+ type: 'custom',
+ renderItem: renderOhlcItem,
+ dimensions: ['-', 'open', 'close', 'low', 'high'],
+ encode: {
+ x: 0,
+ y: [1, 2, 3, 4],
+ tooltip: [1, 2, 3, 4],
+ },
+ itemStyle: {
+ color: seriesColor,
+ },
+ data: ohlcData.map((ohlc, categoryIndex) => {
+ if (!ohlc) {
+ return [];
+ }
+ const [openValue, closeValue] = ohlc;
+ const color = useSeriesColors
+ ? seriesColor
+ : closeValue >= openValue
+ ? increaseHex
+ : decreaseHex;
+ return toOhlcBarDatum(ohlc, categoryIndex, color);
+ }),
+ };
+ }
+ return {
+ name,
+ type: 'candlestick',
+ data: ohlcData.map(toCandlestickDatum),
+ itemStyle: useSeriesColors
+ ? getSeriesItemStyle(seriesColor, theme.colorBgContainer)
+ : getDirectionItemStyle(increaseHex, decreaseHex),
+ };
+ });
+
+ const periods = parseMovingAveragePeriods(movingAverages);
+ const qualifyMaNames = seriesNames.length > 1;
+ const movingAverageSeries: LineSeriesOption[] = ohlcBySeries.flatMap(
+ (ohlcData, index) => {
+ const closes = ohlcData.map(ohlc => ohlc?.[1] ?? null);
+ const seriesLabel = qualifyMaNames ? seriesNames[index] : undefined;
+ const seriesColor = useSeriesColors
+ ? colorScale(seriesNames[index], sliceId)
+ : undefined;
+ return periods.map(period => ({
+ name: movingAverageName(period, seriesLabel),
+ type: 'line' as const,
+ data: calculateMA(closes, period),
+ smooth: true,
+ showSymbol: false,
+ itemStyle: seriesColor ? { color: seriesColor } : undefined,
+ lineStyle: {
+ opacity: MA_LINE_OPACITY,
+ ...(seriesColor ? { color: seriesColor } : {}),
+ },
+ }));
+ },
+ );
+
+ const legendData = [
+ ...seriesNames,
+ ...movingAverageSeries.map(series => String(series.name)),
+ ].sort((a, b) => {
+ if (!legendSort) {
+ return 0;
+ }
+ return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
+ });
+
+ const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({
+ chartHeight: height,
+ chartWidth: width,
+ legendItems: legendData,
+ legendMargin,
+ orientation: legendOrientation,
+ show: showLegend,
+ theme,
+ type: legendType,
+ });
+ const legendPadding = getChartPadding(
+ showLegend,
+ legendOrientation,
+ effectiveLegendMargin,
+ );
+
+ const dataZoom = zoomable
+ ? [
+ {
+ type: 'inside',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ },
+ {
+ type: 'slider',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ bottom: TIMESERIES_CONSTANTS.zoomBottom,
+ },
+ ]
+ : [];
+
+ const echartOptions: EChartsCoreOption = {
+ grid: {
+ ...defaultGrid,
+ top: theme.sizeUnit * 5 + legendPadding.top,
+ bottom:
+ theme.sizeUnit * (showXAxis ? 5 : 3) +
+ legendPadding.bottom +
+ convertInteger(xAxisTitleMargin) +
+ (zoomable ? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable : 0),
+ left:
+ theme.sizeUnit * (showYAxis ? 5 : 2) +
+ legendPadding.left +
+ convertInteger(yAxisTitleMargin),
+ right: theme.sizeUnit * 5 + legendPadding.right,
+ },
+ legend: {
+ ...getLegendProps(
+ effectiveLegendType,
+ legendOrientation,
+ showLegend,
+ theme,
+ zoomable,
+ legendState,
+ ),
+ data: legendData,
+ },
+ xAxis: {
+ show: showXAxis,
+ type: AxisType.Category,
+ data: xLabels,
+ name: xAxisTitle,
+ nameGap: convertInteger(xAxisTitleMargin),
+ nameLocation: 'middle',
+ axisLabel: {
+ rotate: xAxisLabelRotation,
+ interval: xAxisLabelInterval === 'auto' ? 'auto' : 0,
+ hideOverlap: true,
+ },
+ },
+ yAxis: {
+ ...defaultYAxis,
+ show: showYAxis,
+ type: AxisType.Value,
+ name: yAxisTitle,
+ nameGap: convertInteger(yAxisTitleMargin),
+ nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
+ axisLabel: { formatter: yAxisFormatter },
+ },
+ tooltip: {
+ ...getDefaultTooltip(refs),
+ trigger: 'axis',
+ axisPointer: { type: 'cross' },
+ show: !inContextMenu,
+ formatter: (params: CallbackDataParams | CallbackDataParams[]) => {
+ const items = ensureIsArray(params) as AxisTooltipParams[];
+ const [item] = items;
+ if (!item) {
+ return '';
+ }
+ const categoryIndex = item.dataIndex;
+ const categoryDatum =
+ Number.isInteger(categoryIndex) &&
+ categoryIndex >= 0 &&
+ categoryIndex < xRecords.length
+ ? xRecords[categoryIndex]
+ : {};
+ const categoryLabel = Number.isInteger(categoryIndex)
+ ? xLabels[categoryIndex]
+ : undefined;
+ const title =
+ getOwnValue(coltypeMapping, xAxisName) === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum: categoryDatum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter,
+ })
+ : String(
+ item.axisValueLabel ??
+ item.axisValue ??
+ item.name ??
+ categoryLabel ??
+ '',
+ );
+ return formatTooltip({
+ params: items,
+ numberFormatter,
+ title,
+ increaseLabel: upLabel,
+ decreaseLabel: downLabel,
+ showSeriesName: seriesNames.length > 1,
+ });
+ },
+ },
+ series: [...priceSeries, ...movingAverageSeries],
+ toolbox: {
+ show: zoomable,
+ feature: {
+ dataZoom: {
+ yAxisIndex: false,
+ title: {
+ zoom: 'zoom area',
+ back: 'restore zoom',
+ },
Review Comment:
fix in [0249394](https://github.com/apache/superset/pull/43863/commits/0249394adc95a0a410f1e12c0a27e463a6ce0626)
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3936654579
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/utils.ts:
##########
@@ -0,0 +1,78 @@
+/**
+ * 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 { ensureIsArray } from '@superset-ui/core';
+
+export const MOVING_AVERAGE_PERIODS = [5, 10, 15, 20, 30, 60];
+export const MA_LINE_OPACITY = 0.5;
+
+export function movingAverageName(period: number, seriesName?: string): string {
+ const label = `MA${period}`;
+ return seriesName ? `${seriesName} ${label}` : label;
+}
+
+function parseMovingAveragePeriod(item: unknown): number | null {
+ if (typeof item === 'number') {
+ return Number.isInteger(item) && item > 1 ? item : null;
+ }
+ const match = String(item)
+ .trim()
+ .match(/^(?:MA)?(\d+)$/i);
+ if (!match) {
+ return null;
+ }
+ const period = Number(match[1]);
+ return Number.isInteger(period) && period > 1 ? period : null;
+}
+
+export function parseMovingAveragePeriods(value: unknown): number[] {
+ const periods = ensureIsArray(value)
+ .map(parseMovingAveragePeriod)
+ .filter((period): period is number => period !== null);
+ return [...new Set(periods)].sort((left, right) => left - right);
+}
+
+/**
+ * Simple moving average of close prices: the first `period - 1` points
+ * are omitted, then each value is the mean of the current close and the
+ * previous `period - 1` closes.
+ */
+export function calculateMA(
+ closes: Array<number | null>,
+ period: number,
+): Array<number | '-'> {
+ const result: Array<number | '-'> = [];
+ for (let i = 0; i < closes.length; i += 1) {
+ if (i < period - 1) {
+ result.push('-');
+ continue;
+ }
+ let sum = 0;
+ let valid = true;
+ for (let j = 0; j < period; j += 1) {
+ const close = closes[i - j];
+ if (close === null) {
+ valid = false;
+ break;
+ }
+ sum += close;
+ }
+ result.push(valid ? sum / period : '-');
+ }
Review Comment:
fix in 76978b1
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3935202066
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,512 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(datum[openLabel]);
+ const close = toNumber(datum[closeLabel]);
+ const low = toNumber(datum[lowLabel]);
+ const high = toNumber(datum[highLabel]);
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: customEchartOptionsInput, ...rawFormData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+ const formData = {
+ ...DEFAULT_FORM_DATA,
+ ...rawFormData,
+ };
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ } = formData;
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: string[] = [];
+ const xLabels: string[] = [];
+ const xKeySet = new Set<string>();
+ data.forEach(datum => {
+ const raw = datum[xAxisName];
+ const key = raw == null ? NULL_STRING : String(raw);
+ if (xKeySet.has(key)) {
+ return;
+ }
+ xKeySet.add(key);
+ xKeys.push(key);
+ xLabels.push(
+ coltypeMapping[xAxisName] === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter: axisTimeFormatter,
+ })
+ : extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ }),
+ );
+ });
+
+ const seriesNames = seriesName
+ ? [
+ ...new Set(
+ data.map(datum =>
+ datum[seriesName] == null ? NULL_STRING : String(datum[seriesName]),
+ ),
+ ),
+ ]
+ : [CANDLESTICK_SERIES_NAME];
+
+ const recordsBySeriesAndX = new Map<string, DataRecord>();
+ data.forEach(datum => {
+ const xKey =
+ datum[xAxisName] == null ? NULL_STRING : String(datum[xAxisName]);
+ const seriesKey = seriesName
+ ? datum[seriesName] == null
+ ? NULL_STRING
+ : String(datum[seriesName])
+ : seriesNames[0];
+ recordsBySeriesAndX.set(`${seriesKey}::${xKey}`, datum);
Review Comment:
Nice catch; also fixed in 2b9cd3f
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4112698782
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,767 @@
+/**
+ * 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 {
+ AxisType,
+ CategoricalColorNamespace,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ getXAxisLabel,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import { t } from '@apache-superset/core/translation';
+import type { CustomSeriesOption, CustomSeriesRenderItem } from 'echarts';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+ OHLC_TICK_WIDTH_RATIO,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { getPadding } from '../Timeseries/transformers';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+type AxisTooltipParams = CallbackDataParams & {
+ axisValue?: string | number;
+ axisValueLabel?: string;
+};
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(ohlc: OhlcValue | null): CandlestickDatum {
+ return ohlc ?? [];
+}
+
+function toOhlcBarDatum(
+ ohlc: OhlcValue | null,
+ categoryIndex: number,
+ color: string,
+) {
+ if (!ohlc) {
+ return [];
+ }
+ return {
+ value: [categoryIndex, ...ohlc],
+ itemStyle: {
+ color,
+ },
+ };
+}
+
+function getDirectionItemStyle(increaseHex: string, decreaseHex: string) {
+ return {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ };
+}
+
+function getSeriesItemStyle(seriesColor: string, hollowFill: string) {
+ return {
+ color: seriesColor,
+ color0: hollowFill,
+ borderColor: seriesColor,
+ borderColor0: seriesColor,
+ };
+}
+
+const renderOhlcItem: CustomSeriesRenderItem = (_params, item) => {
+ const x = toNumber(item.value(0));
+ const open = toNumber(item.value(1));
+ const close = toNumber(item.value(2));
+ const low = toNumber(item.value(3));
+ const high = toNumber(item.value(4));
+ if (
+ x === null ||
+ open === null ||
+ close === null ||
+ low === null ||
+ high === null
+ ) {
+ return null;
+ }
+
+ const openPoint = item.coord([x, open]);
+ const closePoint = item.coord([x, close]);
+ const lowPoint = item.coord([x, low]);
+ const highPoint = item.coord([x, high]);
+ const categorySize = item.size?.([1, 0]);
+ const categoryWidth = Array.isArray(categorySize)
+ ? categorySize[0]
+ : categorySize;
+ if (categoryWidth == null || !Number.isFinite(categoryWidth)) {
+ return null;
+ }
+ const halfWidth = categoryWidth * OHLC_TICK_WIDTH_RATIO;
+ const style = item.style({
+ stroke: item.visual('color'),
+ });
+
+ return {
+ type: 'group',
+ children: [
+ {
+ type: 'line',
+ shape: {
+ x1: lowPoint[0],
+ y1: lowPoint[1],
+ x2: highPoint[0],
+ y2: highPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: openPoint[0],
+ y1: openPoint[1],
+ x2: openPoint[0] - halfWidth,
+ y2: openPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: closePoint[0],
+ y1: closePoint[1],
+ x2: closePoint[0] + halfWidth,
+ y2: closePoint[1],
+ },
+ style,
+ },
+ ],
+ };
+};
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function extractLineValue(item: CallbackDataParams): number | null {
+ const raw = item.value ?? item.data;
+ if (Array.isArray(raw)) {
+ const y = Number(raw[raw.length - 1]);
+ return Number.isFinite(y) ? y : null;
+ }
+ const numeric = Number(raw);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function appendOhlcRows(
+ rows: string[][],
+ ohlc: OhlcValue,
+ numberFormatter: NumberFormatter | CurrencyFormatter,
+) {
+ const [open, close, low, high] = ohlc;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ const candles = params.flatMap(item => {
+ const ohlc = extractOhlc(item.value ?? item.data);
+ return ohlc ? [{ item, ohlc }] : [];
+ });
+
+ candles.forEach(({ item, ohlc }) => {
+ const [open, close] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ const seriesLabel = String(item.seriesName ?? '');
+ rows.push([seriesLabel ? `${seriesLabel} (${direction})` : direction]);
+ appendOhlcRows(rows, ohlc, numberFormatter);
+ });
+
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = extractLineValue(item);
+ if (value === null) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, title);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: _echartOptions, ...formData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged, onContextMenu } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ candlestickSeriesName,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ seriesStyle,
+ colorScheme,
+ sliceId,
+ colorByDirection,
+ }: EchartsCandlestickFormData = { ...DEFAULT_FORM_DATA, ...formData };
+
+ // Matches buildQuery.ts's getXAxisColumn: an unset x_axis with
+ // granularity_sqla present still queries DTTM_ALIAS, so the transform must
+ // resolve the same column or every row collapses into one empty category.
+ const xAxisName = (getXAxisLabel(chartProps.rawFormData) as string) ?? '';
Review Comment:
<!-- Bito Reply -->
The suggestion is correct. `getXAxisLabel` returns an optional string, so the `as string` assertion is redundant. Removing it allows the nullish coalescing operator (`?? ''`) to handle the optionality safely, which is cleaner and maintains the same runtime behavior.
**superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts**
```
// Matches buildQuery.ts's getXAxisColumn: an unset x_axis with
// granularity_sqla present still queries DTTM_ALIAS, so the transform must
// resolve the same column or every row collapses into one empty category.
const xAxisName = (getXAxisLabel(chartProps.rawFormData) as string) ?? '';
```
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4112696941
##########
superset-frontend/packages/superset-ui-core/src/query/extractQueryFields.ts:
##########
@@ -49,6 +49,10 @@ export default function extractQueryFields(
secondary_metric: 'metrics',
left_metric: 'metrics',
right_metric: 'metrics',
+ open: 'metrics',
Review Comment:
fix in 1ec5fe3
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/EchartsCandlestick.tsx:
##########
@@ -0,0 +1,176 @@
+/**
+ * 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 { useRef } from 'react';
+import {
+ BinaryQueryObjectFilterClause,
+ TimeGranularity,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import Echart from '../components/Echart';
+import { EchartsHandler, EventHandlers } from '../types';
+import { CandlestickChartTransformedProps } from './types';
+
+type ContextMenuEvent = {
+ event?: { stop?: () => void; event?: PointerEvent };
+ dataIndex?: number;
+ seriesName?: string;
+ seriesType?: string;
+};
+
+function toFilterValue(value: unknown): string | number | boolean | null {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof Date) {
+ return value.valueOf();
+ }
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+ return String(value);
+}
+
+function toFilterClause(
+ col: string,
+ value: unknown,
+ formattedVal: string,
+ grain?: TimeGranularity,
+): BinaryQueryObjectFilterClause {
+ const val = toFilterValue(value);
+ if (val == null) {
+ return {
+ col,
+ op: 'IS NULL' as BinaryQueryObjectFilterClause['op'],
+ val: null,
+ formattedVal,
+ };
+ }
+ return {
+ col,
+ op: '==',
+ val,
+ formattedVal,
+ ...(grain ? { grain } : {}),
+ };
+}
+
+export default function EchartsCandlestick(
+ props: CandlestickChartTransformedProps,
+) {
+ const {
+ height,
+ width,
+ echartOptions,
+ refs,
+ onLegendStateChanged,
+ onContextMenu,
+ formData,
+ coltypeMapping,
+ xAxisColumn,
+ seriesColumn,
+ xValues,
+ xLabels,
+ seriesValues,
+ } = props;
+ const echartRef = useRef<EchartsHandler | null>(null);
+ // eslint-disable-next-line no-param-reassign
+ refs.echartRef = echartRef;
+
+ const hideTooltip = () => {
+ echartRef.current?.getEchartInstance()?.dispatchAction({ type: 'hideTip' });
+ };
+
+ const eventHandlers: EventHandlers = {
+ legendselectchanged: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ legendselectall: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ legendinverseselect: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ contextmenu: (eventParams: ContextMenuEvent) => {
+ if (!onContextMenu) {
+ return;
+ }
+ eventParams.event?.stop?.();
+ hideTooltip();
+ const pointerEvent = eventParams.event?.event;
+ if (!pointerEvent) {
+ return;
+ }
+ const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
+ const categoryIndex = eventParams.dataIndex;
+ if (
+ xAxisColumn &&
+ categoryIndex != null &&
+ categoryIndex >= 0 &&
+ categoryIndex < xValues.length
+ ) {
+ const xValue = xValues[categoryIndex];
+ const isTemporal =
+ coltypeMapping?.[xAxisColumn] === GenericDataType.Temporal;
+ drillToDetailFilters.push(
+ toFilterClause(
+ xAxisColumn,
+ xValue,
+ xLabels[categoryIndex] ?? String(xValue ?? ''),
+ isTemporal
+ ? (formData.timeGrainSqla as TimeGranularity | undefined)
+ : undefined,
+ ),
+ );
+ }
+ if (seriesColumn && eventParams.seriesType !== 'line') {
+ const seriesValue = seriesValues.find(
Review Comment:
fix in 1ec5fe3
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4051010390
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/controlPanel.tsx:
##########
@@ -0,0 +1,353 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t } from '@apache-superset/core/translation';
+import { ensureIsArray, validateNonEmpty } from '@superset-ui/core';
+import {
+ ControlPanelConfig,
+ ControlPanelsContainerProps,
+ ControlSubSectionHeader,
+ D3_TIME_FORMAT_DOCS,
+ DEFAULT_TIME_FORMAT,
+ getStandardizedControls,
+ sections,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import {
+ legendSection,
+ tooltipTimeFormatControl,
+ tooltipValuesFormatControl,
+ xAxisLabelInterval,
+ xAxisLabelRotation,
+} from '../controls';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_INCREASE_COLOR,
+ DEFAULT_SERIES_STYLE,
+} from './constants';
+import { MOVING_AVERAGE_PERIODS } from './utils';
+
+function hasSeriesDimension({
+ controls,
+}: ControlPanelsContainerProps): boolean {
+ return ensureIsArray(controls?.series?.value).length > 0;
+}
+
+function showColorByDirection(props: ControlPanelsContainerProps): boolean {
+ return !hasSeriesDimension(props);
+}
Review Comment:
fix in [1459e67](https://github.com/apache/superset/pull/43863/commits/1459e67e91fd4370a0b6457e7207e59ae5b82b4f)
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "rusackas (via GitHub)" <gi...@apache.org>.
rusackas commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5769565224
Candlestick chart looks great, but there are still three open threads worth clearing before this merges. @sadpandajoe's null-sentinel and tooltip-trigger catches both got a "fixed" reply from you, but the threads are still open so I can't tell those landed on the current head. Copilot's context-menu point on `inContextMenu` never being true for this plugin also hasn't been addressed. I'll see if I can help fix these items.
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4078166459
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/EchartsCandlestick.tsx:
##########
@@ -0,0 +1,167 @@
+/**
+ * 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 { useRef } from 'react';
+import {
+ BinaryQueryObjectFilterClause,
+ TimeGranularity,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import Echart from '../components/Echart';
+import { EchartsHandler, EventHandlers } from '../types';
+import { CandlestickChartTransformedProps } from './types';
+
+type ContextMenuEvent = {
+ event?: { stop?: () => void; event?: PointerEvent };
+ dataIndex?: number;
+ seriesName?: string;
+ seriesType?: string;
+};
+
+function toFilterValue(value: unknown): string | number | boolean | null {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof Date) {
+ return value.valueOf();
+ }
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+ return String(value);
+}
+
+function toFilterClause(
+ col: string,
+ value: unknown,
+ formattedVal: string,
+ grain?: TimeGranularity,
+): BinaryQueryObjectFilterClause {
+ return {
+ col,
+ op: '==',
+ val: toFilterValue(value),
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>CWE-703: Null filter regression</b></div>
<div id="fix">
The removed null check changes behavior: `toFilterValue` (lines 36-51) returns `null` for null/undefined x-values, so the clause is now `{ op: '==', val: null }`. Backend `get_sqla_query` (`models/helpers.py` skips the None-value validation for EQUALS) renders `col = NULL` via `handle_comparison_filter`, which never matches, so drill-to-detail on a null x-category returns no rows. Sibling charts (`EchartsTimeseries.tsx:370`, `EchartsTreemap.tsx:78`) still emit `op: 'IS NULL'`. Please restore the IS NULL branch. ([CWE-703](https://cwe.mitre.org/data/definitions/703.html))
</div>
</div>
<small><i>Code Review Run #a03ce0</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/translations/messages.pot:
##########
@@ -18566,6 +18664,9 @@ msgstr ""
msgid "value descending"
msgstr ""
+msgid "valuename"
+msgstr ""
+
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Stale pot msgid valuename</b></div>
<div id="fix">
msgid "valuename" is not extractable from source: no superset/**.py, template, or frontend file mapped by superset/translations/babel.cfg contains this literal, so a fresh extraction drops it and scripts/translations/check_pot_drift.py (CI: superset-translations.yml) fails on a stale template entry. Remove the entry or regenerate via scripts/translations/babel_update.sh.
</div>
</div>
<small><i>Code Review Run #a03ce0</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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4094350576
##########
superset/translations/ca/LC_MESSAGES/messages.po:
##########
@@ -19577,6 +19693,15 @@ msgstr ""
"Quan està marcat, el mapa farà zoom a les teves dades després de cada "
"consulta"
+msgid ""
+"When enabled, increasing candles use the increase color and decreasing "
+"candles use the decrease color. Turn off to use one series color, with a "
+"filled body for increases and a hollow body for decreases."
+msgstr ""
+"Si està activat, les veles alcistes usen el color de pujada i les "
+"baixistes el de baixada. Desactiveu-ho per usar un sol color de sèrie, amb "
Review Comment:
<!-- Bito Reply -->
The suggestion provided by the reviewer is appropriate and improves the code by aligning the terminology with the established labels in the project. Using 'color de pujada' and 'color de baixada' ensures consistency with the existing 'Augment' and 'Disminuir' labels, and correcting the register to informal plural matches the file's style.
**superset/translations/ca/LC_MESSAGES/messages.po**
```
"Si està activat, les veles alcistes usen el color d'augment i les "
"baixistes el de disminució. Desactiva-ho per usar un sol color de sèrie, amb "
```
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3936557422
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,533 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toCategoryKey(value: unknown): string {
+ return value == null ? NULL_STRING : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: customEchartOptionsInput, ...rawFormData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+ const formData = {
+ ...DEFAULT_FORM_DATA,
+ ...rawFormData,
Review Comment:
`transformProps` is reading camelCase formData keys (`xAxis`, `showLegend`, `legendOrientation`, `increaseColor`, `movingAverages`, `echartOptions`, etc.), but the control panel / tests use snake_case keys (`x_axis`, `show_legend`, `legend_orientation`, `increase_color`, `moving_averages`, `echart_options`, etc.). This will cause user-selected settings (including X axis column, colors, legend, tooltip formats, zoom, moving averages, and custom echart options) to be ignored at runtime. **Fix (required):** align the Candlestick plugin to Superset’s standard formData naming—either update this destructuring (and related references) to snake_case keys, or introduce an explicit mapping layer (snake_case → camelCase) used consistently across `controlPanel`, `DEFAULT_FORM_DATA`, `types`, and `transformProps`.
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,533 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toCategoryKey(value: unknown): string {
+ return value == null ? NULL_STRING : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: customEchartOptionsInput, ...rawFormData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+ const formData = {
+ ...DEFAULT_FORM_DATA,
+ ...rawFormData,
+ };
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
Review Comment:
`transformProps` is reading camelCase formData keys (`xAxis`, `showLegend`, `legendOrientation`, `increaseColor`, `movingAverages`, `echartOptions`, etc.), but the control panel / tests use snake_case keys (`x_axis`, `show_legend`, `legend_orientation`, `increase_color`, `moving_averages`, `echart_options`, etc.). This will cause user-selected settings (including X axis column, colors, legend, tooltip formats, zoom, moving averages, and custom echart options) to be ignored at runtime. **Fix (required):** align the Candlestick plugin to Superset’s standard formData naming—either update this destructuring (and related references) to snake_case keys, or introduce an explicit mapping layer (snake_case → camelCase) used consistently across `controlPanel`, `DEFAULT_FORM_DATA`, `types`, and `transformProps`.
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/types.ts:
##########
@@ -0,0 +1,68 @@
+/**
+ * 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 {
+ ChartDataResponseResult,
+ ChartProps,
+ QueryFormColumn,
+ QueryFormData,
+ QueryFormMetric,
+ RgbaColor,
+} from '@superset-ui/core';
+import {
+ BaseChartProps,
+ BaseTransformedProps,
+ LegendFormData,
+ TitleFormData,
+} from '../types';
+
+export type EchartsCandlestickFormData = QueryFormData &
+ LegendFormData &
+ TitleFormData & {
+ xAxis: QueryFormColumn;
+ open: QueryFormMetric;
+ close: QueryFormMetric;
+ high: QueryFormMetric;
+ low: QueryFormMetric;
+ series?: QueryFormColumn | QueryFormColumn[];
+ increaseColor: RgbaColor;
+ decreaseColor: RgbaColor;
+ increaseLabel?: string;
+ decreaseLabel?: string;
+ showXAxis: boolean;
+ showYAxis: boolean;
+ xAxisTimeFormat?: string;
+ xAxisLabelRotation: number;
+ xAxisLabelInterval: string;
+ yAxisFormat: string;
+ tooltipTimeFormat?: string;
+ tooltipValuesFormat?: string;
+ zoomable: boolean;
+ movingAverages?: (number | string)[];
+ echartOptions?: string;
Review Comment:
The Candlestick `FormData` type uses camelCase field names (e.g. `xAxis`, `increaseColor`, `showXAxis`, `movingAverages`, `echartOptions`) but the corresponding controls are defined with snake_case names (e.g. `x_axis`, `increase_color`, `show_x_axis`, `moving_averages`, `echart_options`). This mismatch will both hide real integration errors (since the type won’t reflect what Explore actually passes) and contributes to runtime bugs. **Fix (required):** rename these fields in the type to the actual snake_case formData keys used by chart controls, and tighten `series` to a single optional column (since `controlOverrides.series` sets `multi: false`).
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/EchartsCandlestick.tsx:
##########
@@ -0,0 +1,51 @@
+/**
+ * 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 Echart from '../components/Echart';
+import { EventHandlers } from '../types';
+import { CandlestickChartTransformedProps } from './types';
+
+export default function EchartsCandlestick(
+ props: CandlestickChartTransformedProps,
+) {
+ const { height, width, echartOptions, refs, onLegendStateChanged, formData } =
+ props;
+
+ const eventHandlers: EventHandlers = {
+ legendselectchanged: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ legendselectall: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ legendinverseselect: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ };
+
+ return (
+ <Echart
+ refs={refs}
+ height={height}
+ width={width}
+ echartOptions={echartOptions}
+ eventHandlers={eventHandlers}
+ vizType={formData.vizType}
Review Comment:
Superset Explore form data uses `viz_type` (snake_case), not `vizType`. Passing `undefined` here prevents `Echart` from applying chart-type theme overrides (`theme.echartsOptionsOverridesByChartType`) and may break any chart-type-specific operational settings (e.g. aria/animation overrides by type). **Fix (required):** pass the correct viz type field from formData (typically `formData.viz_type`), consistent with other ECharts chart components.
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/utils.ts:
##########
@@ -0,0 +1,78 @@
+/**
+ * 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 { ensureIsArray } from '@superset-ui/core';
+
+export const MOVING_AVERAGE_PERIODS = [5, 10, 15, 20, 30, 60];
+export const MA_LINE_OPACITY = 0.5;
+
+export function movingAverageName(period: number, seriesName?: string): string {
+ const label = `MA${period}`;
+ return seriesName ? `${seriesName} ${label}` : label;
+}
+
+function parseMovingAveragePeriod(item: unknown): number | null {
+ if (typeof item === 'number') {
+ return Number.isInteger(item) && item > 1 ? item : null;
+ }
+ const match = String(item)
+ .trim()
+ .match(/^(?:MA)?(\d+)$/i);
+ if (!match) {
+ return null;
+ }
+ const period = Number(match[1]);
+ return Number.isInteger(period) && period > 1 ? period : null;
+}
+
+export function parseMovingAveragePeriods(value: unknown): number[] {
+ const periods = ensureIsArray(value)
+ .map(parseMovingAveragePeriod)
+ .filter((period): period is number => period !== null);
+ return [...new Set(periods)].sort((left, right) => left - right);
+}
+
+/**
+ * Simple moving average of close prices: the first `period - 1` points
+ * are omitted, then each value is the mean of the current close and the
+ * previous `period - 1` closes.
+ */
+export function calculateMA(
+ closes: Array<number | null>,
+ period: number,
+): Array<number | '-'> {
+ const result: Array<number | '-'> = [];
+ for (let i = 0; i < closes.length; i += 1) {
+ if (i < period - 1) {
+ result.push('-');
+ continue;
+ }
+ let sum = 0;
+ let valid = true;
+ for (let j = 0; j < period; j += 1) {
+ const close = closes[i - j];
+ if (close === null) {
+ valid = false;
+ break;
+ }
+ sum += close;
+ }
+ result.push(valid ? sum / period : '-');
+ }
Review Comment:
`calculateMA` is `O(n * period)` per series per MA period (nested loop summing each window from scratch). With long time series and multiple MA periods (and/or multiple series), this can become noticeably expensive. Consider a sliding-window approach (maintain rolling sum + count, reset/track invalid windows when encountering `null`) to reduce per-period work to `O(n)`.
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,533 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toCategoryKey(value: unknown): string {
+ return value == null ? NULL_STRING : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: customEchartOptionsInput, ...rawFormData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+ const formData = {
+ ...DEFAULT_FORM_DATA,
+ ...rawFormData,
+ };
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
Review Comment:
`transformProps` is reading camelCase formData keys (`xAxis`, `showLegend`, `legendOrientation`, `increaseColor`, `movingAverages`, `echartOptions`, etc.), but the control panel / tests use snake_case keys (`x_axis`, `show_legend`, `legend_orientation`, `increase_color`, `moving_averages`, `echart_options`, etc.). This will cause user-selected settings (including X axis column, colors, legend, tooltip formats, zoom, moving averages, and custom echart options) to be ignored at runtime. **Fix (required):** align the Candlestick plugin to Superset’s standard formData naming—either update this destructuring (and related references) to snake_case keys, or introduce an explicit mapping layer (snake_case → camelCase) used consistently across `controlPanel`, `DEFAULT_FORM_DATA`, `types`, and `transformProps`.
##########
superset-frontend/plugins/plugin-chart-echarts/test/Candlestick/buildQuery.test.ts:
##########
@@ -0,0 +1,57 @@
+/**
+ * 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 { QueryFormData, VizType } from '@superset-ui/core';
+import buildQuery from '../../src/Candlestick/buildQuery';
+
+const formData = {
+ datasource: '5__table',
+ viz_type: VizType.Candlestick,
+ x_axis: 'date',
+ open: 'open',
+ close: 'close',
+ high: 'high',
+ low: 'low',
+} as QueryFormData;
+
+test('builds query fields from OHLC metrics and x-axis', () => {
Review Comment:
The PR’s “TESTING INSTRUCTIONS” mention running `.../test/Butterfly`, but this PR adds Candlestick tests under `.../test/Candlestick`. The instructions should be updated to point to the Candlestick test suite (or the intended aggregate test command) so reviewers can validate the feature quickly.
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5553860973
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "sadpandajoe (via GitHub)" <gi...@apache.org>.
sadpandajoe commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3970958612
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,551 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: _echartOptions, ...formData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ candlestickSeriesName,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ }: EchartsCandlestickFormData = { ...DEFAULT_FORM_DATA, ...formData };
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const defaultSeriesLabel =
+ candlestickSeriesName?.trim() || CANDLESTICK_SERIES_NAME;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: LookupKey[] = [];
+ const xLabels: string[] = [];
+ const xRecords: DataRecord[] = [];
+ const xKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, xAxisName));
+ if (xKeySet.has(key)) {
+ return;
+ }
+ xKeySet.add(key);
+ xKeys.push(key);
+ xRecords.push(datum);
+ xLabels.push(
+ getOwnValue(coltypeMapping, xAxisName) === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter: axisTimeFormatter,
+ })
+ : extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ }),
+ );
+ });
+
+ const seriesKeys: LookupKey[] = [];
+ const seriesNames: string[] = [];
+ if (seriesName) {
+ const seriesKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, seriesName));
+ if (seriesKeySet.has(key)) {
+ return;
+ }
+ seriesKeySet.add(key);
+ seriesKeys.push(key);
+ seriesNames.push(
+ extractGroupbyLabel({
+ datum,
+ groupby: [seriesName],
+ coltypeMapping,
+ }),
+ );
+ });
+ } else {
+ seriesKeys.push(CANDLESTICK_SERIES_NAME);
+ seriesNames.push(defaultSeriesLabel);
+ }
+
+ const recordsBySeriesAndX = new Map<LookupKey, Map<LookupKey, DataRecord>>();
+ data.forEach(datum => {
+ const xKey = toLookupKey(getOwnValue(datum, xAxisName));
+ const seriesKey = seriesName
+ ? toLookupKey(getOwnValue(datum, seriesName))
+ : CANDLESTICK_SERIES_NAME;
+ let byX = recordsBySeriesAndX.get(seriesKey);
+ if (!byX) {
+ byX = new Map();
+ recordsBySeriesAndX.set(seriesKey, byX);
+ }
+ byX.set(xKey, datum);
+ });
+
+ const candlestickSeries: CandlestickSeriesOption[] = seriesKeys.map(
+ (key, index) => ({
+ name: seriesNames[index],
+ type: 'candlestick',
+ data: xKeys.map(xKey =>
+ toCandlestickDatum(
+ recordsBySeriesAndX.get(key)?.get(xKey),
+ openLabel,
+ closeLabel,
+ lowLabel,
+ highLabel,
+ ),
+ ),
+ itemStyle: {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ },
+ }),
+ );
+
+ const periods = parseMovingAveragePeriods(movingAverages);
+ const qualifyMaNames = seriesNames.length > 1;
+ const movingAverageSeries: LineSeriesOption[] = candlestickSeries.flatMap(
+ candle => {
+ const closes = (candle.data ?? []).map(item =>
+ Array.isArray(item) && Number.isFinite(Number(item[1]))
+ ? Number(item[1])
+ : null,
+ );
+ const seriesLabel = qualifyMaNames ? String(candle.name) : undefined;
+ return periods.map(period => ({
+ name: movingAverageName(period, seriesLabel),
+ type: 'line' as const,
+ data: calculateMA(closes, period),
+ smooth: true,
+ showSymbol: false,
+ lineStyle: {
+ opacity: MA_LINE_OPACITY,
+ },
+ }));
+ },
+ );
+
+ const legendData = [
+ ...seriesNames,
+ ...movingAverageSeries.map(series => String(series.name)),
+ ].sort((a, b) => {
+ if (!legendSort) {
+ return 0;
+ }
+ return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
+ });
+
+ const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({
+ chartHeight: height,
+ chartWidth: width,
+ legendItems: legendData,
+ legendMargin,
+ orientation: legendOrientation,
+ show: showLegend,
+ theme,
+ type: legendType,
+ });
+ const legendPadding = getChartPadding(
+ showLegend,
+ legendOrientation,
+ effectiveLegendMargin,
+ );
+
+ const dataZoom = zoomable
+ ? [
+ {
+ type: 'inside',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ },
+ {
+ type: 'slider',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ bottom: TIMESERIES_CONSTANTS.zoomBottom,
+ },
+ ]
+ : [];
+
+ const echartOptions: EChartsCoreOption = {
+ grid: {
+ ...defaultGrid,
+ top: theme.sizeUnit * 5 + legendPadding.top,
+ bottom:
+ theme.sizeUnit * (showXAxis ? 5 : 3) +
+ legendPadding.bottom +
+ convertInteger(xAxisTitleMargin) +
+ (zoomable ? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable : 0),
+ left:
+ theme.sizeUnit * (showYAxis ? 5 : 2) +
+ legendPadding.left +
+ convertInteger(yAxisTitleMargin),
+ right: theme.sizeUnit * 5 + legendPadding.right,
+ },
+ legend: {
+ ...getLegendProps(
+ effectiveLegendType,
+ legendOrientation,
+ showLegend,
+ theme,
+ zoomable,
+ legendState,
+ ),
+ data: legendData,
+ },
+ xAxis: {
+ show: showXAxis,
+ type: AxisType.Category,
+ data: xLabels,
+ name: xAxisTitle,
+ nameGap: convertInteger(xAxisTitleMargin),
+ nameLocation: 'middle',
+ axisLabel: {
+ rotate: xAxisLabelRotation,
+ interval: xAxisLabelInterval === 'auto' ? 'auto' : 0,
+ hideOverlap: true,
+ },
+ },
+ yAxis: {
+ ...defaultYAxis,
+ show: showYAxis,
+ type: AxisType.Value,
+ name: yAxisTitle,
+ nameGap: convertInteger(yAxisTitleMargin),
+ nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
+ axisLabel: { formatter: yAxisFormatter },
+ },
+ tooltip: {
+ ...getDefaultTooltip(refs),
+ trigger: 'item',
Review Comment:
With `trigger: 'item'`, ECharts invokes this formatter for only the hovered series, so it can never combine a candle's OHLC values with its moving-average rows; the current test supplies an array that this configuration does not produce. Should this use the axis-triggered tooltip contract (or stop promising the combined MA row)?
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5787037198
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4051014706
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,729 @@
+/**
+ * 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 {
+ AxisType,
+ CategoricalColorNamespace,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { CustomSeriesOption, CustomSeriesRenderItem } from 'echarts';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+ OHLC_TICK_WIDTH_RATIO,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+type AxisTooltipParams = CallbackDataParams & {
+ axisValue?: string | number;
+ axisValueLabel?: string;
+};
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(ohlc: OhlcValue | null): CandlestickDatum {
+ return ohlc ?? [];
+}
+
+function toOhlcBarDatum(
+ ohlc: OhlcValue | null,
+ categoryIndex: number,
+ color: string,
+) {
+ if (!ohlc) {
+ return [];
+ }
+ return {
+ value: [categoryIndex, ...ohlc],
+ itemStyle: {
+ color,
+ },
+ };
+}
+
+function getDirectionItemStyle(increaseHex: string, decreaseHex: string) {
+ return {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ };
+}
+
+function getSeriesItemStyle(seriesColor: string, hollowFill: string) {
+ return {
+ color: seriesColor,
+ color0: hollowFill,
+ borderColor: seriesColor,
+ borderColor0: seriesColor,
+ };
+}
+
+const renderOhlcItem: CustomSeriesRenderItem = (_params, item) => {
+ const x = toNumber(item.value(0));
+ const open = toNumber(item.value(1));
+ const close = toNumber(item.value(2));
+ const low = toNumber(item.value(3));
+ const high = toNumber(item.value(4));
+ if (
+ x === null ||
+ open === null ||
+ close === null ||
+ low === null ||
+ high === null
+ ) {
+ return null;
+ }
+
+ const openPoint = item.coord([x, open]);
+ const closePoint = item.coord([x, close]);
+ const lowPoint = item.coord([x, low]);
+ const highPoint = item.coord([x, high]);
+ const categorySize = item.size?.([1, 0]);
+ const categoryWidth = Array.isArray(categorySize)
+ ? categorySize[0]
+ : categorySize;
+ if (categoryWidth == null || !Number.isFinite(categoryWidth)) {
+ return null;
+ }
+ const halfWidth = categoryWidth * OHLC_TICK_WIDTH_RATIO;
+ const style = item.style({
+ stroke: item.visual('color'),
+ });
+
+ return {
+ type: 'group',
+ children: [
+ {
+ type: 'line',
+ shape: {
+ x1: lowPoint[0],
+ y1: lowPoint[1],
+ x2: highPoint[0],
+ y2: highPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: openPoint[0],
+ y1: openPoint[1],
+ x2: openPoint[0] - halfWidth,
+ y2: openPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: closePoint[0],
+ y1: closePoint[1],
+ x2: closePoint[0] + halfWidth,
+ y2: closePoint[1],
+ },
+ style,
+ },
+ ],
+ };
+};
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function extractLineValue(item: CallbackDataParams): number | null {
+ const raw = item.value ?? item.data;
+ if (Array.isArray(raw)) {
+ const y = Number(raw[raw.length - 1]);
+ return Number.isFinite(y) ? y : null;
+ }
+ const numeric = Number(raw);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function appendOhlcRows(
+ rows: string[][],
+ ohlc: OhlcValue,
+ numberFormatter: NumberFormatter | CurrencyFormatter,
+) {
+ const [open, close, low, high] = ohlc;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+ showSeriesName,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+ showSeriesName: boolean;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candles = params.flatMap(item => {
+ const ohlc = extractOhlc(item.value ?? item.data);
+ return ohlc ? [{ item, ohlc }] : [];
+ });
+
+ candles.forEach(({ item, ohlc }) => {
+ const [open, close] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ if (showSeriesName) {
+ const seriesLabel = String(item.seriesName ?? '');
+ if (seriesLabel) {
+ rows.push([`${seriesLabel} (${direction})`]);
+ }
+ } else {
+ heading = title ? `${title} (${direction})` : direction;
+ }
+ appendOhlcRows(rows, ohlc, numberFormatter);
+ });
+
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = extractLineValue(item);
+ if (value === null) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: _echartOptions, ...formData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ candlestickSeriesName,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ seriesStyle,
+ colorScheme,
+ sliceId,
+ colorByDirection,
+ }: EchartsCandlestickFormData = { ...DEFAULT_FORM_DATA, ...formData };
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const defaultSeriesLabel =
+ candlestickSeriesName?.trim() || CANDLESTICK_SERIES_NAME;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const colorScale = CategoricalColorNamespace.getScale(colorScheme);
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: LookupKey[] = [];
+ const xLabels: string[] = [];
+ const xRecords: DataRecord[] = [];
+ const xKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, xAxisName));
+ if (xKeySet.has(key)) {
+ return;
+ }
+ xKeySet.add(key);
+ xKeys.push(key);
+ xRecords.push(datum);
+ xLabels.push(
+ getOwnValue(coltypeMapping, xAxisName) === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter: axisTimeFormatter,
+ })
+ : extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ }),
+ );
+ });
+
+ const seriesKeys: LookupKey[] = [];
+ const seriesNames: string[] = [];
+ if (seriesName) {
+ const seriesKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, seriesName));
+ if (seriesKeySet.has(key)) {
+ return;
+ }
+ seriesKeySet.add(key);
+ seriesKeys.push(key);
+ seriesNames.push(
+ extractGroupbyLabel({
+ datum,
+ groupby: [seriesName],
+ coltypeMapping,
+ }),
+ );
+ });
+ } else {
+ seriesKeys.push(CANDLESTICK_SERIES_NAME);
+ seriesNames.push(defaultSeriesLabel);
+ }
+
+ const useSeriesColors = seriesNames.length > 1 || colorByDirection === false;
+
+ const recordsBySeriesAndX = new Map<LookupKey, Map<LookupKey, DataRecord>>();
+ data.forEach(datum => {
+ const xKey = toLookupKey(getOwnValue(datum, xAxisName));
+ const seriesKey = seriesName
+ ? toLookupKey(getOwnValue(datum, seriesName))
+ : CANDLESTICK_SERIES_NAME;
+ let byX = recordsBySeriesAndX.get(seriesKey);
+ if (!byX) {
+ byX = new Map();
+ recordsBySeriesAndX.set(seriesKey, byX);
+ }
+ byX.set(xKey, datum);
+ });
+
+ const ohlcBySeries: (OhlcValue | null)[][] = seriesKeys.map(key =>
+ xKeys.map(xKey => {
+ const datum = recordsBySeriesAndX.get(key)?.get(xKey);
+ return datum
+ ? getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel)
+ : null;
+ }),
+ );
+
+ const priceSeries: (CandlestickSeriesOption | CustomSeriesOption)[] =
+ ohlcBySeries.map((ohlcData, index) => {
+ const name = seriesNames[index];
+ const seriesColor = useSeriesColors
+ ? colorScale(name, sliceId)
+ : increaseHex;
+ if (seriesStyle === 'ohlc') {
+ return {
+ name,
+ type: 'custom',
+ renderItem: renderOhlcItem,
+ dimensions: ['-', 'open', 'close', 'low', 'high'],
+ encode: {
+ x: 0,
+ y: [1, 2, 3, 4],
+ tooltip: [1, 2, 3, 4],
+ },
+ itemStyle: {
+ color: seriesColor,
+ },
+ data: ohlcData.map((ohlc, categoryIndex) => {
+ if (!ohlc) {
+ return [];
+ }
+ const [openValue, closeValue] = ohlc;
+ const color = useSeriesColors
+ ? seriesColor
+ : closeValue >= openValue
+ ? increaseHex
+ : decreaseHex;
+ return toOhlcBarDatum(ohlc, categoryIndex, color);
+ }),
+ };
+ }
+ return {
+ name,
+ type: 'candlestick',
+ data: ohlcData.map(toCandlestickDatum),
+ itemStyle: useSeriesColors
+ ? getSeriesItemStyle(seriesColor, theme.colorBgContainer)
+ : getDirectionItemStyle(increaseHex, decreaseHex),
+ };
+ });
+
+ const periods = parseMovingAveragePeriods(movingAverages);
+ const qualifyMaNames = seriesNames.length > 1;
+ const movingAverageSeries: LineSeriesOption[] = ohlcBySeries.flatMap(
+ (ohlcData, index) => {
+ const closes = ohlcData.map(ohlc => ohlc?.[1] ?? null);
+ const seriesLabel = qualifyMaNames ? seriesNames[index] : undefined;
+ const seriesColor = useSeriesColors
+ ? colorScale(seriesNames[index], sliceId)
+ : undefined;
+ return periods.map(period => ({
+ name: movingAverageName(period, seriesLabel),
+ type: 'line' as const,
+ data: calculateMA(closes, period),
+ smooth: true,
+ showSymbol: false,
+ itemStyle: seriesColor ? { color: seriesColor } : undefined,
+ lineStyle: {
+ opacity: MA_LINE_OPACITY,
+ ...(seriesColor ? { color: seriesColor } : {}),
+ },
+ }));
+ },
+ );
+
+ const legendData = [
+ ...seriesNames,
+ ...movingAverageSeries.map(series => String(series.name)),
+ ].sort((a, b) => {
+ if (!legendSort) {
+ return 0;
+ }
+ return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
+ });
+
+ const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({
+ chartHeight: height,
+ chartWidth: width,
+ legendItems: legendData,
+ legendMargin,
+ orientation: legendOrientation,
+ show: showLegend,
+ theme,
+ type: legendType,
+ });
+ const legendPadding = getChartPadding(
+ showLegend,
+ legendOrientation,
+ effectiveLegendMargin,
+ );
+
+ const dataZoom = zoomable
+ ? [
+ {
+ type: 'inside',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ },
+ {
+ type: 'slider',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ bottom: TIMESERIES_CONSTANTS.zoomBottom,
+ },
+ ]
+ : [];
+
+ const echartOptions: EChartsCoreOption = {
+ grid: {
+ ...defaultGrid,
+ top: theme.sizeUnit * 5 + legendPadding.top,
+ bottom:
+ theme.sizeUnit * (showXAxis ? 5 : 3) +
+ legendPadding.bottom +
+ convertInteger(xAxisTitleMargin) +
+ (zoomable ? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable : 0),
+ left:
+ theme.sizeUnit * (showYAxis ? 5 : 2) +
+ legendPadding.left +
+ convertInteger(yAxisTitleMargin),
+ right: theme.sizeUnit * 5 + legendPadding.right,
+ },
Review Comment:
fix in [6686525](https://github.com/apache/superset/pull/43863/commits/6686525c35eff32991b6bac6aa4ff8c14d27ca4b)
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4094338533
##########
superset/translations/cs/LC_MESSAGES/messages.po:
##########
@@ -9043,6 +9095,12 @@ msgstr "Jednotka velikosti popisku"
msgid "Label threshold"
msgstr "Prahová hodnota popisku"
+msgid "Label used for decreasing candles in the tooltip."
+msgstr "Popisek klesajících svíček v popisku."
+
+msgid "Label used for increasing candles in the tooltip."
+msgstr "Popisek rostoucích svíček v popisku."
Review Comment:
fix in [c0dcb9c](https://github.com/apache/superset/pull/43863/commits/c0dcb9cf604c5fe18d1c4a4ff3920af65c95ae2b)
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4094339068
##########
superset/translations/cs/LC_MESSAGES/messages.po:
##########
@@ -13848,10 +13945,16 @@ msgstr "Nastavení zvýšení série"
msgid "Series limit"
msgstr "Limit sérií"
+msgid "Series name"
+msgstr "Název řady"
+
#, fuzzy
msgid "Series settings"
msgstr "Nastavení sérií"
+msgid "Series style"
+msgstr "Styl řady"
Review Comment:
<!-- Bito Reply -->
The suggestion is appropriate because it improves consistency across the translation file. Aligning the new strings with the existing terminology used for other series-related settings ensures a uniform user experience within the control family.
**superset/translations/cs/LC_MESSAGES/messages.po**
```
msgid "Series name"
msgstr "Název série"
msgid "Series style"
msgstr "Styl série"
```
##########
superset/translations/cs/LC_MESSAGES/messages.po:
##########
@@ -9043,6 +9095,12 @@ msgstr "Jednotka velikosti popisku"
msgid "Label threshold"
msgstr "Prahová hodnota popisku"
+msgid "Label used for decreasing candles in the tooltip."
+msgstr "Popisek klesajících svíček v popisku."
+
+msgid "Label used for increasing candles in the tooltip."
+msgstr "Popisek rostoucích svíček v popisku."
Review Comment:
<!-- Bito Reply -->
The suggestion is appropriate and improves consistency. Using "v tooltipu" aligns with the terminology used elsewhere in the translation catalog, avoiding the collision where "popisku" was used for both label and tooltip.
**superset/translations/cs/LC_MESSAGES/messages.po**
```
+msgid "Label used for decreasing candles in the tooltip."
+msgstr "Popisek klesajících svíček v tooltipu."
+
+msgid "Label used for increasing candles in the tooltip."
+msgstr "Popisek rostoucích svíček v tooltipu."
```
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4094345374
##########
superset/translations/cs/LC_MESSAGES/messages.po:
##########
@@ -9043,6 +9095,12 @@ msgstr "Jednotka velikosti popisku"
msgid "Label threshold"
msgstr "Prahová hodnota popisku"
+msgid "Label used for decreasing candles in the tooltip."
+msgstr "Popisek klesajících svíček v popisku."
Review Comment:
<!-- Bito Reply -->
The suggestion to use "v tooltipu" instead of "v popisku" is appropriate. In the context of the translation string, "Popisek" refers to the label, and using "popisku" for the tooltip creates a word collision that makes the sentence ambiguous. Using "v tooltipu" clearly distinguishes the tooltip from the label, improving the clarity of the translation.
**superset/translations/cs/LC_MESSAGES/messages.po**
```
+msgid "Label used for decreasing candles in the tooltip."
+msgstr "Popisek klesajících svíček v tooltipu."
```
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4086711049
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/EchartsCandlestick.tsx:
##########
@@ -0,0 +1,167 @@
+/**
+ * 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 { useRef } from 'react';
+import {
+ BinaryQueryObjectFilterClause,
+ TimeGranularity,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import Echart from '../components/Echart';
+import { EchartsHandler, EventHandlers } from '../types';
+import { CandlestickChartTransformedProps } from './types';
+
+type ContextMenuEvent = {
+ event?: { stop?: () => void; event?: PointerEvent };
+ dataIndex?: number;
+ seriesName?: string;
+ seriesType?: string;
+};
+
+function toFilterValue(value: unknown): string | number | boolean | null {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof Date) {
+ return value.valueOf();
+ }
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+ return String(value);
+}
+
+function toFilterClause(
+ col: string,
+ value: unknown,
+ formattedVal: string,
+ grain?: TimeGranularity,
+): BinaryQueryObjectFilterClause {
+ return {
+ col,
+ op: '==',
+ val: toFilterValue(value),
Review Comment:
fix in [cd29b4c](https://github.com/apache/superset/pull/43863/commits/cd29b4cecc1f485a53f9836ade7c8b1322c07469)
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4094336441
##########
superset/translations/cs/LC_MESSAGES/messages.po:
##########
@@ -13848,10 +13945,16 @@ msgstr "Nastavení zvýšení série"
msgid "Series limit"
msgstr "Limit sérií"
+msgid "Series name"
+msgstr "Název řady"
+
#, fuzzy
msgid "Series settings"
msgstr "Nastavení sérií"
+msgid "Series style"
+msgstr "Styl řady"
Review Comment:
fix in [c0dcb9c](https://github.com/apache/superset/pull/43863/commits/c0dcb9cf604c5fe18d1c4a4ff3920af65c95ae2b)
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5813090055
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4093431992
##########
superset/translations/sr_Latn/LC_MESSAGES/messages.po:
##########
@@ -13343,6 +13432,13 @@ msgstr ""
"kao vaš vremenski opseg, i koristite \"Prilagođeno\" da podesite "
"prilagođeni opseg za poređenje."
+msgid ""
+"Overlay simple moving averages of the close price (MA5, MA10, MA15, ...)."
+" Type a custom period to add it."
+msgstr ""
+"Preklopite proste pokretne prosekе cene zatvaranja (MA5, MA10, MA15, ...). "
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>CWE-451: Cyrillic Homoglyph In Latin Locale</b></div>
<div id="fix">
Line 13439 contains a Cyrillic 'е' (U+0435) inside the Latin-script word 'prosekе' ('proseci' translation). sr_Latn must be Latin script; the homoglyph is invisible to reviewers, breaks search/copy of the rendered label, and signals unreviewed machine translation. Replace it with Latin 'e'. ([CWE-451](https://cwe.mitre.org/data/definitions/451.html))
</div>
</div>
<small><i>Code Review Run #457542</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/translations/ta/LC_MESSAGES/messages.po:
##########
@@ -9916,6 +9986,13 @@ msgstr "இலக்கு நோட்களின் பெயர்"
msgid "Name of your tag"
msgstr "உங்கள் குறிச்சொல்லின் பெயர்"
+msgid ""
+"Name used for the candlestick series in the legend and tooltip when no "
+"series dimension is set."
+msgstr ""
+"தொடர் பரிமாணம் அமைக்கப்படாதபோது புராணத்திலும் உதவிக்குறிப்பிலும் பயன்படும் "
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>CWE-1007: Legend Mistranslated As Mythology</b></div>
<div id="fix">
The `legend` in "Name used for the candlestick series in the legend and tooltip..." is translated as "புராணத்திலும்" (mythology). Elsewhere this file consistently renders legend as "விளக்கக் குறிப்பிலும்" (e.g. "Additional padding for legend.", "Show legend" entries), and "புராண" occurs nowhere else. This is a wrong-word mistranslation users will see in the chart legend/tooltip. ([CWE-1007](https://cwe.mitre.org/data/definitions/1007.html))
</div>
</div>
<small><i>Code Review Run #457542</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/translations/ca/LC_MESSAGES/messages.po:
##########
@@ -9393,6 +9446,12 @@ msgstr "Línia d'Etiqueta"
msgid "Label threshold"
msgstr "Llindar d'etiqueta"
+msgid "Label used for decreasing candles in the tooltip."
+msgstr "Etiqueta de les veles baixistes al consell emergent."
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Tooltip term inconsistency</b></div>
<div id="fix">
This entry translates 'tooltip' as 'consell emergent', but the file's established convention keeps 'tooltip' untranslated (10 msgstrs, e.g. 'Si mostrar el valor total al tooltip'). This diff introduces all 3 'consell emergent' occurrences. Use 'tooltip' here for terminology consistency.
</div>
<details>
<summary>
<b>Code suggestion</b>
</summary>
<blockquote>Check the AI-generated fix before applying</blockquote>
<div id="code">
````suggestion
msgstr "Etiqueta de les veles baixistes al tooltip."
````
</div>
</details>
</div>
<small><i>Code Review Run #457542</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/translations/ca/LC_MESSAGES/messages.po:
##########
@@ -11536,6 +11626,13 @@ msgstr ""
"mateixa longitud que el teu rang de temps i usa \"Personalitzat\" per "
"establir un rang de comparació personalitzat."
+msgid ""
+"Overlay simple moving averages of the close price (MA5, MA10, MA15, ...)."
+" Type a custom period to add it."
+msgstr ""
+"Superposa mitjanes mòbils simples del preu de tancament (MA5, MA10, MA15, "
+"...). Escriviu un període personalitzat per afegir-lo."
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Formal register inconsistency</b></div>
<div id="fix">
This entry uses the formal-plural imperative 'Escriviu', but the file's dominant register is informal singular (117 informal hits like 'el teu rang de temps' at line 11626 in the adjacent entry vs 6 formal-plural verbs). Use 'Escriu' for register consistency.
</div>
<details>
<summary>
<b>Code suggestion</b>
</summary>
<blockquote>Check the AI-generated fix before applying</blockquote>
<div id="code">
````suggestion
"...). Escriu un període personalitzat per afegir-lo."
````
</div>
</details>
</div>
<small><i>Code Review Run #457542</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/translations/cs/LC_MESSAGES/messages.po:
##########
@@ -9043,6 +9095,12 @@ msgstr "Jednotka velikosti popisku"
msgid "Label threshold"
msgstr "Prahová hodnota popisku"
+msgid "Label used for decreasing candles in the tooltip."
+msgstr "Popisek klesajících svíček v popisku."
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Label/tooltip word collision</b></div>
<div id="fix">
"Popisek klesajících svíček v popisku." uses "popisku" for tooltip while "Popisek" in the same sentence means label, so the string reads as "label ... in the label". The catalog renders tooltip as "tooltipu" elsewhere (e.g. line 4333 "Sloupce k zobrazení v tooltipu."). Use "v tooltipu" to remove the collision.
</div>
</div>
<small><i>Code Review Run #457542</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/translations/zh_TW/LC_MESSAGES/messages.po:
##########
@@ -1046,6 +1046,12 @@ msgid ""
" bars that extend left and right from a central axis."
msgstr ""
+msgid ""
+"A candlestick chart shows opening, closing, high, and low values for each"
+" period. It is commonly used for financial and other time-series range "
+"analysis."
+msgstr "K線圖顯示每個週期的開盤、收盤、最高和最低值。常用於金融及其他時間序列區間分析。"
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>K線 term spacing inconsistent</b></div>
<div id="fix">
The same candlestick term is written two ways in this diff: "K線" (lines 1053, 3434, 3437, 3442) vs "K 線" (9817, 9820, 11181, 12020) — file-wide grep counts 4 vs 4 occurrences, all introduced here. Users will see inconsistent labels for one concept; line 20399 adds a third register (陽線/陰線 vs 上漲/下跌 K 線). Pick one form (suggest "K線", matching the chart names) and apply it to all candlestick strings.
</div>
</div>
<small><i>Code Review Run #457542</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/translations/ca/LC_MESSAGES/messages.po:
##########
@@ -9393,6 +9446,12 @@ msgstr "Línia d'Etiqueta"
msgid "Label threshold"
msgstr "Llindar d'etiqueta"
+msgid "Label used for decreasing candles in the tooltip."
+msgstr "Etiqueta de les veles baixistes al consell emergent."
+
+msgid "Label used for increasing candles in the tooltip."
+msgstr "Etiqueta de les veles alcistes al consell emergent."
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Tooltip term inconsistency</b></div>
<div id="fix">
Same terminology inconsistency as line 9450: 'consell emergent' vs the established 'tooltip' convention (10 msgstrs). This is the second of three instances introduced by this diff. Use 'tooltip' for consistency.
</div>
<details>
<summary>
<b>Code suggestion</b>
</summary>
<blockquote>Check the AI-generated fix before applying</blockquote>
<div id="code">
````suggestion
msgstr "Etiqueta de les veles alcistes al tooltip."
````
</div>
</details>
</div>
<small><i>Code Review Run #457542</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/translations/ca/LC_MESSAGES/messages.po:
##########
@@ -10634,6 +10702,13 @@ msgstr "Nom dels nodes destí"
msgid "Name of your tag"
msgstr "Nom de la teva etiqueta"
+msgid ""
+"Name used for the candlestick series in the legend and tooltip when no "
+"series dimension is set."
+msgstr ""
+"Nom de la sèrie de candlestick a la llegenda i al consell emergent quan no "
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Tooltip term inconsistency</b></div>
<div id="fix">
Third instance of 'consell emergent' vs the established 'tooltip' convention (10 msgstrs). This entry describes the legend and tooltip together; using divergent terms for 'tooltip' here compounds the inconsistency from lines 9450/9453.
</div>
<details>
<summary>
<b>Code suggestion</b>
</summary>
<blockquote>Check the AI-generated fix before applying</blockquote>
<div id="code">
````suggestion
"Nom de la sèrie de candlestick a la llegenda i al tooltip quan no "
````
</div>
</details>
</div>
<small><i>Code Review Run #457542</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/translations/ca/LC_MESSAGES/messages.po:
##########
@@ -19577,6 +19693,15 @@ msgstr ""
"Quan està marcat, el mapa farà zoom a les teves dades després de cada "
"consulta"
+msgid ""
+"When enabled, increasing candles use the increase color and decreasing "
+"candles use the decrease color. Turn off to use one series color, with a "
+"filled body for increases and a hollow body for decreases."
+msgstr ""
+"Si està activat, les veles alcistes usen el color de pujada i les "
+"baixistes el de baixada. Desactiveu-ho per usar un sol color de sèrie, amb "
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Color label terminology mismatch</b></div>
<div id="fix">
'color de pujada'/'color de baixada' don't match the established control labels 'Increase color'→'Augment' (line 8941) and 'Decrease color'→'Disminuir' (line 5763) that this text references. Also 'Desactiveu-ho' is formal plural vs the file's informal register. Align both.
</div>
<details>
<summary>
<b>Code suggestion</b>
</summary>
<blockquote>Check the AI-generated fix before applying</blockquote>
<div id="code">
````suggestion
"Si està activat, les veles alcistes usen el color d'augment i les "
"baixistes el de disminució. Desactiva-ho per usar un sol color de sèrie, amb "
````
</div>
</details>
</div>
<small><i>Code Review Run #457542</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/translations/cs/LC_MESSAGES/messages.po:
##########
@@ -9043,6 +9095,12 @@ msgstr "Jednotka velikosti popisku"
msgid "Label threshold"
msgstr "Prahová hodnota popisku"
+msgid "Label used for decreasing candles in the tooltip."
+msgstr "Popisek klesajících svíček v popisku."
+
+msgid "Label used for increasing candles in the tooltip."
+msgstr "Popisek rostoucích svíček v popisku."
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Label/tooltip word collision</b></div>
<div id="fix">
Same collision as line 9099: "Popisek rostoucích svíček v popisku." uses "popisku" for tooltip while "Popisek" means label, and the catalog elsewhere renders tooltip as "tooltipu" (line 4333). Use "v tooltipu" for consistency and clarity.
</div>
</div>
<small><i>Code Review Run #457542</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/translations/cs/LC_MESSAGES/messages.po:
##########
@@ -13848,10 +13945,16 @@ msgstr "Nastavení zvýšení série"
msgid "Series limit"
msgstr "Limit sérií"
+msgid "Series name"
+msgstr "Název řady"
+
#, fuzzy
msgid "Series settings"
msgstr "Nastavení sérií"
+msgid "Series style"
+msgstr "Styl řady"
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Inconsistent series term</b></div>
<div id="fix">
"Series style" is translated "Styl řady" while sibling msgid `Series Style` (line 13931) is "Styl série"; the new `Series colors` (13937) and `Series name` (13949) likewise use "řad/řady" against 11 sibling `Series*` entries that use "série" (grep: 51 "séri" lines vs 17 "řad" tokens, most of the latter being the fixed phrase "časových řad"). The same control family will render both terms. Align the three new strings on "série".
</div>
</div>
<small><i>Code Review Run #457542</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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3972708183
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,551 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: _echartOptions, ...formData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ candlestickSeriesName,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ }: EchartsCandlestickFormData = { ...DEFAULT_FORM_DATA, ...formData };
+
+ const xAxisName = xAxis ? getColumnLabel(xAxis) : '';
+ const seriesColumns = ensureIsArray(seriesControl).map(getColumnLabel);
+ const [seriesName] = seriesColumns;
+ const defaultSeriesLabel =
+ candlestickSeriesName?.trim() || CANDLESTICK_SERIES_NAME;
+ const openLabel = open ? getMetricLabel(open) : '';
+ const closeLabel = close ? getMetricLabel(close) : '';
+ const highLabel = high ? getMetricLabel(high) : '';
+ const lowLabel = low ? getMetricLabel(low) : '';
+ const timeFormatter = getTimeFormatter(tooltipTimeFormat || xAxisTimeFormat);
+ const axisTimeFormatter = getTimeFormatter(xAxisTimeFormat);
+ const numberFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: tooltipValuesFormat || yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(tooltipValuesFormat || yAxisFormat);
+ const yAxisFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({
+ d3Format: yAxisFormat,
+ currency: currencyFormat,
+ })
+ : getNumberFormatter(yAxisFormat);
+
+ const increaseHex = rgbToHex(
+ increaseColor.r,
+ increaseColor.g,
+ increaseColor.b,
+ );
+ const decreaseHex = rgbToHex(
+ decreaseColor.r,
+ decreaseColor.g,
+ decreaseColor.b,
+ );
+ const upLabel = increaseLabel || DIRECTION_LABELS.INCREASE;
+ const downLabel = decreaseLabel || DIRECTION_LABELS.DECREASE;
+
+ const xKeys: LookupKey[] = [];
+ const xLabels: string[] = [];
+ const xRecords: DataRecord[] = [];
+ const xKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, xAxisName));
+ if (xKeySet.has(key)) {
+ return;
+ }
+ xKeySet.add(key);
+ xKeys.push(key);
+ xRecords.push(datum);
+ xLabels.push(
+ getOwnValue(coltypeMapping, xAxisName) === GenericDataType.Temporal
+ ? extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ timeFormatter: axisTimeFormatter,
+ })
+ : extractGroupbyLabel({
+ datum,
+ groupby: [xAxisName],
+ coltypeMapping,
+ }),
+ );
+ });
+
+ const seriesKeys: LookupKey[] = [];
+ const seriesNames: string[] = [];
+ if (seriesName) {
+ const seriesKeySet = new Set<LookupKey>();
+ data.forEach(datum => {
+ const key = toLookupKey(getOwnValue(datum, seriesName));
+ if (seriesKeySet.has(key)) {
+ return;
+ }
+ seriesKeySet.add(key);
+ seriesKeys.push(key);
+ seriesNames.push(
+ extractGroupbyLabel({
+ datum,
+ groupby: [seriesName],
+ coltypeMapping,
+ }),
+ );
+ });
+ } else {
+ seriesKeys.push(CANDLESTICK_SERIES_NAME);
+ seriesNames.push(defaultSeriesLabel);
+ }
+
+ const recordsBySeriesAndX = new Map<LookupKey, Map<LookupKey, DataRecord>>();
+ data.forEach(datum => {
+ const xKey = toLookupKey(getOwnValue(datum, xAxisName));
+ const seriesKey = seriesName
+ ? toLookupKey(getOwnValue(datum, seriesName))
+ : CANDLESTICK_SERIES_NAME;
+ let byX = recordsBySeriesAndX.get(seriesKey);
+ if (!byX) {
+ byX = new Map();
+ recordsBySeriesAndX.set(seriesKey, byX);
+ }
+ byX.set(xKey, datum);
+ });
+
+ const candlestickSeries: CandlestickSeriesOption[] = seriesKeys.map(
+ (key, index) => ({
+ name: seriesNames[index],
+ type: 'candlestick',
+ data: xKeys.map(xKey =>
+ toCandlestickDatum(
+ recordsBySeriesAndX.get(key)?.get(xKey),
+ openLabel,
+ closeLabel,
+ lowLabel,
+ highLabel,
+ ),
+ ),
+ itemStyle: {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ },
+ }),
+ );
+
+ const periods = parseMovingAveragePeriods(movingAverages);
+ const qualifyMaNames = seriesNames.length > 1;
+ const movingAverageSeries: LineSeriesOption[] = candlestickSeries.flatMap(
+ candle => {
+ const closes = (candle.data ?? []).map(item =>
+ Array.isArray(item) && Number.isFinite(Number(item[1]))
+ ? Number(item[1])
+ : null,
+ );
+ const seriesLabel = qualifyMaNames ? String(candle.name) : undefined;
+ return periods.map(period => ({
+ name: movingAverageName(period, seriesLabel),
+ type: 'line' as const,
+ data: calculateMA(closes, period),
+ smooth: true,
+ showSymbol: false,
+ lineStyle: {
+ opacity: MA_LINE_OPACITY,
+ },
+ }));
+ },
+ );
+
+ const legendData = [
+ ...seriesNames,
+ ...movingAverageSeries.map(series => String(series.name)),
+ ].sort((a, b) => {
+ if (!legendSort) {
+ return 0;
+ }
+ return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
+ });
+
+ const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({
+ chartHeight: height,
+ chartWidth: width,
+ legendItems: legendData,
+ legendMargin,
+ orientation: legendOrientation,
+ show: showLegend,
+ theme,
+ type: legendType,
+ });
+ const legendPadding = getChartPadding(
+ showLegend,
+ legendOrientation,
+ effectiveLegendMargin,
+ );
+
+ const dataZoom = zoomable
+ ? [
+ {
+ type: 'inside',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ },
+ {
+ type: 'slider',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ bottom: TIMESERIES_CONSTANTS.zoomBottom,
+ },
+ ]
+ : [];
+
+ const echartOptions: EChartsCoreOption = {
+ grid: {
+ ...defaultGrid,
+ top: theme.sizeUnit * 5 + legendPadding.top,
+ bottom:
+ theme.sizeUnit * (showXAxis ? 5 : 3) +
+ legendPadding.bottom +
+ convertInteger(xAxisTitleMargin) +
+ (zoomable ? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable : 0),
+ left:
+ theme.sizeUnit * (showYAxis ? 5 : 2) +
+ legendPadding.left +
+ convertInteger(yAxisTitleMargin),
+ right: theme.sizeUnit * 5 + legendPadding.right,
+ },
+ legend: {
+ ...getLegendProps(
+ effectiveLegendType,
+ legendOrientation,
+ showLegend,
+ theme,
+ zoomable,
+ legendState,
+ ),
+ data: legendData,
+ },
+ xAxis: {
+ show: showXAxis,
+ type: AxisType.Category,
+ data: xLabels,
+ name: xAxisTitle,
+ nameGap: convertInteger(xAxisTitleMargin),
+ nameLocation: 'middle',
+ axisLabel: {
+ rotate: xAxisLabelRotation,
+ interval: xAxisLabelInterval === 'auto' ? 'auto' : 0,
+ hideOverlap: true,
+ },
+ },
+ yAxis: {
+ ...defaultYAxis,
+ show: showYAxis,
+ type: AxisType.Value,
+ name: yAxisTitle,
+ nameGap: convertInteger(yAxisTitleMargin),
+ nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
+ axisLabel: { formatter: yAxisFormatter },
+ },
+ tooltip: {
+ ...getDefaultTooltip(refs),
+ trigger: 'item',
Review Comment:
The axis trigger tooltip has been restored; it now combines candlestick chart data with the moving average.
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r3941129397
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,533 @@
+/**
+ * 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 {
+ AxisType,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getChartPadding,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type OhlcValue = [number, number, number, number];
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toCategoryKey(value: unknown): string {
+ return value == null ? NULL_STRING : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(
+ datum: DataRecord | undefined,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): CandlestickDatum {
+ if (!datum) {
+ return [];
+ }
+ return getOhlc(datum, openLabel, closeLabel, lowLabel, highLabel) ?? [];
+}
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ let heading = title;
+ const candle = params.find(item => extractOhlc(item.value ?? item.data));
+ if (candle) {
+ const ohlc = extractOhlc(candle.value ?? candle.data);
+ if (ohlc) {
+ const [open, close, low, high] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ heading = title ? `${title} (${direction})` : direction;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+ }
+ }
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = Number(item.value);
+ if (!Number.isFinite(value)) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, heading);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: customEchartOptionsInput, ...rawFormData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+ const formData = {
+ ...DEFAULT_FORM_DATA,
+ ...rawFormData,
+ };
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ xAxis,
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
Review Comment:
The conversion is already taking place in superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts; I replaced it with formData in [134e138](https://github.com/apache/superset/pull/43863/commits/134e13832628b61a7136f027f1083b70a5293870):
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4052900328
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/controlPanel.tsx:
##########
@@ -0,0 +1,427 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t } from '@apache-superset/core/translation';
+import {
+ ensureIsArray,
+ getColumnLabel,
+ QueryFormColumn,
+ validateNonEmpty,
+} from '@superset-ui/core';
+import {
+ ControlPanelConfig,
+ ControlPanelsContainerProps,
+ ControlSubSectionHeader,
+ D3_TIME_FORMAT_DOCS,
+ DEFAULT_TIME_FORMAT,
+ getStandardizedControls,
+ sections,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import {
+ legendSection,
+ tooltipTimeFormatControl,
+ tooltipValuesFormatControl,
+ xAxisLabelInterval,
+ xAxisLabelRotation,
+} from '../controls';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_INCREASE_COLOR,
+ DEFAULT_SERIES_STYLE,
+} from './constants';
+import { MOVING_AVERAGE_PERIODS } from './utils';
+
+function hasSeriesDimension({
+ controls,
+}: ControlPanelsContainerProps): boolean {
+ return ensureIsArray(controls?.series?.value).length > 0;
+}
+
+type QueryRow = Record<string, unknown>;
+type ChartQueryResponse = { data?: QueryRow[] };
+
+const uniqueSeriesCountByResponse = new WeakMap<
+ ChartQueryResponse,
+ Map<string, number>
+>();
+const hasMultipleSeriesByProps = new WeakMap<
+ ControlPanelsContainerProps,
+ boolean
+>();
+
+function getSeriesColumnLabel(
+ props: ControlPanelsContainerProps,
+): string | undefined {
+ const [series] = ensureIsArray(props.controls?.series?.value);
+ if (series == null) {
+ return undefined;
+ }
+ return getColumnLabel(series as QueryFormColumn);
+}
+
+function getChartQueryResponse(
+ props: ControlPanelsContainerProps,
+): ChartQueryResponse | undefined {
+ return (props as { chart?: { queriesResponse?: ChartQueryResponse[] } }).chart
+ ?.queriesResponse?.[0];
+}
+
+function countUniqueSeriesValues(
+ props: ControlPanelsContainerProps,
+): number | undefined {
+ const seriesColumn = getSeriesColumnLabel(props);
+ if (!seriesColumn) {
+ return undefined;
+ }
+ const queryResponse = getChartQueryResponse(props);
+ const rows = queryResponse?.data;
+ if (!queryResponse || !rows) {
+ return undefined;
+ }
+ let cachedByColumn = uniqueSeriesCountByResponse.get(queryResponse);
+ if (!cachedByColumn) {
+ cachedByColumn = new Map<string, number>();
+ uniqueSeriesCountByResponse.set(queryResponse, cachedByColumn);
+ }
+ const cachedCount = cachedByColumn.get(seriesColumn);
+ if (cachedCount !== undefined) {
+ return cachedCount;
+ }
+ const count = new Set(
+ rows.map(row =>
+ row[seriesColumn] == null ? null : String(row[seriesColumn]),
+ ),
+ ).size;
+ cachedByColumn.set(seriesColumn, count);
+ return count;
+}
+
+function hasMultipleSeries(props: ControlPanelsContainerProps): boolean {
+ const cached = hasMultipleSeriesByProps.get(props);
+ if (cached !== undefined) {
+ return cached;
+ }
+ const result = (countUniqueSeriesValues(props) ?? 0) > 1;
+ hasMultipleSeriesByProps.set(props, result);
+ return result;
Review Comment:
<div>
<div id="suggestion">
<div id="issue"><b>Redundant double memoization</b></div>
<div id="fix">
The `hasMultipleSeriesByProps` cache is redundant: `countUniqueSeriesValues` already memoizes per (response, seriesColumn) in `uniqueSeriesCountByResponse`, so this outer layer can only hit when the inner cache would. It adds module-level state whose validity silently depends on props identity (same props object ⇒ same `controls.series.value` and chart response) — an invariant nothing documents. Removing lines 64-67 and this wrapper keeps behavior identical with one cache to reason about.
</div>
</div>
<small><i>Code Review Run #96c21f</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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4052914974
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/controlPanel.tsx:
##########
@@ -0,0 +1,427 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t } from '@apache-superset/core/translation';
+import {
+ ensureIsArray,
+ getColumnLabel,
+ QueryFormColumn,
+ validateNonEmpty,
+} from '@superset-ui/core';
+import {
+ ControlPanelConfig,
+ ControlPanelsContainerProps,
+ ControlSubSectionHeader,
+ D3_TIME_FORMAT_DOCS,
+ DEFAULT_TIME_FORMAT,
+ getStandardizedControls,
+ sections,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import {
+ legendSection,
+ tooltipTimeFormatControl,
+ tooltipValuesFormatControl,
+ xAxisLabelInterval,
+ xAxisLabelRotation,
+} from '../controls';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_INCREASE_COLOR,
+ DEFAULT_SERIES_STYLE,
+} from './constants';
+import { MOVING_AVERAGE_PERIODS } from './utils';
+
+function hasSeriesDimension({
+ controls,
+}: ControlPanelsContainerProps): boolean {
+ return ensureIsArray(controls?.series?.value).length > 0;
+}
+
+type QueryRow = Record<string, unknown>;
+type ChartQueryResponse = { data?: QueryRow[] };
+
+const uniqueSeriesCountByResponse = new WeakMap<
+ ChartQueryResponse,
+ Map<string, number>
+>();
+const hasMultipleSeriesByProps = new WeakMap<
+ ControlPanelsContainerProps,
+ boolean
+>();
+
+function getSeriesColumnLabel(
+ props: ControlPanelsContainerProps,
+): string | undefined {
+ const [series] = ensureIsArray(props.controls?.series?.value);
+ if (series == null) {
+ return undefined;
+ }
+ return getColumnLabel(series as QueryFormColumn);
+}
+
+function getChartQueryResponse(
+ props: ControlPanelsContainerProps,
+): ChartQueryResponse | undefined {
+ return (props as { chart?: { queriesResponse?: ChartQueryResponse[] } }).chart
+ ?.queriesResponse?.[0];
+}
+
+function countUniqueSeriesValues(
+ props: ControlPanelsContainerProps,
+): number | undefined {
+ const seriesColumn = getSeriesColumnLabel(props);
+ if (!seriesColumn) {
+ return undefined;
+ }
+ const queryResponse = getChartQueryResponse(props);
+ const rows = queryResponse?.data;
+ if (!queryResponse || !rows) {
+ return undefined;
+ }
+ let cachedByColumn = uniqueSeriesCountByResponse.get(queryResponse);
+ if (!cachedByColumn) {
+ cachedByColumn = new Map<string, number>();
+ uniqueSeriesCountByResponse.set(queryResponse, cachedByColumn);
+ }
+ const cachedCount = cachedByColumn.get(seriesColumn);
+ if (cachedCount !== undefined) {
+ return cachedCount;
+ }
+ const count = new Set(
+ rows.map(row =>
+ row[seriesColumn] == null ? null : String(row[seriesColumn]),
+ ),
+ ).size;
+ cachedByColumn.set(seriesColumn, count);
+ return count;
+}
+
+function hasMultipleSeries(props: ControlPanelsContainerProps): boolean {
+ const cached = hasMultipleSeriesByProps.get(props);
+ if (cached !== undefined) {
+ return cached;
+ }
+ const result = (countUniqueSeriesValues(props) ?? 0) > 1;
+ hasMultipleSeriesByProps.set(props, result);
+ return result;
Review Comment:
fix in [912cd70](https://github.com/apache/superset/pull/43863/commits/912cd70867fb0e2bfcdb03ca8c083eae0ecfa3b9)
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #43863:
URL: https://github.com/apache/superset/pull/43863#issuecomment-5824632873
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
<details>
<summary>
Bito Usage Guide
</summary>
**Commands**
Type the following command in the pull request comment and save the comment.
- `/review` - Manually triggers an incremental AI Review.
- `/review full` - Manually triggers a full AI Review.
- `/pause` - Pauses automatic reviews on this pull request.
- `/resume` - Resumes automatic reviews.
- `/resolve` - Marks all Bito-posted review comments as resolved.
- `/abort` - Cancels all in-progress reviews.
Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
**Configuration**
This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
**Documentation & Help**
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
- <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
</details>
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(plugin-chart-echarts): add Candlestick Chart [superset]
Posted by "SBIN2010 (via GitHub)" <gi...@apache.org>.
SBIN2010 commented on code in PR #43863:
URL: https://github.com/apache/superset/pull/43863#discussion_r4112697745
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/controlPanel.tsx:
##########
@@ -0,0 +1,417 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t } from '@apache-superset/core/translation';
+import {
+ ensureIsArray,
+ getColumnLabel,
+ QueryFormColumn,
+ validateNonEmpty,
+} from '@superset-ui/core';
+import {
+ ControlPanelConfig,
+ ControlPanelsContainerProps,
+ ControlSubSectionHeader,
+ D3_TIME_FORMAT_DOCS,
+ DEFAULT_TIME_FORMAT,
+ getStandardizedControls,
+ sections,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import {
+ legendSection,
+ tooltipTimeFormatControl,
+ tooltipValuesFormatControl,
+ xAxisLabelInterval,
+ xAxisLabelRotation,
+} from '../controls';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_INCREASE_COLOR,
+ DEFAULT_SERIES_STYLE,
+} from './constants';
+import { MOVING_AVERAGE_PERIODS } from './utils';
+
+function hasSeriesDimension({
+ controls,
+}: ControlPanelsContainerProps): boolean {
+ return ensureIsArray(controls?.series?.value).length > 0;
+}
+
+type QueryRow = Record<string, unknown>;
+type ChartQueryResponse = { data?: QueryRow[] };
+
+const uniqueSeriesCountByResponse = new WeakMap<
+ ChartQueryResponse,
+ Map<string, number>
+>();
+
+function getSeriesColumnLabel(
+ props: ControlPanelsContainerProps,
+): string | undefined {
+ const [series] = ensureIsArray(props.controls?.series?.value);
+ if (series == null) {
+ return undefined;
+ }
+ return getColumnLabel(series as QueryFormColumn);
+}
+
+function getChartQueryResponse(
+ props: ControlPanelsContainerProps,
+): ChartQueryResponse | undefined {
+ return (props as { chart?: { queriesResponse?: ChartQueryResponse[] } }).chart
+ ?.queriesResponse?.[0];
+}
+
+function countUniqueSeriesValues(
+ props: ControlPanelsContainerProps,
+): number | undefined {
+ const seriesColumn = getSeriesColumnLabel(props);
+ if (!seriesColumn) {
+ return undefined;
+ }
+ const queryResponse = getChartQueryResponse(props);
+ const rows = queryResponse?.data;
+ if (!queryResponse || !rows) {
+ return undefined;
+ }
+ let cachedByColumn = uniqueSeriesCountByResponse.get(queryResponse);
+ if (!cachedByColumn) {
+ cachedByColumn = new Map<string, number>();
+ uniqueSeriesCountByResponse.set(queryResponse, cachedByColumn);
+ }
+ const cachedCount = cachedByColumn.get(seriesColumn);
+ if (cachedCount !== undefined) {
+ return cachedCount;
+ }
+ const count = new Set(
+ rows.map(row =>
+ row[seriesColumn] == null ? null : String(row[seriesColumn]),
+ ),
+ ).size;
+ cachedByColumn.set(seriesColumn, count);
+ return count;
+}
+
+function hasMultipleSeries(props: ControlPanelsContainerProps): boolean {
+ return (countUniqueSeriesValues(props) ?? 0) > 1;
+}
+
+function showColorByDirection(props: ControlPanelsContainerProps): boolean {
+ return !hasMultipleSeries(props);
+}
+
+function showDirectionColors(props: ControlPanelsContainerProps): boolean {
+ return (
+ showColorByDirection(props) &&
+ props.controls?.color_by_direction?.value !== false
+ );
+}
+
+function showColorScheme(props: ControlPanelsContainerProps): boolean {
+ return (
+ hasMultipleSeries(props) ||
+ props.controls?.color_by_direction?.value === false
+ );
+}
+
+const config: ControlPanelConfig = {
+ controlPanelSections: [
+ {
+ label: t('Query'),
+ expanded: true,
+ controlSetRows: [
+ ['x_axis'],
+ ['time_grain_sqla'],
+ [
+ {
+ name: 'open',
+ config: {
+ ...sharedControls.metric,
+ label: t('Open'),
+ description: t('Opening value for each period.'),
+ validators: [validateNonEmpty],
+ },
+ },
+ ],
+ [
+ {
+ name: 'close',
+ config: {
+ ...sharedControls.metric,
+ label: t('Close'),
+ description: t('Closing value for each period.'),
+ validators: [validateNonEmpty],
+ },
+ },
+ ],
+ [
+ {
+ name: 'high',
+ config: {
+ ...sharedControls.metric,
+ label: t('High'),
+ description: t('Highest value for each period.'),
+ validators: [validateNonEmpty],
+ },
+ },
+ ],
+ [
+ {
+ name: 'low',
+ config: {
+ ...sharedControls.metric,
+ label: t('Low'),
+ description: t('Lowest value for each period.'),
+ validators: [validateNonEmpty],
+ },
+ },
+ ],
+ ['series'],
+ ['adhoc_filters'],
+ ['row_limit'],
+ ],
+ },
+ sections.titleControls,
+ {
+ label: t('Chart Options'),
+ expanded: true,
+ controlSetRows: [
+ ['zoomable'],
+ [
+ {
+ name: 'series_style',
+ config: {
+ type: 'SelectControl',
+ label: t('Series style'),
+ renderTrigger: true,
+ default: DEFAULT_SERIES_STYLE,
+ clearable: false,
+ choices: [
+ ['candlestick', t('Candlestick')],
+ ['ohlc', t('OHLC')],
+ ],
+ description: t(
+ 'Candlestick draws a filled body between open and close. OHLC draws ticks for open and close on a high-low stem.',
+ ),
+ },
+ },
+ ],
+ [
+ {
+ name: 'candlestick_series_name',
+ config: {
+ type: 'TextControl',
+ label: t('Series name'),
+ default: CANDLESTICK_SERIES_NAME,
Review Comment:
fix in 1ec5fe3
##########
superset-frontend/plugins/plugin-chart-echarts/src/Candlestick/transformProps.ts:
##########
@@ -0,0 +1,767 @@
+/**
+ * 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 {
+ AxisType,
+ CategoricalColorNamespace,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ getColumnLabel,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ getXAxisLabel,
+ NumberFormatter,
+ rgbToHex,
+ tooltipHtml,
+} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
+import { t } from '@apache-superset/core/translation';
+import type { CustomSeriesOption, CustomSeriesRenderItem } from 'echarts';
+import type { CandlestickSeriesOption, LineSeriesOption } from 'echarts/charts';
+import type { EChartsCoreOption } from 'echarts/core';
+import type { CallbackDataParams } from 'echarts/types/src/util/types';
+import {
+ CandlestickChartTransformedProps,
+ EchartsCandlestickChartProps,
+ EchartsCandlestickFormData,
+ OhlcValue,
+ LookupKey,
+} from './types';
+import {
+ CANDLESTICK_SERIES_NAME,
+ DEFAULT_DECREASE_COLOR,
+ DEFAULT_FORM_DATA,
+ DEFAULT_INCREASE_COLOR,
+ DIRECTION_LABELS,
+ OHLC_LABELS,
+ OHLC_TICK_WIDTH_RATIO,
+} from './constants';
+import { defaultGrid, defaultYAxis } from '../defaults';
+import { getDefaultTooltip } from '../utils/tooltip';
+import {
+ extractGroupbyLabel,
+ getColtypesMapping,
+ getLegendProps,
+} from '../utils/series';
+import { convertInteger } from '../utils/convertInteger';
+import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
+import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
+import { TIMESERIES_CONSTANTS } from '../constants';
+import { getPadding } from '../Timeseries/transformers';
+import { LegendOrientation, LegendType, Refs } from '../types';
+import { resolveLegendLayout } from '../utils/legendLayout';
+import {
+ calculateMA,
+ MA_LINE_OPACITY,
+ movingAverageName,
+ parseMovingAveragePeriods,
+} from './utils';
+
+type CandlestickDatum = NonNullable<CandlestickSeriesOption['data']>[number];
+type AxisTooltipParams = CallbackDataParams & {
+ axisValue?: string | number;
+ axisValueLabel?: string;
+};
+const NULL_LOOKUP_KEY = Symbol('candlestick-null');
+
+function toNumber(value: unknown): number | null {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function getOwnValue<T extends object>(
+ object: T,
+ key: string,
+): T[keyof T] | undefined {
+ return key && Object.hasOwn(object, key) ? object[key as keyof T] : undefined;
+}
+
+function toLookupKey(value: unknown): LookupKey {
+ return value == null ? NULL_LOOKUP_KEY : String(value);
+}
+
+function getOhlc(
+ datum: DataRecord,
+ openLabel: string,
+ closeLabel: string,
+ lowLabel: string,
+ highLabel: string,
+): OhlcValue | null {
+ const open = toNumber(getOwnValue(datum, openLabel));
+ const close = toNumber(getOwnValue(datum, closeLabel));
+ const low = toNumber(getOwnValue(datum, lowLabel));
+ const high = toNumber(getOwnValue(datum, highLabel));
+ if (open === null || close === null || low === null || high === null) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function toCandlestickDatum(ohlc: OhlcValue | null): CandlestickDatum {
+ return ohlc ?? [];
+}
+
+function toOhlcBarDatum(
+ ohlc: OhlcValue | null,
+ categoryIndex: number,
+ color: string,
+) {
+ if (!ohlc) {
+ return [];
+ }
+ return {
+ value: [categoryIndex, ...ohlc],
+ itemStyle: {
+ color,
+ },
+ };
+}
+
+function getDirectionItemStyle(increaseHex: string, decreaseHex: string) {
+ return {
+ color: increaseHex,
+ color0: decreaseHex,
+ borderColor: increaseHex,
+ borderColor0: decreaseHex,
+ };
+}
+
+function getSeriesItemStyle(seriesColor: string, hollowFill: string) {
+ return {
+ color: seriesColor,
+ color0: hollowFill,
+ borderColor: seriesColor,
+ borderColor0: seriesColor,
+ };
+}
+
+const renderOhlcItem: CustomSeriesRenderItem = (_params, item) => {
+ const x = toNumber(item.value(0));
+ const open = toNumber(item.value(1));
+ const close = toNumber(item.value(2));
+ const low = toNumber(item.value(3));
+ const high = toNumber(item.value(4));
+ if (
+ x === null ||
+ open === null ||
+ close === null ||
+ low === null ||
+ high === null
+ ) {
+ return null;
+ }
+
+ const openPoint = item.coord([x, open]);
+ const closePoint = item.coord([x, close]);
+ const lowPoint = item.coord([x, low]);
+ const highPoint = item.coord([x, high]);
+ const categorySize = item.size?.([1, 0]);
+ const categoryWidth = Array.isArray(categorySize)
+ ? categorySize[0]
+ : categorySize;
+ if (categoryWidth == null || !Number.isFinite(categoryWidth)) {
+ return null;
+ }
+ const halfWidth = categoryWidth * OHLC_TICK_WIDTH_RATIO;
+ const style = item.style({
+ stroke: item.visual('color'),
+ });
+
+ return {
+ type: 'group',
+ children: [
+ {
+ type: 'line',
+ shape: {
+ x1: lowPoint[0],
+ y1: lowPoint[1],
+ x2: highPoint[0],
+ y2: highPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: openPoint[0],
+ y1: openPoint[1],
+ x2: openPoint[0] - halfWidth,
+ y2: openPoint[1],
+ },
+ style,
+ },
+ {
+ type: 'line',
+ shape: {
+ x1: closePoint[0],
+ y1: closePoint[1],
+ x2: closePoint[0] + halfWidth,
+ y2: closePoint[1],
+ },
+ style,
+ },
+ ],
+ };
+};
+
+function extractOhlc(value: unknown): OhlcValue | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const raw = value.length >= 5 ? value.slice(1, 5) : value.slice(0, 4);
+ if (raw.length !== 4) {
+ return null;
+ }
+ const [open, close, low, high] = raw.map(item => Number(item));
+ if ([open, close, low, high].some(item => !Number.isFinite(item))) {
+ return null;
+ }
+ return [open, close, low, high];
+}
+
+function extractLineValue(item: CallbackDataParams): number | null {
+ const raw = item.value ?? item.data;
+ if (Array.isArray(raw)) {
+ const y = Number(raw[raw.length - 1]);
+ return Number.isFinite(y) ? y : null;
+ }
+ const numeric = Number(raw);
+ return Number.isFinite(numeric) ? numeric : null;
+}
+
+function appendOhlcRows(
+ rows: string[][],
+ ohlc: OhlcValue,
+ numberFormatter: NumberFormatter | CurrencyFormatter,
+) {
+ const [open, close, low, high] = ohlc;
+ rows.push(
+ [OHLC_LABELS.OPEN, numberFormatter(open)],
+ [OHLC_LABELS.CLOSE, numberFormatter(close)],
+ [OHLC_LABELS.LOW, numberFormatter(low)],
+ [OHLC_LABELS.HIGH, numberFormatter(high)],
+ );
+}
+
+function formatTooltip({
+ params,
+ numberFormatter,
+ title,
+ increaseLabel,
+ decreaseLabel,
+}: {
+ params: CallbackDataParams[];
+ numberFormatter: NumberFormatter | CurrencyFormatter;
+ title: string;
+ increaseLabel: string;
+ decreaseLabel: string;
+}) {
+ const rows: string[][] = [];
+ const candles = params.flatMap(item => {
+ const ohlc = extractOhlc(item.value ?? item.data);
+ return ohlc ? [{ item, ohlc }] : [];
+ });
+
+ candles.forEach(({ item, ohlc }) => {
+ const [open, close] = ohlc;
+ const direction = close >= open ? increaseLabel : decreaseLabel;
+ const seriesLabel = String(item.seriesName ?? '');
+ rows.push([seriesLabel ? `${seriesLabel} (${direction})` : direction]);
+ appendOhlcRows(rows, ohlc, numberFormatter);
+ });
+
+ params.forEach(item => {
+ if (item.seriesType !== 'line') {
+ return;
+ }
+ const value = extractLineValue(item);
+ if (value === null) {
+ return;
+ }
+ rows.push([String(item.seriesName ?? ''), numberFormatter(value)]);
+ });
+ if (!rows.length) {
+ return '';
+ }
+ return tooltipHtml(rows, title);
+}
+
+export default function transformProps(
+ chartProps: EchartsCandlestickChartProps,
+): CandlestickChartTransformedProps {
+ const {
+ width,
+ height,
+ formData: { echartOptions: _echartOptions, ...formData },
+ hooks,
+ queriesData,
+ inContextMenu,
+ theme,
+ legendState = {},
+ } = chartProps;
+
+ const [queryData] = queriesData;
+ const { data = [] } = queryData;
+ const { onLegendStateChanged, onContextMenu } = hooks;
+ const refs: Refs = {};
+ const coltypeMapping = getColtypesMapping(queryData);
+
+ const {
+ open,
+ close,
+ high,
+ low,
+ series: seriesControl,
+ candlestickSeriesName,
+ increaseColor = DEFAULT_INCREASE_COLOR,
+ decreaseColor = DEFAULT_DECREASE_COLOR,
+ increaseLabel,
+ decreaseLabel,
+ showXAxis,
+ showYAxis,
+ xAxisTimeFormat,
+ xAxisTitle,
+ xAxisTitleMargin,
+ xAxisLabelRotation,
+ xAxisLabelInterval,
+ yAxisTitle,
+ yAxisTitleMargin,
+ yAxisTitlePosition,
+ yAxisFormat,
+ currencyFormat,
+ tooltipTimeFormat,
+ tooltipValuesFormat,
+ showLegend,
+ legendMargin,
+ legendOrientation = LegendOrientation.Top,
+ legendType = LegendType.Scroll,
+ legendSort,
+ zoomable,
+ movingAverages,
+ seriesStyle,
+ colorScheme,
+ sliceId,
+ colorByDirection,
+ }: EchartsCandlestickFormData = { ...DEFAULT_FORM_DATA, ...formData };
+
+ // Matches buildQuery.ts's getXAxisColumn: an unset x_axis with
+ // granularity_sqla present still queries DTTM_ALIAS, so the transform must
+ // resolve the same column or every row collapses into one empty category.
+ const xAxisName = (getXAxisLabel(chartProps.rawFormData) as string) ?? '';
Review Comment:
fix in 1ec5fe3
--
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: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org