@cratis/components 0.1.9 → 0.1.10
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/PivotViewer/PivotViewer.css +1258 -0
- package/dist/cjs/PivotViewer/components/Spinner.css +77 -0
- package/dist/cjs/TimeMachine/EventsView.css +213 -0
- package/dist/cjs/TimeMachine/TimeMachine.css +567 -0
- package/dist/esm/PivotViewer/PivotViewer.css +1258 -0
- package/dist/esm/PivotViewer/components/Spinner.css +77 -0
- package/dist/esm/TimeMachine/EventsView.css +213 -0
- package/dist/esm/TimeMachine/TimeMachine.css +567 -0
- package/package.json +3 -4
- 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/global.d.ts +0 -11
- package/index.ts +0 -22
- package/useOverlayZIndex.ts +0 -32
- package/vite.config.ts +0 -80
|
@@ -1,229 +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 type { ReactNode } from 'react';
|
|
5
|
-
import type { ItemId, LayoutResult, GroupingResult } from '../engine/types';
|
|
6
|
-
import type { ViewMode } from './Toolbar';
|
|
7
|
-
import type { PivotDimensionFilter } from '../hooks/useDimensionState';
|
|
8
|
-
import { Spinner } from './Spinner';
|
|
9
|
-
import { PivotCanvas } from './PivotCanvas';
|
|
10
|
-
import { AxisLabels } from './AxisLabels';
|
|
11
|
-
import { DetailPanel } from './DetailPanel';
|
|
12
|
-
|
|
13
|
-
export interface PivotViewerMainProps<TItem extends object> {
|
|
14
|
-
data: TItem[];
|
|
15
|
-
ready: boolean;
|
|
16
|
-
isLoading: boolean;
|
|
17
|
-
visibleIds: Uint32Array;
|
|
18
|
-
grouping: GroupingResult;
|
|
19
|
-
layout: LayoutResult;
|
|
20
|
-
cardWidth: number;
|
|
21
|
-
cardHeight: number;
|
|
22
|
-
zoomLevel: number;
|
|
23
|
-
scrollPosition: { x: number; y: number };
|
|
24
|
-
containerDimensions: { width: number; height: number };
|
|
25
|
-
selectedItem: TItem | null;
|
|
26
|
-
hoveredGroupIndex: number | null;
|
|
27
|
-
isZooming: boolean;
|
|
28
|
-
viewMode: ViewMode;
|
|
29
|
-
cardRenderer?: (item: TItem) => ReactNode;
|
|
30
|
-
resolveId: (item: TItem, index: number) => ItemId;
|
|
31
|
-
emptyContent?: ReactNode;
|
|
32
|
-
dimensionFilter: PivotDimensionFilter;
|
|
33
|
-
onCardClick: (item: TItem, e: MouseEvent, id: number | string) => void;
|
|
34
|
-
onPanStart: (e: React.MouseEvent) => void;
|
|
35
|
-
onPanMove: (e: React.MouseEvent) => void;
|
|
36
|
-
onPanEnd: () => void;
|
|
37
|
-
onGroupHover: (index: number | null) => void;
|
|
38
|
-
onAxisLabelClick: (value: string) => void;
|
|
39
|
-
onCloseDetail: () => void;
|
|
40
|
-
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
41
|
-
axisLabelsRef: React.RefObject<HTMLDivElement | null>;
|
|
42
|
-
spacerRef: React.RefObject<HTMLDivElement | null>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function PivotViewerMain<TItem extends object>({
|
|
46
|
-
data,
|
|
47
|
-
ready,
|
|
48
|
-
isLoading,
|
|
49
|
-
visibleIds,
|
|
50
|
-
grouping,
|
|
51
|
-
layout,
|
|
52
|
-
cardWidth,
|
|
53
|
-
cardHeight,
|
|
54
|
-
zoomLevel,
|
|
55
|
-
scrollPosition,
|
|
56
|
-
containerDimensions,
|
|
57
|
-
selectedItem,
|
|
58
|
-
hoveredGroupIndex,
|
|
59
|
-
isZooming,
|
|
60
|
-
viewMode,
|
|
61
|
-
cardRenderer,
|
|
62
|
-
resolveId,
|
|
63
|
-
emptyContent,
|
|
64
|
-
dimensionFilter,
|
|
65
|
-
onCardClick,
|
|
66
|
-
onPanStart,
|
|
67
|
-
onPanMove,
|
|
68
|
-
onPanEnd,
|
|
69
|
-
onGroupHover,
|
|
70
|
-
onAxisLabelClick,
|
|
71
|
-
onCloseDetail,
|
|
72
|
-
containerRef,
|
|
73
|
-
axisLabelsRef,
|
|
74
|
-
spacerRef,
|
|
75
|
-
}: PivotViewerMainProps<TItem>) {
|
|
76
|
-
const handleViewportClick = (e: React.MouseEvent) => {
|
|
77
|
-
if (isZooming || !containerRef.current) return;
|
|
78
|
-
|
|
79
|
-
const container = containerRef.current;
|
|
80
|
-
const rect = container.getBoundingClientRect();
|
|
81
|
-
// Use live DOM scroll position for accurate hit testing
|
|
82
|
-
const scrollLeft = container.scrollLeft;
|
|
83
|
-
const scrollTop = container.scrollTop;
|
|
84
|
-
|
|
85
|
-
const clickX = e.clientX - rect.left + scrollLeft;
|
|
86
|
-
const clickY = e.clientY - rect.top + scrollTop;
|
|
87
|
-
|
|
88
|
-
const worldX = clickX / zoomLevel;
|
|
89
|
-
const worldY = clickY / zoomLevel;
|
|
90
|
-
|
|
91
|
-
// Check visible items
|
|
92
|
-
for (let i = 0; i < visibleIds.length; i++) {
|
|
93
|
-
const id = visibleIds[i];
|
|
94
|
-
const pos = layout.positions.get(id);
|
|
95
|
-
if (pos) {
|
|
96
|
-
if (worldX >= pos.x && worldX <= pos.x + cardWidth &&
|
|
97
|
-
worldY >= pos.y && worldY <= pos.y + cardHeight) {
|
|
98
|
-
const item = data[id];
|
|
99
|
-
if (item) {
|
|
100
|
-
onCardClick(item, e.nativeEvent as unknown as MouseEvent, id);
|
|
101
|
-
}
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const handleViewportMouseMove = (e: React.MouseEvent) => {
|
|
109
|
-
if (isZooming || !containerRef.current) return;
|
|
110
|
-
|
|
111
|
-
const container = containerRef.current;
|
|
112
|
-
const rect = container.getBoundingClientRect();
|
|
113
|
-
const scrollLeft = container.scrollLeft;
|
|
114
|
-
const scrollTop = container.scrollTop;
|
|
115
|
-
|
|
116
|
-
const mouseX = e.clientX - rect.left + scrollLeft;
|
|
117
|
-
const mouseY = e.clientY - rect.top + scrollTop;
|
|
118
|
-
|
|
119
|
-
const worldX = mouseX / zoomLevel;
|
|
120
|
-
const worldY = mouseY / zoomLevel;
|
|
121
|
-
|
|
122
|
-
let isOverCard = false;
|
|
123
|
-
for (let i = 0; i < visibleIds.length; i++) {
|
|
124
|
-
const id = visibleIds[i];
|
|
125
|
-
const pos = layout.positions.get(id);
|
|
126
|
-
if (pos) {
|
|
127
|
-
if (worldX >= pos.x && worldX <= pos.x + cardWidth &&
|
|
128
|
-
worldY >= pos.y && worldY <= pos.y + cardHeight) {
|
|
129
|
-
isOverCard = true;
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
container.style.cursor = isOverCard ? 'pointer' : 'default';
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
return isLoading ? (
|
|
139
|
-
<Spinner />
|
|
140
|
-
) : (
|
|
141
|
-
<div className="pv-groups-wrapper">
|
|
142
|
-
<div style={{ position: 'relative', flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
|
143
|
-
<div
|
|
144
|
-
className={`pv-viewport ${isZooming ? 'pv-zooming' : ''}`}
|
|
145
|
-
ref={containerRef}
|
|
146
|
-
style={{ overflow: 'auto', position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 1 }}
|
|
147
|
-
onClick={handleViewportClick}
|
|
148
|
-
onMouseMove={handleViewportMouseMove}
|
|
149
|
-
>
|
|
150
|
-
{/* Spacer for scrolling - explicitly rendered to allow synchronous updates during animation */}
|
|
151
|
-
<div
|
|
152
|
-
ref={spacerRef}
|
|
153
|
-
style={{
|
|
154
|
-
position: 'absolute',
|
|
155
|
-
top: 0,
|
|
156
|
-
left: 0,
|
|
157
|
-
width: layout.totalWidth * zoomLevel,
|
|
158
|
-
height: layout.totalHeight * zoomLevel,
|
|
159
|
-
pointerEvents: 'none'
|
|
160
|
-
}}
|
|
161
|
-
/>
|
|
162
|
-
|
|
163
|
-
{!ready && (
|
|
164
|
-
<div className="pv-loading">Building indexes...</div>
|
|
165
|
-
)}
|
|
166
|
-
|
|
167
|
-
{ready && visibleIds.length === 0 && (
|
|
168
|
-
<div className="pv-empty">
|
|
169
|
-
{emptyContent ?? 'No items to display.'}
|
|
170
|
-
</div>
|
|
171
|
-
)}
|
|
172
|
-
|
|
173
|
-
{ready && visibleIds.length > 0 && (
|
|
174
|
-
<PivotCanvas
|
|
175
|
-
items={data}
|
|
176
|
-
layout={layout}
|
|
177
|
-
grouping={grouping}
|
|
178
|
-
visibleIds={visibleIds}
|
|
179
|
-
cardWidth={cardWidth}
|
|
180
|
-
cardHeight={cardHeight}
|
|
181
|
-
zoomLevel={zoomLevel}
|
|
182
|
-
panX={scrollPosition.x}
|
|
183
|
-
panY={scrollPosition.y}
|
|
184
|
-
viewportWidth={containerDimensions.width}
|
|
185
|
-
viewportHeight={containerDimensions.height}
|
|
186
|
-
selectedId={selectedItem ? resolveId(selectedItem, 0) : null}
|
|
187
|
-
hoveredGroupIndex={hoveredGroupIndex}
|
|
188
|
-
isZooming={isZooming}
|
|
189
|
-
cardRenderer={cardRenderer}
|
|
190
|
-
resolveId={resolveId}
|
|
191
|
-
onCardClick={onCardClick}
|
|
192
|
-
onPanStart={onPanStart as any}
|
|
193
|
-
onPanMove={onPanMove as any}
|
|
194
|
-
onPanEnd={onPanEnd}
|
|
195
|
-
containerRef={containerRef}
|
|
196
|
-
viewMode={viewMode}
|
|
197
|
-
/>
|
|
198
|
-
)}
|
|
199
|
-
</div>
|
|
200
|
-
<DetailPanel
|
|
201
|
-
selectedItem={selectedItem}
|
|
202
|
-
onClose={onCloseDetail}
|
|
203
|
-
/>
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
{viewMode === 'grouped' && grouping.groups.length > 0 && (
|
|
207
|
-
<AxisLabels
|
|
208
|
-
groups={grouping.groups.map((g) => ({
|
|
209
|
-
key: g.key,
|
|
210
|
-
value: g.value,
|
|
211
|
-
label: String(g.value),
|
|
212
|
-
items: [],
|
|
213
|
-
count: g.ids.length,
|
|
214
|
-
}))}
|
|
215
|
-
bucketWidths={layout.bucketWidths || []}
|
|
216
|
-
zoomLevel={zoomLevel}
|
|
217
|
-
dimensionFilter={dimensionFilter}
|
|
218
|
-
hoveredGroup={hoveredGroupIndex !== null ? String(grouping.groups[hoveredGroupIndex]?.value) : null}
|
|
219
|
-
onHover={(label) => {
|
|
220
|
-
const index = grouping.groups.findIndex(g => String(g.value) === label);
|
|
221
|
-
onGroupHover(index >= 0 ? index : null);
|
|
222
|
-
}}
|
|
223
|
-
onClick={onAxisLabelClick}
|
|
224
|
-
containerRef={axisLabelsRef}
|
|
225
|
-
/>
|
|
226
|
-
)}
|
|
227
|
-
</div>
|
|
228
|
-
);
|
|
229
|
-
}
|
|
@@ -1,220 +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 { PivotPrimitive } from '../types';
|
|
6
|
-
|
|
7
|
-
export interface RangeHistogramFilterProps {
|
|
8
|
-
values: PivotPrimitive[];
|
|
9
|
-
min: number;
|
|
10
|
-
max: number;
|
|
11
|
-
buckets?: number;
|
|
12
|
-
selectedRange: [number, number] | null;
|
|
13
|
-
onChange: (range: [number, number] | null) => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface HistogramBucket {
|
|
17
|
-
start: number;
|
|
18
|
-
end: number;
|
|
19
|
-
count: number;
|
|
20
|
-
maxCount: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function RangeHistogramFilter({
|
|
24
|
-
values,
|
|
25
|
-
min,
|
|
26
|
-
max,
|
|
27
|
-
buckets = 20,
|
|
28
|
-
selectedRange,
|
|
29
|
-
onChange,
|
|
30
|
-
}: RangeHistogramFilterProps) {
|
|
31
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
32
|
-
const [isDragging, setIsDragging] = useState<'left' | 'right' | 'range' | null>(null);
|
|
33
|
-
const [dragStart, setDragStart] = useState<{ x: number; range: [number, number] } | null>(null);
|
|
34
|
-
|
|
35
|
-
const numericValues = useMemo(() => {
|
|
36
|
-
return values
|
|
37
|
-
.map((v) => {
|
|
38
|
-
if (typeof v === 'number') return v;
|
|
39
|
-
if (v instanceof Date) return v.getTime();
|
|
40
|
-
const parsed = Number(v);
|
|
41
|
-
return Number.isNaN(parsed) ? null : parsed;
|
|
42
|
-
})
|
|
43
|
-
.filter((v): v is number => v !== null);
|
|
44
|
-
}, [values]);
|
|
45
|
-
|
|
46
|
-
const histogram = useMemo((): HistogramBucket[] => {
|
|
47
|
-
const range = max - min;
|
|
48
|
-
if (range <= 0 || numericValues.length === 0) {
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const bucketSize = range / buckets;
|
|
53
|
-
const bucketCounts: number[] = Array(buckets).fill(0);
|
|
54
|
-
|
|
55
|
-
numericValues.forEach((value) => {
|
|
56
|
-
const bucketIndex = Math.min(
|
|
57
|
-
Math.floor((value - min) / bucketSize),
|
|
58
|
-
buckets - 1
|
|
59
|
-
);
|
|
60
|
-
if (bucketIndex >= 0 && bucketIndex < buckets) {
|
|
61
|
-
bucketCounts[bucketIndex]++;
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const maxCount = Math.max(...bucketCounts, 1);
|
|
66
|
-
|
|
67
|
-
return bucketCounts.map((count, i) => ({
|
|
68
|
-
start: min + i * bucketSize,
|
|
69
|
-
end: min + (i + 1) * bucketSize,
|
|
70
|
-
count,
|
|
71
|
-
maxCount,
|
|
72
|
-
}));
|
|
73
|
-
}, [numericValues, min, max, buckets]);
|
|
74
|
-
|
|
75
|
-
const currentRange = selectedRange ?? [min, max];
|
|
76
|
-
|
|
77
|
-
const getPositionFromValue = useCallback(
|
|
78
|
-
(value: number) => {
|
|
79
|
-
const range = max - min;
|
|
80
|
-
if (range <= 0) return 0;
|
|
81
|
-
return ((value - min) / range) * 100;
|
|
82
|
-
},
|
|
83
|
-
[min, max]
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const handleMouseDown = (
|
|
87
|
-
e: React.MouseEvent,
|
|
88
|
-
handle: 'left' | 'right' | 'range'
|
|
89
|
-
) => {
|
|
90
|
-
(e as any).preventDefault?.();
|
|
91
|
-
setIsDragging(handle);
|
|
92
|
-
setDragStart({ x: e.clientX, range: [...currentRange] as [number, number] });
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
if (!isDragging || !dragStart || !containerRef.current) return;
|
|
97
|
-
|
|
98
|
-
const container = containerRef.current;
|
|
99
|
-
const rect = container.getBoundingClientRect();
|
|
100
|
-
const range = max - min;
|
|
101
|
-
|
|
102
|
-
const handleMouseMove = (e: MouseEvent) => {
|
|
103
|
-
const deltaX = e.clientX - dragStart.x;
|
|
104
|
-
const deltaPercent = (deltaX / rect.width) * 100;
|
|
105
|
-
const deltaValue = (deltaPercent / 100) * range;
|
|
106
|
-
|
|
107
|
-
let newRange: [number, number] = [...dragStart.range];
|
|
108
|
-
|
|
109
|
-
if (isDragging === 'left') {
|
|
110
|
-
newRange[0] = Math.max(min, Math.min(dragStart.range[0] + deltaValue, newRange[1] - range * 0.01));
|
|
111
|
-
} else if (isDragging === 'right') {
|
|
112
|
-
newRange[1] = Math.min(max, Math.max(dragStart.range[1] + deltaValue, newRange[0] + range * 0.01));
|
|
113
|
-
} else if (isDragging === 'range') {
|
|
114
|
-
const rangeWidth = dragStart.range[1] - dragStart.range[0];
|
|
115
|
-
let newStart = dragStart.range[0] + deltaValue;
|
|
116
|
-
let newEnd = dragStart.range[1] + deltaValue;
|
|
117
|
-
|
|
118
|
-
if (newStart < min) {
|
|
119
|
-
newStart = min;
|
|
120
|
-
newEnd = min + rangeWidth;
|
|
121
|
-
}
|
|
122
|
-
if (newEnd > max) {
|
|
123
|
-
newEnd = max;
|
|
124
|
-
newStart = max - rangeWidth;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
newRange = [newStart, newEnd];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
onChange(newRange);
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const handleMouseUp = () => {
|
|
134
|
-
setIsDragging(null);
|
|
135
|
-
setDragStart(null);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
document.addEventListener('mousemove', handleMouseMove);
|
|
139
|
-
document.addEventListener('mouseup', handleMouseUp);
|
|
140
|
-
|
|
141
|
-
return () => {
|
|
142
|
-
document.removeEventListener('mousemove', handleMouseMove);
|
|
143
|
-
document.removeEventListener('mouseup', handleMouseUp);
|
|
144
|
-
};
|
|
145
|
-
}, [isDragging, dragStart, min, max, onChange]);
|
|
146
|
-
|
|
147
|
-
const handleBarClick = (bucket: HistogramBucket) => {
|
|
148
|
-
onChange([bucket.start, bucket.end]);
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const handleClear = () => {
|
|
152
|
-
onChange(null);
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const leftPos = getPositionFromValue(currentRange[0]);
|
|
156
|
-
const rightPos = getPositionFromValue(currentRange[1]);
|
|
157
|
-
|
|
158
|
-
const formatValue = (value: number) => {
|
|
159
|
-
if (Number.isInteger(value)) return value.toString();
|
|
160
|
-
return value.toFixed(1);
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
return (
|
|
164
|
-
<div className="pv-range-histogram" ref={containerRef}>
|
|
165
|
-
<div className="pv-histogram-bars">
|
|
166
|
-
{histogram.map((bucket, i) => {
|
|
167
|
-
const heightPercent = (bucket.count / bucket.maxCount) * 100;
|
|
168
|
-
const isInRange =
|
|
169
|
-
bucket.start >= currentRange[0] && bucket.end <= currentRange[1];
|
|
170
|
-
const isPartiallyInRange =
|
|
171
|
-
bucket.end > currentRange[0] && bucket.start < currentRange[1];
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<button
|
|
175
|
-
key={i}
|
|
176
|
-
className={`pv-histogram-bar ${isInRange ? 'in-range' : ''} ${isPartiallyInRange && !isInRange ? 'partial' : ''}`}
|
|
177
|
-
style={{ height: `${heightPercent}%` }}
|
|
178
|
-
onClick={() => handleBarClick(bucket)}
|
|
179
|
-
title={`${formatValue(bucket.start)} - ${formatValue(bucket.end)}: ${bucket.count} items`}
|
|
180
|
-
type="button"
|
|
181
|
-
/>
|
|
182
|
-
);
|
|
183
|
-
})}
|
|
184
|
-
</div>
|
|
185
|
-
|
|
186
|
-
<div className="pv-range-slider">
|
|
187
|
-
<div className="pv-range-track" />
|
|
188
|
-
<div
|
|
189
|
-
className="pv-range-selection"
|
|
190
|
-
style={{
|
|
191
|
-
left: `${leftPos}%`,
|
|
192
|
-
width: `${rightPos - leftPos}%`,
|
|
193
|
-
}}
|
|
194
|
-
onMouseDown={(e) => handleMouseDown(e, 'range')}
|
|
195
|
-
/>
|
|
196
|
-
<div
|
|
197
|
-
className="pv-range-handle pv-range-handle-left"
|
|
198
|
-
style={{ left: `${leftPos}%` }}
|
|
199
|
-
onMouseDown={(e) => handleMouseDown(e, 'left')}
|
|
200
|
-
/>
|
|
201
|
-
<div
|
|
202
|
-
className="pv-range-handle pv-range-handle-right"
|
|
203
|
-
style={{ left: `${rightPos}%` }}
|
|
204
|
-
onMouseDown={(e) => handleMouseDown(e, 'right')}
|
|
205
|
-
/>
|
|
206
|
-
</div>
|
|
207
|
-
|
|
208
|
-
<div className="pv-range-labels">
|
|
209
|
-
<span className="pv-range-value">{formatValue(currentRange[0])}</span>
|
|
210
|
-
<span className="pv-range-value">{formatValue(currentRange[1])}</span>
|
|
211
|
-
</div>
|
|
212
|
-
|
|
213
|
-
{selectedRange && (
|
|
214
|
-
<button type="button" className="pv-range-clear" onClick={handleClear}>
|
|
215
|
-
Clear Range
|
|
216
|
-
</button>
|
|
217
|
-
)}
|
|
218
|
-
</div>
|
|
219
|
-
);
|
|
220
|
-
}
|
|
@@ -1,21 +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 './Spinner.css';
|
|
5
|
-
|
|
6
|
-
export function Spinner() {
|
|
7
|
-
return (
|
|
8
|
-
<div className="pv-loading">
|
|
9
|
-
<div className="pv-spinner">
|
|
10
|
-
<div className="pv-spinner-ring" />
|
|
11
|
-
<div className="pv-spinner-ring" />
|
|
12
|
-
<div className="pv-spinner-ring" />
|
|
13
|
-
<div className="pv-spinner-ring" />
|
|
14
|
-
<div className="pv-spinner-ring" />
|
|
15
|
-
<div className="pv-spinner-ring" />
|
|
16
|
-
<div className="pv-spinner-ring" />
|
|
17
|
-
<div className="pv-spinner-ring" />
|
|
18
|
-
</div>
|
|
19
|
-
</div>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
@@ -1,130 +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 type { PivotDimension } from '../types';
|
|
5
|
-
import { ZOOM_MIN, ZOOM_MAX, ZOOM_STEP } from '../utils/utils';
|
|
6
|
-
|
|
7
|
-
export type ViewMode = 'collection' | 'grouped';
|
|
8
|
-
|
|
9
|
-
export interface ToolbarProps<TItem extends object> {
|
|
10
|
-
hasFilters: boolean;
|
|
11
|
-
filtersOpen: boolean;
|
|
12
|
-
filteredCount: number;
|
|
13
|
-
viewMode: ViewMode;
|
|
14
|
-
zoomLevel: number;
|
|
15
|
-
activeDimensionKey: string;
|
|
16
|
-
dimensions: PivotDimension<TItem>[];
|
|
17
|
-
activeFilterCount: number;
|
|
18
|
-
onFiltersToggle: () => void;
|
|
19
|
-
onViewModeChange: (mode: ViewMode) => void;
|
|
20
|
-
onZoomIn: () => void;
|
|
21
|
-
onZoomOut: () => void;
|
|
22
|
-
onZoomSlider: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
23
|
-
onDimensionChange: (key: string) => void;
|
|
24
|
-
filterButtonRef: React.RefObject<HTMLButtonElement | null>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function Toolbar<TItem extends object>({
|
|
28
|
-
hasFilters,
|
|
29
|
-
filtersOpen,
|
|
30
|
-
filteredCount,
|
|
31
|
-
viewMode,
|
|
32
|
-
zoomLevel,
|
|
33
|
-
activeDimensionKey,
|
|
34
|
-
dimensions,
|
|
35
|
-
activeFilterCount,
|
|
36
|
-
onFiltersToggle,
|
|
37
|
-
onViewModeChange,
|
|
38
|
-
onZoomIn,
|
|
39
|
-
onZoomOut,
|
|
40
|
-
onZoomSlider,
|
|
41
|
-
onDimensionChange,
|
|
42
|
-
filterButtonRef,
|
|
43
|
-
}: ToolbarProps<TItem>) {
|
|
44
|
-
const labelText = 'Sort by';
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<header className="pv-toolbar">
|
|
48
|
-
<div className="pv-toolbar-left">
|
|
49
|
-
{hasFilters && (
|
|
50
|
-
<button
|
|
51
|
-
ref={filterButtonRef}
|
|
52
|
-
type="button"
|
|
53
|
-
className={`pv-filter-icon-button ${filtersOpen ? 'active' : ''}`}
|
|
54
|
-
onClick={onFiltersToggle}
|
|
55
|
-
title="Filters"
|
|
56
|
-
>
|
|
57
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
58
|
-
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
|
|
59
|
-
</svg>
|
|
60
|
-
{activeFilterCount > 0 && (
|
|
61
|
-
<span className="pv-filter-badge">{activeFilterCount}</span>
|
|
62
|
-
)}
|
|
63
|
-
</button>
|
|
64
|
-
)}
|
|
65
|
-
<h1>Pivot Viewer</h1>
|
|
66
|
-
<span className="pv-count">{filteredCount} events</span>
|
|
67
|
-
</div>
|
|
68
|
-
<div className="pv-toolbar-right">
|
|
69
|
-
<div className="pv-zoom-controls">
|
|
70
|
-
<button
|
|
71
|
-
type="button"
|
|
72
|
-
onClick={onZoomOut}
|
|
73
|
-
disabled={zoomLevel <= ZOOM_MIN}
|
|
74
|
-
title="Zoom out"
|
|
75
|
-
>
|
|
76
|
-
−
|
|
77
|
-
</button>
|
|
78
|
-
<input
|
|
79
|
-
type="range"
|
|
80
|
-
className="pv-zoom-slider"
|
|
81
|
-
min={ZOOM_MIN}
|
|
82
|
-
max={ZOOM_MAX}
|
|
83
|
-
step={ZOOM_STEP}
|
|
84
|
-
value={zoomLevel}
|
|
85
|
-
onChange={onZoomSlider}
|
|
86
|
-
title={`Zoom: ${Math.round(zoomLevel * 100)}%`}
|
|
87
|
-
/>
|
|
88
|
-
<span className="pv-zoom-level">{Math.round(zoomLevel * 100)}%</span>
|
|
89
|
-
<button
|
|
90
|
-
type="button"
|
|
91
|
-
onClick={onZoomIn}
|
|
92
|
-
disabled={zoomLevel >= ZOOM_MAX}
|
|
93
|
-
title="Zoom in"
|
|
94
|
-
>
|
|
95
|
-
+
|
|
96
|
-
</button>
|
|
97
|
-
</div>
|
|
98
|
-
<div className="pv-view-toggle">
|
|
99
|
-
<button
|
|
100
|
-
type="button"
|
|
101
|
-
className={viewMode === 'collection' ? 'active' : ''}
|
|
102
|
-
onClick={() => onViewModeChange('collection')}
|
|
103
|
-
>
|
|
104
|
-
Collection
|
|
105
|
-
</button>
|
|
106
|
-
<button
|
|
107
|
-
type="button"
|
|
108
|
-
className={viewMode === 'grouped' ? 'active' : ''}
|
|
109
|
-
onClick={() => onViewModeChange('grouped')}
|
|
110
|
-
>
|
|
111
|
-
Buckets
|
|
112
|
-
</button>
|
|
113
|
-
</div>
|
|
114
|
-
<label className="pv-dimension-select">
|
|
115
|
-
<span>{labelText}</span>
|
|
116
|
-
<select
|
|
117
|
-
value={activeDimensionKey}
|
|
118
|
-
onChange={(event) => onDimensionChange(event.target.value)}
|
|
119
|
-
>
|
|
120
|
-
{dimensions.map((dimension) => (
|
|
121
|
-
<option key={dimension.key} value={dimension.key}>
|
|
122
|
-
{dimension.label}
|
|
123
|
-
</option>
|
|
124
|
-
))}
|
|
125
|
-
</select>
|
|
126
|
-
</label>
|
|
127
|
-
</div>
|
|
128
|
-
</header>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
@@ -1,10 +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 { Toolbar, type ToolbarProps } from './Toolbar';
|
|
5
|
-
|
|
6
|
-
export type ToolbarContainerProps<TItem extends object> = ToolbarProps<TItem>;
|
|
7
|
-
|
|
8
|
-
export function ToolbarContainer<TItem extends object>(props: ToolbarContainerProps<TItem>) {
|
|
9
|
-
return <Toolbar {...props} />;
|
|
10
|
-
}
|
|
@@ -1,12 +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
|
-
export { PivotCanvas } from './PivotCanvas';
|
|
5
|
-
export { FilterPanel } from './FilterPanel';
|
|
6
|
-
export { Toolbar } from './Toolbar';
|
|
7
|
-
export { DetailPanel } from './DetailPanel';
|
|
8
|
-
export { AxisLabels } from './AxisLabels';
|
|
9
|
-
export { Spinner } from './Spinner';
|
|
10
|
-
export { RangeHistogramFilter } from './RangeHistogramFilter';
|
|
11
|
-
|
|
12
|
-
export type { ViewMode } from './Toolbar';
|