@flux-ui/statistics 3.0.0-next.74 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,5 +12,5 @@ export interface CandlestickChartOptionsInput {
12
12
  readonly splitLines?: boolean;
13
13
  readonly advancedOptions?: EChartsOption;
14
14
  }
15
- export declare function candlestickLegendItemBuilder(s: FluxStatisticsChartCandlestickSeries): readonly ChartLegendItem[];
15
+ export declare function candlestickLegendItemBuilder(s: FluxStatisticsChartCandlestickSeries, _color: string, index: number): readonly ChartLegendItem[];
16
16
  export declare function buildCandlestickChartOptions(input: CandlestickChartOptionsInput): EChartsOption;
@@ -0,0 +1,22 @@
1
+ import { FluxIconName, FluxStatisticsChartColor } from '@flux-ui/types';
2
+ import { EChartsOption } from '../../composable';
3
+ import { TooltipStyleClasses, Translator } from './types';
4
+ export interface CandlestickTooltipPoint {
5
+ readonly label?: string;
6
+ readonly open: number;
7
+ readonly close: number;
8
+ readonly low: number;
9
+ readonly high: number;
10
+ }
11
+ export interface CandlestickTooltipInput {
12
+ readonly t: Translator;
13
+ readonly styles: TooltipStyleClasses;
14
+ readonly getSeries: () => readonly {
15
+ readonly name?: string;
16
+ readonly icon?: FluxIconName;
17
+ readonly positiveColor?: FluxStatisticsChartColor;
18
+ readonly negativeColor?: FluxStatisticsChartColor;
19
+ readonly data: readonly CandlestickTooltipPoint[];
20
+ }[];
21
+ }
22
+ export declare function buildCandlestickTooltip(input: CandlestickTooltipInput): EChartsOption;
@@ -3,7 +3,9 @@ import { TooltipStyleClasses, Translator } from './types';
3
3
  export interface TreemapTooltipNode {
4
4
  readonly name: string;
5
5
  readonly value?: number;
6
- readonly color?: string;
6
+ readonly itemStyle?: {
7
+ readonly color?: string;
8
+ };
7
9
  }
