@cratis/components 0.1.9 → 0.1.12
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.
- package/dist/cjs/CommandForm/CommandFormFields.js +9 -3
- package/dist/cjs/CommandForm/CommandFormFields.js.map +1 -1
- package/dist/cjs/CommandForm/ValidationMessage.js +24 -0
- package/dist/cjs/CommandForm/ValidationMessage.js.map +1 -0
- package/dist/cjs/CommandForm/asCommandFormField.js +47 -0
- package/dist/cjs/CommandForm/asCommandFormField.js.map +1 -0
- package/dist/cjs/CommandForm/fields/CheckboxField.js +13 -0
- package/dist/cjs/CommandForm/fields/CheckboxField.js.map +1 -0
- package/dist/cjs/CommandForm/fields/DropdownField.js +13 -0
- package/dist/cjs/CommandForm/fields/DropdownField.js.map +1 -0
- package/dist/cjs/CommandForm/fields/InputTextField.js +13 -0
- package/dist/cjs/CommandForm/fields/InputTextField.js.map +1 -0
- package/dist/cjs/CommandForm/fields/NumberField.js +13 -0
- package/dist/cjs/CommandForm/fields/NumberField.js.map +1 -0
- package/dist/cjs/CommandForm/fields/SliderField.js +17 -0
- package/dist/cjs/CommandForm/fields/SliderField.js.map +1 -0
- package/dist/cjs/CommandForm/fields/TextAreaField.js +13 -0
- package/dist/cjs/CommandForm/fields/TextAreaField.js.map +1 -0
- package/dist/cjs/CommandForm/index.js +15 -7
- package/dist/cjs/CommandForm/index.js.map +1 -1
- package/dist/cjs/PivotViewer/PivotViewer.css +1258 -0
- package/dist/cjs/PivotViewer/PivotViewer.js +14 -0
- package/dist/cjs/PivotViewer/PivotViewer.js.map +1 -1
- package/dist/cjs/PivotViewer/components/PivotCanvas.js +33 -10
- package/dist/cjs/PivotViewer/components/PivotCanvas.js.map +1 -1
- package/dist/cjs/PivotViewer/components/PivotViewerMain.js +1 -1
- package/dist/cjs/PivotViewer/components/PivotViewerMain.js.map +1 -1
- package/dist/cjs/PivotViewer/components/Spinner.css +77 -0
- package/dist/cjs/PivotViewer/components/pivot/sprites.js +79 -15
- package/dist/cjs/PivotViewer/components/pivot/sprites.js.map +1 -1
- package/dist/cjs/PivotViewer/components/pivot/visibility.js +36 -10
- package/dist/cjs/PivotViewer/components/pivot/visibility.js.map +1 -1
- package/dist/cjs/PivotViewer/engine/layout.js +2 -1
- package/dist/cjs/PivotViewer/engine/layout.js.map +1 -1
- package/dist/cjs/PivotViewer/hooks/usePivotEngine.js +37 -2
- package/dist/cjs/PivotViewer/hooks/usePivotEngine.js.map +1 -1
- package/dist/cjs/PivotViewer/index.js +3 -0
- package/dist/cjs/PivotViewer/index.js.map +1 -1
- package/dist/cjs/PivotViewer/types.js +22 -0
- package/dist/cjs/PivotViewer/types.js.map +1 -0
- package/dist/cjs/TimeMachine/EventsView.css +213 -0
- package/dist/cjs/TimeMachine/TimeMachine.css +567 -0
- package/dist/cjs/TimeMachine/TimeMachine.js +8 -3
- package/dist/cjs/TimeMachine/TimeMachine.js.map +1 -1
- package/dist/esm/CommandForm/CommandForm.stories.d.ts +1 -0
- package/dist/esm/CommandForm/CommandForm.stories.d.ts.map +1 -1
- package/dist/esm/CommandForm/CommandForm.stories.js +34 -1
- package/dist/esm/CommandForm/CommandForm.stories.js.map +1 -1
- package/dist/esm/CommandForm/CommandFormFields.d.ts.map +1 -1
- package/dist/esm/CommandForm/CommandFormFields.js +9 -3
- package/dist/esm/CommandForm/CommandFormFields.js.map +1 -1
- package/dist/esm/CommandForm/UserRegistrationCommand.d.ts +63 -0
- package/dist/esm/CommandForm/UserRegistrationCommand.d.ts.map +1 -0
- package/dist/esm/CommandForm/UserRegistrationCommand.js +143 -0
- package/dist/esm/CommandForm/UserRegistrationCommand.js.map +1 -0
- package/dist/esm/CommandForm/ValidationMessage.d.ts +8 -0
- package/dist/esm/CommandForm/ValidationMessage.d.ts.map +1 -0
- package/dist/esm/CommandForm/ValidationMessage.js +22 -0
- package/dist/esm/CommandForm/ValidationMessage.js.map +1 -0
- package/dist/esm/CommandForm/asCommandFormField.d.ts +32 -0
- package/dist/esm/CommandForm/asCommandFormField.d.ts.map +1 -0
- package/dist/esm/CommandForm/asCommandFormField.js +45 -0
- package/dist/esm/CommandForm/asCommandFormField.js.map +1 -0
- package/dist/esm/CommandForm/fields/CheckboxField.d.ts +10 -0
- package/dist/esm/CommandForm/fields/CheckboxField.d.ts.map +1 -0
- package/dist/esm/CommandForm/fields/CheckboxField.js +11 -0
- package/dist/esm/CommandForm/fields/CheckboxField.js.map +1 -0
- package/dist/esm/CommandForm/fields/DropdownField.d.ts +15 -0
- package/dist/esm/CommandForm/fields/DropdownField.d.ts.map +1 -0
- package/dist/esm/CommandForm/fields/DropdownField.js +11 -0
- package/dist/esm/CommandForm/fields/DropdownField.js.map +1 -0
- package/dist/esm/CommandForm/fields/InputTextField.d.ts +11 -0
- package/dist/esm/CommandForm/fields/InputTextField.d.ts.map +1 -0
- package/dist/esm/CommandForm/fields/InputTextField.js +11 -0
- package/dist/esm/CommandForm/fields/InputTextField.js.map +1 -0
- package/dist/esm/CommandForm/fields/NumberField.d.ts +13 -0
- package/dist/esm/CommandForm/fields/NumberField.d.ts.map +1 -0
- package/dist/esm/CommandForm/fields/NumberField.js +11 -0
- package/dist/esm/CommandForm/fields/NumberField.js.map +1 -0
- package/dist/esm/CommandForm/fields/SliderField.d.ts +12 -0
- package/dist/esm/CommandForm/fields/SliderField.d.ts.map +1 -0
- package/dist/esm/CommandForm/fields/SliderField.js +15 -0
- package/dist/esm/CommandForm/fields/SliderField.js.map +1 -0
- package/dist/esm/CommandForm/fields/TextAreaField.d.ts +12 -0
- package/dist/esm/CommandForm/fields/TextAreaField.d.ts.map +1 -0
- package/dist/esm/CommandForm/fields/TextAreaField.js +11 -0
- package/dist/esm/CommandForm/fields/TextAreaField.js.map +1 -0
- package/dist/esm/CommandForm/fields/index.d.ts +7 -0
- package/dist/esm/CommandForm/fields/index.d.ts.map +1 -0
- package/dist/esm/CommandForm/fields/index.js +7 -0
- package/dist/esm/CommandForm/fields/index.js.map +1 -0
- package/dist/esm/CommandForm/index.d.ts +3 -4
- package/dist/esm/CommandForm/index.d.ts.map +1 -1
- package/dist/esm/CommandForm/index.js +8 -4
- package/dist/esm/CommandForm/index.js.map +1 -1
- package/dist/esm/PivotViewer/PivotViewer.css +1258 -0
- package/dist/esm/PivotViewer/PivotViewer.d.ts.map +1 -1
- package/dist/esm/PivotViewer/PivotViewer.js +14 -0
- package/dist/esm/PivotViewer/PivotViewer.js.map +1 -1
- package/dist/esm/PivotViewer/PivotViewer.stories.d.ts +1 -0
- package/dist/esm/PivotViewer/PivotViewer.stories.d.ts.map +1 -1
- package/dist/esm/PivotViewer/PivotViewer.stories.js +43 -3
- package/dist/esm/PivotViewer/PivotViewer.stories.js.map +1 -1
- package/dist/esm/PivotViewer/components/PivotCanvas.d.ts.map +1 -1
- package/dist/esm/PivotViewer/components/PivotCanvas.js +33 -10
- package/dist/esm/PivotViewer/components/PivotCanvas.js.map +1 -1
- package/dist/esm/PivotViewer/components/PivotViewerMain.js +1 -1
- package/dist/esm/PivotViewer/components/PivotViewerMain.js.map +1 -1
- package/dist/esm/PivotViewer/components/Spinner.css +77 -0
- package/dist/esm/PivotViewer/components/pivot/sprites.d.ts.map +1 -1
- package/dist/esm/PivotViewer/components/pivot/sprites.js +79 -15
- package/dist/esm/PivotViewer/components/pivot/sprites.js.map +1 -1
- package/dist/esm/PivotViewer/components/pivot/visibility.d.ts.map +1 -1
- package/dist/esm/PivotViewer/components/pivot/visibility.js +36 -10
- package/dist/esm/PivotViewer/components/pivot/visibility.js.map +1 -1
- package/dist/esm/PivotViewer/engine/layout.js +2 -1
- package/dist/esm/PivotViewer/engine/layout.js.map +1 -1
- package/dist/esm/PivotViewer/engine/pivot.worker.d.ts.map +1 -1
- package/dist/esm/PivotViewer/engine/pivot.worker.js +22 -7
- package/dist/esm/PivotViewer/engine/pivot.worker.js.map +1 -1
- package/dist/esm/PivotViewer/hooks/useFilteredData.d.ts +2 -2
- package/dist/esm/PivotViewer/hooks/useFilteredData.d.ts.map +1 -1
- package/dist/esm/PivotViewer/hooks/useFilteredData.js +4 -2
- package/dist/esm/PivotViewer/hooks/useFilteredData.js.map +1 -1
- package/dist/esm/PivotViewer/hooks/usePivotEngine.d.ts.map +1 -1
- package/dist/esm/PivotViewer/hooks/usePivotEngine.js +37 -2
- package/dist/esm/PivotViewer/hooks/usePivotEngine.js.map +1 -1
- package/dist/esm/PivotViewer/index.d.ts +2 -1
- package/dist/esm/PivotViewer/index.d.ts.map +1 -1
- package/dist/esm/PivotViewer/index.js +1 -0
- package/dist/esm/PivotViewer/index.js.map +1 -1
- package/dist/esm/PivotViewer/types.d.ts +4 -1
- package/dist/esm/PivotViewer/types.d.ts.map +1 -1
- package/dist/esm/PivotViewer/types.js +19 -2
- package/dist/esm/PivotViewer/types.js.map +1 -1
- package/dist/esm/TimeMachine/EventsView.css +213 -0
- package/dist/esm/TimeMachine/TimeMachine.css +567 -0
- package/dist/esm/TimeMachine/TimeMachine.d.ts.map +1 -1
- package/dist/esm/TimeMachine/TimeMachine.js +8 -3
- package/dist/esm/TimeMachine/TimeMachine.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +31 -32
- package/.storybook/main.ts +0 -24
- package/CommandDialog/CommandDialog.stories.tsx +0 -25
- package/CommandDialog/CommandDialog.tsx +0 -161
- package/CommandDialog/index.ts +0 -4
- package/CommandForm/CommandForm.stories.tsx +0 -24
- package/CommandForm/CommandForm.tsx +0 -266
- package/CommandForm/CommandFormField.tsx +0 -27
- package/CommandForm/CommandFormFields.tsx +0 -142
- package/CommandForm/DatePickerField.tsx +0 -57
- package/CommandForm/DropdownField.tsx +0 -65
- package/CommandForm/InputTextField.tsx +0 -62
- package/CommandForm/SliderField.tsx +0 -68
- package/CommandForm/index.ts +0 -10
- package/Common/ErrorBoundary.stories.tsx +0 -10
- package/Common/ErrorBoundary.tsx +0 -41
- package/Common/FormElement.stories.tsx +0 -10
- package/Common/FormElement.tsx +0 -20
- package/Common/Page.stories.tsx +0 -10
- package/Common/Page.tsx +0 -21
- package/Common/index.ts +0 -6
- package/DataPage/DataPage.stories.tsx +0 -10
- package/DataPage/DataPage.tsx +0 -191
- package/DataPage/index.ts +0 -4
- package/DataTables/DataTableForObservableQuery.stories.tsx +0 -10
- package/DataTables/DataTableForObservableQuery.tsx +0 -97
- package/DataTables/DataTableForQuery.stories.tsx +0 -10
- package/DataTables/DataTableForQuery.tsx +0 -97
- package/DataTables/index.ts +0 -5
- package/Dialogs/BusyIndicatorDialog.stories.tsx +0 -26
- package/Dialogs/BusyIndicatorDialog.tsx +0 -26
- package/Dialogs/ConfirmationDialog.stories.tsx +0 -36
- package/Dialogs/ConfirmationDialog.tsx +0 -75
- package/Dialogs/index.ts +0 -5
- package/Dropdown/Dropdown.tsx +0 -23
- package/Dropdown/index.ts +0 -4
- package/PivotViewer/PivotViewer.stories.tsx +0 -24
- package/PivotViewer/PivotViewer.tsx +0 -791
- package/PivotViewer/components/AxisLabels.tsx +0 -69
- package/PivotViewer/components/DetailPanel.tsx +0 -108
- package/PivotViewer/components/FilterPanel.tsx +0 -189
- package/PivotViewer/components/FilterPanelContainer.tsx +0 -10
- package/PivotViewer/components/PivotCanvas.tsx +0 -660
- package/PivotViewer/components/PivotViewerMain.tsx +0 -229
- package/PivotViewer/components/RangeHistogramFilter.tsx +0 -220
- package/PivotViewer/components/Spinner.tsx +0 -21
- package/PivotViewer/components/Toolbar.tsx +0 -130
- package/PivotViewer/components/ToolbarContainer.tsx +0 -10
- package/PivotViewer/components/index.ts +0 -12
- package/PivotViewer/components/pivot/animation.ts +0 -108
- package/PivotViewer/components/pivot/buckets.ts +0 -152
- package/PivotViewer/components/pivot/colorResolver.ts +0 -67
- package/PivotViewer/components/pivot/constants.ts +0 -46
- package/PivotViewer/components/pivot/sprites.ts +0 -265
- package/PivotViewer/components/pivot/visibility.ts +0 -319
- package/PivotViewer/constants.ts +0 -9
- package/PivotViewer/engine/layout.ts +0 -149
- package/PivotViewer/engine/pivot.worker.ts +0 -86
- package/PivotViewer/engine/store.ts +0 -437
- package/PivotViewer/engine/types.ts +0 -255
- package/PivotViewer/hooks/index.ts +0 -13
- package/PivotViewer/hooks/useContainerDimensions.ts +0 -45
- package/PivotViewer/hooks/useDimensionState.ts +0 -53
- package/PivotViewer/hooks/useFilterOptions.ts +0 -36
- package/PivotViewer/hooks/useFilterPanelDrag.ts +0 -49
- package/PivotViewer/hooks/useFilterState.ts +0 -106
- package/PivotViewer/hooks/useFilteredData.ts +0 -119
- package/PivotViewer/hooks/usePanning.ts +0 -163
- package/PivotViewer/hooks/usePivotEngine.ts +0 -252
- package/PivotViewer/hooks/useSelectedItem.ts +0 -402
- package/PivotViewer/hooks/useWheelZoom.ts +0 -114
- package/PivotViewer/hooks/useZoomState.ts +0 -34
- package/PivotViewer/index.ts +0 -7
- package/PivotViewer/types.ts +0 -59
- package/PivotViewer/utils/animations.ts +0 -249
- package/PivotViewer/utils/constants.ts +0 -20
- package/PivotViewer/utils/index.ts +0 -6
- package/PivotViewer/utils/selection.ts +0 -292
- package/PivotViewer/utils/utils.ts +0 -259
- package/TimeMachine/EventsView.stories.tsx +0 -10
- package/TimeMachine/EventsView.tsx +0 -119
- package/TimeMachine/Properties.stories.tsx +0 -10
- package/TimeMachine/Properties.tsx +0 -98
- package/TimeMachine/ReadModelView.stories.tsx +0 -10
- package/TimeMachine/ReadModelView.tsx +0 -143
- package/TimeMachine/TimeMachine.stories.tsx +0 -10
- package/TimeMachine/TimeMachine.tsx +0 -244
- package/TimeMachine/index.ts +0 -8
- package/TimeMachine/types.ts +0 -23
- package/dist/cjs/CommandForm/DatePickerField.js +0 -31
- package/dist/cjs/CommandForm/DatePickerField.js.map +0 -1
- package/dist/cjs/CommandForm/DropdownField.js +0 -31
- package/dist/cjs/CommandForm/DropdownField.js.map +0 -1
- package/dist/cjs/CommandForm/InputTextField.js +0 -32
- package/dist/cjs/CommandForm/InputTextField.js.map +0 -1
- package/dist/cjs/CommandForm/SliderField.js +0 -34
- package/dist/cjs/CommandForm/SliderField.js.map +0 -1
- package/dist/esm/CommandForm/DatePickerField.d.ts +0 -20
- package/dist/esm/CommandForm/DatePickerField.d.ts.map +0 -1
- package/dist/esm/CommandForm/DatePickerField.js +0 -29
- package/dist/esm/CommandForm/DatePickerField.js.map +0 -1
- package/dist/esm/CommandForm/DropdownField.d.ts +0 -24
- package/dist/esm/CommandForm/DropdownField.d.ts.map +0 -1
- package/dist/esm/CommandForm/DropdownField.js +0 -29
- package/dist/esm/CommandForm/DropdownField.js.map +0 -1
- package/dist/esm/CommandForm/InputTextField.d.ts +0 -20
- package/dist/esm/CommandForm/InputTextField.d.ts.map +0 -1
- package/dist/esm/CommandForm/InputTextField.js +0 -30
- package/dist/esm/CommandForm/InputTextField.js.map +0 -1
- package/dist/esm/CommandForm/SliderField.d.ts +0 -23
- package/dist/esm/CommandForm/SliderField.d.ts.map +0 -1
- package/dist/esm/CommandForm/SliderField.js +0 -32
- package/dist/esm/CommandForm/SliderField.js.map +0 -1
- package/global.d.ts +0 -11
- package/index.ts +0 -22
- package/useOverlayZIndex.ts +0 -32
- package/vite.config.ts +0 -80
|
@@ -1,791 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
-
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
-
|
|
4
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
-
import type { PivotViewerProps } from './types';
|
|
6
|
-
import type { FilterSpec, GroupSpec, FieldValue, GroupingResult, ItemId } from './engine/types';
|
|
7
|
-
import { usePivotEngine } from './hooks/usePivotEngine';
|
|
8
|
-
import { computeLayout } from './engine/layout';
|
|
9
|
-
import { useFilterState } from './hooks/useFilterState';
|
|
10
|
-
import { useDimensionState } from './hooks/useDimensionState';
|
|
11
|
-
import { useZoomState } from './hooks/useZoomState';
|
|
12
|
-
import { handleCardSelection } from './utils/selection';
|
|
13
|
-
import { animateZoomAndScroll, smoothScrollTo } from './utils/animations';
|
|
14
|
-
import {
|
|
15
|
-
BASE_CARD_WIDTH,
|
|
16
|
-
BASE_CARD_HEIGHT,
|
|
17
|
-
CARDS_PER_COLUMN,
|
|
18
|
-
GROUP_SPACING,
|
|
19
|
-
} from './constants';
|
|
20
|
-
import { ZOOM_MAX, MIN_ZOOM_ON_SELECT, ZOOM_MULTIPLIER, DETAIL_PANEL_WIDTH } from './utils/constants';
|
|
21
|
-
import { calculateCenterScrollPosition } from './utils/animations';
|
|
22
|
-
import './PivotViewer.css';
|
|
23
|
-
import { PivotViewerMain } from './components/PivotViewerMain';
|
|
24
|
-
import { FilterPanelContainer } from './components/FilterPanelContainer';
|
|
25
|
-
import { ToolbarContainer } from './components/ToolbarContainer';
|
|
26
|
-
import { usePanning, useWheelZoom, useFilterOptions } from './hooks';
|
|
27
|
-
import { useContainerDimensions } from './hooks/useContainerDimensions';
|
|
28
|
-
import type { ViewMode } from './components/Toolbar';
|
|
29
|
-
|
|
30
|
-
export function PivotViewer<TItem extends object>({
|
|
31
|
-
data,
|
|
32
|
-
dimensions,
|
|
33
|
-
filters,
|
|
34
|
-
defaultDimensionKey,
|
|
35
|
-
cardRenderer,
|
|
36
|
-
getItemId,
|
|
37
|
-
searchFields,
|
|
38
|
-
className,
|
|
39
|
-
emptyContent,
|
|
40
|
-
isLoading = false,
|
|
41
|
-
}: PivotViewerProps<TItem>) {
|
|
42
|
-
// Refs
|
|
43
|
-
const containerRef = useRef<HTMLDivElement>(null!);
|
|
44
|
-
const filterButtonRef = useRef<HTMLButtonElement>(null!);
|
|
45
|
-
const axisLabelsRef = useRef<HTMLDivElement>(null!);
|
|
46
|
-
const spacerRef = useRef<HTMLDivElement>(null!);
|
|
47
|
-
|
|
48
|
-
// State
|
|
49
|
-
const [search, setSearch] = useState('');
|
|
50
|
-
const [viewMode, setViewMode] = useState<ViewMode>('collection');
|
|
51
|
-
|
|
52
|
-
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
53
|
-
const [selectedItem, setSelectedItem] = useState<TItem | null>(null);
|
|
54
|
-
const [isZooming, setIsZooming] = useState(false);
|
|
55
|
-
const [visibleIds, setVisibleIds] = useState<Uint32Array>(new Uint32Array(0));
|
|
56
|
-
const [grouping, setGrouping] = useState<GroupingResult>({ groups: [] });
|
|
57
|
-
const [hoveredGroupIndex, setHoveredGroupIndex] = useState<number | null>(null);
|
|
58
|
-
const [preSelectionState, setPreSelectionState] = useState<{ zoom: number; scrollLeft: number; scrollTop: number } | null>(null);
|
|
59
|
-
const [, setAnimationMode] = useState<'layout' | 'filter'>('layout');
|
|
60
|
-
const [scrollPosition, setScrollPosition] = useState({ x: 0, y: 0 });
|
|
61
|
-
|
|
62
|
-
// Filter hooks
|
|
63
|
-
const {
|
|
64
|
-
filterState,
|
|
65
|
-
rangeFilterState,
|
|
66
|
-
expandedFilterKey,
|
|
67
|
-
setExpandedFilterKey,
|
|
68
|
-
handleToggleFilter,
|
|
69
|
-
handleClearFilter,
|
|
70
|
-
handleRangeChange,
|
|
71
|
-
} = useFilterState(filters);
|
|
72
|
-
|
|
73
|
-
// Dimension hooks
|
|
74
|
-
const {
|
|
75
|
-
activeDimensionKey,
|
|
76
|
-
setActiveDimensionKey,
|
|
77
|
-
activeDimension,
|
|
78
|
-
dimensionFilter,
|
|
79
|
-
handleAxisLabelClick,
|
|
80
|
-
} = useDimensionState(dimensions, defaultDimensionKey);
|
|
81
|
-
|
|
82
|
-
// Track what type of change triggered the update (for animation mode)
|
|
83
|
-
const prevFilterStateRef = useRef(filterState);
|
|
84
|
-
const prevRangeFilterStateRef = useRef(rangeFilterState);
|
|
85
|
-
const prevSearchRef = useRef(search);
|
|
86
|
-
const prevDimensionRef = useRef(activeDimensionKey);
|
|
87
|
-
const prevViewModeRef = useRef(viewMode);
|
|
88
|
-
const isFirstRenderRef = useRef(true);
|
|
89
|
-
|
|
90
|
-
// Zoom and pan hooks
|
|
91
|
-
const {
|
|
92
|
-
zoomLevel,
|
|
93
|
-
setZoomLevel,
|
|
94
|
-
handleZoomIn,
|
|
95
|
-
handleZoomOut,
|
|
96
|
-
handleZoomSlider,
|
|
97
|
-
} = useZoomState(1);
|
|
98
|
-
|
|
99
|
-
const {
|
|
100
|
-
isPanning,
|
|
101
|
-
handlePanStart,
|
|
102
|
-
handlePanMove,
|
|
103
|
-
handlePanEnd,
|
|
104
|
-
} = usePanning(containerRef, undefined, setScrollPosition);
|
|
105
|
-
|
|
106
|
-
useWheelZoom(containerRef, zoomLevel, setZoomLevel);
|
|
107
|
-
|
|
108
|
-
// Track container dimensions for responsive layout
|
|
109
|
-
const containerDimensions = useContainerDimensions(containerRef, isLoading);
|
|
110
|
-
|
|
111
|
-
useEffect(() => {
|
|
112
|
-
const container = containerRef.current;
|
|
113
|
-
if (!container) return;
|
|
114
|
-
|
|
115
|
-
const handleScroll = () => {
|
|
116
|
-
setScrollPosition({
|
|
117
|
-
x: container.scrollLeft,
|
|
118
|
-
y: container.scrollTop,
|
|
119
|
-
});
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
container.addEventListener('scroll', handleScroll);
|
|
123
|
-
return () => container.removeEventListener('scroll', handleScroll);
|
|
124
|
-
}, []);
|
|
125
|
-
|
|
126
|
-
// Zoom reset removed to persist zoom level across view changes
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// Track what type of change triggered the update for animation mode
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
// Skip the first render
|
|
132
|
-
if (isFirstRenderRef.current) {
|
|
133
|
-
isFirstRenderRef.current = false;
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const filterChanged = prevFilterStateRef.current !== filterState;
|
|
138
|
-
const rangeChanged = prevRangeFilterStateRef.current !== rangeFilterState;
|
|
139
|
-
const searchChanged = prevSearchRef.current !== search;
|
|
140
|
-
const dimensionChanged = prevDimensionRef.current !== activeDimensionKey;
|
|
141
|
-
const viewModeChanged = prevViewModeRef.current !== viewMode;
|
|
142
|
-
|
|
143
|
-
// If filters or search changed, use filter animation (fade/scale)
|
|
144
|
-
// If dimension or view mode changed, use layout animation (fly)
|
|
145
|
-
if (filterChanged || rangeChanged || searchChanged) {
|
|
146
|
-
setAnimationMode('filter');
|
|
147
|
-
} else if (dimensionChanged || viewModeChanged) {
|
|
148
|
-
setAnimationMode('layout');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
prevFilterStateRef.current = filterState;
|
|
152
|
-
prevRangeFilterStateRef.current = rangeFilterState;
|
|
153
|
-
prevSearchRef.current = search;
|
|
154
|
-
prevDimensionRef.current = activeDimensionKey;
|
|
155
|
-
prevViewModeRef.current = viewMode;
|
|
156
|
-
}, [filterState, rangeFilterState, search, activeDimensionKey, viewMode]);
|
|
157
|
-
|
|
158
|
-
// Sync axis labels scroll with container scroll
|
|
159
|
-
useEffect(() => {
|
|
160
|
-
const container = containerRef.current;
|
|
161
|
-
const axisLabels = axisLabelsRef.current;
|
|
162
|
-
|
|
163
|
-
if (!container || !axisLabels || viewMode !== 'grouped') return;
|
|
164
|
-
|
|
165
|
-
const handleScroll = () => {
|
|
166
|
-
axisLabels.scrollLeft = container.scrollLeft;
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
// Sync immediately
|
|
170
|
-
handleScroll();
|
|
171
|
-
|
|
172
|
-
container.addEventListener('scroll', handleScroll);
|
|
173
|
-
return () => container.removeEventListener('scroll', handleScroll);
|
|
174
|
-
}, [viewMode]);
|
|
175
|
-
|
|
176
|
-
// Build field extractors for the columnar store
|
|
177
|
-
const fieldExtractors = useMemo(() => {
|
|
178
|
-
const extractors = new Map<string, (item: TItem) => FieldValue>();
|
|
179
|
-
|
|
180
|
-
for (const dim of dimensions) {
|
|
181
|
-
extractors.set(dim.key, (item) => {
|
|
182
|
-
const val = dim.getValue(item);
|
|
183
|
-
if (val instanceof Date) return val.getTime();
|
|
184
|
-
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean' || val === null) {
|
|
185
|
-
return val;
|
|
186
|
-
}
|
|
187
|
-
return String(val);
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (filters) {
|
|
192
|
-
for (const filter of filters) {
|
|
193
|
-
extractors.set(filter.key, (item) => {
|
|
194
|
-
const val = filter.getValue(item);
|
|
195
|
-
if (val instanceof Date) return val.getTime();
|
|
196
|
-
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean' || val === null) {
|
|
197
|
-
return val;
|
|
198
|
-
}
|
|
199
|
-
return String(val);
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return extractors;
|
|
205
|
-
}, [dimensions, filters]);
|
|
206
|
-
|
|
207
|
-
const indexFields = useMemo(() => {
|
|
208
|
-
const fields = new Set<string>();
|
|
209
|
-
|
|
210
|
-
for (const dim of dimensions) {
|
|
211
|
-
fields.add(dim.key);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (filters) {
|
|
215
|
-
for (const filter of filters) {
|
|
216
|
-
fields.add(filter.key);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return Array.from(fields);
|
|
221
|
-
}, [dimensions, filters]);
|
|
222
|
-
|
|
223
|
-
// Initialize the Web Worker engine
|
|
224
|
-
const { ready, applyFilters: engineApplyFilters, computeGrouping, sortIds } = usePivotEngine({
|
|
225
|
-
data,
|
|
226
|
-
fieldExtractors,
|
|
227
|
-
indexFields,
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// Build filter specs from UI state
|
|
231
|
-
const currentFilters = useMemo((): FilterSpec[] => {
|
|
232
|
-
const specs: FilterSpec[] = [];
|
|
233
|
-
|
|
234
|
-
// Search filter
|
|
235
|
-
const searchTerm = search.trim().toLowerCase();
|
|
236
|
-
if (searchTerm && searchFields && searchFields.length > 0) {
|
|
237
|
-
// TODO: Implement search in worker
|
|
238
|
-
// For now, search will be handled client-side after worker filtering
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Categorical filters
|
|
242
|
-
for (const [key, values] of Object.entries(filterState)) {
|
|
243
|
-
const valueSet = values as Set<string>;
|
|
244
|
-
if (valueSet.size > 0) {
|
|
245
|
-
specs.push({
|
|
246
|
-
field: key,
|
|
247
|
-
type: 'categorical',
|
|
248
|
-
values: valueSet,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Range filters
|
|
254
|
-
for (const [key, range] of Object.entries(rangeFilterState)) {
|
|
255
|
-
if (range && (range[0] !== null || range[1] !== null)) {
|
|
256
|
-
const min = range[0] ?? -Infinity;
|
|
257
|
-
const max = range[1] ?? Infinity;
|
|
258
|
-
specs.push({
|
|
259
|
-
field: key,
|
|
260
|
-
type: 'numeric',
|
|
261
|
-
range: { min, max },
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Dimension filter (bucket filter)
|
|
267
|
-
if (dimensionFilter && activeDimension) {
|
|
268
|
-
specs.push({
|
|
269
|
-
field: activeDimension.key,
|
|
270
|
-
type: 'categorical',
|
|
271
|
-
values: new Set([dimensionFilter]),
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return specs;
|
|
276
|
-
}, [filterState, rangeFilterState, search, searchFields, dimensionFilter, activeDimension]);
|
|
277
|
-
|
|
278
|
-
const currentGroupBy = useMemo((): GroupSpec => {
|
|
279
|
-
return {
|
|
280
|
-
field: activeDimensionKey || dimensions[0]?.key || '',
|
|
281
|
-
buckets: 10,
|
|
282
|
-
};
|
|
283
|
-
}, [activeDimensionKey, dimensions]);
|
|
284
|
-
|
|
285
|
-
// Apply filters
|
|
286
|
-
useEffect(() => {
|
|
287
|
-
if (!ready) return;
|
|
288
|
-
|
|
289
|
-
engineApplyFilters(currentFilters).then((result) => {
|
|
290
|
-
setVisibleIds(result.visibleIds);
|
|
291
|
-
});
|
|
292
|
-
}, [ready, currentFilters, engineApplyFilters]);
|
|
293
|
-
|
|
294
|
-
// Compute grouping
|
|
295
|
-
useEffect(() => {
|
|
296
|
-
if (!ready || visibleIds.length === 0) {
|
|
297
|
-
setGrouping({ groups: [] });
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (viewMode === 'collection') {
|
|
302
|
-
// In collection mode, create a single group with all items
|
|
303
|
-
// Sort items if activeDimensionKey is set
|
|
304
|
-
if (activeDimensionKey) {
|
|
305
|
-
sortIds(visibleIds, activeDimensionKey).then((sortedIds) => {
|
|
306
|
-
setGrouping({
|
|
307
|
-
groups: [{
|
|
308
|
-
key: 'all',
|
|
309
|
-
label: 'All Items',
|
|
310
|
-
value: 'all',
|
|
311
|
-
ids: sortedIds,
|
|
312
|
-
count: sortedIds.length
|
|
313
|
-
}]
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
} else {
|
|
317
|
-
setGrouping({
|
|
318
|
-
groups: [{
|
|
319
|
-
key: 'all',
|
|
320
|
-
label: 'All Items',
|
|
321
|
-
value: 'all',
|
|
322
|
-
ids: visibleIds,
|
|
323
|
-
count: visibleIds.length
|
|
324
|
-
}]
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
computeGrouping(visibleIds, currentGroupBy).then((result) => {
|
|
331
|
-
setGrouping(result);
|
|
332
|
-
});
|
|
333
|
-
}, [ready, visibleIds, currentGroupBy, viewMode, computeGrouping, sortIds, activeDimensionKey]);
|
|
334
|
-
|
|
335
|
-
// Compute layout
|
|
336
|
-
const layout = useMemo(() => {
|
|
337
|
-
// Calculate layout at base dimensions (zoom is applied as transform)
|
|
338
|
-
const cardWidth = BASE_CARD_WIDTH;
|
|
339
|
-
const cardHeight = BASE_CARD_HEIGHT;
|
|
340
|
-
const containerWidth = containerDimensions.width / zoomLevel;
|
|
341
|
-
// For grouped mode, use fixed container height to ensure stable layout during zoom
|
|
342
|
-
const containerHeight = viewMode === 'collection'
|
|
343
|
-
? containerDimensions.height / zoomLevel
|
|
344
|
-
: containerDimensions.height;
|
|
345
|
-
|
|
346
|
-
const result = computeLayout(grouping, {
|
|
347
|
-
viewMode,
|
|
348
|
-
cardWidth,
|
|
349
|
-
cardHeight,
|
|
350
|
-
cardsPerColumn: CARDS_PER_COLUMN,
|
|
351
|
-
groupSpacing: GROUP_SPACING,
|
|
352
|
-
containerWidth,
|
|
353
|
-
containerHeight,
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
return result;
|
|
357
|
-
}, [grouping, viewMode, zoomLevel, containerDimensions.width, containerDimensions.height]);
|
|
358
|
-
|
|
359
|
-
const resolveId = useCallback((item: TItem, index: number): ItemId => {
|
|
360
|
-
if (getItemId) {
|
|
361
|
-
const id = getItemId(item, index);
|
|
362
|
-
return typeof id === 'number' ? id : index;
|
|
363
|
-
}
|
|
364
|
-
const id = (item as Record<string, unknown>)['id'];
|
|
365
|
-
return typeof id === 'number' ? id : index;
|
|
366
|
-
}, [getItemId]);
|
|
367
|
-
|
|
368
|
-
// Scroll positioning when switching view modes or grouping changes
|
|
369
|
-
const lastProcessedViewMode = useRef(viewMode);
|
|
370
|
-
const lastProcessedGrouping = useRef(grouping);
|
|
371
|
-
|
|
372
|
-
useEffect(() => {
|
|
373
|
-
const viewModeChanged = lastProcessedViewMode.current !== viewMode;
|
|
374
|
-
const groupingChanged = lastProcessedGrouping.current !== grouping;
|
|
375
|
-
|
|
376
|
-
if (!viewModeChanged && !groupingChanged) return;
|
|
377
|
-
|
|
378
|
-
lastProcessedViewMode.current = viewMode;
|
|
379
|
-
lastProcessedGrouping.current = grouping;
|
|
380
|
-
|
|
381
|
-
const container = containerRef.current;
|
|
382
|
-
if (!container) return;
|
|
383
|
-
|
|
384
|
-
// If we have a selected item, we want to keep it centered in the new layout
|
|
385
|
-
if (selectedItem) {
|
|
386
|
-
// Resolve ID
|
|
387
|
-
let itemId = resolveId(selectedItem, 0);
|
|
388
|
-
|
|
389
|
-
// Ensure ID type matches layout
|
|
390
|
-
if (typeof itemId === 'string' && !layout.positions.has(itemId)) {
|
|
391
|
-
const numId = Number(itemId);
|
|
392
|
-
if (!isNaN(numId) && layout.positions.has(numId)) itemId = numId;
|
|
393
|
-
} else if (typeof itemId === 'number' && !layout.positions.has(itemId)) {
|
|
394
|
-
const strId = String(itemId);
|
|
395
|
-
if (layout.positions.has(strId)) itemId = strId;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const position = layout.positions.get(itemId);
|
|
399
|
-
if (position) {
|
|
400
|
-
const cardPosition = {
|
|
401
|
-
x: position.x,
|
|
402
|
-
y: position.y,
|
|
403
|
-
width: BASE_CARD_WIDTH,
|
|
404
|
-
height: BASE_CARD_HEIGHT
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
const detailWidth = viewMode === 'collection' ? 0 : DETAIL_PANEL_WIDTH;
|
|
408
|
-
|
|
409
|
-
const { scrollLeft, scrollTop } = calculateCenterScrollPosition(
|
|
410
|
-
container,
|
|
411
|
-
cardPosition,
|
|
412
|
-
zoomLevel,
|
|
413
|
-
detailWidth,
|
|
414
|
-
layout.totalHeight
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
container.scrollTo({ left: scrollLeft, top: scrollTop });
|
|
418
|
-
|
|
419
|
-
// Clear pre-selection state as we've moved to a new context
|
|
420
|
-
setPreSelectionState(null);
|
|
421
|
-
}
|
|
422
|
-
} else if (viewMode === 'grouped') {
|
|
423
|
-
// Default behavior for grouped view: scroll to bottom
|
|
424
|
-
// Use a small timeout to ensure the spacer has been resized
|
|
425
|
-
setTimeout(() => {
|
|
426
|
-
container.scrollTop = container.scrollHeight;
|
|
427
|
-
// Sync scroll position state immediately to avoid stale values on first click
|
|
428
|
-
setScrollPosition({ x: container.scrollLeft, y: container.scrollTop });
|
|
429
|
-
}, 0);
|
|
430
|
-
}
|
|
431
|
-
}, [viewMode, grouping, layout, selectedItem, resolveId, zoomLevel]);
|
|
432
|
-
|
|
433
|
-
const handleCardClick = useCallback((item: TItem, e: MouseEvent, id?: number | string) => {
|
|
434
|
-
if (isPanning) return;
|
|
435
|
-
|
|
436
|
-
const container = containerRef.current;
|
|
437
|
-
if (!container) return;
|
|
438
|
-
|
|
439
|
-
// Use the passed ID (index) if available, otherwise fallback to resolveId
|
|
440
|
-
// Note: resolveId might be unreliable for looking up layout positions if IDs are strings
|
|
441
|
-
let itemId = (id !== undefined && id !== null) ? id : resolveId(item, 0);
|
|
442
|
-
|
|
443
|
-
// Ensure itemId matches layout keys type (number vs string)
|
|
444
|
-
// If layout has number keys and itemId is string, try converting
|
|
445
|
-
if (typeof itemId === 'string' && !layout.positions.has(itemId)) {
|
|
446
|
-
const numId = Number(itemId);
|
|
447
|
-
if (!isNaN(numId) && layout.positions.has(numId)) {
|
|
448
|
-
itemId = numId;
|
|
449
|
-
}
|
|
450
|
-
} else if (typeof itemId === 'number' && !layout.positions.has(itemId)) {
|
|
451
|
-
const strId = String(itemId);
|
|
452
|
-
if (layout.positions.has(strId)) {
|
|
453
|
-
itemId = strId;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const selectedId = selectedItem ? (data.indexOf(selectedItem) !== -1 ? data.indexOf(selectedItem) : resolveId(selectedItem, 0)) : null;
|
|
458
|
-
|
|
459
|
-
// Get card position from layout
|
|
460
|
-
const position = layout.positions.get(itemId);
|
|
461
|
-
|
|
462
|
-
const cardPosition = position ? {
|
|
463
|
-
x: position.x,
|
|
464
|
-
y: position.y,
|
|
465
|
-
width: BASE_CARD_WIDTH,
|
|
466
|
-
height: BASE_CARD_HEIGHT
|
|
467
|
-
} : null;
|
|
468
|
-
|
|
469
|
-
// Calculate target position for animation
|
|
470
|
-
let targetCardPosition: { x: number; y: number; width: number; height: number } | null = null;
|
|
471
|
-
let getCardPositionAtZoom: ((zoom: number) => { x: number; y: number; width: number; height: number } | null) | undefined = undefined;
|
|
472
|
-
let targetTotalHeight = layout.totalHeight;
|
|
473
|
-
|
|
474
|
-
if (viewMode === 'grouped' && cardPosition) {
|
|
475
|
-
// Calculate target zoom (logic duplicated from zoomAndCenterCard)
|
|
476
|
-
const targetZoom = Math.min(ZOOM_MAX, Math.max(MIN_ZOOM_ON_SELECT, zoomLevel * ZOOM_MULTIPLIER));
|
|
477
|
-
|
|
478
|
-
// Calculate target layout
|
|
479
|
-
const targetContainerWidth = containerDimensions.width / targetZoom;
|
|
480
|
-
// For grouped mode, use fixed container height to ensure stable layout during zoom
|
|
481
|
-
const targetContainerHeight = containerDimensions.height;
|
|
482
|
-
|
|
483
|
-
const targetLayout = computeLayout(grouping, {
|
|
484
|
-
viewMode,
|
|
485
|
-
cardWidth: BASE_CARD_WIDTH,
|
|
486
|
-
cardHeight: BASE_CARD_HEIGHT,
|
|
487
|
-
cardsPerColumn: CARDS_PER_COLUMN,
|
|
488
|
-
groupSpacing: GROUP_SPACING,
|
|
489
|
-
containerWidth: targetContainerWidth,
|
|
490
|
-
containerHeight: targetContainerHeight,
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
targetTotalHeight = targetLayout.totalHeight;
|
|
494
|
-
|
|
495
|
-
const targetPos = targetLayout.positions.get(itemId);
|
|
496
|
-
if (targetPos) {
|
|
497
|
-
targetCardPosition = {
|
|
498
|
-
x: targetPos.x,
|
|
499
|
-
y: targetPos.y,
|
|
500
|
-
width: BASE_CARD_WIDTH,
|
|
501
|
-
height: BASE_CARD_HEIGHT
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Provide callback for accurate position during animation
|
|
506
|
-
getCardPositionAtZoom = (zoom: number) => {
|
|
507
|
-
const currentContainerWidth = containerDimensions.width / zoom;
|
|
508
|
-
// For grouped mode, use fixed container height to ensure stable layout during zoom
|
|
509
|
-
const currentContainerHeight = containerDimensions.height;
|
|
510
|
-
|
|
511
|
-
const currentLayout = computeLayout(grouping, {
|
|
512
|
-
viewMode,
|
|
513
|
-
cardWidth: BASE_CARD_WIDTH,
|
|
514
|
-
cardHeight: BASE_CARD_HEIGHT,
|
|
515
|
-
cardsPerColumn: CARDS_PER_COLUMN,
|
|
516
|
-
groupSpacing: GROUP_SPACING,
|
|
517
|
-
containerWidth: currentContainerWidth,
|
|
518
|
-
containerHeight: currentContainerHeight,
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
const pos = currentLayout.positions.get(itemId);
|
|
522
|
-
return pos ? { x: pos.x, y: pos.y, width: BASE_CARD_WIDTH, height: BASE_CARD_HEIGHT } : null;
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Callback to get layout size at a specific zoom level (for spacer updates)
|
|
527
|
-
const getLayoutSizeAtZoom = (zoom: number) => {
|
|
528
|
-
if (viewMode === 'collection') {
|
|
529
|
-
return { width: layout.totalWidth, height: layout.totalHeight };
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const currentContainerWidth = containerDimensions.width / zoom;
|
|
533
|
-
// For grouped mode, use fixed container height to ensure stable layout during zoom
|
|
534
|
-
const currentContainerHeight = containerDimensions.height;
|
|
535
|
-
|
|
536
|
-
const currentLayout = computeLayout(grouping, {
|
|
537
|
-
viewMode,
|
|
538
|
-
cardWidth: BASE_CARD_WIDTH,
|
|
539
|
-
cardHeight: BASE_CARD_HEIGHT,
|
|
540
|
-
cardsPerColumn: CARDS_PER_COLUMN,
|
|
541
|
-
groupSpacing: GROUP_SPACING,
|
|
542
|
-
containerWidth: currentContainerWidth,
|
|
543
|
-
containerHeight: currentContainerHeight,
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
return { width: currentLayout.totalWidth, height: currentLayout.totalHeight };
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
handleCardSelection({
|
|
550
|
-
item,
|
|
551
|
-
itemId,
|
|
552
|
-
selectedItemId: selectedId,
|
|
553
|
-
container,
|
|
554
|
-
cardPosition,
|
|
555
|
-
targetCardPosition,
|
|
556
|
-
getCardPositionAtZoom,
|
|
557
|
-
getLayoutSizeAtZoom,
|
|
558
|
-
spacer: spacerRef.current,
|
|
559
|
-
preSelectionState,
|
|
560
|
-
startScrollPosition: { x: scrollPosition.x, y: scrollPosition.y },
|
|
561
|
-
setZoomLevel,
|
|
562
|
-
setIsZooming,
|
|
563
|
-
setSelectedItem,
|
|
564
|
-
setPreSelectionState,
|
|
565
|
-
viewMode,
|
|
566
|
-
zoomLevel,
|
|
567
|
-
totalHeight: targetTotalHeight,
|
|
568
|
-
});
|
|
569
|
-
}, [isPanning, selectedItem, zoomLevel, preSelectionState, viewMode, resolveId, setZoomLevel, layout, grouping, containerDimensions, scrollPosition]);
|
|
570
|
-
|
|
571
|
-
const closeDetail = useCallback(() => {
|
|
572
|
-
const container = containerRef.current;
|
|
573
|
-
if (!container || !selectedItem) {
|
|
574
|
-
setSelectedItem(null);
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Try to find the index of the selected item in the data array
|
|
579
|
-
// This is more reliable than resolveId for layout lookup
|
|
580
|
-
const index = data.indexOf(selectedItem);
|
|
581
|
-
let itemId: string | number = index !== -1 ? index : resolveId(selectedItem, 0);
|
|
582
|
-
|
|
583
|
-
// Ensure itemId matches layout keys type (number vs string)
|
|
584
|
-
if (typeof itemId === 'string' && !layout.positions.has(itemId)) {
|
|
585
|
-
const numId = Number(itemId);
|
|
586
|
-
if (!isNaN(numId) && layout.positions.has(numId)) {
|
|
587
|
-
itemId = numId;
|
|
588
|
-
}
|
|
589
|
-
} else if (typeof itemId === 'number' && !layout.positions.has(itemId)) {
|
|
590
|
-
const strId = String(itemId);
|
|
591
|
-
if (layout.positions.has(strId)) {
|
|
592
|
-
itemId = strId;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Get card position from layout
|
|
597
|
-
const position = layout.positions.get(itemId);
|
|
598
|
-
const cardPosition = position ? {
|
|
599
|
-
x: position.x,
|
|
600
|
-
y: position.y,
|
|
601
|
-
width: BASE_CARD_WIDTH,
|
|
602
|
-
height: BASE_CARD_HEIGHT
|
|
603
|
-
} : null;
|
|
604
|
-
|
|
605
|
-
if (!preSelectionState) {
|
|
606
|
-
setSelectedItem(null);
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Collection mode: just scroll back
|
|
611
|
-
if (viewMode === 'collection') {
|
|
612
|
-
setSelectedItem(null);
|
|
613
|
-
smoothScrollTo(container, preSelectionState.scrollLeft, preSelectionState.scrollTop, true);
|
|
614
|
-
setPreSelectionState(null);
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// Grouped mode: animate zoom out if zoom changed
|
|
619
|
-
const zoomChanged = Math.abs(preSelectionState.zoom - zoomLevel) > 0.001;
|
|
620
|
-
|
|
621
|
-
if (!zoomChanged || !cardPosition) {
|
|
622
|
-
setSelectedItem(null);
|
|
623
|
-
smoothScrollTo(container, preSelectionState.scrollLeft, preSelectionState.scrollTop, true);
|
|
624
|
-
setPreSelectionState(null);
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Calculate target position for animation (zooming out)
|
|
629
|
-
let targetCardPosition: { x: number; y: number; width: number; height: number } | null = null;
|
|
630
|
-
let getCardPositionAtZoom: ((zoom: number) => { x: number; y: number; width: number; height: number } | null) | undefined = undefined;
|
|
631
|
-
|
|
632
|
-
if (viewMode === 'grouped') {
|
|
633
|
-
const targetZoom = preSelectionState.zoom;
|
|
634
|
-
|
|
635
|
-
const targetContainerWidth = containerDimensions.width / targetZoom;
|
|
636
|
-
// For grouped mode, use fixed container height to ensure stable layout during zoom
|
|
637
|
-
const targetContainerHeight = containerDimensions.height;
|
|
638
|
-
|
|
639
|
-
const targetLayout = computeLayout(grouping, {
|
|
640
|
-
viewMode,
|
|
641
|
-
cardWidth: BASE_CARD_WIDTH,
|
|
642
|
-
cardHeight: BASE_CARD_HEIGHT,
|
|
643
|
-
cardsPerColumn: CARDS_PER_COLUMN,
|
|
644
|
-
groupSpacing: GROUP_SPACING,
|
|
645
|
-
containerWidth: targetContainerWidth,
|
|
646
|
-
containerHeight: targetContainerHeight,
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
const targetPos = targetLayout.positions.get(itemId);
|
|
650
|
-
if (targetPos) {
|
|
651
|
-
targetCardPosition = {
|
|
652
|
-
x: targetPos.x,
|
|
653
|
-
y: targetPos.y,
|
|
654
|
-
width: BASE_CARD_WIDTH,
|
|
655
|
-
height: BASE_CARD_HEIGHT
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Provide callback for accurate position during animation
|
|
660
|
-
getCardPositionAtZoom = (zoom: number) => {
|
|
661
|
-
const currentContainerWidth = containerDimensions.width / zoom;
|
|
662
|
-
// For grouped mode, use fixed container height to ensure stable layout during zoom
|
|
663
|
-
const currentContainerHeight = containerDimensions.height;
|
|
664
|
-
|
|
665
|
-
const currentLayout = computeLayout(grouping, {
|
|
666
|
-
viewMode,
|
|
667
|
-
cardWidth: BASE_CARD_WIDTH,
|
|
668
|
-
cardHeight: BASE_CARD_HEIGHT,
|
|
669
|
-
cardsPerColumn: CARDS_PER_COLUMN,
|
|
670
|
-
groupSpacing: GROUP_SPACING,
|
|
671
|
-
containerWidth: currentContainerWidth,
|
|
672
|
-
containerHeight: currentContainerHeight,
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
const pos = currentLayout.positions.get(itemId);
|
|
676
|
-
return pos ? { x: pos.x, y: pos.y, width: BASE_CARD_WIDTH, height: BASE_CARD_HEIGHT } : null;
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
setIsZooming(true);
|
|
681
|
-
|
|
682
|
-
animateZoomAndScroll({
|
|
683
|
-
container,
|
|
684
|
-
cardPosition,
|
|
685
|
-
targetCardPosition,
|
|
686
|
-
getCardPositionAtZoom,
|
|
687
|
-
startZoom: zoomLevel,
|
|
688
|
-
targetZoom: preSelectionState.zoom,
|
|
689
|
-
targetScrollLeft: preSelectionState.scrollLeft,
|
|
690
|
-
targetScrollTop: preSelectionState.scrollTop,
|
|
691
|
-
onUpdate: setZoomLevel,
|
|
692
|
-
onComplete: () => {
|
|
693
|
-
setIsZooming(false);
|
|
694
|
-
setSelectedItem(null);
|
|
695
|
-
setPreSelectionState(null);
|
|
696
|
-
},
|
|
697
|
-
});
|
|
698
|
-
}, [preSelectionState, selectedItem, zoomLevel, viewMode, resolveId, setZoomLevel, layout, grouping, containerDimensions]);
|
|
699
|
-
|
|
700
|
-
// Use base card dimensions - zoom is applied as transform in canvas
|
|
701
|
-
const cardWidth = BASE_CARD_WIDTH;
|
|
702
|
-
const cardHeight = BASE_CARD_HEIGHT;
|
|
703
|
-
|
|
704
|
-
// Calculate filter options
|
|
705
|
-
const filterOptions = useFilterOptions(data, filters, filterState, rangeFilterState);
|
|
706
|
-
|
|
707
|
-
const hasFilters = Boolean(filters && filters.length > 0);
|
|
708
|
-
const activeFilterCount = Object.values(filterState).reduce((sum: number, vals) => sum + (vals as Set<string>).size, 0) +
|
|
709
|
-
Object.values(rangeFilterState).filter(r => r !== null).length;
|
|
710
|
-
|
|
711
|
-
const viewerClassName = [
|
|
712
|
-
'pivot-viewer',
|
|
713
|
-
className,
|
|
714
|
-
hasFilters ? (filtersOpen ? 'filters-open' : 'filters-closed') : 'no-filters',
|
|
715
|
-
viewMode === 'grouped' ? 'bucket-mode' : 'collection-mode',
|
|
716
|
-
]
|
|
717
|
-
.filter(Boolean)
|
|
718
|
-
.join(' ');
|
|
719
|
-
|
|
720
|
-
return (
|
|
721
|
-
<div className={viewerClassName}>
|
|
722
|
-
<FilterPanelContainer
|
|
723
|
-
isOpen={filtersOpen && hasFilters}
|
|
724
|
-
search={search}
|
|
725
|
-
filterState={filterState}
|
|
726
|
-
rangeFilterState={rangeFilterState}
|
|
727
|
-
expandedFilterKey={expandedFilterKey}
|
|
728
|
-
filterOptions={filterOptions}
|
|
729
|
-
anchorRef={filterButtonRef}
|
|
730
|
-
onClose={() => setFiltersOpen(false)}
|
|
731
|
-
onSearchChange={setSearch}
|
|
732
|
-
onFilterToggle={handleToggleFilter}
|
|
733
|
-
onFilterClear={handleClearFilter}
|
|
734
|
-
onRangeChange={handleRangeChange}
|
|
735
|
-
onExpandedFilterChange={setExpandedFilterKey}
|
|
736
|
-
/>
|
|
737
|
-
|
|
738
|
-
<main className="pv-main">
|
|
739
|
-
<ToolbarContainer
|
|
740
|
-
hasFilters={hasFilters}
|
|
741
|
-
filtersOpen={filtersOpen}
|
|
742
|
-
filteredCount={visibleIds.length}
|
|
743
|
-
viewMode={viewMode}
|
|
744
|
-
zoomLevel={zoomLevel}
|
|
745
|
-
activeDimensionKey={activeDimensionKey}
|
|
746
|
-
dimensions={dimensions}
|
|
747
|
-
activeFilterCount={activeFilterCount}
|
|
748
|
-
onFiltersToggle={() => setFiltersOpen((prev) => !prev)}
|
|
749
|
-
onViewModeChange={setViewMode}
|
|
750
|
-
onZoomIn={handleZoomIn}
|
|
751
|
-
onZoomOut={handleZoomOut}
|
|
752
|
-
onZoomSlider={handleZoomSlider}
|
|
753
|
-
onDimensionChange={setActiveDimensionKey}
|
|
754
|
-
filterButtonRef={filterButtonRef}
|
|
755
|
-
/>
|
|
756
|
-
|
|
757
|
-
<PivotViewerMain
|
|
758
|
-
data={data}
|
|
759
|
-
ready={ready}
|
|
760
|
-
isLoading={isLoading}
|
|
761
|
-
visibleIds={visibleIds}
|
|
762
|
-
grouping={grouping}
|
|
763
|
-
layout={layout}
|
|
764
|
-
cardWidth={cardWidth}
|
|
765
|
-
cardHeight={cardHeight}
|
|
766
|
-
zoomLevel={zoomLevel}
|
|
767
|
-
scrollPosition={scrollPosition}
|
|
768
|
-
containerDimensions={containerDimensions}
|
|
769
|
-
selectedItem={selectedItem}
|
|
770
|
-
hoveredGroupIndex={hoveredGroupIndex}
|
|
771
|
-
isZooming={isZooming}
|
|
772
|
-
viewMode={viewMode}
|
|
773
|
-
cardRenderer={cardRenderer}
|
|
774
|
-
resolveId={resolveId}
|
|
775
|
-
emptyContent={emptyContent}
|
|
776
|
-
dimensionFilter={dimensionFilter}
|
|
777
|
-
onCardClick={handleCardClick}
|
|
778
|
-
onPanStart={handlePanStart as (e: React.MouseEvent) => void}
|
|
779
|
-
onPanMove={handlePanMove as (e: React.MouseEvent) => void}
|
|
780
|
-
onPanEnd={handlePanEnd}
|
|
781
|
-
onGroupHover={setHoveredGroupIndex}
|
|
782
|
-
onAxisLabelClick={handleAxisLabelClick}
|
|
783
|
-
onCloseDetail={closeDetail}
|
|
784
|
-
containerRef={containerRef}
|
|
785
|
-
axisLabelsRef={axisLabelsRef}
|
|
786
|
-
spacerRef={spacerRef}
|
|
787
|
-
/>
|
|
788
|
-
</main>
|
|
789
|
-
</div>
|
|
790
|
-
);
|
|
791
|
-
}
|