diff --git a/semcore/d3-chart/src/Area.jsx b/semcore/d3-chart/src/Area.jsx index f2573d9516..c6c7d62756 100644 --- a/semcore/d3-chart/src/Area.jsx +++ b/semcore/d3-chart/src/Area.jsx @@ -6,10 +6,16 @@ import { area, curveLinear, line } from 'd3-shape'; import React from 'react'; import AnimatedClipPath from './AnimatedClipPath'; +import { DATA_TYPE, FORECAST, POTENTIAL } from './component/Chart/AbstractChart.type'; import { SvgElement } from './component/SvgElement'; import createElement from './createElement'; import Dots from './Dots'; -import { PatternFill } from './Pattern'; +import { + PatternFill, + ForecastGradient, + PotentialGradient, + StrokeMask, +} from './Pattern'; import style from './style/area.shadow.css'; import { definedData, @@ -102,6 +108,102 @@ class AreaRoot extends Component { onClick(index, e); } + renderForecast() { + const SArea = this.Element; + const SAreaLine = SvgElement; + const { + styles, + hide, + d3, + d3Line, + color, + uid, + size, + duration, + y, + transparent, + resolveColor, + } = this.asProps; + const data = this.asProps.data.filter((item) => item[y] !== interpolateValue && item[DATA_TYPE] === FORECAST); + + return sstyled(styles)( + <> + + + + + , + ); + } + + renderPotential() { + const SArea = this.Element; + const SAreaLine = SvgElement; + const { + styles, + hide, + d3, + d3Line, + uid, + size, + duration, + y, + transparent, + } = this.asProps; + const data = this.asProps.data.filter((item) => item[y] !== interpolateValue && item[DATA_TYPE] === POTENTIAL); + + return sstyled(styles)( + <> + + + + + + + , + ); + } + render() { const SArea = this.Element; const SAreaLine = SvgElement; @@ -121,9 +223,10 @@ class AreaRoot extends Component { forcedAdvancedMode, resolveColor, patterns, + withGradient, } = this.asProps; const advancedMode = forcedAdvancedMode || !!findComponent(Children, [Area.Line.displayName]); - const data = this.asProps.data.filter((item) => item[y] !== interpolateValue); + const data = this.asProps.data.filter((item) => item[y] !== interpolateValue && item[DATA_TYPE] !== FORECAST && item[DATA_TYPE] !== POTENTIAL); this.asProps.dataHintsHandler.specifyDataRowFields(x, y); this.asProps.dataHintsHandler.establishDataType('time-series'); @@ -152,8 +255,10 @@ class AreaRoot extends Component { use:duration={`${duration}ms`} transparent={transparent} onClickCapture={this.handlerOnClick.bind(this)} + withGradient={patterns ? undefined : withGradient} /> {duration && } + {duration && } {patterns && ( )} + {this.renderForecast()} + {this.renderPotential()} , ); } diff --git a/semcore/d3-chart/src/Dots.jsx b/semcore/d3-chart/src/Dots.jsx index dde55a07a8..59816ea9b5 100644 --- a/semcore/d3-chart/src/Dots.jsx +++ b/semcore/d3-chart/src/Dots.jsx @@ -3,14 +3,18 @@ import trottle from '@semcore/core/lib/utils/rafTrottle'; import { bisector } from 'd3-array'; import React from 'react'; +import { HIGHLIGHT_DOT, GOOD, BAD, INSIGHTFUL } from './component/Chart/AbstractChart.type'; import createElement from './createElement'; -import { PatternSymbol, getPatternSymbolSize } from './Pattern'; +import { PatternSymbol, getPatternSymbolSize, PotentialGradient } from './Pattern'; import style from './style/dot.shadow.css'; import { eventToPoint, invert, interpolateValue, getChartDefaultColorName } from './utils'; const BASE_RADIUS = 3.5; const ACTIVE_RADIUS = 4.5; + function Dots(props) { + const SDotHighlight = 'circle'; + const SDotBorder = 'circle'; const { Element: SDot, styles, @@ -29,6 +33,7 @@ function Dots(props) { resolveColor, patterns, onClick, + uid, } = props; const bisect = bisector((d) => d[x]).center; const [activeIndex, setActiveIndex] = React.useState(null); @@ -69,6 +74,22 @@ function Dots(props) { onClick(index, e); }, [scale, data, onClick]); + const resolveHighlightColor = React.useCallback((highlight) => { + switch (highlight) { + case GOOD: { + return resolveColor('--intergalactic-chart-data-success'); + } + case BAD: { + return resolveColor('--intergalactic-chart-data-critical'); + } + case INSIGHTFUL: { + return `url(#dotGradient_${uid})`; + } + default: + return ''; + } + }, []); + React.useEffect(() => { const unsubscribeMouseMoveRoot = eventEmitter.subscribe('onMouseMoveChart', (e) => { e.persist(); @@ -97,8 +118,8 @@ function Dots(props) { const active = i === activeIndex; const visible = typeof display === 'function' - ? display(i, i === activeIndex, !isPrev && !isNext) - : display || i === activeIndex || (!isPrev && !isNext); + ? display(i, active, !isPrev && !isNext, d) + : display || active || (!isPrev && !isNext); const radius = active ? ACTIVE_RADIUS : radiusBase; if (!d3.defined()(d)) return acc; if (!visible) return acc; @@ -106,22 +127,48 @@ function Dots(props) { if (!patterns) { acc.push( sstyled(styles)( - , + + {d[HIGHLIGHT_DOT] && ( + <> + + + + )} + + , ), ); } else { @@ -149,6 +196,7 @@ function Dots(props) { const SDots = 'g'; return sstyled(styles)( + { patterns && ( - ) } + )} {dots} , ); diff --git a/semcore/d3-chart/src/Pattern.tsx b/semcore/d3-chart/src/Pattern.tsx index 04396e90dd..e2cea61bda 100644 --- a/semcore/d3-chart/src/Pattern.tsx +++ b/semcore/d3-chart/src/Pattern.tsx @@ -1,6 +1,9 @@ +import { sstyled } from '@semcore/core'; import propsForElement from '@semcore/core/lib/utils/propsForElement'; +import { line } from 'd3-shape'; import React from 'react'; +import style from './style/pattern.shadow.css'; /** * Object that is fully describes the pattern */ @@ -625,6 +628,88 @@ const resolvePattern = (patternKey: string, patternsConfig: PatternsConfig = tru patternIndex = patternIndex % patterns.length; return patterns[patternIndex]; }; + +export const DefaultStrokePattern = (props: { id: string }) => { + return ( + + + + + + + ); +}; +export const LightStrokePattern = (props: { id: string }) => { + return ( + + + + + + ); +}; +export const StrokeMask = (props: { id: string }) => { + return ( + + + + + + + + + ); +}; +export const ForecastGradient = (props: { id: string }) => { + return ( + + + + + + + + ); +}; +export const PotentialGradient = (props: { id: string; type?: 'line' }) => { + const SLinearGradient = 'linearGradient'; + const SStopFrom = 'stop'; + const SStopTo = 'stop'; + + return sstyled(style)( + + + + + + , + ); +}; + export const PatternFill = ({ id, color, diff --git a/semcore/d3-chart/src/component/Chart/AbstractChart.tsx b/semcore/d3-chart/src/component/Chart/AbstractChart.tsx index 0409ed99f3..61ddbe5df8 100644 --- a/semcore/d3-chart/src/component/Chart/AbstractChart.tsx +++ b/semcore/d3-chart/src/component/Chart/AbstractChart.tsx @@ -52,7 +52,7 @@ export abstract class AbstractChart< /** * Padding from the end's of chart to the container (except axis sides) */ - protected plotPadding = 6; + protected plotPadding = 10; protected dataHints = makeDataHintsContainer(); diff --git a/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts b/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts index 63c83aad6e..8b9243ef82 100644 --- a/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts +++ b/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts @@ -6,11 +6,20 @@ import type React from 'react'; import type { PatternsConfig } from '../../Pattern'; // @ts-ignore import type { PlotSummarizerConfig } from '../../Plot'; -import type { PlotEventEmitter } from '../../utils'; +import type { interpolateValue, PlotEventEmitter } from '../../utils'; import type { BaseChartLegendProps } from '../ChartLegend/BaseLegend.type'; import type { TrendProps } from '../ChartLegend/LegendFlex/LegendFlex.type'; import type { LegendItemKey } from '../ChartLegend/LegendItem/LegendItem.type'; +export const GOOD = Symbol('GOOD'); +export const BAD = Symbol('BAD'); +export const INSIGHTFUL = Symbol('INSIGHTFUL'); +export const HIGHLIGHT_DOT = Symbol('HIGHLIGHT_DOT'); + +export const DATA_TYPE = Symbol('DATA_TYPE'); +export const FORECAST = Symbol('FORECAST_DATA'); +export const POTENTIAL = Symbol('POTENTIAL_DATA'); + export type BaseLegendProps = BaseChartLegendProps & { /** * Disable hover (for transition items legend of each not hovered) @@ -44,7 +53,10 @@ export type BaseLegendProps = BaseChartLegendProps & { ); export type ObjectDataKey = string; -export type ObjectData = Record; +export type ObjectData = Record & { + [HIGHLIGHT_DOT]?: typeof GOOD | typeof BAD | typeof INSIGHTFUL; + [DATA_TYPE]?: typeof POTENTIAL | typeof FORECAST; +}; export type ListData = ObjectData[]; /** diff --git a/semcore/d3-chart/src/component/Chart/AreaChart.tsx b/semcore/d3-chart/src/component/Chart/AreaChart.tsx index 80624f394c..846118d88a 100644 --- a/semcore/d3-chart/src/component/Chart/AreaChart.tsx +++ b/semcore/d3-chart/src/component/Chart/AreaChart.tsx @@ -7,7 +7,14 @@ import React from 'react'; import { Area, minMax, HoverLine, StackedArea } from '../..'; import type { ChartState } from './AbstractChart'; import { AbstractChart } from './AbstractChart'; -import type { AreaChartData, AreaChartProps, AreaChartType, AreaChartDefaultProps } from './AreaChart.type'; +import type { ObjectData } from './AbstractChart.type'; +import { HIGHLIGHT_DOT } from './AbstractChart.type'; +import type { + AreaChartData, + AreaChartProps, + AreaChartType, + AreaChartDefaultProps, +} from './AreaChart.type'; import { localizedMessages } from '../../translations/__intergalactic-dynamic-locales'; class AreaChartComponent extends AbstractChart< @@ -27,6 +34,7 @@ class AreaChartComponent extends AbstractChart< showXAxis: true, showYAxis: true, showTooltip: true, + stacked: true, locale: 'en', } as const; @@ -84,8 +92,9 @@ class AreaChartComponent extends AbstractChart< transparent={highlightedItem !== -1 && highlightedItem !== index} curve={curve} onClick={onClickArea} + withGradient > - + ) ); @@ -94,6 +103,8 @@ class AreaChartComponent extends AbstractChart< ); } + const withGradient = dataDefinitions.filter((item) => item.checked).length === 1; + return dataDefinitions.map((item, index) => { return ( item.checked && ( @@ -105,8 +116,9 @@ class AreaChartComponent extends AbstractChart< transparent={highlightedItem !== -1 && highlightedItem !== index} curve={curve} onClick={onClickArea} + withGradient={withGradient} > - + ) ); @@ -136,6 +148,10 @@ class AreaChartComponent extends AbstractChart< protected getLegendAriaLabel(): string { return this.asProps.getI18nText('legendForChart', { chartType: 'Area' }); } + + protected displayDots = (i: number, isActive: boolean, noAround: boolean, data: ObjectData): boolean => { + return isActive || noAround || Boolean(data[HIGHLIGHT_DOT]); + }; } /** diff --git a/semcore/d3-chart/src/component/Chart/AreaChart.type.ts b/semcore/d3-chart/src/component/Chart/AreaChart.type.ts index 0697a682e2..9ec1d4fe74 100644 --- a/semcore/d3-chart/src/component/Chart/AreaChart.type.ts +++ b/semcore/d3-chart/src/component/Chart/AreaChart.type.ts @@ -3,10 +3,9 @@ import type { Intergalactic } from '@semcore/core'; import type { ScaleLinear, ScaleTime } from 'd3-scale'; import type { CurveFactory } from 'd3-shape'; -import type { AriaNameProps, BaseChartProps } from './AbstractChart.type'; -import type { interpolateValue } from '../../utils'; +import type { AriaNameProps, BaseChartProps, ObjectData } from './AbstractChart.type'; -export type AreaChartData = Array>; +export type AreaChartData = Array; export type AreaChartProps = BaseChartProps & { /** Field name that groups the data points */ @@ -19,7 +18,11 @@ export type AreaChartProps = BaseChartProps & { showDots?: boolean; /** D3 curve factory for line interpolation (e.g., curveLinear, curveCardinal) */ curve?: CurveFactory; - /** Enables stacked area chart mode */ + /** + * Enables stacked area chart mode + * @deprecated True by default. AreaChart should be only stacked. + * @default true + */ stacked?: boolean; /** Callback triggered when a user clicks on a chart at a position corresponding to a data item */ onClickArea?: (index: number, event: React.SyntheticEvent) => void; diff --git a/semcore/d3-chart/src/component/Chart/index.ts b/semcore/d3-chart/src/component/Chart/index.ts index cd3ee5bbc3..0afd4d430c 100644 --- a/semcore/d3-chart/src/component/Chart/index.ts +++ b/semcore/d3-chart/src/component/Chart/index.ts @@ -1,3 +1,4 @@ +import { GOOD, BAD, INSIGHTFUL, HIGHLIGHT_DOT, DATA_TYPE, POTENTIAL, FORECAST } from './AbstractChart.type'; import { AreaChart } from './AreaChart'; import { BarChart } from './BarChart'; import { BubbleChart } from './BubbleChart'; @@ -23,3 +24,13 @@ export default { Cigarette: CigaretteChart, CompactHorizontalBar: CompactHorizontalBarChart, }; + +export { + GOOD, + BAD, + INSIGHTFUL, + HIGHLIGHT_DOT, + DATA_TYPE, + FORECAST, + POTENTIAL, +}; diff --git a/semcore/d3-chart/src/index.js b/semcore/d3-chart/src/index.js index e97384de3f..d2b3b57c61 100644 --- a/semcore/d3-chart/src/index.js +++ b/semcore/d3-chart/src/index.js @@ -1,6 +1,7 @@ export { default as Plot } from './Plot'; export { ChartLegend, ChartLegendTable } from './component/ChartLegend'; export { default as Chart } from './component/Chart'; +export * from './component/Chart'; export { default as StackGroupBar } from './component/StackGroupBar/StackGroupBar'; export { SvgElement } from './component/SvgElement'; export { XAxis, YAxis } from './Axis'; diff --git a/semcore/d3-chart/src/style/area.shadow.css b/semcore/d3-chart/src/style/area.shadow.css index 27072527fa..009aad758e 100644 --- a/semcore/d3-chart/src/style/area.shadow.css +++ b/semcore/d3-chart/src/style/area.shadow.css @@ -3,6 +3,9 @@ SArea { fill: var(--intergalactic-chart-palette-order-1, oklch(0.23 0.01 140)); fill-opacity: 0.1; +} + +SArea[withGradient] { mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.48) 60%, transparent 100%); } @@ -12,7 +15,7 @@ SArea[color] { SArea[pattern] { fill: var(--pattern); - fill-opacity: 0.7; + fill-opacity: 0.5; } SArea[transparent] { @@ -48,4 +51,4 @@ SNull { SNull[hide] { display: none; -} \ No newline at end of file +} diff --git a/semcore/d3-chart/src/style/dot.shadow.css b/semcore/d3-chart/src/style/dot.shadow.css index 18534242fa..687d42c263 100644 --- a/semcore/d3-chart/src/style/dot.shadow.css +++ b/semcore/d3-chart/src/style/dot.shadow.css @@ -28,6 +28,19 @@ SDot[color] { fill: var(--color); } +SDotHighlight[color] { + opacity: 0.4; + fill: var(--color); +} + +SDotBorder { + fill: none; + stroke-width: 1px; + stroke: var(--intergalactic-chart-grid-line, oklch(0.95 0.002 180)); + opacity: 0.5; + stroke-dasharray: 2; +} + SDot[transparent] { opacity: 0.3; -} \ No newline at end of file +} diff --git a/semcore/d3-chart/src/style/pattern.shadow.css b/semcore/d3-chart/src/style/pattern.shadow.css new file mode 100644 index 0000000000..41859e8b33 --- /dev/null +++ b/semcore/d3-chart/src/style/pattern.shadow.css @@ -0,0 +1,10 @@ + +SLinearGradient { + SStopFrom { + stop-color: var(--violet-300, oklch(0.74 0.17 303)); + } + + SStopTo { + stop-color: var(--green-100, oklch(0.9 0.11 175)); + } +} diff --git a/stories/components/d3-chart/__mocks__/area.ts b/stories/components/d3-chart/__mocks__/area.ts index 43376283a3..f7e1910570 100644 --- a/stories/components/d3-chart/__mocks__/area.ts +++ b/stories/components/d3-chart/__mocks__/area.ts @@ -1,17 +1,15 @@ -import { interpolateValue } from '@semcore/ui/d3-chart'; +import { interpolateValue, GOOD, HIGHLIGHT_DOT, BAD, INSIGHTFUL, DATA_TYPE, FORECAST, POTENTIAL } from '@semcore/ui/d3-chart'; export default { Default: [ { time: new Date(2025, 0, 1), line: 3 }, { time: new Date(2025, 0, 5), line: 7 }, - { time: new Date(2025, 0, 10), line: 1 }, - { time: new Date(2025, 0, 15), line: 9 }, + { time: new Date(2025, 0, 10), line: 5 }, + { time: new Date(2025, 0, 15), line: 9, [HIGHLIGHT_DOT]: GOOD } as const, { time: new Date(2025, 0, 20), line: 4 }, { time: new Date(2025, 0, 25), line: 6 }, - { time: new Date(2025, 0, 30), line: 2 }, + { time: new Date(2025, 0, 30), line: 2, [HIGHLIGHT_DOT]: BAD } as const, { time: new Date(2025, 1, 4), line: 8 }, - { time: new Date(2025, 1, 9), line: 0 }, - { time: new Date(2025, 1, 14), line: 5 }, ], Interpolation: [ { time: new Date(2025, 0, 1), line1: 5, line2: 3 }, @@ -45,4 +43,26 @@ export default { { x: 4, y: 1 }, { x: 5, y: null }, ], + Forecast: [ + { time: new Date(2025, 0, 1), line: 3 }, + { time: new Date(2025, 0, 5), line: 7 }, + { time: new Date(2025, 0, 10), line: 1 }, + { time: new Date(2025, 0, 15), line: 9 }, + { time: new Date(2025, 0, 20), line: 4 }, + { time: new Date(2025, 0, 25), line: 6 }, + { time: new Date(2025, 0, 25), line: 6, [DATA_TYPE]: FORECAST } as const, + { time: new Date(2025, 0, 30), line: 2, [DATA_TYPE]: FORECAST } as const, + { time: new Date(2025, 1, 4), line: 8, [DATA_TYPE]: FORECAST } as const, + ], + Potential: [ + { time: new Date(2025, 0, 1), line: 3 }, + { time: new Date(2025, 0, 5), line: 7 }, + { time: new Date(2025, 0, 10), line: 1 }, + { time: new Date(2025, 0, 15), line: 9 }, + { time: new Date(2025, 0, 20), line: 4 }, + { time: new Date(2025, 1, 14), line: 5 }, + { time: new Date(2025, 1, 14), line: 5, [DATA_TYPE]: POTENTIAL } as const, + { time: new Date(2025, 1, 18), line: 7, [DATA_TYPE]: POTENTIAL } as const, + { time: new Date(2025, 1, 28), line: 8, [DATA_TYPE]: POTENTIAL, [HIGHLIGHT_DOT]: INSIGHTFUL } as const, + ], }; diff --git a/stories/components/d3-chart/docs/examples/area-chart/basic-usage.tsx b/stories/components/d3-chart/docs/examples/area-chart/basic-usage.tsx index 8ca932ff25..c7d5ae76bd 100644 --- a/stories/components/d3-chart/docs/examples/area-chart/basic-usage.tsx +++ b/stories/components/d3-chart/docs/examples/area-chart/basic-usage.tsx @@ -1,3 +1,4 @@ +import { Flex } from '@semcore/ui/base-components'; import { Chart } from '@semcore/ui/d3-chart'; import React from 'react'; @@ -14,14 +15,32 @@ function formatDate(value: any) { const Demo = () => { return ( - + + + + + ); };