hpo-react-visualizer 0.0.1 → 0.0.2
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/index.cjs +376 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +376 -30
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/HpoVisualizer.tsx +88 -22
- package/src/ZoomControls.tsx +125 -0
- package/src/__tests__/hpoLabel.test.ts +71 -0
- package/src/__tests__/useZoom.test.ts +110 -0
- package/src/__tests__/zoomControls.test.tsx +106 -0
- package/src/constants.ts +90 -1
- package/src/index.ts +4 -0
- package/src/types.ts +32 -0
- package/src/useZoom.ts +196 -0
package/dist/index.d.cts
CHANGED
|
@@ -5,6 +5,11 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
5
5
|
* Supported organ identifiers
|
|
6
6
|
*/
|
|
7
7
|
type OrganId = "head" | "eye" | "ear" | "heart" | "lung" | "digestive" | "kidney" | "integument" | "constitutional" | "limbs" | "nervous" | "breast" | "thoracicCavity" | "voice" | "metabolism" | "cell" | "endocrine" | "neoplasm" | "immune" | "muscle" | "growth" | "prenatal" | "blood";
|
|
8
|
+
/**
|
|
9
|
+
* HPO official category labels
|
|
10
|
+
* @see https://hpo.jax.org/
|
|
11
|
+
*/
|
|
12
|
+
type HPOLabel = "others" | "Growth abnormality" | "Abnormality of the genitourinary system" | "Abnormality of the immune system" | "Abnormality of the digestive system" | "Abnormality of metabolism/homeostasis" | "Abnormality of head or neck" | "Abnormality of the musculoskeletal system" | "Abnormality of the nervous system" | "Abnormality of the respiratory system" | "Abnormality of the eye" | "Abnormality of the cardiovascular system" | "Abnormality of the ear" | "Abnormality of prenatal development or birth" | "Abnormality of the integument" | "Abnormality of the breast" | "Abnormality of the endocrine system" | "Abnormality of blood and blood-forming tissues" | "Abnormality of limbs" | "Abnormality of the voice" | "Constitutional symptom" | "Neoplasm" | "Abnormal cellular phenotype" | "Abnormality of the thoracic cavity";
|
|
8
13
|
/**
|
|
9
14
|
* Color scheme for each organ.
|
|
10
15
|
*/
|
|
@@ -129,6 +134,8 @@ interface HpoVisualizerProps {
|
|
|
129
134
|
className?: string;
|
|
130
135
|
/** Additional inline styles */
|
|
131
136
|
style?: CSSProperties;
|
|
137
|
+
/** Maximum zoom level (default: 5 = 500%) */
|
|
138
|
+
maxZoom?: number;
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
/**
|
|
@@ -143,6 +150,19 @@ declare const ORGAN_NAMES_EN: Record<OrganId, string>;
|
|
|
143
150
|
* Display names for organs (Korean)
|
|
144
151
|
*/
|
|
145
152
|
declare const ORGAN_NAMES_KO: Record<OrganId, string>;
|
|
153
|
+
/**
|
|
154
|
+
* All HPO category labels
|
|
155
|
+
*/
|
|
156
|
+
declare const HPO_LABELS: readonly HPOLabel[];
|
|
157
|
+
/**
|
|
158
|
+
* Mapping from HPO label to OrganId
|
|
159
|
+
* Note: "others" has no corresponding OrganId
|
|
160
|
+
*/
|
|
161
|
+
declare const HPO_LABEL_TO_ORGAN: Record<Exclude<HPOLabel, "others">, OrganId>;
|
|
162
|
+
/**
|
|
163
|
+
* Mapping from OrganId to HPO label
|
|
164
|
+
*/
|
|
165
|
+
declare const ORGAN_TO_HPO_LABEL: Record<OrganId, Exclude<HPOLabel, "others">>;
|
|
146
166
|
/**
|
|
147
167
|
* Color palettes for each color scheme.
|
|
148
168
|
* Based on Tailwind CSS color palette.
|
|
@@ -162,8 +182,9 @@ declare const ANIMATION_DURATION_MS = 150;
|
|
|
162
182
|
* - Click to select/deselect organs
|
|
163
183
|
* - Hover effects work independently even when an organ is selected
|
|
164
184
|
* - Exposes hover and selection state via callbacks
|
|
185
|
+
* - Zoom and pan support with mouse wheel and drag
|
|
165
186
|
*/
|
|
166
|
-
declare function HpoVisualizer({ organs, visibleOrgans, hoveredOrgan: controlledHovered, selectedOrgan: controlledSelected, onHover, onSelect, colorPalette: inputColorPalette, width, height, className, style, }: HpoVisualizerProps): react_jsx_runtime.JSX.Element;
|
|
187
|
+
declare function HpoVisualizer({ organs, visibleOrgans, hoveredOrgan: controlledHovered, selectedOrgan: controlledSelected, onHover, onSelect, colorPalette: inputColorPalette, width, height, className, style, maxZoom, }: HpoVisualizerProps): react_jsx_runtime.JSX.Element;
|
|
167
188
|
|
|
168
189
|
interface OrganSvgWrapperProps {
|
|
169
190
|
/** Organ identifier */
|
|
@@ -241,4 +262,4 @@ interface UseOrganInteractionResult {
|
|
|
241
262
|
*/
|
|
242
263
|
declare function useOrganInteraction(options?: UseOrganInteractionOptions): UseOrganInteractionResult;
|
|
243
264
|
|
|
244
|
-
export { ANIMATION_DURATION_MS, type ColorScale as ColorPalette, type ColorName as ColorScheme, DEFAULT_COLOR_PALETTE, HpoVisualizer, type HpoVisualizerProps, ORGAN_COMPONENTS, ORGAN_IDS, ORGAN_NAMES_EN, ORGAN_NAMES_KO, type OrganConfig, type OrganId, type OrganInteractionHandlers, type OrganInteractionState, type OrganStyle, OrganSvg, type OrganSvgProps, type OrganSvgWrapperProps, type UseOrganInteractionOptions, type UseOrganInteractionResult, useOrganInteraction };
|
|
265
|
+
export { ANIMATION_DURATION_MS, type ColorScale as ColorPalette, type ColorName as ColorScheme, DEFAULT_COLOR_PALETTE, type HPOLabel, HPO_LABELS, HPO_LABEL_TO_ORGAN, HpoVisualizer, type HpoVisualizerProps, ORGAN_COMPONENTS, ORGAN_IDS, ORGAN_NAMES_EN, ORGAN_NAMES_KO, ORGAN_TO_HPO_LABEL, type OrganConfig, type OrganId, type OrganInteractionHandlers, type OrganInteractionState, type OrganStyle, OrganSvg, type OrganSvgProps, type OrganSvgWrapperProps, type UseOrganInteractionOptions, type UseOrganInteractionResult, useOrganInteraction };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
5
5
|
* Supported organ identifiers
|
|
6
6
|
*/
|
|
7
7
|
type OrganId = "head" | "eye" | "ear" | "heart" | "lung" | "digestive" | "kidney" | "integument" | "constitutional" | "limbs" | "nervous" | "breast" | "thoracicCavity" | "voice" | "metabolism" | "cell" | "endocrine" | "neoplasm" | "immune" | "muscle" | "growth" | "prenatal" | "blood";
|
|
8
|
+
/**
|
|
9
|
+
* HPO official category labels
|
|
10
|
+
* @see https://hpo.jax.org/
|
|
11
|
+
*/
|
|
12
|
+
type HPOLabel = "others" | "Growth abnormality" | "Abnormality of the genitourinary system" | "Abnormality of the immune system" | "Abnormality of the digestive system" | "Abnormality of metabolism/homeostasis" | "Abnormality of head or neck" | "Abnormality of the musculoskeletal system" | "Abnormality of the nervous system" | "Abnormality of the respiratory system" | "Abnormality of the eye" | "Abnormality of the cardiovascular system" | "Abnormality of the ear" | "Abnormality of prenatal development or birth" | "Abnormality of the integument" | "Abnormality of the breast" | "Abnormality of the endocrine system" | "Abnormality of blood and blood-forming tissues" | "Abnormality of limbs" | "Abnormality of the voice" | "Constitutional symptom" | "Neoplasm" | "Abnormal cellular phenotype" | "Abnormality of the thoracic cavity";
|
|
8
13
|
/**
|
|
9
14
|
* Color scheme for each organ.
|
|
10
15
|
*/
|
|
@@ -129,6 +134,8 @@ interface HpoVisualizerProps {
|
|
|
129
134
|
className?: string;
|
|
130
135
|
/** Additional inline styles */
|
|
131
136
|
style?: CSSProperties;
|
|
137
|
+
/** Maximum zoom level (default: 5 = 500%) */
|
|
138
|
+
maxZoom?: number;
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
/**
|
|
@@ -143,6 +150,19 @@ declare const ORGAN_NAMES_EN: Record<OrganId, string>;
|
|
|
143
150
|
* Display names for organs (Korean)
|
|
144
151
|
*/
|
|
145
152
|
declare const ORGAN_NAMES_KO: Record<OrganId, string>;
|
|
153
|
+
/**
|
|
154
|
+
* All HPO category labels
|
|
155
|
+
*/
|
|
156
|
+
declare const HPO_LABELS: readonly HPOLabel[];
|
|
157
|
+
/**
|
|
158
|
+
* Mapping from HPO label to OrganId
|
|
159
|
+
* Note: "others" has no corresponding OrganId
|
|
160
|
+
*/
|
|
161
|
+
declare const HPO_LABEL_TO_ORGAN: Record<Exclude<HPOLabel, "others">, OrganId>;
|
|
162
|
+
/**
|
|
163
|
+
* Mapping from OrganId to HPO label
|
|
164
|
+
*/
|
|
165
|
+
declare const ORGAN_TO_HPO_LABEL: Record<OrganId, Exclude<HPOLabel, "others">>;
|
|
146
166
|
/**
|
|
147
167
|
* Color palettes for each color scheme.
|
|
148
168
|
* Based on Tailwind CSS color palette.
|
|
@@ -162,8 +182,9 @@ declare const ANIMATION_DURATION_MS = 150;
|
|
|
162
182
|
* - Click to select/deselect organs
|
|
163
183
|
* - Hover effects work independently even when an organ is selected
|
|
164
184
|
* - Exposes hover and selection state via callbacks
|
|
185
|
+
* - Zoom and pan support with mouse wheel and drag
|
|
165
186
|
*/
|
|
166
|
-
declare function HpoVisualizer({ organs, visibleOrgans, hoveredOrgan: controlledHovered, selectedOrgan: controlledSelected, onHover, onSelect, colorPalette: inputColorPalette, width, height, className, style, }: HpoVisualizerProps): react_jsx_runtime.JSX.Element;
|
|
187
|
+
declare function HpoVisualizer({ organs, visibleOrgans, hoveredOrgan: controlledHovered, selectedOrgan: controlledSelected, onHover, onSelect, colorPalette: inputColorPalette, width, height, className, style, maxZoom, }: HpoVisualizerProps): react_jsx_runtime.JSX.Element;
|
|
167
188
|
|
|
168
189
|
interface OrganSvgWrapperProps {
|
|
169
190
|
/** Organ identifier */
|
|
@@ -241,4 +262,4 @@ interface UseOrganInteractionResult {
|
|
|
241
262
|
*/
|
|
242
263
|
declare function useOrganInteraction(options?: UseOrganInteractionOptions): UseOrganInteractionResult;
|
|
243
264
|
|
|
244
|
-
export { ANIMATION_DURATION_MS, type ColorScale as ColorPalette, type ColorName as ColorScheme, DEFAULT_COLOR_PALETTE, HpoVisualizer, type HpoVisualizerProps, ORGAN_COMPONENTS, ORGAN_IDS, ORGAN_NAMES_EN, ORGAN_NAMES_KO, type OrganConfig, type OrganId, type OrganInteractionHandlers, type OrganInteractionState, type OrganStyle, OrganSvg, type OrganSvgProps, type OrganSvgWrapperProps, type UseOrganInteractionOptions, type UseOrganInteractionResult, useOrganInteraction };
|
|
265
|
+
export { ANIMATION_DURATION_MS, type ColorScale as ColorPalette, type ColorName as ColorScheme, DEFAULT_COLOR_PALETTE, type HPOLabel, HPO_LABELS, HPO_LABEL_TO_ORGAN, HpoVisualizer, type HpoVisualizerProps, ORGAN_COMPONENTS, ORGAN_IDS, ORGAN_NAMES_EN, ORGAN_NAMES_KO, ORGAN_TO_HPO_LABEL, type OrganConfig, type OrganId, type OrganInteractionHandlers, type OrganInteractionState, type OrganStyle, OrganSvg, type OrganSvgProps, type OrganSvgWrapperProps, type UseOrganInteractionOptions, type UseOrganInteractionResult, useOrganInteraction };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useMemo, useState, useCallback, useId } from 'react';
|
|
3
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { useMemo, useState, useCallback, useId, useRef, useEffect } from 'react';
|
|
3
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
5
|
// src/constants.ts
|
|
6
6
|
var ORGAN_IDS = [
|
|
@@ -78,6 +78,82 @@ var ORGAN_NAMES_KO = {
|
|
|
78
78
|
prenatal: "\uD0DC\uC544",
|
|
79
79
|
blood: "\uD608\uC561"
|
|
80
80
|
};
|
|
81
|
+
var HPO_LABELS = [
|
|
82
|
+
"others",
|
|
83
|
+
"Growth abnormality",
|
|
84
|
+
"Abnormality of the genitourinary system",
|
|
85
|
+
"Abnormality of the immune system",
|
|
86
|
+
"Abnormality of the digestive system",
|
|
87
|
+
"Abnormality of metabolism/homeostasis",
|
|
88
|
+
"Abnormality of head or neck",
|
|
89
|
+
"Abnormality of the musculoskeletal system",
|
|
90
|
+
"Abnormality of the nervous system",
|
|
91
|
+
"Abnormality of the respiratory system",
|
|
92
|
+
"Abnormality of the eye",
|
|
93
|
+
"Abnormality of the cardiovascular system",
|
|
94
|
+
"Abnormality of the ear",
|
|
95
|
+
"Abnormality of prenatal development or birth",
|
|
96
|
+
"Abnormality of the integument",
|
|
97
|
+
"Abnormality of the breast",
|
|
98
|
+
"Abnormality of the endocrine system",
|
|
99
|
+
"Abnormality of blood and blood-forming tissues",
|
|
100
|
+
"Abnormality of limbs",
|
|
101
|
+
"Abnormality of the voice",
|
|
102
|
+
"Constitutional symptom",
|
|
103
|
+
"Neoplasm",
|
|
104
|
+
"Abnormal cellular phenotype",
|
|
105
|
+
"Abnormality of the thoracic cavity"
|
|
106
|
+
];
|
|
107
|
+
var HPO_LABEL_TO_ORGAN = {
|
|
108
|
+
"Growth abnormality": "growth",
|
|
109
|
+
"Abnormality of the genitourinary system": "kidney",
|
|
110
|
+
"Abnormality of the immune system": "immune",
|
|
111
|
+
"Abnormality of the digestive system": "digestive",
|
|
112
|
+
"Abnormality of metabolism/homeostasis": "metabolism",
|
|
113
|
+
"Abnormality of head or neck": "head",
|
|
114
|
+
"Abnormality of the musculoskeletal system": "muscle",
|
|
115
|
+
"Abnormality of the nervous system": "nervous",
|
|
116
|
+
"Abnormality of the respiratory system": "lung",
|
|
117
|
+
"Abnormality of the eye": "eye",
|
|
118
|
+
"Abnormality of the cardiovascular system": "heart",
|
|
119
|
+
"Abnormality of the ear": "ear",
|
|
120
|
+
"Abnormality of prenatal development or birth": "prenatal",
|
|
121
|
+
"Abnormality of the integument": "integument",
|
|
122
|
+
"Abnormality of the breast": "breast",
|
|
123
|
+
"Abnormality of the endocrine system": "endocrine",
|
|
124
|
+
"Abnormality of blood and blood-forming tissues": "blood",
|
|
125
|
+
"Abnormality of limbs": "limbs",
|
|
126
|
+
"Abnormality of the voice": "voice",
|
|
127
|
+
"Constitutional symptom": "constitutional",
|
|
128
|
+
Neoplasm: "neoplasm",
|
|
129
|
+
"Abnormal cellular phenotype": "cell",
|
|
130
|
+
"Abnormality of the thoracic cavity": "thoracicCavity"
|
|
131
|
+
};
|
|
132
|
+
var ORGAN_TO_HPO_LABEL = {
|
|
133
|
+
growth: "Growth abnormality",
|
|
134
|
+
kidney: "Abnormality of the genitourinary system",
|
|
135
|
+
immune: "Abnormality of the immune system",
|
|
136
|
+
digestive: "Abnormality of the digestive system",
|
|
137
|
+
metabolism: "Abnormality of metabolism/homeostasis",
|
|
138
|
+
head: "Abnormality of head or neck",
|
|
139
|
+
muscle: "Abnormality of the musculoskeletal system",
|
|
140
|
+
nervous: "Abnormality of the nervous system",
|
|
141
|
+
lung: "Abnormality of the respiratory system",
|
|
142
|
+
eye: "Abnormality of the eye",
|
|
143
|
+
heart: "Abnormality of the cardiovascular system",
|
|
144
|
+
ear: "Abnormality of the ear",
|
|
145
|
+
prenatal: "Abnormality of prenatal development or birth",
|
|
146
|
+
integument: "Abnormality of the integument",
|
|
147
|
+
breast: "Abnormality of the breast",
|
|
148
|
+
endocrine: "Abnormality of the endocrine system",
|
|
149
|
+
blood: "Abnormality of blood and blood-forming tissues",
|
|
150
|
+
limbs: "Abnormality of limbs",
|
|
151
|
+
voice: "Abnormality of the voice",
|
|
152
|
+
constitutional: "Constitutional symptom",
|
|
153
|
+
neoplasm: "Neoplasm",
|
|
154
|
+
cell: "Abnormal cellular phenotype",
|
|
155
|
+
thoracicCavity: "Abnormality of the thoracic cavity"
|
|
156
|
+
};
|
|
81
157
|
var DEFAULT_COLOR_PALETTE = {
|
|
82
158
|
blue: {
|
|
83
159
|
100: "#DBEAFE",
|
|
@@ -1301,6 +1377,211 @@ function useOrganInteraction(options = {}) {
|
|
|
1301
1377
|
isHovered
|
|
1302
1378
|
};
|
|
1303
1379
|
}
|
|
1380
|
+
function useZoom(options = {}) {
|
|
1381
|
+
const { minZoom = 1, maxZoom = 5, zoomStep = 0.5 } = options;
|
|
1382
|
+
const [zoom, setZoom] = useState(1);
|
|
1383
|
+
const [pan, setPan] = useState({ x: 0, y: 0 });
|
|
1384
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
1385
|
+
const dragStartRef = useRef({ x: 0, y: 0 });
|
|
1386
|
+
const panStartRef = useRef({ x: 0, y: 0 });
|
|
1387
|
+
const containerRef = useRef(null);
|
|
1388
|
+
const clampZoom = useCallback(
|
|
1389
|
+
(value) => Math.max(minZoom, Math.min(maxZoom, value)),
|
|
1390
|
+
[minZoom, maxZoom]
|
|
1391
|
+
);
|
|
1392
|
+
const zoomIn = useCallback(() => {
|
|
1393
|
+
setZoom((prev) => clampZoom(prev + zoomStep));
|
|
1394
|
+
}, [clampZoom, zoomStep]);
|
|
1395
|
+
const zoomOut = useCallback(() => {
|
|
1396
|
+
setZoom((prev) => clampZoom(prev - zoomStep));
|
|
1397
|
+
}, [clampZoom, zoomStep]);
|
|
1398
|
+
const resetZoom = useCallback(() => {
|
|
1399
|
+
setZoom(1);
|
|
1400
|
+
setPan({ x: 0, y: 0 });
|
|
1401
|
+
}, []);
|
|
1402
|
+
useEffect(() => {
|
|
1403
|
+
const container = containerRef.current;
|
|
1404
|
+
if (!container) return;
|
|
1405
|
+
const wheelZoomStep = 0.1;
|
|
1406
|
+
const handleWheel = (e) => {
|
|
1407
|
+
e.preventDefault();
|
|
1408
|
+
const rect = container.getBoundingClientRect();
|
|
1409
|
+
const mouseX = e.clientX - rect.left - rect.width / 2;
|
|
1410
|
+
const mouseY = e.clientY - rect.top - rect.height / 2;
|
|
1411
|
+
const delta = e.deltaY > 0 ? -wheelZoomStep : wheelZoomStep;
|
|
1412
|
+
setZoom((prevZoom) => {
|
|
1413
|
+
const newZoom = clampZoom(prevZoom + delta);
|
|
1414
|
+
if (newZoom === prevZoom) return prevZoom;
|
|
1415
|
+
setPan((prevPan) => {
|
|
1416
|
+
const contentX = (mouseX - prevPan.x) / prevZoom;
|
|
1417
|
+
const contentY = (mouseY - prevPan.y) / prevZoom;
|
|
1418
|
+
const newPanX = mouseX - contentX * newZoom;
|
|
1419
|
+
const newPanY = mouseY - contentY * newZoom;
|
|
1420
|
+
if (newZoom <= 1) {
|
|
1421
|
+
return { x: 0, y: 0 };
|
|
1422
|
+
}
|
|
1423
|
+
const maxPanX = rect.width * (newZoom - 1) / 2;
|
|
1424
|
+
const maxPanY = rect.height * (newZoom - 1) / 2;
|
|
1425
|
+
return {
|
|
1426
|
+
x: Math.max(-maxPanX, Math.min(maxPanX, newPanX)),
|
|
1427
|
+
y: Math.max(-maxPanY, Math.min(maxPanY, newPanY))
|
|
1428
|
+
};
|
|
1429
|
+
});
|
|
1430
|
+
return newZoom;
|
|
1431
|
+
});
|
|
1432
|
+
};
|
|
1433
|
+
container.addEventListener("wheel", handleWheel, { passive: false });
|
|
1434
|
+
return () => {
|
|
1435
|
+
container.removeEventListener("wheel", handleWheel);
|
|
1436
|
+
};
|
|
1437
|
+
}, [clampZoom]);
|
|
1438
|
+
const clampPan = useCallback((newPan, currentZoom) => {
|
|
1439
|
+
const container = containerRef.current;
|
|
1440
|
+
if (!container || currentZoom <= 1) {
|
|
1441
|
+
return { x: 0, y: 0 };
|
|
1442
|
+
}
|
|
1443
|
+
const containerWidth = container.clientWidth;
|
|
1444
|
+
const containerHeight = container.clientHeight;
|
|
1445
|
+
const maxPanX = containerWidth * (currentZoom - 1) / 2;
|
|
1446
|
+
const maxPanY = containerHeight * (currentZoom - 1) / 2;
|
|
1447
|
+
return {
|
|
1448
|
+
x: Math.max(-maxPanX, Math.min(maxPanX, newPan.x)),
|
|
1449
|
+
y: Math.max(-maxPanY, Math.min(maxPanY, newPan.y))
|
|
1450
|
+
};
|
|
1451
|
+
}, []);
|
|
1452
|
+
useEffect(() => {
|
|
1453
|
+
setPan((currentPan) => clampPan(currentPan, zoom));
|
|
1454
|
+
}, [zoom, clampPan]);
|
|
1455
|
+
const handleMouseDown = useCallback(
|
|
1456
|
+
(e) => {
|
|
1457
|
+
if (e.button !== 0 || zoom <= 1) return;
|
|
1458
|
+
setIsDragging(true);
|
|
1459
|
+
dragStartRef.current = { x: e.clientX, y: e.clientY };
|
|
1460
|
+
panStartRef.current = { ...pan };
|
|
1461
|
+
e.preventDefault();
|
|
1462
|
+
},
|
|
1463
|
+
[pan, zoom]
|
|
1464
|
+
);
|
|
1465
|
+
const handleMouseMove = useCallback(
|
|
1466
|
+
(e) => {
|
|
1467
|
+
if (!isDragging || zoom <= 1) return;
|
|
1468
|
+
const dx = e.clientX - dragStartRef.current.x;
|
|
1469
|
+
const dy = e.clientY - dragStartRef.current.y;
|
|
1470
|
+
const newPan = {
|
|
1471
|
+
x: panStartRef.current.x + dx,
|
|
1472
|
+
y: panStartRef.current.y + dy
|
|
1473
|
+
};
|
|
1474
|
+
setPan(clampPan(newPan, zoom));
|
|
1475
|
+
},
|
|
1476
|
+
[isDragging, zoom, clampPan]
|
|
1477
|
+
);
|
|
1478
|
+
const handleMouseUp = useCallback(() => {
|
|
1479
|
+
setIsDragging(false);
|
|
1480
|
+
}, []);
|
|
1481
|
+
const isDefaultZoom = zoom === 1 && pan.x === 0 && pan.y === 0;
|
|
1482
|
+
return {
|
|
1483
|
+
zoom,
|
|
1484
|
+
pan,
|
|
1485
|
+
zoomIn,
|
|
1486
|
+
zoomOut,
|
|
1487
|
+
resetZoom,
|
|
1488
|
+
handleMouseDown,
|
|
1489
|
+
handleMouseMove,
|
|
1490
|
+
handleMouseUp,
|
|
1491
|
+
isDragging,
|
|
1492
|
+
isDefaultZoom,
|
|
1493
|
+
containerRef
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
var buttonBaseStyle = {
|
|
1497
|
+
width: 32,
|
|
1498
|
+
height: 32,
|
|
1499
|
+
display: "flex",
|
|
1500
|
+
alignItems: "center",
|
|
1501
|
+
justifyContent: "center",
|
|
1502
|
+
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
|
1503
|
+
border: "1px solid #d1d5db",
|
|
1504
|
+
borderRadius: 6,
|
|
1505
|
+
cursor: "pointer",
|
|
1506
|
+
fontSize: 18,
|
|
1507
|
+
fontWeight: "bold",
|
|
1508
|
+
color: "#374151",
|
|
1509
|
+
transition: "background-color 0.15s ease",
|
|
1510
|
+
userSelect: "none"
|
|
1511
|
+
};
|
|
1512
|
+
var buttonDisabledStyle = {
|
|
1513
|
+
...buttonBaseStyle,
|
|
1514
|
+
opacity: 0.4,
|
|
1515
|
+
cursor: "not-allowed"
|
|
1516
|
+
};
|
|
1517
|
+
var getContainerStyle = (isVisible) => ({
|
|
1518
|
+
position: "absolute",
|
|
1519
|
+
bottom: 12,
|
|
1520
|
+
right: 12,
|
|
1521
|
+
display: "flex",
|
|
1522
|
+
flexDirection: "column",
|
|
1523
|
+
gap: 4,
|
|
1524
|
+
zIndex: 10,
|
|
1525
|
+
opacity: isVisible ? 1 : 0,
|
|
1526
|
+
transition: "opacity 0.2s ease-in-out",
|
|
1527
|
+
pointerEvents: isVisible ? "auto" : "none"
|
|
1528
|
+
});
|
|
1529
|
+
var getResetContainerStyle = (isVisible) => ({
|
|
1530
|
+
position: "absolute",
|
|
1531
|
+
bottom: 12,
|
|
1532
|
+
left: 12,
|
|
1533
|
+
zIndex: 10,
|
|
1534
|
+
opacity: isVisible ? 1 : 0,
|
|
1535
|
+
transition: "opacity 0.2s ease-in-out",
|
|
1536
|
+
pointerEvents: isVisible ? "auto" : "none"
|
|
1537
|
+
});
|
|
1538
|
+
var resetButtonStyle = {
|
|
1539
|
+
...buttonBaseStyle,
|
|
1540
|
+
width: "auto",
|
|
1541
|
+
padding: "6px 12px",
|
|
1542
|
+
fontSize: 12,
|
|
1543
|
+
fontWeight: 500
|
|
1544
|
+
};
|
|
1545
|
+
function ZoomControls({
|
|
1546
|
+
onZoomIn,
|
|
1547
|
+
onZoomOut,
|
|
1548
|
+
onReset,
|
|
1549
|
+
showResetButton,
|
|
1550
|
+
zoom,
|
|
1551
|
+
minZoom,
|
|
1552
|
+
maxZoom,
|
|
1553
|
+
isVisible
|
|
1554
|
+
}) {
|
|
1555
|
+
const isMinZoom = zoom <= minZoom;
|
|
1556
|
+
const isMaxZoom = zoom >= maxZoom;
|
|
1557
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1558
|
+
/* @__PURE__ */ jsxs("div", { style: getContainerStyle(isVisible), children: [
|
|
1559
|
+
/* @__PURE__ */ jsx(
|
|
1560
|
+
"button",
|
|
1561
|
+
{
|
|
1562
|
+
type: "button",
|
|
1563
|
+
onClick: onZoomIn,
|
|
1564
|
+
disabled: isMaxZoom,
|
|
1565
|
+
style: isMaxZoom ? buttonDisabledStyle : buttonBaseStyle,
|
|
1566
|
+
"aria-label": "Zoom in",
|
|
1567
|
+
children: "+"
|
|
1568
|
+
}
|
|
1569
|
+
),
|
|
1570
|
+
/* @__PURE__ */ jsx(
|
|
1571
|
+
"button",
|
|
1572
|
+
{
|
|
1573
|
+
type: "button",
|
|
1574
|
+
onClick: onZoomOut,
|
|
1575
|
+
disabled: isMinZoom,
|
|
1576
|
+
style: isMinZoom ? buttonDisabledStyle : buttonBaseStyle,
|
|
1577
|
+
"aria-label": "Zoom out",
|
|
1578
|
+
children: "\u2212"
|
|
1579
|
+
}
|
|
1580
|
+
)
|
|
1581
|
+
] }),
|
|
1582
|
+
/* @__PURE__ */ jsx("div", { style: getResetContainerStyle(isVisible && showResetButton), children: /* @__PURE__ */ jsx("button", { type: "button", onClick: onReset, style: resetButtonStyle, "aria-label": "Reset zoom", children: "\u21BA" }) })
|
|
1583
|
+
] });
|
|
1584
|
+
}
|
|
1304
1585
|
var ORGAN_POSITIONS = {
|
|
1305
1586
|
growth: { x: 104, y: 1, width: 12, height: 349, viewBox: "0 0 12 349" },
|
|
1306
1587
|
constitutional: { x: 0, y: 0, width: 122, height: 358, viewBox: "0 0 122 358" },
|
|
@@ -1351,6 +1632,7 @@ var FOREGROUND_ORGANS = [
|
|
|
1351
1632
|
"prenatal",
|
|
1352
1633
|
"blood"
|
|
1353
1634
|
];
|
|
1635
|
+
var MIN_ZOOM = 1;
|
|
1354
1636
|
function HpoVisualizer({
|
|
1355
1637
|
organs,
|
|
1356
1638
|
visibleOrgans,
|
|
@@ -1362,9 +1644,24 @@ function HpoVisualizer({
|
|
|
1362
1644
|
width = BODY_VIEWBOX.width,
|
|
1363
1645
|
height = BODY_VIEWBOX.height,
|
|
1364
1646
|
className,
|
|
1365
|
-
style
|
|
1647
|
+
style,
|
|
1648
|
+
maxZoom = 5
|
|
1366
1649
|
}) {
|
|
1367
1650
|
const visualizerID = useId();
|
|
1651
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
1652
|
+
const {
|
|
1653
|
+
zoom,
|
|
1654
|
+
pan,
|
|
1655
|
+
zoomIn,
|
|
1656
|
+
zoomOut,
|
|
1657
|
+
resetZoom,
|
|
1658
|
+
handleMouseDown,
|
|
1659
|
+
handleMouseMove,
|
|
1660
|
+
handleMouseUp,
|
|
1661
|
+
isDragging,
|
|
1662
|
+
isDefaultZoom,
|
|
1663
|
+
containerRef
|
|
1664
|
+
} = useZoom({ minZoom: MIN_ZOOM, maxZoom });
|
|
1368
1665
|
const colorPalette = useMemo(
|
|
1369
1666
|
() => createStrictColorPalette(inputColorPalette),
|
|
1370
1667
|
[inputColorPalette]
|
|
@@ -1401,7 +1698,19 @@ function HpoVisualizer({
|
|
|
1401
1698
|
position: "relative",
|
|
1402
1699
|
width,
|
|
1403
1700
|
height,
|
|
1404
|
-
...style
|
|
1701
|
+
...style,
|
|
1702
|
+
// Apply overflow after style spread to ensure it takes precedence
|
|
1703
|
+
// Use 'clip' instead of 'hidden' to respect padding
|
|
1704
|
+
overflow: "clip"
|
|
1705
|
+
};
|
|
1706
|
+
const contentStyle = {
|
|
1707
|
+
position: "relative",
|
|
1708
|
+
width: "100%",
|
|
1709
|
+
height: "100%",
|
|
1710
|
+
transform: `scale(${zoom}) translate(${pan.x / zoom}px, ${pan.y / zoom}px)`,
|
|
1711
|
+
transformOrigin: "center center",
|
|
1712
|
+
cursor: isDragging ? "grabbing" : zoom > 1 ? "grab" : "default",
|
|
1713
|
+
transition: isDragging ? "none" : "transform 0.1s ease-out"
|
|
1405
1714
|
};
|
|
1406
1715
|
const viewBox = `0 0 ${BODY_VIEWBOX.width} ${BODY_VIEWBOX.height}`;
|
|
1407
1716
|
const scale = Math.min(Number(width) / BODY_VIEWBOX.width, Number(height) / BODY_VIEWBOX.height);
|
|
@@ -1438,36 +1747,73 @@ function HpoVisualizer({
|
|
|
1438
1747
|
`${visualizerID}-${organId}`
|
|
1439
1748
|
);
|
|
1440
1749
|
};
|
|
1441
|
-
return /* @__PURE__ */ jsxs(
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1750
|
+
return /* @__PURE__ */ jsxs(
|
|
1751
|
+
"div",
|
|
1752
|
+
{
|
|
1753
|
+
ref: containerRef,
|
|
1754
|
+
className,
|
|
1755
|
+
style: containerStyle,
|
|
1756
|
+
onMouseEnter: () => setIsHovering(true),
|
|
1757
|
+
onMouseLeave: () => {
|
|
1758
|
+
setIsHovering(false);
|
|
1759
|
+
handleMouseUp();
|
|
1760
|
+
},
|
|
1761
|
+
onMouseDown: handleMouseDown,
|
|
1762
|
+
onMouseMove: handleMouseMove,
|
|
1763
|
+
onMouseUp: handleMouseUp,
|
|
1764
|
+
role: "application",
|
|
1765
|
+
tabIndex: 0,
|
|
1766
|
+
children: [
|
|
1767
|
+
/* @__PURE__ */ jsxs("div", { style: contentStyle, children: [
|
|
1768
|
+
BACKGROUND_ORGANS.map(
|
|
1769
|
+
(organId) => renderOrgan(organId, visibleOrganIds.includes(organId))
|
|
1770
|
+
),
|
|
1771
|
+
/* @__PURE__ */ jsxs(
|
|
1772
|
+
"svg",
|
|
1457
1773
|
{
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1774
|
+
x: translateX,
|
|
1775
|
+
y: "0",
|
|
1776
|
+
width,
|
|
1777
|
+
height,
|
|
1778
|
+
viewBox,
|
|
1779
|
+
style: { position: "absolute", top: "0", left: "0" },
|
|
1780
|
+
pointerEvents: "none",
|
|
1781
|
+
children: [
|
|
1782
|
+
/* @__PURE__ */ jsx("title", { children: "Human body" }),
|
|
1783
|
+
/* @__PURE__ */ jsx(
|
|
1784
|
+
Body,
|
|
1785
|
+
{
|
|
1786
|
+
colorScale: colorPalette[DEFAULT_COLOR_NAME],
|
|
1787
|
+
style: {
|
|
1788
|
+
fill: "#fff"
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
)
|
|
1792
|
+
]
|
|
1462
1793
|
}
|
|
1794
|
+
),
|
|
1795
|
+
FOREGROUND_ORGANS.map(
|
|
1796
|
+
(organId) => renderOrgan(organId, visibleOrganIds.includes(organId))
|
|
1463
1797
|
)
|
|
1464
|
-
]
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1798
|
+
] }),
|
|
1799
|
+
/* @__PURE__ */ jsx(
|
|
1800
|
+
ZoomControls,
|
|
1801
|
+
{
|
|
1802
|
+
onZoomIn: zoomIn,
|
|
1803
|
+
onZoomOut: zoomOut,
|
|
1804
|
+
onReset: resetZoom,
|
|
1805
|
+
showResetButton: !isDefaultZoom,
|
|
1806
|
+
zoom,
|
|
1807
|
+
minZoom: MIN_ZOOM,
|
|
1808
|
+
maxZoom,
|
|
1809
|
+
isVisible: isHovering
|
|
1810
|
+
}
|
|
1811
|
+
)
|
|
1812
|
+
]
|
|
1813
|
+
}
|
|
1814
|
+
);
|
|
1469
1815
|
}
|
|
1470
1816
|
|
|
1471
|
-
export { ANIMATION_DURATION_MS, DEFAULT_COLOR_PALETTE, HpoVisualizer, ORGAN_COMPONENTS, ORGAN_IDS, ORGAN_NAMES_EN, ORGAN_NAMES_KO, OrganSvg, useOrganInteraction };
|
|
1817
|
+
export { ANIMATION_DURATION_MS, DEFAULT_COLOR_PALETTE, HPO_LABELS, HPO_LABEL_TO_ORGAN, HpoVisualizer, ORGAN_COMPONENTS, ORGAN_IDS, ORGAN_NAMES_EN, ORGAN_NAMES_KO, ORGAN_TO_HPO_LABEL, OrganSvg, useOrganInteraction };
|
|
1472
1818
|
//# sourceMappingURL=index.js.map
|
|
1473
1819
|
//# sourceMappingURL=index.js.map
|