vccs 0.5.0 → 0.6.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.
Files changed (38) hide show
  1. package/dist/cartesian/cartesian-axis/CartesianAxis.d.ts +1 -1
  2. package/dist/cartesian/cartesianPositionToCSSTranslate.d.ts +15 -0
  3. package/dist/cartesian/getCartesianPosition.d.ts +45 -0
  4. package/dist/cartesian/index.d.ts +1 -0
  5. package/dist/cartesian/type.d.ts +8 -0
  6. package/dist/components/Tooltip.d.ts +1 -1
  7. package/dist/components/legend/hooks/useLegend.d.ts +3 -0
  8. package/dist/components/legend/type.d.ts +18 -2
  9. package/dist/components/legend/utils.d.ts +9 -0
  10. package/dist/es/cartesian/axis/XAxis.mjs +11 -5
  11. package/dist/es/cartesian/axis/YAxis.mjs +11 -5
  12. package/dist/es/cartesian/cartesianPositionToCSSTranslate.mjs +28 -0
  13. package/dist/es/cartesian/getCartesianPosition.mjs +176 -0
  14. package/dist/es/chart/ChartsWrapper.mjs +6 -10
  15. package/dist/es/chart/generateCategoricalChart.mjs +32 -53
  16. package/dist/es/components/legend/Legend.mjs +5 -3
  17. package/dist/es/components/legend/hooks/useLegend.mjs +50 -9
  18. package/dist/es/components/legend/type.mjs +9 -1
  19. package/dist/es/components/legend/utils.mjs +12 -1
  20. package/dist/es/container/ResponsiveContainer.vue_vue_type_script_setup_true_lang.mjs +2 -11
  21. package/dist/es/hooks/useResponsiveSize.mjs +27 -0
  22. package/dist/es/hooks/useRoundedSize.mjs +31 -0
  23. package/dist/es/shape/Sector.mjs +1 -1
  24. package/dist/es/state/legendSlice.mjs +2 -0
  25. package/dist/es/state/selectors/axisSelectors.mjs +1 -1
  26. package/dist/es/state/selectors/barSelectors.mjs +1 -1
  27. package/dist/es/state/selectors/pieSelectors.mjs +1 -1
  28. package/dist/es/state/selectors/polarAxisSelectors.mjs +1 -1
  29. package/dist/es/state/selectors/radialBarSelectors.mjs +1 -1
  30. package/dist/es/state/selectors/selectLegendArea.mjs +19 -0
  31. package/dist/es/state/selectors/tooltipSelectors.mjs +1 -1
  32. package/dist/es/utils/chart.mjs +1 -1
  33. package/dist/es/utils/legend.mjs +23 -1
  34. package/dist/hooks/useResponsiveSize.d.ts +22 -0
  35. package/dist/hooks/useRoundedSize.d.ts +22 -0
  36. package/dist/state/legendSlice.d.ts +3 -0
  37. package/dist/state/selectors/selectLegendArea.d.ts +7 -0
  38. package/package.json +1 -1
@@ -205,9 +205,9 @@ export declare const CartesianAxis: import('vue').DefineComponent<import('vue').
205
205
  height: number;
206
206
  x: number;
207
207
  y: number;
208
+ viewBox: Record<string, any>;
208
209
  orientation: string;
209
210
  stroke: string;
210
- viewBox: Record<string, any>;
211
211
  label: any;
212
212
  ticks: readonly CartesianTickItem[];
213
213
  hide: boolean;
@@ -0,0 +1,15 @@
1
+ import { TextAnchor, TextVerticalAnchor } from './getCartesianPosition';
2
+ /**
3
+ * CalculatedCartesianPosition returns horizontalAnchor and verticalAnchor
4
+ * which work directly with an SVG text element
5
+ * but for use in HTML context we have to translate them into CSS transform(translate)
6
+ * so this is what this function is doing.
7
+ *
8
+ * horizontalPosition is type: 'start' | 'middle' | 'end' | 'inherit'
9
+ * verticalPosition is type: 'start' | 'middle' | 'end'
10
+ *
11
+ * @param horizontalPosition as returned from {@link getCartesianPosition}
12
+ * @param verticalPosition as returned from {@link getCartesianPosition}
13
+ * @return CSS transform string
14
+ */
15
+ export declare function cartesianPositionToCSSTranslate(horizontalPosition: TextAnchor, verticalPosition: TextVerticalAnchor): string;
@@ -0,0 +1,45 @@
1
+ import { CartesianViewBoxRequired, TrapezoidViewBox } from './type';
2
+ export type TextAnchor = 'start' | 'middle' | 'end' | 'inherit';
3
+ export type TextVerticalAnchor = 'start' | 'middle' | 'end';
4
+ export declare function cartesianViewBoxToTrapezoid(box: undefined): undefined;
5
+ export declare function cartesianViewBoxToTrapezoid(box: CartesianViewBoxRequired | TrapezoidViewBox): TrapezoidViewBox;
6
+ /**
7
+ * @inline
8
+ */
9
+ export type CartesianPosition = 'top' | 'left' | 'right' | 'bottom' | 'center' | 'insideLeft' | 'insideRight' | 'insideTop' | 'insideBottom' | 'insideTopLeft' | 'insideBottomLeft' | 'insideTopRight' | 'insideBottomRight' | {
10
+ x: number | string;
11
+ y: number | string;
12
+ };
13
+ export type GetCartesianPositionOptions = {
14
+ viewBox: TrapezoidViewBox | CartesianViewBoxRequired;
15
+ parentViewBox?: CartesianViewBoxRequired;
16
+ /**
17
+ * The offset to the specified "position". Direction of the offset depends on the position.
18
+ */
19
+ offset?: number;
20
+ /**
21
+ * The position of the element relative to the view box.
22
+ */
23
+ position?: CartesianPosition;
24
+ /**
25
+ * If true, the returned width and height will be clamped to keep the element within the parentViewBox.
26
+ * This is useful for preventing labels from overflowing the chart area.
27
+ */
28
+ clamp?: boolean;
29
+ };
30
+ export type CalculatedCartesianPosition = {
31
+ x: number;
32
+ y: number;
33
+ horizontalAnchor: TextAnchor;
34
+ verticalAnchor: TextVerticalAnchor;
35
+ width?: number;
36
+ height?: number;
37
+ };
38
+ /**
39
+ * Calculates the position and alignment for a generic element in a Cartesian coordinate system.
40
+ *
41
+ * @param options - The options including viewBox, position, and offset.
42
+ * @returns The calculated x, y, alignment and size.
43
+ */
44
+ export declare function getCartesianPosition(options: GetCartesianPositionOptions): CalculatedCartesianPosition;
45
+ export declare function isOutsidePosition(position: CartesianPosition | undefined): boolean;
@@ -11,3 +11,4 @@ export * from './reference-line';
11
11
  export * from './scatter';
12
12
  export * from './z-axis';
13
13
  export * from './funnel';
14
+ export type { CartesianPosition } from './getCartesianPosition';
@@ -18,4 +18,12 @@ export interface CartesianViewBox {
18
18
  }
19
19
  export type ViewBox = CartesianViewBox | PolarViewBox;
20
20
  export type CartesianViewBoxRequired = Required<CartesianViewBox>;
21
+ export interface TrapezoidViewBox {
22
+ x: number;
23
+ y: number;
24
+ upperWidth: number;
25
+ lowerWidth: number;
26
+ width: number;
27
+ height: number;
28
+ }
21
29
  export type Sign = 0 | 1 | -1;
@@ -268,8 +268,8 @@ export declare const Tooltip: import('vue').DefineComponent<import('vue').Extrac
268
268
  readonly default: " : ";
269
269
  };