8
10
  export interface TreemapTooltipInput {
9
11
  readonly t: Translator;
@@ -1,5 +1,7 @@
1
1
  export type { BoxPlotTooltipInput, BoxPlotTooltipPoint } from './buildBoxPlotTooltip';
2
2
  export { buildBoxPlotTooltip } from './buildBoxPlotTooltip';
3
+ export type { CandlestickTooltipInput, CandlestickTooltipPoint } from './buildCandlestickTooltip';
4
+ export { buildCandlestickTooltip } from './buildCandlestickTooltip';
3
5
  export type { CartesianTooltipInput } from './buildCartesianTooltip';
4
6
  export { buildCartesianTooltip } from './buildCartesianTooltip';
5
7
  export type { GaugeTooltipInput } from './buildGaugeTooltip';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flux-ui/statistics",
3
3
  "description": "Statistics components for the Flux UI library.",
4
- "version": "3.0.0-next.74",
4
+ "version": "3.0.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "funding": "https://github.com/sponsors/basmilius",
@@ -51,9 +51,9 @@
51
51
  "dependencies": {
52
52
  "@basmilius/common": "^3.40.0",
53
53
  "@basmilius/utils": "^3.40.0",
54
- "@flux-ui/components": "3.0.0-next.74",
55
- "@flux-ui/internals": "3.0.0-next.74",
56
- "@flux-ui/types": "3.0.0-next.74",
54
+ "@flux-ui/components": "3.0.0",
55
+ "@flux-ui/internals": "3.0.0",
56
+ "@flux-ui/types": "3.0.0",
57
57
  "clsx": "^2.1.1"
58
58
  },
59
59
  "peerDependencies": {
@@ -10,7 +10,7 @@
10
10
  import { merge } from 'lodash-es';
11
11
  import { computed, useTemplateRef } from 'vue';
12
12
  import { type EChartsOption, useECharts } from '~flux/statistics/composable';
13
- import { buildBaseOptions, deepResolveCssVars } from '~flux/statistics/util';
13
+ import { buildBaseOptions, deepResolveCssVars, useCssVarVersion } from '~flux/statistics/util';
14
14
  import $style from '~flux/statistics/css/Chart.module.scss';
15
15
 
16
16
  const {
@@ -23,7 +23,11 @@
23
23
 
24
24
  const defaults = buildBaseOptions();
25
25
 
26
+ const themeVersion = useCssVarVersion();
27
+
26
28
  const mergedOptions = computed<EChartsOption>(() => {
29
+ themeVersion.value;
30
+
27
31
  const merged = merge({}, defaults, options) as EChartsOption & { color?: unknown; series?: unknown };
28
32
 
29
33
  if (options && (options as { color?: unknown }).color !== undefined) {
@@ -73,12 +73,21 @@
73
73
  const formattedCurrent = computed(() => format ? format(current) : current);
74
74
  const formattedPrevious = computed(() => format ? format(previous) : previous);
75
75
 
76
+ const isDeltaIndeterminate = computed(() => previous === 0 && current !== 0);
77
+
76
78
  const deltaValue = computed(() => {
77
79
  if (previous === 0) {
78
80
  return 0;
79
81
  }
80
82
 
81
- return ((current - previous) / Math.abs(previous)) * 100;
83
+ const delta = ((current - previous) / Math.abs(previous)) * 100;
84
+ const rounded = Math.round(delta * 10) / 10;
85
+
86
+ if (rounded === 0) {
87
+ return 0;
88
+ }
89
+
90
+ return delta;
82
91
  });
83
92
 
84
93
  const deltaColor = computed<FluxColor>(() => {
@@ -106,6 +115,10 @@
106
115
  });
107
116
 
108
117
  const formattedDelta = computed(() => {
118
+ if (isDeltaIndeterminate.value) {
119
+ return '—';
120
+ }
121
+
109
122
  const sign = deltaValue.value > 0 ? '+' : '';
110
123
 
111
124
  return `${sign}${deltaValue.value.toFixed(1)}%`;
@@ -1,10 +1,10 @@
1
1
  <template>
2
- <div :class="$style.statisticsLegend">
2
+ <div :class="containerClass">
3
3
  <slot v-if="hasSlot"/>
4
4
  <FluxStatisticsLegendItem
5
5
  v-else
6
6
  v-for="(item, index) in autoItems"
7
- :key="item.label"
7
+ :key="`${index}-${item.label}`"
8
8
  :color="item.color as `#${string}` | undefined"
9
9
  :icon="item.icon"
10
10
  :is-hovered="legendContext?.hoveredIndex.value === index"
@@ -18,17 +18,37 @@
18
18
  <script
19
19
  lang="ts"
20
20
  setup>
21
- import { computed, inject, useSlots } from 'vue';
22
- import { FluxStatisticsChartLegendInjectionKey } from '~flux/statistics/composable';
21
+ import type { FluxDirection } from '@flux-ui/types';
22
+ import { clsx } from 'clsx';
23
+ import { computed, inject, provide, toRef, useSlots } from 'vue';
24
+ import { FluxStatisticsChartLegendInjectionKey, type FluxStatisticsLegendVariant, FluxStatisticsLegendVariantInjectionKey } from '~flux/statistics/composable';
23
25
  import FluxStatisticsLegendItem from './FluxStatisticsLegendItem.vue';
24
26
  import $style from '~flux/statistics/css/Legend.module.scss';
25
27
 
28
+ const {
29
+ direction = 'horizontal',
30
+ variant = 'detailed'
31
+ } = defineProps<{
32
+ readonly direction?: FluxDirection;
33
+ readonly variant?: FluxStatisticsLegendVariant;
34
+ }>();
35
+
26
36
  const slots = useSlots();
27
37
  const legendContext = inject(FluxStatisticsChartLegendInjectionKey, null);
28
38
 
29
39
  const hasSlot = computed(() => !!slots.default);
30
40
  const autoItems = computed(() => legendContext?.items.value ?? []);
31
41
 
42
+ const containerClass = computed(() => {
43
+ if (variant === 'compact') {
44
+ return clsx($style.statisticsLegendCompact, direction === 'vertical' ? $style.isVertical : $style.isHorizontal);
45
+ }
46
+
47
+ return $style.statisticsLegend;
48
+ });
49
+
50
+ provide(FluxStatisticsLegendVariantInjectionKey, toRef(() => variant));
51
+
32
52
  function onItemMouseEnter(index: number): void {
33
53
  if (legendContext) {
34
54
  legendContext.hoveredIndex.value = index;
@@ -1,6 +1,9 @@
1
1
  <template>
2
2
  <div
3
- :class="clsx($style.statisticsLegendItem, isHovered && $style.isHovered)"
3
+ :class="clsx(
4
+ variant === 'compact' ? $style.statisticsLegendItemCompact : $style.statisticsLegendItem,
5
+ variant !== 'compact' && isHovered && $style.isHovered
6
+ )"
4
7
  :style="{
5
8
  '--color': colorValue
6
9
  }">
@@ -34,7 +37,8 @@
34
37
  import { FluxIcon } from '@flux-ui/components';
35
38
  import type { FluxColor, FluxIconName } from '@flux-ui/types';
36
39
  import { clsx } from 'clsx';
37
- import { computed } from 'vue';
40
+ import { computed, inject } from 'vue';
41
+ import { FluxStatisticsLegendVariantInjectionKey } from '~flux/statistics/composable';
38
42
  import $style from '~flux/statistics/css/Legend.module.scss';
39
43
 
40
44
  const {
@@ -47,6 +51,9 @@
47
51
  readonly value?: string | number;
48
52
  }>();
49
53
 
54
+ const variantRef = inject(FluxStatisticsLegendVariantInjectionKey, null);
55
+ const variant = computed(() => variantRef?.value ?? 'detailed');
56
+
50
57
  const colorValue = computed(() => {
51
58
  if (!color) {
52
59
  return;
@@ -10,3 +10,5 @@ export type { UseChartSlicesSetupReturn } from './useChartSlicesSetup';
10
10
  export { useChartSlicesSetup } from './useChartSlicesSetup';
11
11
  export type { EChartsInstance, EChartsOption, UseEChartsReturn } from './useECharts';
12
12
  export { useECharts } from './useECharts';
13
+ export type { FluxStatisticsLegendVariant } from './useLegendVariant';
14
+ export { FluxStatisticsLegendVariantInjectionKey } from './useLegendVariant';
@@ -23,13 +23,22 @@ export function useChartHoverSync(
23
23
  let attached: EChartsInstance | null = null;
24
24
  let syncing = false;
25
25
 
26
+ const toSeriesIndex = (itemIndex: number): number => {
27
+ return legendContext.items.value[itemIndex]?.seriesIndex ?? itemIndex;
28
+ };
29
+
30
+ const toItemIndex = (seriesIndex: number): number => {
31
+ const mapped = legendContext.items.value.findIndex(item => item.seriesIndex === seriesIndex);
32
+ return mapped !== -1 ? mapped : seriesIndex;
33
+ };
34
+
26
35
  const onMouseOver = (params: { seriesIndex?: number; dataIndex?: number }) => {
27
36
  if (syncing) {
28
37
  return;
29
38
  }
30
39
 
31
40
  const index = mode === 'series'
32
- ? params.seriesIndex ?? null
41
+ ? (params.seriesIndex !== undefined ? toItemIndex(params.seriesIndex) : null)
33
42
  : params.dataIndex ?? null;
34
43
 
35
44
  legendContext.hoveredIndex.value = index;
@@ -51,7 +60,7 @@ export function useChartHoverSync(
51
60
 
52
61
  if (index !== null) {
53
62
  if (mode === 'series') {
54
- instance.dispatchAction({ type: 'highlight', seriesIndex: index });
63
+ instance.dispatchAction({ type: 'highlight', seriesIndex: toSeriesIndex(index) });
55
64
  } else {
56
65
  instance.dispatchAction({ type: 'highlight', seriesIndex: forcedSeriesIndex, dataIndex: index });
57
66
  }
@@ -5,6 +5,7 @@ export interface ChartLegendItem {
5
5
  readonly color?: string;
6
6
  readonly icon?: FluxIconName;
7
7
  readonly label: string;
8
+ readonly seriesIndex?: number;
8
9
  readonly value?: string | number;
9
10
  }
10
11
 
@@ -1,6 +1,6 @@
1
1
  import { useResizeObserver } from '@basmilius/common';
2
2
  import { init, type EChartsCoreOption } from 'echarts/core';
3
- import { markRaw, onBeforeUnmount, onMounted, ref, toValue, type MaybeRefOrGetter, type Ref } from 'vue';
3
+ import { markRaw, onBeforeUnmount, onMounted, ref, toValue, watch, type MaybeRefOrGetter, type Ref } from 'vue';
4
4
  import '~flux/statistics/echarts';
5
5
 
6
6
  export type EChartsOption = EChartsCoreOption;
@@ -27,6 +27,10 @@ export function useECharts(
27
27
  chartInstance.value.setOption(toValue(options));
28
28
  });
29
29
 
30
+ watch(() => toValue(options), value => {
31
+ chartInstance.value?.setOption(value, {notMerge: true});
32
+ });
33
+
30
34
  onBeforeUnmount(() => {
31
35
  if (pendingResize !== null) {
32
36
  cancelAnimationFrame(pendingResize);
@@ -0,0 +1,5 @@
1
+ import type { InjectionKey, Ref } from 'vue';
2
+
3
+ export type FluxStatisticsLegendVariant = 'detailed' | 'compact';
4
+
5
+ export const FluxStatisticsLegendVariantInjectionKey: InjectionKey<Ref<FluxStatisticsLegendVariant>> = Symbol('flux-statistics-legend-variant');
@@ -68,3 +68,48 @@
68
68
  content: '';
69
69
  border-top: 1px dashed var(--surface-stroke);
70
70
  }
71
+
72
+ .statisticsLegendCompact {
73
+ display: flex;
74
+ user-select: none;
75
+
76
+ &.isHorizontal {
77
+ flex-flow: row wrap;
78
+ gap: 12px 18px;
79
+ }
80
+
81
+ &.isVertical {
82
+ flex-flow: column;
83
+ gap: 12px;
84
+ }
85
+ }
86
+
87
+ .statisticsLegendItemCompact {
88
+ --color: var(--primary-600);
89
+
90
+ display: flex;
91
+ align-items: center;
92
+ flex-flow: row nowrap;
93
+ gap: 6px;
94
+ font-size: 14px;
95
+ line-height: 1;
96
+ white-space: nowrap;
97
+
98
+ .statisticsLegendItemColor {
99
+ margin: 0;
100
+ border-radius: var(--radius-full);
101
+ }
102
+
103
+ .statisticsLegendItemIcon {
104
+ margin: 0;
105
+ }
106
+
107
+ .statisticsLegendItemLabel {
108
+ flex-grow: 0;
109
+ font-weight: 400;
110
+ }
111
+
112
+ .statisticsLegendItemValue {
113
+ margin-left: 0;
114
+ }
115
+ }
@@ -3,7 +3,7 @@ import { merge } from 'lodash-es';
3
3
  import type { ChartLegendItem, EChartsOption } from '~flux/statistics/composable';
4
4
  import { resolveChartColor, toCandlestickSeries } from '../../series';
5
5
  import type { TooltipStyleClasses, Translator } from '../../tooltips';
6
- import { buildCartesianTooltip } from '../../tooltips';
6
+ import { buildCandlestickTooltip } from '../../tooltips';
7
7
  import { buildCartesianBaseOptions } from '../buildCartesianBaseOptions';
8
8
 
9
9
  export interface CandlestickChartOptionsInput {
@@ -36,16 +36,18 @@ function resolveCandlestickLabels(
36
36
  return undefined;
37
37
  }
38
38
 
39
- export function candlestickLegendItemBuilder(s: FluxStatisticsChartCandlestickSeries): readonly ChartLegendItem[] {
39
+ export function candlestickLegendItemBuilder(s: FluxStatisticsChartCandlestickSeries, _color: string, index: number): readonly ChartLegendItem[] {
40
40
  return [
41
41
  {
42
42
  color: resolveChartColor(s.positiveColor) ?? 'var(--success-500)',
43
43
  icon: s.icon,
44
- label: 'Up'
44
+ label: 'Up',
45
+ seriesIndex: index
45
46
  },
46
47
  {
47
48
  color: resolveChartColor(s.negativeColor) ?? 'var(--danger-500)',
48
- label: 'Down'
49
+ label: 'Down',
50
+ seriesIndex: index
49
51
  }
50
52
  ];
51
53
  }
@@ -65,7 +67,7 @@ export function buildCandlestickChartOptions(input: CandlestickChartOptionsInput
65
67
  : undefined;
66
68
 
67
69
  const tooltipOptions: EChartsOption = tooltip
68
- ? buildCartesianTooltip({ t, styles, getSeriesIcons: () => series.map(s => s.icon) })
70
+ ? buildCandlestickTooltip({ t, styles, getSeries: () => series })
69
71
  : { tooltip: { show: false } };
70
72
 
71
73
  const echartsSeries = series.map(s =>
@@ -0,0 +1,65 @@
1
+ import type { FluxIconName, FluxStatisticsChartColor } from '@flux-ui/types';
2
+ import type { EChartsOption } from '~flux/statistics/composable';
3
+ import { resolveChartColor } from '../series';
4
+ import { renderTooltip } from './render';
5
+ import type { SharedTooltipItem, TooltipParam, TooltipStyleClasses, Translator } from './types';
6
+
7
+ export interface CandlestickTooltipPoint {
8
+ readonly label?: string;
9
+ readonly open: number;
10
+ readonly close: number;
11
+ readonly low: number;
12
+ readonly high: number;
13
+ }
14
+
15
+ export interface CandlestickTooltipInput {
16
+ readonly t: Translator;
17
+ readonly styles: TooltipStyleClasses;
18
+ readonly getSeries: () => readonly { readonly name?: string; readonly icon?: FluxIconName; readonly positiveColor?: FluxStatisticsChartColor; readonly negativeColor?: FluxStatisticsChartColor; readonly data: readonly CandlestickTooltipPoint[] }[];
19
+ }
20
+
21
+ export function buildCandlestickTooltip(input: CandlestickTooltipInput): EChartsOption {
22
+ const { t, styles, getSeries } = input;
23
+
24
+ const formatter = (params: TooltipParam | TooltipParam[]): string => {
25
+ const param = Array.isArray(params) ? params[0] : params;
26
+
27
+ if (!param) {
28
+ return '';
29
+ }
30
+
31
+ const series = getSeries();
32
+ const seriesIndex = param.seriesIndex ?? 0;
33
+ const dataIndex = param.dataIndex ?? 0;
34
+ const s = series[seriesIndex];
35
+ const point = s?.data[dataIndex];
36
+
37
+ if (!s || !point) {
38
+ return '';
39
+ }
40
+
41
+ const positive = resolveChartColor(s.positiveColor) ?? 'var(--success-500)';
42
+ const negative = resolveChartColor(s.negativeColor) ?? 'var(--danger-500)';
43
+ const color = point.close >= point.open ? positive : negative;
44
+ const seriesName = s.name ? t(String(s.name)) : '';
45
+ const pointLabel = point.label ? t(String(point.label)) : '';
46
+ const title = [seriesName, pointLabel].filter(Boolean).join(' — ');
47
+
48
+ const items: SharedTooltipItem[] = [
49
+ { name: 'Open', value: point.open, color },
50
+ { name: 'Close', value: point.close, color },
51
+ { name: 'Lowest', value: point.low, color },
52
+ { name: 'Highest', value: point.high, color }
53
+ ];
54
+
55
+ return renderTooltip(t, styles, title, items);
56
+ };
57
+
58
+ return {
59
+ tooltip: {
60
+ show: true,
61
+ trigger: 'item',
62
+ formatter: formatter as never
63
+ }
64
+ };
65
+ }
@@ -5,7 +5,9 @@ import type { SharedTooltipItem, TooltipParam, TooltipStyleClasses, Translator }
5
5
  export interface TreemapTooltipNode {
6
6
  readonly name: string;
7
7
  readonly value?: number;
8
- readonly color?: string;
8
+ readonly itemStyle?: {
9
+ readonly color?: string;
10
+ };
9
11
  }
10
12
 
11
13
  export interface TreemapTooltipInput {
@@ -29,7 +31,7 @@ export function buildTreemapTooltip(input: TreemapTooltipInput): EChartsOption {
29
31
  return '';
30
32
  }
31
33
 
32
- const color = data.color ?? 'var(--primary-600)';
34
+ const color = data.itemStyle?.color ?? param.color ?? 'var(--primary-600)';
33
35
  const title = data.name ? t(String(data.name)) : '';
34
36
 
35
37
  const items: SharedTooltipItem[] = [
@@ -1,5 +1,7 @@
1
1
  export type { BoxPlotTooltipInput, BoxPlotTooltipPoint } from './buildBoxPlotTooltip';
2
2
  export { buildBoxPlotTooltip } from './buildBoxPlotTooltip';
3
+ export type { CandlestickTooltipInput, CandlestickTooltipPoint } from './buildCandlestickTooltip';
4
+ export { buildCandlestickTooltip } from './buildCandlestickTooltip';
3
5
  export type { CartesianTooltipInput } from './buildCartesianTooltip';
4
6
  export { buildCartesianTooltip } from './buildCartesianTooltip';
5
7
  export type { GaugeTooltipInput } from './buildGaugeTooltip';