270
270
  }>> & Readonly<{}>, {
271
- readonly cursor: boolean | object;
272
271
  readonly offset: number;
272
+ readonly cursor: boolean | object;
273
273
  readonly style: Record<string, any>;
274
274
  readonly isAnimationActive: boolean;
275
275
  readonly includeHidden: boolean;
@@ -1,4 +1,5 @@
1
1
  import { CSSProperties } from 'vue';
2
+ import { LayoutType } from '../../../types';
2
3
  import { LegendPayload } from '../../DefaultLegendContent';
3
4
  import { LegendProps } from '../type';
4
5
  export declare function useLegend(props: LegendProps): {
@@ -14,6 +15,8 @@ export declare function useLegend(props: LegendProps): {
14
15
  processedPayload: import('vue').ComputedRef<LegendPayload[]>;
15
16
  outerStyle: import('vue').ComputedRef<CSSProperties>;
16
17
  legendPortal: import('vue').ComputedRef<HTMLElement>;
18
+ resolvedLayout: import('vue').ComputedRef<LayoutType>;
19
+ positionViewBox: import('vue').ComputedRef<Required<import('../../../cartesian/type').CartesianViewBox>>;
17
20
  syncSettings: () => void;
18
21
  syncSize: () => void;
19
22
  };
@@ -1,5 +1,6 @@
1
1
  import { CSSProperties, PropType, VNode } from 'vue';
2
2
  import { LayoutType, Margin } from '../../types';
3
+ import { CartesianPosition } from '../../cartesian/getCartesianPosition';
3
4
  import { LegendType } from '../../types/legend';
4
5
  import { HorizontalAlignmentType, LegendPayload, VerticalAlignmentType } from '../DefaultLegendContent';
5
6
  import { VuePropsToType, WithSVGProps } from '../../types/common';
@@ -8,8 +9,8 @@ export interface LegendSlots {
8
9
  }
9
10
  export declare const LegendVueProps: {
10
11
  readonly layout: {
11
- readonly type: PropType<LayoutType>;
12
- readonly default: "horizontal";
12
+ readonly type: PropType<LayoutType | "auto">;
13
+ readonly default: "auto";
13
14
  };
14
15
  readonly align: {
15
16
  readonly type: PropType<HorizontalAlignmentType>;
@@ -19,6 +20,21 @@ export declare const LegendVueProps: {
19
20
  readonly type: PropType<VerticalAlignmentType>;
20
21
  readonly default: "bottom";
21
22
  };
23
+ /**
24
+ * The position of the legend relative to the chart.
25
+ * If this is defined, it overrides `align` and `verticalAlign`.
26
+ */
27
+ readonly position: {
28
+ readonly type: PropType<CartesianPosition>;
29
+ readonly default: any;
30
+ };
31
+ /**
32
+ * The offset to the specified `position`. Direction of the offset depends on the position.
33
+ */
34
+ readonly offset: {
35
+ readonly type: NumberConstructor;
36
+ readonly default: 0;
37
+ };
22
38
  readonly width: NumberConstructor;
23
39
  readonly height: NumberConstructor;
24
40
  readonly iconSize: {
@@ -1,11 +1,20 @@
1
1
  import { CSSProperties } from 'vue';
2
2
  import { LayoutType, Margin } from '../../types';
3
+ import { CartesianPosition } from '../../cartesian/getCartesianPosition';
3
4
  import { HorizontalAlignmentType, VerticalAlignmentType } from '../DefaultLegendContent';
4
5
  type PositionInput = {
5
6
  layout?: LayoutType;
6
7
  align?: HorizontalAlignmentType;
7
8
  verticalAlign?: VerticalAlignmentType;
8
9
  };
10
+ export declare function getLayoutForPosition(position: CartesianPosition | undefined): LayoutType;
11
+ export declare function getOutsidePositionOffset(position: CartesianPosition | undefined, offset: number, box: {
12
+ width: number;
13
+ height: number;
14
+ }): {
15
+ top?: number;
16
+ left?: number;
17
+ };
9
18
  export declare function getDefaultPosition(style: CSSProperties | undefined, props: PositionInput, margin: Margin, chartWidth: number, chartHeight: number, box: {
10
19
  width: number;
11
20
  height: number;
@@ -4,7 +4,7 @@ import { implicitXAxis, selectAxisScale, selectTicksOfAxis, selectXAxisPosition,
4
4
  import { useAppDispatch, useAppSelector } from "../../state/hooks.mjs";
5
5
  import { addXAxis, removeXAxis } from "../../state/cartesianAxisSlice.mjs";
6
6
  import { CartesianAxis } from "../cartesian-axis/CartesianAxis.mjs";
7
- import { createVNode, defineComponent, mergeProps, watchEffect } from "vue";
7
+ import { createVNode, defineComponent, mergeProps, onUnmounted, watchEffect } from "vue";
8
8
  //#region src/cartesian/axis/XAxis.tsx
9
9
  /**
10
10
  * @fileOverview X Axis
@@ -78,7 +78,8 @@ var XAxisSettingsDispatcher = /* @__PURE__ */ defineComponent({
78
78
  },
79
79
  setup(props, { slots: dispatcherSlots }) {
80
80
  const dispatch = useAppDispatch();
81
- watchEffect((onCleanup) => {
81
+ let registeredSettings;
82
+ watchEffect(() => {
82
83
  const settings = {
83
84
  interval: props.interval ?? "preserveEnd",
84
85
  id: props.xAxisId,
@@ -105,10 +106,15 @@ var XAxisSettingsDispatcher = /* @__PURE__ */ defineComponent({
105
106
  tick: props.tick ?? true,
106
107
  tickFormatter: props.tickFormatter
107
108
  };
109
+ if (registeredSettings && registeredSettings.id !== settings.id) dispatch(removeXAxis(registeredSettings));
108
110
  dispatch(addXAxis(settings));
109
- onCleanup(() => {
110
- dispatch(removeXAxis(settings));
111
- });
111
+ registeredSettings = settings;
112
+ });
113
+ onUnmounted(() => {
114
+ if (registeredSettings) {
115
+ dispatch(removeXAxis(registeredSettings));
116
+ registeredSettings = void 0;
117
+ }
112
118
  });
113
119
  return () => createVNode(XAxisImpl, props, { default: () => [dispatcherSlots.tick ? { tick: dispatcherSlots.tick } : void 0] });
114
120
  }
@@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from "../../state/hooks.mjs";
5
5
  import { addYAxis, removeYAxis, updateYAxisWidth } from "../../state/cartesianAxisSlice.mjs";
6
6
  import { CartesianAxis } from "../cartesian-axis/CartesianAxis.mjs";
7
7
  import { getCalculatedYAxisWidth } from "../../utils/YAxisUtils.mjs";
8
- import { createVNode, defineComponent, isVNode, mergeProps, nextTick, ref, watch, watchEffect } from "vue";
8
+ import { createVNode, defineComponent, isVNode, mergeProps, nextTick, onUnmounted, ref, watch, watchEffect } from "vue";
9
9
  //#region src/cartesian/axis/YAxis.tsx
10
10
  var YAxisImpl = /* @__PURE__ */ defineComponent({
11
11
  props: { yAxisId: {
@@ -125,7 +125,8 @@ var YAxisSettingsDispatcher = /* @__PURE__ */ defineComponent({
125
125
  },
126
126
  setup(props) {
127
127
  const dispatch = useAppDispatch();
128
- watchEffect((onCleanup) => {
128
+ let registeredSettings;
129
+ watchEffect(() => {
129
130
  const settings = {
130
131
  ...props,
131
132
  interval: props.interval ?? "preserveEnd",
@@ -136,10 +137,15 @@ var YAxisSettingsDispatcher = /* @__PURE__ */ defineComponent({
136
137
  minTickGap: props.minTickGap ?? 5,
137
138
  tick: props.tick ?? true
138
139
  };
140
+ if (registeredSettings && registeredSettings.id !== settings.id) dispatch(removeYAxis(registeredSettings));
139
141
  dispatch(addYAxis(settings));
140
- onCleanup(() => {
141
- dispatch(removeYAxis(settings));
142
- });
142
+ registeredSettings = settings;
143
+ });
144
+ onUnmounted(() => {
145
+ if (registeredSettings) {
146
+ dispatch(removeYAxis(registeredSettings));
147
+ registeredSettings = void 0;
148
+ }
143
149
  });
144
150
  return () => createVNode(YAxisImpl, props, null);
145
151
  }
@@ -0,0 +1,28 @@
1
+ //#region src/cartesian/cartesianPositionToCSSTranslate.ts
2
+ /**
3
+ * CalculatedCartesianPosition returns horizontalAnchor and verticalAnchor
4
+ * which work directly with an SVG text element
5
+ * but for use in HTML context we have to translate them into CSS transform(translate)
6
+ * so this is what this function is doing.
7
+ *
8
+ * horizontalPosition is type: 'start' | 'middle' | 'end' | 'inherit'
9
+ * verticalPosition is type: 'start' | 'middle' | 'end'
10
+ *
11
+ * @param horizontalPosition as returned from {@link getCartesianPosition}
12
+ * @param verticalPosition as returned from {@link getCartesianPosition}
13
+ * @return CSS transform string
14
+ */
15
+ function cartesianPositionToCSSTranslate(horizontalPosition, verticalPosition) {
16
+ if (horizontalPosition === "start" && verticalPosition === "start") return "";
17
+ return `translate(${horizontalPosition === "inherit" ? "0" : {
18
+ start: "0",
19
+ middle: "-50%",
20
+ end: "-100%"
21
+ }[horizontalPosition]}, ${{
22
+ start: "0",
23
+ middle: "-50%",
24
+ end: "-100%"
25
+ }[verticalPosition] ?? "0"})`;
26
+ }
27
+ //#endregion
28
+ export { cartesianPositionToCSSTranslate };
@@ -0,0 +1,176 @@
1
+ import { isNumber, isPercent } from "../utils/validate.mjs";
2
+ import { getPercentValue } from "../utils/data.mjs";
3
+ //#region src/cartesian/getCartesianPosition.ts
4
+ function cartesianViewBoxToTrapezoid(box) {
5
+ if (!box) return;
6
+ return {
7
+ x: box.x,
8
+ y: box.y,
9
+ upperWidth: "upperWidth" in box ? box.upperWidth : box.width,
10
+ lowerWidth: "lowerWidth" in box ? box.lowerWidth : box.width,
11
+ width: box.width,
12
+ height: box.height
13
+ };
14
+ }
15
+ /**
16
+ * Calculates the position and alignment for a generic element in a Cartesian coordinate system.
17
+ *
18
+ * @param options - The options including viewBox, position, and offset.
19
+ * @returns The calculated x, y, alignment and size.
20
+ */
21
+ function getCartesianPosition(options) {
22
+ const { viewBox, position, offset = 0, parentViewBox, clamp } = options;
23
+ const { x, y, height, upperWidth, lowerWidth } = cartesianViewBoxToTrapezoid(viewBox);
24
+ const upperX = x;
25
+ const lowerX = x + (upperWidth - lowerWidth) / 2;
26
+ const middleX = (upperX + lowerX) / 2;
27
+ const midHeightWidth = (upperWidth + lowerWidth) / 2;
28
+ const centerX = upperX + upperWidth / 2;
29
+ const verticalSign = height >= 0 ? 1 : -1;
30
+ const verticalOffset = verticalSign * offset;
31
+ const verticalEnd = verticalSign > 0 ? "end" : "start";
32
+ const verticalStart = verticalSign > 0 ? "start" : "end";
33
+ const horizontalSign = upperWidth >= 0 ? 1 : -1;
34
+ const horizontalOffset = horizontalSign * offset;
35
+ const horizontalEnd = horizontalSign > 0 ? "end" : "start";
36
+ const horizontalStart = horizontalSign > 0 ? "start" : "end";
37
+ if (position === "top") {
38
+ const result = {
39
+ x: upperX + upperWidth / 2,
40
+ y: y - verticalOffset,
41
+ horizontalAnchor: "middle",
42
+ verticalAnchor: verticalEnd
43
+ };
44
+ if (clamp && parentViewBox) {
45
+ result.height = Math.max(y - parentViewBox.y, 0);
46
+ result.width = upperWidth;
47
+ }
48
+ return result;
49
+ }
50
+ if (position === "bottom") {
51
+ const result = {
52
+ x: lowerX + lowerWidth / 2,
53
+ y: y + height + verticalOffset,
54
+ horizontalAnchor: "middle",
55
+ verticalAnchor: verticalStart
56
+ };
57
+ if (clamp && parentViewBox) {
58
+ result.height = Math.max(parentViewBox.y + parentViewBox.height - (y + height), 0);
59
+ result.width = lowerWidth;
60
+ }
61
+ return result;
62
+ }
63
+ if (position === "left") {
64
+ const result = {
65
+ x: middleX - horizontalOffset,
66
+ y: y + height / 2,
67
+ horizontalAnchor: horizontalEnd,
68
+ verticalAnchor: "middle"
69
+ };
70
+ if (clamp && parentViewBox) {
71
+ result.width = Math.max(result.x - parentViewBox.x, 0);
72
+ result.height = height;
73
+ }
74
+ return result;
75
+ }
76
+ if (position === "right") {
77
+ const result = {
78
+ x: middleX + midHeightWidth + horizontalOffset,
79
+ y: y + height / 2,
80
+ horizontalAnchor: horizontalStart,
81
+ verticalAnchor: "middle"
82
+ };
83
+ if (clamp && parentViewBox) {
84
+ result.width = Math.max(parentViewBox.x + parentViewBox.width - result.x, 0);
85
+ result.height = height;
86
+ }
87
+ return result;
88
+ }
89
+ const sizeAttrs = clamp && parentViewBox ? {
90
+ width: midHeightWidth,
91
+ height
92
+ } : {};
93
+ if (position === "insideLeft") return {
94
+ x: middleX + horizontalOffset,
95
+ y: y + height / 2,
96
+ horizontalAnchor: horizontalStart,
97
+ verticalAnchor: "middle",
98
+ ...sizeAttrs
99
+ };
100
+ if (position === "insideRight") return {
101
+ x: middleX + midHeightWidth - horizontalOffset,
102
+ y: y + height / 2,
103
+ horizontalAnchor: horizontalEnd,
104
+ verticalAnchor: "middle",
105
+ ...sizeAttrs
106
+ };
107
+ if (position === "insideTop") return {
108
+ x: upperX + upperWidth / 2,
109
+ y: y + verticalOffset,
110
+ horizontalAnchor: "middle",
111
+ verticalAnchor: verticalStart,
112
+ ...sizeAttrs
113
+ };
114
+ if (position === "insideBottom") return {
115
+ x: lowerX + lowerWidth / 2,
116
+ y: y + height - verticalOffset,
117
+ horizontalAnchor: "middle",
118
+ verticalAnchor: verticalEnd,
119
+ ...sizeAttrs
120
+ };
121
+ if (position === "insideTopLeft") return {
122
+ x: upperX + horizontalOffset,
123
+ y: y + verticalOffset,
124
+ horizontalAnchor: horizontalStart,
125
+ verticalAnchor: verticalStart,
126
+ ...sizeAttrs
127
+ };
128
+ if (position === "insideTopRight") return {
129
+ x: upperX + upperWidth - horizontalOffset,
130
+ y: y + verticalOffset,
131
+ horizontalAnchor: horizontalEnd,
132
+ verticalAnchor: verticalStart,
133
+ ...sizeAttrs
134
+ };
135
+ if (position === "insideBottomLeft") return {
136
+ x: lowerX + horizontalOffset,
137
+ y: y + height - verticalOffset,
138
+ horizontalAnchor: horizontalStart,
139
+ verticalAnchor: verticalEnd,
140
+ ...sizeAttrs
141
+ };
142
+ if (position === "insideBottomRight") return {
143
+ x: lowerX + lowerWidth - horizontalOffset,
144
+ y: y + height - verticalOffset,
145
+ horizontalAnchor: horizontalEnd,
146
+ verticalAnchor: verticalEnd,
147
+ ...sizeAttrs
148
+ };
149
+ if (!!position && typeof position === "object" && (isNumber(position.x) || isPercent(position.x)) && (isNumber(position.y) || isPercent(position.y))) return {
150
+ x: x + getPercentValue(position.x, midHeightWidth),
151
+ y: y + getPercentValue(position.y, height),
152
+ horizontalAnchor: "end",
153
+ verticalAnchor: "end",
154
+ ...sizeAttrs
155
+ };
156
+ return {
157
+ x: centerX,
158
+ y: y + height / 2,
159
+ horizontalAnchor: "middle",
160
+ verticalAnchor: "middle",
161
+ ...sizeAttrs
162
+ };
163
+ }
164
+ var absolutePositions = [
165
+ "top",
166
+ "left",
167
+ "right",
168
+ "bottom"
169
+ ];
170
+ function isOutsidePosition(position) {
171
+ if (position == null) return false;
172
+ if (typeof position === "object") return true;
173
+ return absolutePositions.includes(position);
174
+ }
175
+ //#endregion
176
+ export { getCartesianPosition, isOutsidePosition };
@@ -10,7 +10,7 @@ import { useSynchronisedEventsFromOtherCharts } from "../events/useChartSynchron
10
10
  import { useReportScale } from "../state/utils/useReportScale.mjs";
11
11
  import { providePortalRaw } from "./TooltipPortalContext.mjs";
12
12
  import { provideLegendPortalRaw } from "./LegendPortalContext.mjs";
13
- import { createVNode, defineComponent, onMounted, ref } from "vue";
13
+ import { createVNode, defineComponent, ref, watch } from "vue";
14
14
  import { useResizeObserver } from "@vueuse/core";
15
15
  //#region src/chart/ChartsWrapper.tsx
16
16
  var ChartsWrapper = /* @__PURE__ */ defineComponent({
@@ -62,17 +62,13 @@ var ChartsWrapper = /* @__PURE__ */ defineComponent({
62
62
  legendPortal.value = node;
63
63
  wrapperEl.value = node;
64
64
  };
65
- if (props.responsive) {
66
- useResizeObserver(wrapperEl, (entries) => {
65
+ watch(() => props.responsive, (responsive, _, onCleanup) => {
66
+ if (!responsive) return;
67
+ onCleanup(useResizeObserver(wrapperEl, (entries) => {
67
68
  const { width, height } = entries[0].contentRect;
68
69
  props.onResize?.(width, height);
69
- });
70
- onMounted(() => {
71
- if (!wrapperEl.value) return;
72
- const { width, height } = wrapperEl.value.getBoundingClientRect();
73
- props.onResize?.(width, height);
74
- });
75
- }
70
+ }).stop);
71
+ }, { immediate: true });
76
72
  const myOnClick = (e) => {
77
73
  const chartPointer = getChartPointer(e);
78
74
  if (chartPointer) dispatch(mouseClickAction(chartPointer));
@@ -1,5 +1,4 @@
1
1
  import { classProp } from "../types/common.mjs";
2
- import { validateWidthHeight } from "../utils/validate.mjs";
3
2
  import { provideClipPathId } from "./provideClipPathId.mjs";
4
3
  import { createRechartsStore } from "../state/store.mjs";
5
4
  import Surface_default from "./Surface.mjs";
@@ -11,7 +10,8 @@ import { ReportMainChartProps } from "../state/ReportMainChartProps.mjs";
11
10
  import ReportChartProps_default from "../state/ReportChartProps.mjs";
12
11
  import { applyDefaultProps } from "../utils/props.mjs";
13
12
  import { ReportPolarOptions } from "../state/ReportPolarOptions.mjs";
14
- import { Fragment, createVNode, defineComponent, mergeProps, ref } from "vue";
13
+ import { useResponsiveSize } from "../hooks/useResponsiveSize.mjs";
14
+ import { Fragment, createVNode, defineComponent, mergeProps } from "vue";
15
15
  import { provideStore } from "@reduxjs/vue-redux";
16
16
  //#region src/chart/generateCategoricalChart.tsx
17
17
  var defaultLayout = "horizontal";
@@ -106,57 +106,43 @@ function generateCategoricalChart({ chartName, defaultProps = {}, defaultTooltip
106
106
  eventEmitter: void 0
107
107
  } }, props.id ?? chartName) });
108
108
  const clipPathId = provideClipPathId(props);
109
- const responsiveSize = ref({
110
- width: 0,
111
- height: 0
112
- });
113
- function handleResize(width, height) {
114
- const roundedWidth = Math.round(width);
115
- const roundedHeight = Math.round(height);
116
- if (responsiveSize.value.width === roundedWidth && responsiveSize.value.height === roundedHeight) return;
117
- responsiveSize.value = {
118
- width: roundedWidth,
119
- height: roundedHeight
120
- };
109
+ const { effectiveWidth, effectiveHeight, hasValidSize, handleResize } = useResponsiveSize(props);
110
+ function renderPolarOptions(isPolarChart) {
111
+ if (!isPolarChart) return null;
112
+ return createVNode(ReportPolarOptions, {
113
+ "cx": props.cx ?? "50%",
114
+ "cy": props.cy ?? "50%",
115
+ "startAngle": props.startAngle ?? defaultProps.startAngle ?? 90,
116
+ "endAngle": props.endAngle ?? defaultProps.endAngle ?? -270,
117
+ "innerRadius": props.innerRadius ?? 0,
118
+ "outerRadius": props.outerRadius ?? "80%"
119
+ }, null);
121
120
  }
122
121
  return () => {
123
122
  const { compact, width, height, title, desc, responsive, ...rest } = props;
124
123
  const attributes = { ...attrs };
125
- const effectiveWidth = responsive ? responsiveSize.value.width : width;
126
- const effectiveHeight = responsive ? responsiveSize.value.height : height;
127
- const hasValidSize = validateWidthHeight({
128
- width: effectiveWidth,
129
- height: effectiveHeight
130
- });
131
124
  const layout = props.layout ?? defaultProps.layout ?? defaultLayout;
132
125
  const isPolarChart = layout === "centric" || layout === "radial";
133
126
  if (compact) {
134
- if (!hasValidSize) return null;
127
+ if (!hasValidSize.value) return null;
135
128
  return createVNode(Fragment, null, [
136
129
  createVNode(ChartDataContextProvider, { "chartData": props.data }, null),
137
130
  createVNode(ReportMainChartProps, {
138
- "width": effectiveWidth,
139
- "height": effectiveHeight,
131
+ "width": effectiveWidth.value,
132
+ "height": effectiveHeight.value,
140
133
  "layout": layout,
141
- "margin": props.margin ?? defaultMargin
142
- }, null),
143
- isPolarChart && createVNode(ReportPolarOptions, {
144
- "cx": props.cx ?? "50%",
145
- "cy": props.cy ?? "50%",
146
- "startAngle": props.startAngle ?? defaultProps.startAngle ?? 90,
147
- "endAngle": props.endAngle ?? defaultProps.endAngle ?? -270,
148
- "innerRadius": props.innerRadius ?? 0,
149
- "outerRadius": props.outerRadius ?? "80%"
134
+ "margin": props.margin
150
135
  }, null),
136
+ renderPolarOptions(isPolarChart),
151
137
  createVNode(Surface_default, mergeProps(attrs, rest, {
152
- "width": effectiveWidth,
153
- "height": effectiveHeight,
138
+ "width": effectiveWidth.value,
139
+ "height": effectiveHeight.value,
154
140
  "title": title,
155
141
  "desc": desc
156
142
  }), { default: () => [createVNode(ClipPath_default, { "clipPathId": clipPathId }, null), slots.default?.()] })
157
143
  ]);
158
144
  }
159
- if (!responsive && !hasValidSize) return null;
145
+ if (!responsive && !hasValidSize.value) return null;
160
146
  if (props.accessibilityLayer) {
161
147
  attributes.tabindex = props.tabIndex ?? 0;
162
148
  attributes.role = props.role ?? "application";
@@ -166,36 +152,29 @@ function generateCategoricalChart({ chartName, defaultProps = {}, defaultTooltip
166
152
  for (const [key, value] of Object.entries(attributes)) if (key.startsWith("on") && typeof value === "function") eventHandlerAttrs[key] = value;
167
153
  else svgAttributes[key] = value;
168
154
  return createVNode(Fragment, null, [
169
- hasValidSize && createVNode(ChartDataContextProvider, { "chartData": props.data }, null),
170
- hasValidSize && createVNode(ReportMainChartProps, {
171
- "width": effectiveWidth,
172
- "height": effectiveHeight,
155
+ hasValidSize.value && createVNode(ChartDataContextProvider, { "chartData": props.data }, null),
156
+ hasValidSize.value && createVNode(ReportMainChartProps, {
157
+ "width": effectiveWidth.value,
158
+ "height": effectiveHeight.value,
173
159
  "layout": layout,
174
160
  "margin": props.margin ?? defaultMargin
175
161
  }, null),
176
- hasValidSize && isPolarChart && createVNode(ReportPolarOptions, {
177
- "cx": props.cx ?? "50%",
178
- "cy": props.cy ?? "50%",
179
- "startAngle": props.startAngle ?? defaultProps.startAngle ?? 90,
180
- "endAngle": props.endAngle ?? defaultProps.endAngle ?? -270,
181
- "innerRadius": props.innerRadius ?? 0,
182
- "outerRadius": props.outerRadius ?? "80%"
183
- }, null),
162
+ hasValidSize.value && renderPolarOptions(isPolarChart),
184
163
  createVNode(ChartsWrapper, mergeProps({
185
164
  "responsive": responsive,
186
165
  "onResize": handleResize,
187
166
  "style": props.style,
188
167
  "class": props.class,
189
- "width": effectiveWidth,
190
- "height": effectiveHeight
191
- }, eventHandlerAttrs), { default: () => [hasValidSize && createVNode(Surface_default, mergeProps(svgAttributes, {
192
- "width": effectiveWidth,
193
- "height": effectiveHeight,
168
+ "width": effectiveWidth.value,
169
+ "height": effectiveHeight.value
170
+ }, eventHandlerAttrs), { default: () => [hasValidSize.value && createVNode(Surface_default, mergeProps(svgAttributes, {
171
+ "width": effectiveWidth.value,
172
+ "height": effectiveHeight.value,
194
173
  "title": title,
195
174
  "desc": desc,
196
175
  "style": FULL_WIDTH_AND_HEIGHT
197
176
  }), { default: () => [createVNode(ClipPath_default, { "clipPathId": clipPathId }, null), slots.default?.()] }), slots.tooltip?.()] }),
198
- hasValidSize && createVNode(ReportChartProps_default, props, null)
177
+ hasValidSize.value && createVNode(ReportChartProps_default, props, null)
199
178
  ]);
200
179
  };
201
180
  }
@@ -12,7 +12,7 @@ var Legend_default = /* @__PURE__ */ defineComponent({
12
12
  name: "Legend",
13
13
  props: LegendVueProps,
14
14
  setup(props, { slots }) {
15
- const { legendRef, processedPayload, outerStyle, legendPortal, syncSettings, syncSize } = useLegend(props);
15
+ const { legendRef, processedPayload, outerStyle, legendPortal, resolvedLayout, positionViewBox, syncSettings, syncSize } = useLegend(props);
16
16
  const { getItemStyle, getSvgStyle, formatValue, handleClick, handleMouseEnter, handleMouseLeave } = useLegendContent(props);
17
17
  watchEffect(() => {
18
18
  syncSettings();
@@ -20,7 +20,8 @@ var Legend_default = /* @__PURE__ */ defineComponent({
20
20
  });
21
21
  const renderDefaultContent = () => {
22
22
  if (!processedPayload.value || processedPayload.value.length === 0) return null;
23
- const { layout = "horizontal", align = "center", iconSize = 14 } = props;
23
+ const { align = "center", iconSize = 14 } = props;
24
+ const layout = resolvedLayout.value;
24
25
  const finalStyle = {
25
26
  padding: 0,
26
27
  margin: 0,
@@ -73,13 +74,14 @@ var Legend_default = /* @__PURE__ */ defineComponent({
73
74
  if (!processedPayload.value || processedPayload.value.length === 0) return null;
74
75
  const contentProps = {
75
76
  ...props,
77
+ layout: resolvedLayout.value,
76
78
  payload: processedPayload.value
77
79
  };
78
80
  if (slots.content) return slots.content(contentProps);
79
81
  return renderDefaultContent();
80
82
  };
81
83
  return () => {
82
- if (legendPortal.value == null) return null;
84
+ if (legendPortal.value == null || props.position != null && positionViewBox.value == null) return null;
83
85
  const legendElement = createVNode("div", {
84
86
  "class": "v-charts-legend-wrapper",
85
87
  "style": outerStyle.value,
@@ -1,10 +1,13 @@
1
1
  import { selectLegendPayload } from "../../../state/selectors/legendSelectors.mjs";
2
+ import { getCartesianPosition, isOutsidePosition } from "../../../cartesian/getCartesianPosition.mjs";
2
3
  import { useAppDispatch, useAppSelector } from "../../../state/hooks.mjs";
3
- import { useChartHeight, useChartWidth, useMargin } from "../../../context/chartLayoutContext.mjs";
4
+ import { useChartHeight, useChartWidth, useMargin, useViewBox } from "../../../context/chartLayoutContext.mjs";
4
5
  import { setLegendSettings, setLegendSize } from "../../../state/legendSlice.mjs";
5
6
  import { useLegendPortal } from "../../../chart/LegendPortalContext.mjs";
7
+ import { selectLegendArea } from "../../../state/selectors/selectLegendArea.mjs";
6
8
  import { getUniqPayload } from "../../../utils/payload/getUniqPayload.mjs";
7
- import { defaultUniqBy, getDefaultPosition, getWidthOrHeight } from "../utils.mjs";
9
+ import { cartesianPositionToCSSTranslate } from "../../../cartesian/cartesianPositionToCSSTranslate.mjs";
10
+ import { defaultUniqBy, getDefaultPosition, getLayoutForPosition, getOutsidePositionOffset, getWidthOrHeight } from "../utils.mjs";
8
11
  import { computed, ref } from "vue";
9
12
  import { sortBy } from "es-toolkit/compat";
10
13
  import { useElementBounding } from "@vueuse/core";
@@ -16,14 +19,22 @@ function useLegend(props) {
16
19
  const margin = useMargin();
17
20
  const chartWidth = useChartWidth();
18
21
  const chartHeight = useChartHeight();
22
+ const viewBox = useViewBox();
23
+ const legendArea = useAppSelector(selectLegendArea);
19
24
  const legendRef = ref();
20
25
  const { width: boundingWidth, height: boundingHeight } = useElementBounding(legendRef);
26
+ const resolvedLayout = computed(() => props.layout && props.layout !== "auto" ? props.layout : getLayoutForPosition(props.position));
21
27
  const maxWidth = computed(() => chartWidth.value - (margin.value.left || 0) - (margin.value.right || 0));
22
- const widthOrHeight = computed(() => getWidthOrHeight(props.layout, props.height, props.width, maxWidth.value));
28
+ const widthOrHeight = computed(() => getWidthOrHeight(resolvedLayout.value, props.height, props.width, maxWidth.value));
23
29
  const boundingBox = computed(() => ({
24
30
  width: boundingWidth.value,
25
31
  height: boundingHeight.value
26
32
  }));
33
+ const positionViewBox = computed(() => {
34
+ if (props.position == null) return null;
35
+ return isOutsidePosition(props.position) ? legendArea.value : viewBox.value;
36
+ });
37
+ const shouldReportDimensions = computed(() => props.portal == null && (props.position == null || isOutsidePosition(props.position)));
27
38
  const processedPayload = computed(() => {
28
39
  if (!contextPayload.value || contextPayload.value.length === 0) return [];
29
40
  let finalPayload = getUniqPayload(contextPayload.value, props.payloadUniqBy, defaultUniqBy);
@@ -33,30 +44,58 @@ function useLegend(props) {
33
44
  }
34
45
  return Array.from(finalPayload);
35
46
  });
47
+ const positionStyle = computed(() => {
48
+ if (props.position == null || positionViewBox.value == null) return;
49
+ const positionResult = getCartesianPosition({
50
+ viewBox: positionViewBox.value,
51
+ position: props.position,
52
+ offset: props.offset ?? 0
53
+ });
54
+ const outsidePositionOffset = getOutsidePositionOffset(props.position, props.offset ?? 0, boundingBox.value);
55
+ const positionMaxWidth = resolvedLayout.value === "vertical" ? (positionViewBox.value.width ?? 0) / 2 : positionViewBox.value.width ?? 0;
56
+ const positionMaxHeight = resolvedLayout.value === "horizontal" ? (positionViewBox.value.height ?? 0) / 2 : positionViewBox.value.height ?? 0;
57
+ return {
58
+ width: "max-content",
59
+ height: "max-content",
60
+ maxWidth: `${positionMaxWidth}px`,
61
+ maxHeight: `${positionMaxHeight}px`,
62
+ overflowY: "auto",
63
+ top: `${positionResult.y + (outsidePositionOffset.top ?? 0)}px`,
64
+ left: `${positionResult.x + (outsidePositionOffset.left ?? 0)}px`,
65
+ transform: cartesianPositionToCSSTranslate(positionResult.horizontalAnchor, positionResult.verticalAnchor)
66
+ };
67
+ });
36
68
  const outerStyle = computed(() => {
37
69
  const userStyle = props.wrapperStyle ? { ...props.wrapperStyle } : {};
38
70
  if (props.portal) return userStyle;
39
71
  const baseStyle = {
40
72
  position: "absolute",
41
- width: widthOrHeight.value?.width ? `${widthOrHeight.value.width}px` : props.width ? `${props.width}px` : "auto",
42
- height: widthOrHeight.value?.height ? `${widthOrHeight.value.height}px` : props.height ? `${props.height}px` : "auto"
73
+ width: widthOrHeight.value?.width || props.width || "auto",
74
+ height: widthOrHeight.value?.height || props.height || "auto"
43
75
  };
44
- const positionStyle = getDefaultPosition(userStyle, props, margin.value, chartWidth.value, chartHeight.value, boundingBox.value);
76
+ const calculatedPositionStyle = positionStyle.value ?? getDefaultPosition(userStyle, {
77
+ layout: resolvedLayout.value,
78
+ align: props.align,
79
+ verticalAlign: props.verticalAlign
80
+ }, margin.value, chartWidth.value, chartHeight.value, boundingBox.value);
45
81
  return {
46
82
  ...baseStyle,
47
- ...positionStyle,
83
+ ...calculatedPositionStyle,
48
84
  ...userStyle
49
85
  };
50
86
  });
51
87
  const legendPortal = computed(() => props.portal ?? legendPortalFromContext?.value);
52
88
  const syncSettings = () => {
53
89
  dispatch(setLegendSettings({
54
- layout: props.layout,
90
+ layout: resolvedLayout.value,
55
91
  align: props.align,
56
- verticalAlign: props.verticalAlign
92
+ verticalAlign: props.verticalAlign,
93
+ position: props.position,
94
+ offset: props.offset
57
95
  }));
58
96
  };
59
97
  const syncSize = () => {
98
+ if (!shouldReportDimensions.value) return;
60
99
  dispatch(setLegendSize({
61
100
  width: boundingBox.value.width,
62
101
  height: boundingBox.value.height
@@ -69,6 +108,8 @@ function useLegend(props) {
69
108
  processedPayload,
70
109
  outerStyle,
71
110
  legendPortal,
111
+ resolvedLayout,
112
+ positionViewBox,
72
113
  syncSettings,
73
114
  syncSize
74
115
  };
@@ -2,7 +2,7 @@
2
2
  var LegendVueProps = {
3
3
  layout: {
4
4
  type: String,
5
- default: "horizontal"
5
+ default: "auto"
6
6
  },
7
7
  align: {
8
8
  type: String,
@@ -12,6 +12,14 @@ var LegendVueProps = {
12
12
  type: String,
13
13
  default: "bottom"
14
14
  },
15
+ position: {
16
+ type: [String, Object],
17
+ default: void 0
18
+ },
19
+ offset: {
20
+ type: Number,
21
+ default: 0
22
+ },
15
23
  width: Number,
16
24
  height: Number,
17
25
  iconSize: {
@@ -1,5 +1,16 @@
1
1
  import { isNumber } from "../../utils/validate.mjs";
2
2
  //#region src/components/legend/utils.ts
3
+ function getLayoutForPosition(position) {
4
+ if (position === "left" || position === "right" || position === "insideLeft" || position === "insideRight") return "vertical";
5
+ return "horizontal";
6
+ }
7
+ function getOutsidePositionOffset(position, offset, box) {
8
+ if (position === "top") return { top: box.height + offset };
9
+ if (position === "bottom") return { top: -box.height - offset };
10
+ if (position === "left") return { left: box.width + offset };
11
+ if (position === "right") return { left: -box.width - offset };
12
+ return {};
13
+ }
3
14
  function getDefaultPosition(style, props, margin, chartWidth, chartHeight, box) {
4
15
  const { layout, align, verticalAlign } = props;
5
16
  let hPos = {};
@@ -22,4 +33,4 @@ function defaultUniqBy(entry) {
22
33
  return entry.value;
23
34
  }
24
35
  //#endregion
25
- export { defaultUniqBy, getDefaultPosition, getWidthOrHeight };
36
+ export { defaultUniqBy, getDefaultPosition, getLayoutForPosition, getOutsidePositionOffset, getWidthOrHeight };
@@ -1,5 +1,6 @@
1
1
  import { isPercent } from "../utils/validate.mjs";
2
2
  import { normalizeStyle as normalizeStyle$1 } from "../utils/style.mjs";
3
+ import { useRoundedSize } from "../hooks/useRoundedSize.mjs";
3
4
  import { provideSizeContext } from "./useSizeContext.mjs";
4
5
  import { Fragment, cloneVNode, computed, createElementBlock, createVNode, defineComponent, normalizeClass, normalizeStyle, onMounted, onUnmounted, openBlock, ref, toRef, useSlots } from "vue";
5
6
  import { useThrottleFn } from "@vueuse/core";
@@ -30,7 +31,7 @@ var ResponsiveContainer_vue_vue_type_script_setup_true_lang_default = /* @__PURE
30
31
  const props = __props;
31
32
  const slots = useSlots();
32
33
  const debounce = toRef(props, "debounce");
33
- const sizes = ref({
34
+ const { size: sizes, setSize: setContainerSize } = useRoundedSize({
34
35
  width: props.initialDimension.width,
35
36
  height: props.initialDimension.height
36
37
  });
@@ -57,16 +58,6 @@ var ResponsiveContainer_vue_vue_type_script_setup_true_lang_default = /* @__PURE
57
58
  calculatedWidth: finalCalculatedWidth,
58
59
  calculatedHeight
59
60
  });
60
- function setContainerSize(width, height) {
61
- const roundedWidth = Math.round(width);
62
- const roundedHeight = Math.round(height);
63
- const { width: containerWidth, height: containerHeight } = sizes.value;
64
- if (containerWidth === roundedWidth && containerHeight === roundedHeight) return;
65
- sizes.value = {
66
- width: roundedWidth,
67
- height: roundedHeight
68
- };
69
- }
70
61
  const handleResize = useThrottleFn((entries) => {
71
62
  const { width: containerWidth, height: containerHeight } = entries[0].contentRect;
72
63
  setContainerSize(containerWidth, containerHeight);
@@ -0,0 +1,27 @@
1
+ import { validateWidthHeight } from "../utils/validate.mjs";
2
+ import { useRoundedSize } from "./useRoundedSize.mjs";
3
+ import { computed } from "vue";
4
+ //#region src/hooks/useResponsiveSize.ts
5
+ /**
6
+ * Owns the chart's size resolution: in responsive mode the size is measured
7
+ * from the wrapper div (via `handleResize`), otherwise it comes from props.
8
+ *
9
+ * Render gating (`return null` when the size is invalid) stays with the
10
+ * caller — the rules differ between the compact and normal branches.
11
+ */
12
+ function useResponsiveSize(props) {
13
+ const { size: responsiveSize, setSize: handleResize } = useRoundedSize();
14
+ const effectiveWidth = computed(() => props.responsive ? responsiveSize.value.width : props.width ?? 0);
15
+ const effectiveHeight = computed(() => props.responsive ? responsiveSize.value.height : props.height ?? 0);
16
+ return {
17
+ effectiveWidth,
18
+ effectiveHeight,
19
+ hasValidSize: computed(() => validateWidthHeight({
20
+ width: effectiveWidth.value,
21
+ height: effectiveHeight.value
22
+ })),
23
+ handleResize
24
+ };
25
+ }
26
+ //#endregion
27
+ export { useResponsiveSize };
@@ -0,0 +1,31 @@
1
+ import { ref } from "vue";
2
+ //#region src/hooks/useRoundedSize.ts
3
+ /**
4
+ * Shared size-state core: a size ref plus a setter that rounds to
5
+ * integers and ignores no-change updates.
6
+ *
7
+ * Adapters: `useResponsiveSize` (chart sizing) and `ResponsiveContainer`.
8
+ * ResponsiveContainer is planned for removal (see ADR-0001); when it goes,
9
+ * this module stays behind as the chart's size-state implementation.
10
+ */
11
+ function useRoundedSize(initial = {
12
+ width: 0,
13
+ height: 0
14
+ }) {
15
+ const size = ref(initial);
16
+ function setSize(width, height) {
17
+ const roundedWidth = Math.round(width);
18
+ const roundedHeight = Math.round(height);
19
+ if (size.value.width === roundedWidth && size.value.height === roundedHeight) return;
20
+ size.value = {
21
+ width: roundedWidth,
22
+ height: roundedHeight
23
+ };
24
+ }
25
+ return {
26
+ size,
27
+ setSize
28
+ };
29
+ }
30
+ //#endregion
31
+ export { useRoundedSize };
@@ -1,6 +1,6 @@
1
1
  import { classProp } from "../types/common.mjs";
2
- import { RADIAN, polarToCartesian } from "../utils/polar.mjs";
3
2
  import { getPercentValue, mathSign } from "../utils/data.mjs";
3
+ import { RADIAN, polarToCartesian } from "../utils/polar.mjs";
4
4
  import { createVNode, defineComponent, mergeProps } from "vue";
5
5
  //#region src/shape/Sector.tsx
6
6
  /**
@@ -24,6 +24,8 @@ var legendSlice = createSlice({
24
24
  state.settings.align = action.payload.align;
25
25
  state.settings.layout = action.payload.layout;
26
26
  state.settings.verticalAlign = action.payload.verticalAlign;
27
+ state.settings.position = action.payload.position;
28
+ state.settings.offset = action.payload.offset;
27
29
  },
28
30
  addLegendPayload(state, action) {
29
31
  state.payload.push(castDraft(action.payload));
@@ -2,11 +2,11 @@ import { isCategoricalAxis, isNan, isNumOrStr, isNumber, isWellBehavedNumber } f
2
2
  import { selectChartDataWithIndexes, selectChartDataWithIndexesIfNotInPanorama } from "./dataSelectors.mjs";
3
3
  import { selectChartHeight, selectChartWidth } from "./containerSelectors.mjs";
4
4
  import { selectAllXAxes, selectAllYAxes } from "./selectAllAxes.mjs";
5
+ import { getPercentValue, hasDuplicate, mathSign } from "../../utils/data.mjs";
5
6
  import { selectChartOffset } from "./selectChartOffset.mjs";
6
7
  import { selectBrushDimensions, selectBrushSettings } from "./brushSelectors.mjs";
7
8
  import { selectBarCategoryGap, selectChartName, selectStackOffsetType } from "./rootPropsSelectors.mjs";
8
9
  import { combineAxisRangeWithReverse } from "./combiners/combineAxisRangeWithReverse.mjs";
9
- import { getPercentValue, hasDuplicate, mathSign } from "../../utils/data.mjs";
10
10
  import { selectChartLayout } from "./common.mjs";
11
11
  import { selectAngleAxis, selectAngleAxisRange, selectRadiusAxis, selectRadiusAxisRange } from "./polarAxisSelectors.mjs";
12
12
  import { pickAxisType } from "./pickAxisType.mjs";
@@ -1,8 +1,8 @@
1
1
  import { isNullish } from "../../utils/validate.mjs";
2
2
  import { selectChartDataWithIndexesIfNotInPanorama } from "./dataSelectors.mjs";
3
+ import { getPercentValue } from "../../utils/data.mjs";
3
4
  import { selectChartOffset } from "./selectChartOffset.mjs";
4
5
  import { selectBarCategoryGap, selectBarGap, selectRootBarSize, selectRootMaxBarSize } from "./rootPropsSelectors.mjs";
5
- import { getPercentValue } from "../../utils/data.mjs";
6
6
  import { selectChartLayout } from "./common.mjs";
7
7
  import { getBandSizeOfAxis } from "../../utils/chart.mjs";
8
8
  import { selectAxisWithScale, selectCartesianAxisSize, selectStackGroups, selectTicksOfGraphicalItem, selectUnfilteredCartesianItems } from "./axisSelectors.mjs";
@@ -1,8 +1,8 @@
1
1
  import { isNumber } from "../../utils/validate.mjs";
2
2
  import { selectChartDataAndAlwaysIgnoreIndexes } from "./dataSelectors.mjs";
3
+ import { getPercentValue, mathSign } from "../../utils/data.mjs";
3
4
  import { selectChartOffset } from "./selectChartOffset.mjs";
4
5
  import { getMaxRadius, polarToCartesian } from "../../utils/polar.mjs";
5
- import { getPercentValue, mathSign } from "../../utils/data.mjs";
6
6
  import { getValueByDataKey } from "../../utils/chart.mjs";
7
7
  import { selectUnfilteredPolarItems } from "./polarSelectors.mjs";
8
8
  import { createSelector } from "@reduxjs/toolkit";
@@ -1,8 +1,8 @@
1
1
  import { selectChartHeight, selectChartWidth } from "./containerSelectors.mjs";
2
+ import { getPercentValue } from "../../utils/data.mjs";
2
3
  import { selectChartOffset } from "./selectChartOffset.mjs";
3
4
  import { combineAxisRangeWithReverse } from "./combiners/combineAxisRangeWithReverse.mjs";
4
5
  import { getMaxRadius } from "../../utils/polar.mjs";
5
- import { getPercentValue } from "../../utils/data.mjs";
6
6
  import { selectChartLayout } from "./common.mjs";
7
7
  import { createSelector } from "@reduxjs/toolkit";
8
8
  //#region src/state/selectors/polarAxisSelectors.ts
@@ -1,7 +1,7 @@
1
1
  import { isCategoricalAxis, isNullish } from "../../utils/validate.mjs";
2
2
  import { selectChartDataAndAlwaysIgnoreIndexes, selectChartDataWithIndexes } from "./dataSelectors.mjs";
3
- import { selectBarCategoryGap, selectBarGap, selectRootBarSize, selectRootMaxBarSize, selectStackOffsetType } from "./rootPropsSelectors.mjs";
4
3
  import { mathSign } from "../../utils/data.mjs";
4
+ import { selectBarCategoryGap, selectBarGap, selectRootBarSize, selectRootMaxBarSize, selectStackOffsetType } from "./rootPropsSelectors.mjs";
5
5
  import { selectChartLayout } from "./common.mjs";
6
6
  import { selectAngleAxis, selectPolarViewBox, selectRadiusAxis } from "./polarAxisSelectors.mjs";
7
7
  import { getBandSizeOfAxis, getBaseValueOfBar, getCateCoordinateOfBar, getValueByDataKey, truncateByDomain } from "../../utils/chart.mjs";
@@ -0,0 +1,19 @@
1
+ import { selectChartHeight, selectChartWidth, selectMargin } from "./containerSelectors.mjs";
2
+ import { createSelector } from "@reduxjs/toolkit";
3
+ //#region src/state/selectors/selectLegendArea.ts
4
+ /**
5
+ * The margin-inset chart area. Outside-positioned legends are placed here,
6
+ * beyond any axes.
7
+ */
8
+ var selectLegendArea = createSelector([
9
+ selectChartWidth,
10
+ selectChartHeight,
11
+ selectMargin
12
+ ], (chartWidth, chartHeight, margin) => ({
13
+ x: margin?.left || 0,
14
+ y: margin?.top || 0,
15
+ width: Math.max(chartWidth - (margin?.left || 0) - (margin?.right || 0), 0),
16
+ height: Math.max(chartHeight - (margin?.top || 0) - (margin?.bottom || 0), 0)
17
+ }));
18
+ //#endregion
19
+ export { selectLegendArea };
@@ -1,10 +1,10 @@
1
1
  import { isCategoricalAxis } from "../../utils/validate.mjs";
2
2
  import { selectChartDataWithIndexes } from "./dataSelectors.mjs";
3
3
  import { selectChartHeight, selectChartWidth } from "./containerSelectors.mjs";
4
+ import { findEntryInArray, mathSign } from "../../utils/data.mjs";
4
5
  import { selectChartOffset } from "./selectChartOffset.mjs";
5
6
  import { selectChartName, selectStackOffsetType } from "./rootPropsSelectors.mjs";
6
7
  import { combineAxisRangeWithReverse } from "./combiners/combineAxisRangeWithReverse.mjs";
7
- import { findEntryInArray, mathSign } from "../../utils/data.mjs";
8
8
  import { selectChartLayout } from "./common.mjs";
9
9
  import { getTooltipEntry, getValueByDataKey } from "../../utils/chart.mjs";
10
10
  import { combineAppliedNumericalValuesIncludingErrorValues, combineAppliedValues, combineAreasDomain, combineAxisDomain, combineAxisDomainWithNiceTicks, combineCategoricalDomain, combineDisplayedData, combineDomainOfStackGroups, combineDotsDomain, combineDuplicateDomain, combineGraphicalItemsData, combineGraphicalItemsSettings, combineLinesDomain, combineNiceTicks, combineNumericalDomain, combineRealScaleType, combineScaleFunction, combineStackGroups, filterGraphicalNotStackedItems, filterReferenceElements, getDomainDefinition, itemAxisPredicate, mergeDomains, selectAxisRange, selectAxisSettings, selectHasBar, selectReferenceAreas, selectReferenceDots, selectReferenceLines } from "./axisSelectors.mjs";
@@ -1,6 +1,6 @@
1
1
  import { isNan, isNullish, isNumOrStr, isNumber } from "./validate.mjs";
2
- import { formatAngleOfSector, getAngleOfPoint, polarToCartesian, reverseFormatAngleOfSector } from "./polar.mjs";
3
2
  import { findEntryInArray, mathSign } from "./data.mjs";
3
+ import { formatAngleOfSector, getAngleOfPoint, polarToCartesian, reverseFormatAngleOfSector } from "./polar.mjs";
4
4
  import { toRaw } from "vue";
5
5
  import { stack, stackOffsetExpand, stackOffsetNone, stackOffsetSilhouette, stackOffsetWiggle, stackOrderNone } from "victory-vendor/d3-shape";
6
6
  import { get, isNaN, sortBy } from "lodash-es";
@@ -1,9 +1,31 @@
1
1
  import { isNumber } from "./validate.mjs";
2
+ import { isOutsidePosition } from "../cartesian/getCartesianPosition.mjs";
2
3
  //#region src/utils/legend.ts
3
4
  function appendOffsetOfLegend(offset, legendSettings, legendSize) {
4
5
  if (legendSettings && legendSize) {
5
6
  const { width: boxWidth, height: boxHeight } = legendSize;
6
- const { align, verticalAlign, layout } = legendSettings;
7
+ const { align, verticalAlign, layout, position, offset: legendOffset = 0 } = legendSettings;
8
+ if (position != null) {
9
+ if (isOutsidePosition(position)) {
10
+ if (position === "top" && isNumber(offset.top)) return {
11
+ ...offset,
12
+ top: offset.top + (boxHeight || 0) + legendOffset
13
+ };
14
+ if (position === "bottom" && isNumber(offset.bottom)) return {
15
+ ...offset,
16
+ bottom: offset.bottom + (boxHeight || 0) + legendOffset
17
+ };
18
+ if (position === "left" && isNumber(offset.left)) return {
19
+ ...offset,
20
+ left: offset.left + (boxWidth || 0) + legendOffset
21
+ };
22
+ if (position === "right" && isNumber(offset.right)) return {
23
+ ...offset,
24
+ right: offset.right + (boxWidth || 0) + legendOffset
25
+ };
26
+ }
27
+ return offset;
28
+ }
7
29
  if ((layout === "vertical" || layout === "horizontal" && verticalAlign === "middle") && align !== "center" && isNumber(offset[align])) return {
8
30
  ...offset,
9
31
  [align]: offset[align] + (boxWidth || 0)
@@ -0,0 +1,22 @@
1
+ import { ComputedRef } from 'vue';
2
+ import { CategoricalChartPropsWithOutSvg } from '../chart/generateCategoricalChart';
3
+ type ResponsiveSizeProps = Pick<CategoricalChartPropsWithOutSvg, 'responsive' | 'width' | 'height'>;
4
+ interface UseResponsiveSizeResult {
5
+ /** Final width: measured from the wrapper when `responsive`, otherwise from props. */
6
+ effectiveWidth: ComputedRef<number>;
7
+ /** Final height: measured from the wrapper when `responsive`, otherwise from props. */
8
+ effectiveHeight: ComputedRef<number>;
9
+ /** Whether the effective size passes validateWidthHeight. Render policy is the caller's decision. */
10
+ hasValidSize: ComputedRef<boolean>;
11
+ /** Feeds the wrapper's ResizeObserver callback. Rounds and ignores no-change updates. */
12
+ handleResize: (width: number, height: number) => void;
13
+ }
14
+ /**
15
+ * Owns the chart's size resolution: in responsive mode the size is measured
16
+ * from the wrapper div (via `handleResize`), otherwise it comes from props.
17
+ *
18
+ * Render gating (`return null` when the size is invalid) stays with the
19
+ * caller — the rules differ between the compact and normal branches.
20
+ */
21
+ export declare function useResponsiveSize(props: ResponsiveSizeProps): UseResponsiveSizeResult;
22
+ export {};
@@ -0,0 +1,22 @@
1
+ export interface RoundedSize {
2
+ width: number;
3
+ height: number;
4
+ }
5
+ /**
6
+ * Shared size-state core: a size ref plus a setter that rounds to
7
+ * integers and ignores no-change updates.
8
+ *
9
+ * Adapters: `useResponsiveSize` (chart sizing) and `ResponsiveContainer`.
10
+ * ResponsiveContainer is planned for removal (see ADR-0001); when it goes,
11
+ * this module stays behind as the chart's size-state implementation.
12
+ */
13
+ export declare function useRoundedSize(initial?: RoundedSize): {
14
+ size: import('vue').Ref<{
15
+ width: number;
16
+ height: number;
17
+ }, RoundedSize | {
18
+ width: number;
19
+ height: number;
20
+ }>;
21
+ setSize: (width: number, height: number) => void;
22
+ };
@@ -1,9 +1,12 @@
1
1
  import { HorizontalAlignmentType, LegendPayload, VerticalAlignmentType } from '../components/DefaultLegendContent';
2
+ import { CartesianPosition } from '../cartesian/getCartesianPosition';
2
3
  import { LayoutType, Size } from '../types';
3
4
  export type LegendSettings = {
4
5
  layout: LayoutType;
5
6
  align: HorizontalAlignmentType;
6
7
  verticalAlign: VerticalAlignmentType;
8
+ position?: CartesianPosition;
9
+ offset?: number;
7
10
  };
8
11
  /**
9
12
  * The properties inside this state update independently of each other and quite often.
@@ -0,0 +1,7 @@
1
+ import { CartesianViewBoxRequired } from '../../cartesian/type';
2
+ import { RechartsRootState } from '../store';
3
+ /**
4
+ * The margin-inset chart area. Outside-positioned legends are placed here,
5
+ * beyond any axes.
6
+ */
7
+ export declare const selectLegendArea: (state: RechartsRootState) => CartesianViewBoxRequired;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vccs",
3
3
  "type": "module",
4
- "version": "0.5.0",
4
+ "version": "0.6.0",
5
5
  "description": "Composable charting components for Vue 3 — an unofficial port of Recharts",
6
6
  "author": "rick-hup",
7
7
  "license": "MIT",