hpo-react-visualizer 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +59 -0
  2. package/dist/index.cjs +1483 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +244 -0
  5. package/dist/index.d.ts +244 -0
  6. package/dist/index.js +1473 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +60 -0
  9. package/src/HpoVisualizer.tsx +210 -0
  10. package/src/OrganSvg.tsx +141 -0
  11. package/src/__tests__/colorScheme.test.tsx +146 -0
  12. package/src/__tests__/createStrictColorPalette.test.ts +135 -0
  13. package/src/__tests__/renderOrder.test.tsx +146 -0
  14. package/src/__tests__/setup.ts +1 -0
  15. package/src/constants.ts +158 -0
  16. package/src/index.ts +34 -0
  17. package/src/lib/createStrictColorPalette.ts +57 -0
  18. package/src/lib/index.ts +1 -0
  19. package/src/svg/Blood.tsx +23 -0
  20. package/src/svg/Body.tsx +24 -0
  21. package/src/svg/Breast.tsx +42 -0
  22. package/src/svg/Cell.tsx +79 -0
  23. package/src/svg/Constitutional.tsx +29 -0
  24. package/src/svg/Digestive.tsx +28 -0
  25. package/src/svg/Ear.tsx +23 -0
  26. package/src/svg/Endocrine.tsx +32 -0
  27. package/src/svg/Eye.tsx +23 -0
  28. package/src/svg/Growth.tsx +23 -0
  29. package/src/svg/Head.tsx +51 -0
  30. package/src/svg/Heart.tsx +23 -0
  31. package/src/svg/Immune.tsx +32 -0
  32. package/src/svg/Integument.tsx +58 -0
  33. package/src/svg/Kidney.tsx +41 -0
  34. package/src/svg/Limbs.tsx +46 -0
  35. package/src/svg/Lung.tsx +23 -0
  36. package/src/svg/Metabolism.tsx +41 -0
  37. package/src/svg/Muscle.tsx +225 -0
  38. package/src/svg/Neoplasm.tsx +23 -0
  39. package/src/svg/Nervous.tsx +49 -0
  40. package/src/svg/Prenatal.tsx +23 -0
  41. package/src/svg/ThoracicCavity.tsx +28 -0
  42. package/src/svg/Voice.tsx +23 -0
  43. package/src/svg/index.ts +54 -0
  44. package/src/types.ts +162 -0
  45. package/src/useOrganInteraction.ts +130 -0
@@ -0,0 +1,146 @@
1
+ import { render } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { HpoVisualizer } from "../HpoVisualizer";
4
+ import type { OrganId } from "../types";
5
+
6
+ describe("HpoVisualizer rendering order", () => {
7
+ /**
8
+ * Helper function to get rendered organ IDs in DOM order.
9
+ * OrganSvg elements are now <svg> elements with aria-label attribute.
10
+ * Only includes visible organs (opacity !== 0).
11
+ */
12
+ const getRenderedOrganOrder = (container: HTMLElement): string[] => {
13
+ // Find all OrganSvg elements (they are nested svg elements with aria-label)
14
+ // Exclude the root svg which has aria-label="Human organ visualizer"
15
+ const organElements = container.querySelectorAll(
16
+ "svg[aria-label]:not([aria-label='Human organ visualizer'])",
17
+ );
18
+ return Array.from(organElements)
19
+ .filter((el) => {
20
+ // Filter out hidden organs (opacity: 0)
21
+ // OrganSvg sets opacity as an attribute, not a style property
22
+ const opacity = el.getAttribute("opacity");
23
+ return opacity !== "0";
24
+ })
25
+ .map((el) => el.getAttribute("aria-label") || "");
26
+ };
27
+
28
+ /**
29
+ * Helper function to get z-index of an organ element
30
+ */
31
+ const getOrganZIndex = (container: HTMLElement, organId: string): number => {
32
+ const organElement = container.querySelector(`svg[aria-label="${organId}"]`) as HTMLElement;
33
+ if (!organElement) return -1;
34
+ const style = organElement.style.zIndex;
35
+ return style ? parseInt(style, 10) : 0;
36
+ };
37
+
38
+ it("should render organs in ORGAN_POSITIONS key order regardless of organs array order", () => {
39
+ // Provide organs in reverse alphabetical order
40
+ const organs: { id: OrganId }[] = [
41
+ { id: "voice" },
42
+ { id: "thoracicCavity" },
43
+ { id: "lung" },
44
+ { id: "heart" },
45
+ { id: "head" },
46
+ { id: "eye" },
47
+ { id: "ear" },
48
+ ];
49
+
50
+ const { container } = render(<HpoVisualizer organs={organs} />);
51
+
52
+ const renderedOrder = getRenderedOrganOrder(container);
53
+
54
+ // ORGAN_RENDER_ORDER starts with thoracicCavity -> lung -> ... -> head -> eye -> ear -> voice
55
+ // Expected order should follow ORGAN_RENDER_ORDER for the given organs
56
+ expect(renderedOrder).toEqual([
57
+ "thoracicCavity",
58
+ "lung",
59
+ "heart",
60
+ "head",
61
+ "eye",
62
+ "ear",
63
+ "voice",
64
+ ]);
65
+ });
66
+
67
+ it("should give selectedOrgan higher z-index to appear on top", () => {
68
+ const organs: { id: OrganId }[] = [
69
+ { id: "head" },
70
+ { id: "eye" },
71
+ { id: "ear" },
72
+ { id: "lung" },
73
+ { id: "heart" },
74
+ ];
75
+
76
+ // Select 'head'
77
+ const { container } = render(<HpoVisualizer organs={organs} selectedOrgan="head" />);
78
+
79
+ // 'head' should have higher z-index since it's selected
80
+ expect(getOrganZIndex(container, "head")).toBe(1);
81
+ // Other organs should have z-index 0
82
+ expect(getOrganZIndex(container, "lung")).toBe(0);
83
+ expect(getOrganZIndex(container, "heart")).toBe(0);
84
+ expect(getOrganZIndex(container, "eye")).toBe(0);
85
+ expect(getOrganZIndex(container, "ear")).toBe(0);
86
+ });
87
+
88
+ it("should give selectedOrgan higher z-index even when it would normally be in the middle", () => {
89
+ const organs: { id: OrganId }[] = [
90
+ { id: "head" },
91
+ { id: "lung" },
92
+ { id: "heart" },
93
+ { id: "digestive" },
94
+ { id: "kidney" },
95
+ ];
96
+
97
+ // Select 'heart' which is in the middle of ORGAN_POSITIONS order
98
+ const { container } = render(<HpoVisualizer organs={organs} selectedOrgan="heart" />);
99
+
100
+ // 'heart' should have higher z-index
101
+ expect(getOrganZIndex(container, "heart")).toBe(1);
102
+ // Other organs should have z-index 0
103
+ expect(getOrganZIndex(container, "lung")).toBe(0);
104
+ expect(getOrganZIndex(container, "digestive")).toBe(0);
105
+ expect(getOrganZIndex(container, "kidney")).toBe(0);
106
+ expect(getOrganZIndex(container, "head")).toBe(0);
107
+ });
108
+
109
+ it("should render all organs in ORGAN_RENDER_ORDER (no reordering for selection)", () => {
110
+ const organs: { id: OrganId }[] = [{ id: "head" }, { id: "eye" }, { id: "ear" }];
111
+
112
+ const { container } = render(<HpoVisualizer organs={organs} selectedOrgan="eye" />);
113
+
114
+ const renderedOrder = getRenderedOrganOrder(container);
115
+
116
+ // All organs should be rendered in ORGAN_RENDER_ORDER, not reordered
117
+ expect(renderedOrder).toEqual(["head", "eye", "ear"]);
118
+ // 'eye' has higher z-index because it's selected
119
+ expect(getOrganZIndex(container, "eye")).toBe(1);
120
+ });
121
+
122
+ it("should not render selectedOrgan if it is not in organs array", () => {
123
+ const organs: { id: OrganId }[] = [{ id: "head" }, { id: "eye" }];
124
+
125
+ // Select 'heart' which is not in the organs array
126
+ const { container } = render(<HpoVisualizer organs={organs} selectedOrgan="heart" />);
127
+
128
+ const renderedOrder = getRenderedOrganOrder(container);
129
+
130
+ // 'heart' should not be rendered since it's not in the organs array
131
+ expect(renderedOrder).toEqual(["head", "eye"]);
132
+ expect(renderedOrder).not.toContain("heart");
133
+ });
134
+
135
+ it("should maintain correct order with visibleOrgans prop", () => {
136
+ // Using visibleOrgans instead of organs
137
+ const visibleOrgans: OrganId[] = ["kidney", "heart", "lung", "head"];
138
+
139
+ const { container } = render(<HpoVisualizer visibleOrgans={visibleOrgans} />);
140
+
141
+ const renderedOrder = getRenderedOrganOrder(container);
142
+
143
+ // Should follow ORGAN_RENDER_ORDER: lung, heart, kidney, head
144
+ expect(renderedOrder).toEqual(["lung", "heart", "kidney", "head"]);
145
+ });
146
+ });
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom/vitest";
@@ -0,0 +1,158 @@
1
+ import type { ColorName, OrganId, StrictColorPalette } from "./types";
2
+
3
+ /**
4
+ * All supported organ IDs
5
+ */
6
+ export const ORGAN_IDS: readonly OrganId[] = [
7
+ "digestive",
8
+ "lung",
9
+ "thoracicCavity",
10
+ "breast",
11
+ "heart",
12
+ "head",
13
+ "eye",
14
+ "ear",
15
+ "voice",
16
+ "kidney",
17
+ "integument",
18
+ "constitutional",
19
+ "limbs",
20
+ "nervous",
21
+ "metabolism",
22
+ "cell",
23
+ "endocrine",
24
+ "neoplasm",
25
+ "immune",
26
+ "muscle",
27
+ "growth",
28
+ "prenatal",
29
+ "blood",
30
+ ] as const;
31
+
32
+ /**
33
+ * Display names for organs (English)
34
+ */
35
+ export const ORGAN_NAMES_EN: Record<OrganId, string> = {
36
+ head: "Head",
37
+ eye: "Eye",
38
+ ear: "Ear",
39
+ heart: "Heart",
40
+ lung: "Lung",
41
+ digestive: "Digestive",
42
+ kidney: "Kidney",
43
+ integument: "Integument",
44
+ constitutional: "Constitutional",
45
+ limbs: "Limbs",
46
+ nervous: "Nervous",
47
+ breast: "Breast",
48
+ thoracicCavity: "Thoracic Cavity",
49
+ voice: "Voice",
50
+ metabolism: "Metabolism",
51
+ cell: "Cell",
52
+ endocrine: "Endocrine",
53
+ neoplasm: "Neoplasm",
54
+ immune: "Immune",
55
+ muscle: "Muscle",
56
+ growth: "Growth",
57
+ prenatal: "Prenatal",
58
+ blood: "Blood",
59
+ };
60
+
61
+ /**
62
+ * Display names for organs (Korean)
63
+ */
64
+ export const ORGAN_NAMES_KO: Record<OrganId, string> = {
65
+ head: "머리",
66
+ eye: "눈",
67
+ ear: "귀",
68
+ heart: "심장",
69
+ lung: "폐",
70
+ digestive: "소화기",
71
+ kidney: "신장",
72
+ integument: "외피",
73
+ constitutional: "전신",
74
+ limbs: "팔다리",
75
+ nervous: "신경계",
76
+ breast: "유방",
77
+ thoracicCavity: "흉강",
78
+ voice: "음성",
79
+ metabolism: "대사",
80
+ cell: "세포",
81
+ endocrine: "내분비",
82
+ neoplasm: "신생물",
83
+ immune: "면역",
84
+ muscle: "근육",
85
+ growth: "성장",
86
+ prenatal: "태아",
87
+ blood: "혈액",
88
+ };
89
+
90
+ /**
91
+ * Color palettes for each color scheme.
92
+ * Based on Tailwind CSS color palette.
93
+ */
94
+ export const DEFAULT_COLOR_PALETTE: StrictColorPalette = {
95
+ blue: {
96
+ 100: "#DBEAFE",
97
+ 200: "#BFDBFE",
98
+ 300: "#93C5FD",
99
+ alpha: {
100
+ 100: "#1A7EFF26",
101
+ 200: "#0975FB42",
102
+ 300: "#0478fa6e",
103
+ },
104
+ },
105
+ yellow: {
106
+ 100: "#FFF3D4",
107
+ 200: "#FFECBA",
108
+ 300: "#FFCF54",
109
+ alpha: {
110
+ 100: "#FFE50D30",
111
+ 200: "#FCDB025C",
112
+ 300: "#F2C90575",
113
+ },
114
+ },
115
+ gray: {
116
+ 100: "#F0F2F5",
117
+ 200: "#E4E6EB",
118
+ 300: "#BEC2CC",
119
+ alpha: {
120
+ 100: "#0000000D",
121
+ 200: "#101A231A",
122
+ 300: "#020F1C26",
123
+ },
124
+ },
125
+ };
126
+
127
+ /**
128
+ * Default color scheme used when no colorScheme is specified
129
+ */
130
+ export const DEFAULT_COLOR_NAME: ColorName = "blue";
131
+
132
+ /**
133
+ * Default stroke color
134
+ */
135
+ export const DEFAULT_STROKE_COLOR = "#ff142d";
136
+
137
+ /**
138
+ * Default stroke width;
139
+ */
140
+ export const DEFAULT_STROKE_WIDTH = 0.5;
141
+
142
+ /**
143
+ * Animation duration in milliseconds
144
+ */
145
+ export const ANIMATION_DURATION_MS = 150;
146
+
147
+ /**
148
+ * CSS transition string for smooth style changes
149
+ */
150
+ export const TRANSITION_STYLE = `all ${ANIMATION_DURATION_MS}ms ease-out`;
151
+
152
+ /**
153
+ * Body SVG viewBox dimensions
154
+ */
155
+ export const BODY_VIEWBOX = {
156
+ width: 122,
157
+ height: 358,
158
+ };
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ // Main component
2
+
3
+ // Constants
4
+ export {
5
+ ANIMATION_DURATION_MS,
6
+ DEFAULT_COLOR_PALETTE,
7
+ ORGAN_IDS,
8
+ ORGAN_NAMES_EN,
9
+ ORGAN_NAMES_KO,
10
+ } from "./constants";
11
+ export { HpoVisualizer } from "./HpoVisualizer";
12
+ export type { OrganSvgWrapperProps } from "./OrganSvg";
13
+ // OrganSvg wrapper for custom compositions
14
+ export { OrganSvg } from "./OrganSvg";
15
+ // Organ SVG components for advanced usage
16
+ export { ORGAN_COMPONENTS } from "./svg/index";
17
+ // Types
18
+ export type {
19
+ ColorName as ColorScheme,
20
+ ColorScale as ColorPalette,
21
+ HpoVisualizerProps,
22
+ OrganConfig,
23
+ OrganId,
24
+ OrganInteractionHandlers,
25
+ OrganInteractionState,
26
+ OrganStyle,
27
+ OrganSvgProps,
28
+ } from "./types";
29
+ export type {
30
+ UseOrganInteractionOptions,
31
+ UseOrganInteractionResult,
32
+ } from "./useOrganInteraction";
33
+ // Hook for custom implementations
34
+ export { useOrganInteraction } from "./useOrganInteraction";
@@ -0,0 +1,57 @@
1
+ import { DEFAULT_COLOR_PALETTE } from "../constants";
2
+ import type {
3
+ ColorName,
4
+ ColorPalette,
5
+ ColorScale,
6
+ ColorStep,
7
+ SolidColorScale,
8
+ StrictColorPalette,
9
+ } from "../types";
10
+
11
+ const COLOR_NAMES: ColorName[] = ["blue", "yellow", "gray"];
12
+ const COLOR_STEPS: ColorStep[] = [100, 200, 300];
13
+
14
+ /**
15
+ * Generate alpha colors from solid colors using CSS color-mix.
16
+ * The generated colors are 50% transparent.
17
+ */
18
+ function generateAlphaColors(solidColors: SolidColorScale): SolidColorScale {
19
+ return COLOR_STEPS.reduce((acc, step) => {
20
+ acc[step] = `color-mix(in srgb, ${solidColors[step]} 50%, transparent)`;
21
+ return acc;
22
+ }, {} as SolidColorScale);
23
+ }
24
+
25
+ /**
26
+ * Create a StrictColorPalette from a partial ColorPalette.
27
+ *
28
+ * - Missing ColorNames are filled from DEFAULT_COLOR_PALETTE.
29
+ * - Missing alpha values are generated using CSS color-mix.
30
+ *
31
+ * @param palette - A partial ColorPalette to convert
32
+ * @returns A StrictColorPalette with all colors and alpha values filled
33
+ */
34
+ export function createStrictColorPalette(palette?: Partial<ColorPalette>): StrictColorPalette {
35
+ return COLOR_NAMES.reduce((result, colorName) => {
36
+ // Get the color scale from input palette, fallback to default
37
+ const inputScale: ColorScale | undefined = palette?.[colorName];
38
+ const defaultScale: ColorScale = DEFAULT_COLOR_PALETTE[colorName];
39
+
40
+ // Merge solid colors: use input if provided, otherwise use default
41
+ const solidColors: SolidColorScale = {
42
+ 100: inputScale?.[100] ?? defaultScale[100],
43
+ 200: inputScale?.[200] ?? defaultScale[200],
44
+ 300: inputScale?.[300] ?? defaultScale[300],
45
+ };
46
+
47
+ // For alpha: use input alpha if provided, otherwise generate using color-mix
48
+ const alphaColors: SolidColorScale = inputScale?.alpha ?? generateAlphaColors(solidColors);
49
+
50
+ result[colorName] = {
51
+ ...solidColors,
52
+ alpha: alphaColors,
53
+ };
54
+
55
+ return result;
56
+ }, {} as StrictColorPalette);
57
+ }
@@ -0,0 +1 @@
1
+ export { createStrictColorPalette } from "./createStrictColorPalette";
@@ -0,0 +1,23 @@
1
+ import { TRANSITION_STYLE } from "../constants";
2
+ import type { OrganSvgProps } from "../types";
3
+
4
+ export function Blood({ style, colorScale, isActive = false, className }: OrganSvgProps) {
5
+ const defaultColor = colorScale[200];
6
+ const activeColor = colorScale[300];
7
+ const fill = style?.fill ?? (isActive ? activeColor : defaultColor);
8
+
9
+ return (
10
+ <g className={className} data-organ="blood">
11
+ <path
12
+ d={BLOOD_PATH}
13
+ fill={fill}
14
+ stroke={style?.stroke}
15
+ strokeWidth={style?.strokeWidth}
16
+ style={{ transition: TRANSITION_STYLE }}
17
+ />
18
+ </g>
19
+ );
20
+ }
21
+
22
+ const BLOOD_PATH =
23
+ "M6 6.51934C6 8.44383 4.65873 10 3 10C1.34127 10 0 8.44383 0 6.51934C0 4 3 0 3 0C3 0 6 4 6 6.51934Z";
@@ -0,0 +1,24 @@
1
+ import { TRANSITION_STYLE } from "../constants";
2
+ import type { OrganSvgProps } from "../types";
3
+
4
+ export function Body({ style, className }: OrganSvgProps) {
5
+ const fill = style?.fill ?? "none";
6
+
7
+ return (
8
+ <g className={className} data-organ="body">
9
+ <defs>
10
+ <path id="hpo-body-image" d={BODY_PATH} style={{ transition: TRANSITION_STYLE }} />
11
+ </defs>
12
+ <use
13
+ href="#hpo-body-image"
14
+ d={BODY_PATH}
15
+ fill={fill}
16
+ stroke={style?.stroke}
17
+ strokeWidth={style?.strokeWidth}
18
+ />
19
+ </g>
20
+ );
21
+ }
22
+
23
+ const BODY_PATH =
24
+ "M61.4512 0.5C67.0266 0.5 70.5742 1.80208 73.3867 3.44141V3.44238C80.0658 7.33983 84.1767 13.2667 84.1768 20.3652V28.9541L84.8936 28.6074H84.8945C84.8959 28.6068 84.8987 28.6052 84.9023 28.6035C84.9102 28.5999 84.9233 28.5944 84.9404 28.5869C84.9751 28.5718 85.028 28.5496 85.0947 28.5244C85.2293 28.4736 85.4175 28.411 85.6318 28.3633C86.0811 28.2632 86.5514 28.2494 86.9014 28.4297C89.4653 29.7518 90.3704 33.5368 88.6943 37.1162L88.5244 37.4619C87.4804 39.4869 85.8308 40.9054 84.123 41.5166L83.8701 41.6064L83.8057 41.8672C82.5437 46.9691 78.975 53.2855 73.3271 57.418L72.7744 57.8105C72.1554 58.2358 71.7578 58.937 71.7578 59.71V64.0176C71.7578 64.9512 71.8821 65.8528 72.1055 66.7148L72.1064 66.7178C72.9621 69.9436 75.6339 72.4909 79.0898 73.1367C84.4858 74.1479 90.3636 75.3253 95.4502 77.4551C100.534 79.5836 104.761 82.6365 106.938 87.3594C110.541 95.182 112.618 108.493 113.991 120.607C114.676 126.652 115.184 132.374 115.621 136.938C115.839 139.217 116.04 141.211 116.237 142.808C116.433 144.393 116.628 145.622 116.841 146.354C117.624 149.045 118.554 155.824 119.035 163.83C119.515 171.824 119.544 180.978 118.545 188.407L118.469 188.974H118.477C118.107 192.204 119.19 195.616 120.135 198.868C121.152 202.367 122.014 205.692 121.144 208.736C120.183 210.643 119.518 212.261 118.927 213.573C118.324 214.912 117.8 215.923 117.11 216.706C115.855 218.131 113.952 218.898 109.567 219.122L108.656 219.159L108.644 219.16C107.736 219.212 107.186 219.013 106.902 218.774C106.641 218.555 106.55 218.259 106.636 217.912C106.819 217.166 107.89 216.106 110.124 215.842L110.352 215.815L110.48 215.625C112.404 212.769 112.932 209.269 110.838 206.244L110.837 206.241L110.767 206.149C110.019 205.229 108.481 205.412 108.074 206.604V206.605C107.597 208.022 107.637 209.433 107.521 210.605C107.403 211.784 107.133 212.873 106.123 213.97C105.848 214.131 105.635 214.151 105.473 214.112C105.294 214.069 105.091 213.934 104.894 213.646C104.49 213.057 104.212 211.958 104.275 210.473V210.472C104.36 208.42 104.688 205.945 104.993 203.827C105.145 202.775 105.292 201.803 105.398 201.033C105.503 200.28 105.577 199.659 105.568 199.328L105.567 199.291L105.561 199.255C105.246 197.443 105.701 196.177 106.342 194.85C106.941 193.607 107.741 192.258 108.085 190.436L108.147 190.064C108.71 186.289 107.761 181.818 106.445 176.914C105.12 171.975 103.413 166.567 102.359 160.79V160.789L102.21 159.929C101.876 157.891 101.637 155.663 101.445 153.631C101.228 151.324 101.069 149.244 100.904 148.031V148.03C100.27 143.404 99.2463 138.906 98.2344 134.416C97.221 129.919 96.2185 125.429 95.6113 120.783L94.623 120.763C90.1407 146.857 91.0932 161.186 95.6191 176.598V176.599C98.1952 187.276 99.4995 208.275 99.3574 219.291C99.1144 238.354 98.1907 259.581 97.959 269.391C97.9168 271.12 98.0249 272.84 98.2471 274.551H98.248C99.5678 284.662 98.2406 296.152 97.5918 306.975C97.2505 312.716 96.1878 317.677 95.1934 322.59C94.2003 327.495 93.276 332.351 93.2461 337.82C93.2038 345.504 96.6429 352.149 102.017 358.143L102.023 358.15C102.71 358.884 103.566 359.604 104.443 360.321C105.329 361.045 106.238 361.767 107.07 362.528C108.751 364.065 109.985 365.644 110.046 367.46C110.081 368.506 109.485 369.476 108.532 369.968H108.531C106.915 370.803 104.54 371.27 101.993 371.435C99.4574 371.598 96.8071 371.459 94.6777 371.121H94.6787C92.9357 370.842 91.3275 370.009 90.0586 368.769H90.0596C88.0439 366.795 85.4237 363.963 84.5781 362.886L84.542 362.841L84.4971 362.805C83.5915 362.079 82.2688 361.343 81.0879 360.58C79.8665 359.791 78.7442 358.946 78.0518 357.94H78.0527C76.8179 356.143 77.0022 354.136 77.668 352.427L77.6826 352.389L77.6904 352.35C79.2145 345.265 79.4248 341.103 79.2051 337.924C79.0955 336.339 78.8795 335.007 78.6748 333.705C78.4954 332.564 78.325 331.443 78.2334 330.17L78.1992 329.614C78.1202 328.037 77.7906 326.066 77.3555 323.69C76.9184 321.304 76.3713 318.492 75.8438 315.195C74.7898 308.609 73.817 300.103 73.9854 289.349C74.0273 286.622 73.6175 281.152 73.0508 275.772C72.5197 270.731 71.8445 265.712 71.2588 263.057L71.1426 262.558C67.7629 248.839 65.7514 228.99 65.2158 219.585L65.1289 217.849C65.1022 217.03 65.0379 215.975 64.7236 215.082C64.4028 214.171 63.7812 213.332 62.6084 213.162L62.5723 213.156H59.4297L59.3936 213.162C58.2205 213.333 57.6011 214.171 57.2812 215.082C56.9676 215.976 56.9036 217.031 56.874 217.849C56.5302 226.057 54.5908 246.588 51.1924 261.166L50.8594 262.558C50.2484 265.043 49.5176 270.395 48.9512 275.772C48.3844 281.152 47.9756 286.622 48.0176 289.349C48.186 300.106 47.2134 308.614 46.1602 315.2C45.6331 318.496 45.0867 321.308 44.6494 323.693C44.214 326.069 43.8832 328.039 43.8027 329.614C43.7256 331.123 43.5322 332.4 43.3271 333.704C43.1225 335.006 42.9064 336.338 42.7969 337.922C42.5772 341.1 42.7875 345.262 44.3115 352.35L44.3203 352.389L44.335 352.427C45.0006 354.136 45.184 356.143 43.9492 357.94V357.941C43.2598 358.949 42.1381 359.796 40.916 360.585C39.7353 361.347 38.4108 362.082 37.5059 362.804L37.4609 362.84L37.4248 362.885C36.5723 363.963 33.9581 366.796 31.9434 368.769C30.6744 370.009 29.0663 370.842 27.3232 371.121C25.1943 371.459 22.5452 371.597 20.0098 371.433C17.4625 371.268 15.086 370.801 13.4697 369.968H13.4707C12.5215 369.475 11.9214 368.504 11.9561 367.46C12.017 365.641 13.2513 364.062 14.9316 362.526C15.7642 361.766 16.6728 361.044 17.5586 360.32C18.3259 359.693 19.0772 359.064 19.7129 358.425L19.9785 358.15L19.9854 358.143C25.3591 352.149 28.7981 345.504 28.7559 337.82C28.7289 332.351 27.8065 327.495 26.8135 322.59C25.8811 317.984 24.8867 313.336 24.4824 308.042L24.4102 306.975C23.7554 296.152 22.4294 284.657 23.7549 274.552V274.551C23.977 272.84 24.0851 271.12 24.043 269.391C23.8113 259.581 22.8876 238.354 22.6445 219.291C22.5084 208.274 23.8057 187.275 26.3818 176.598C30.9079 161.192 31.8623 146.863 27.3799 120.763L26.3906 120.783C25.7835 125.429 24.781 129.921 23.7676 134.419C22.8189 138.629 21.8608 142.845 21.2217 147.165L21.0977 148.03C20.9297 149.244 20.7715 151.325 20.5547 153.631C20.3363 155.953 20.0563 158.531 19.6426 160.789V160.79C18.5894 166.564 16.8821 171.971 15.5566 176.91C14.2406 181.814 13.2919 186.287 13.8545 190.064C14.1533 192.074 15.0207 193.521 15.6602 194.846C16.3006 196.173 16.7564 197.44 16.4414 199.256L16.4355 199.29L16.4346 199.325C16.4241 199.657 16.4972 200.279 16.6006 201.032C16.7064 201.803 16.8531 202.774 17.0049 203.827C17.2721 205.681 17.5568 207.807 17.6826 209.685L17.7266 210.473C17.7895 211.958 17.5117 213.057 17.1084 213.646C16.9114 213.935 16.7084 214.069 16.5293 214.112C16.3668 214.151 16.1536 214.131 15.8779 213.969C14.8719 212.869 14.6015 211.78 14.4834 210.603C14.3658 209.43 14.4052 208.022 13.9277 206.605L13.8848 206.492C13.407 205.358 11.8528 205.251 11.165 206.243C9.06384 209.263 9.59991 212.77 11.5225 215.625L11.6504 215.815L11.8779 215.842C14.1119 216.106 15.1825 217.166 15.3662 217.912C15.4515 218.259 15.361 218.555 15.0996 218.774C14.8155 219.013 14.2665 219.212 13.3584 219.16L13.3457 219.159C8.30652 218.997 6.23107 218.227 4.8916 216.708C4.20187 215.926 3.6784 214.915 3.0752 213.576C2.48355 212.263 1.81799 210.645 0.857422 208.736C-0.0127993 205.692 0.850371 202.367 1.86719 198.868C2.83803 195.527 3.95427 192.017 3.49219 188.709L3.44336 188.39C2.44713 180.964 2.47547 171.818 2.95508 163.83C3.43582 155.824 4.36614 149.045 5.14941 146.354C5.36227 145.622 5.55753 144.393 5.75293 142.808C5.94977 141.211 6.15078 139.217 6.36914 136.938C6.80626 132.374 7.31392 126.652 7.99902 120.607C9.3291 108.872 11.3194 96.0125 14.7188 88.1084L15.0527 87.3594C17.2289 82.6335 21.4566 79.5787 26.54 77.4502C31.6266 75.3204 37.5045 74.1449 42.9004 73.1367C46.3571 72.4908 49.0223 69.9424 49.8838 66.7178L49.8848 66.7148C50.1082 65.8526 50.2324 64.9451 50.2324 64.0176V59.71C50.2324 58.9367 49.8342 58.2357 49.2148 57.8105H49.2158C43.2479 53.6939 39.4872 47.1335 38.1846 41.8672L38.1201 41.6055L37.8662 41.5156L37.5469 41.3926C36.0634 40.7746 34.6524 39.5409 33.6699 37.834L33.4658 37.4609C31.5635 33.7829 32.4425 29.7943 35.0889 28.4297C35.4327 28.2525 35.9027 28.2654 36.3555 28.3652C36.5711 28.4128 36.7607 28.4757 36.8965 28.5264C36.964 28.5515 37.0176 28.5728 37.0527 28.5879C37.0702 28.5954 37.0829 28.6019 37.0908 28.6055C37.0944 28.6071 37.0972 28.6077 37.0986 28.6084V28.6094L37.8135 28.9482V20.3652C37.8136 13.2673 41.9228 7.34495 48.6016 3.44141L48.6025 3.44238C51.4152 1.80285 54.9631 0.5 60.5391 0.5C60.6102 0.500004 60.6807 0.501438 60.7568 0.50293C60.8317 0.504396 60.9124 0.505859 60.9951 0.505859H61.0146C61.1574 0.500377 61.2959 0.5 61.4512 0.5Z";
@@ -0,0 +1,42 @@
1
+ import { TRANSITION_STYLE } from "../constants";
2
+ import type { OrganSvgProps } from "../types";
3
+
4
+ const BREAST_PATH =
5
+ "M69.8438 2C70.6451 10.8345 68.3715 18.6386 60.9102 21.7764C50.8924 25.9885 42.3578 23.3784 39.5127 20.8213C37.9582 19.4101 36.5048 16.0566 35.001 16.0566C33.4974 16.0572 32.0447 19.4208 30.4903 20.8213C27.6555 23.3784 19.1209 25.9885 9.09282 21.7764C1.63155 18.6387 -0.650501 10.8344 0.152386 2H69.8438Z";
6
+ const LEFT_PATH =
7
+ "M12.9172 15.7864C14.2966 16.5464 14.1514 17.7592 13.4736 18.4718C12.7959 19.1845 11.2841 19.5035 10.1041 18.9497C8.88656 18.3783 8.65291 17.457 8.99608 16.5829C9.44575 15.4374 11.1824 14.8305 12.9172 15.7864Z";
8
+ const RIGHT_PATH =
9
+ "M56.1746 15.7864C54.7952 16.5464 54.9404 17.7592 55.6182 18.4718C56.2959 19.1845 57.8077 19.5035 58.9877 18.9497C60.2052 18.3783 60.4389 17.457 60.0957 16.5829C59.646 15.4374 57.9094 14.8305 56.1746 15.7864Z";
10
+
11
+ export function Breast({ style, colorScale, isActive = false, className }: OrganSvgProps) {
12
+ const defaultColor = colorScale[100];
13
+ const activeColor = colorScale[300];
14
+ const fill = style?.fill ?? (isActive ? activeColor : defaultColor);
15
+
16
+ return (
17
+ <g className={className} data-organ="breast">
18
+ <path
19
+ d={BREAST_PATH}
20
+ fill="url(#paint_breast)"
21
+ stroke={style?.stroke}
22
+ strokeWidth={style?.strokeWidth}
23
+ style={{ transition: TRANSITION_STYLE }}
24
+ />
25
+ <path d={LEFT_PATH} fill={fill} style={{ transition: TRANSITION_STYLE }} />
26
+ <path d={RIGHT_PATH} fill={fill} style={{ transition: TRANSITION_STYLE }} />
27
+ <defs>
28
+ <linearGradient
29
+ id="paint_breast"
30
+ x1="36"
31
+ y1="2"
32
+ x2="36"
33
+ y2="22"
34
+ gradientUnits="userSpaceOnUse"
35
+ >
36
+ <stop stopColor="white" stopOpacity="0" />
37
+ <stop offset="1" stopColor={fill} style={{ transition: TRANSITION_STYLE }} />
38
+ </linearGradient>
39
+ </defs>
40
+ </g>
41
+ );
42
+ }
@@ -0,0 +1,79 @@
1
+ import { TRANSITION_STYLE } from "../constants";
2
+ import type { OrganSvgProps } from "../types";
3
+
4
+ export function Cell({ style, colorScale, isActive = false, className }: OrganSvgProps) {
5
+ const defaultColor = colorScale[100];
6
+ const activeColor = colorScale[200];
7
+ const fill = style?.fill ?? (isActive ? activeColor : defaultColor);
8
+ const highlightFill = colorScale.alpha[200];
9
+
10
+ return (
11
+ <g className={className} data-organ="cell">
12
+ <path
13
+ d={CELL_A_PATH}
14
+ fill={fill}
15
+ stroke={style?.stroke}
16
+ strokeWidth={style?.strokeWidth}
17
+ style={{ transition: TRANSITION_STYLE }}
18
+ />
19
+ <path
20
+ d={CELL_A_HIGHLIGHT_PATH}
21
+ fill={highlightFill}
22
+ style={{ transition: TRANSITION_STYLE }}
23
+ />
24
+ <path
25
+ d={CELL_B_PATH}
26
+ fill={fill}
27
+ stroke={style?.stroke}
28
+ strokeWidth={style?.strokeWidth}
29
+ style={{ transition: TRANSITION_STYLE }}
30
+ />
31
+ <path
32
+ d={CELL_B_HIGHLIGHT_PATH}
33
+ fill={highlightFill}
34
+ style={{ transition: TRANSITION_STYLE }}
35
+ />
36
+ <path
37
+ d={CELL_C_PATH}
38
+ fill={fill}
39
+ stroke={style?.stroke}
40
+ strokeWidth={style?.strokeWidth}
41
+ style={{ transition: TRANSITION_STYLE }}
42
+ />
43
+ <path
44
+ d={CELL_C_HIGHLIGHT_PATH}
45
+ fill={highlightFill}
46
+ style={{ transition: TRANSITION_STYLE }}
47
+ />
48
+ <path
49
+ d={CELL_D_PATH}
50
+ fill={fill}
51
+ stroke={style?.stroke}
52
+ strokeWidth={style?.strokeWidth}
53
+ style={{ transition: TRANSITION_STYLE }}
54
+ />
55
+ <path
56
+ d={CELL_D_HIGHLIGHT_PATH}
57
+ fill={highlightFill}
58
+ style={{ transition: TRANSITION_STYLE }}
59
+ />
60
+ </g>
61
+ );
62
+ }
63
+
64
+ const CELL_A_PATH =
65
+ "M1.55326 11.38C-0.381257 10.7111 -0.556186 7.81929 1.29601 6.4029C2.93212 5.14388 5.02099 5.95044 5.24737 7.94716C5.49433 10.0914 3.40546 12.0193 1.55326 11.38Z";
66
+ const CELL_A_HIGHLIGHT_PATH =
67
+ "M3.05597 10.1119C2.52089 9.89551 2.46944 8.94141 2.98394 8.47912C3.4367 8.066 4.01294 8.33158 4.07468 8.98076C4.14671 9.67912 3.57047 10.3185 3.05597 10.1119Z";
68
+ const CELL_B_PATH =
69
+ "M9.66139 5.60695C11.1534 6.20695 11.2872 8.48891 10.6081 9.77744C9.39385 12.0594 6.72874 11.0561 6.8728 8.26269C6.95512 6.69875 8.16934 5.00695 9.66139 5.61678V5.60695Z";
70
+ const CELL_B_HIGHLIGHT_PATH =
71
+ "M9.16759 6.37354C9.72325 6.61945 9.72325 7.426 9.43513 7.85879C8.93092 8.626 7.93279 8.21289 8.04598 7.22928C8.10772 6.6883 8.61193 6.11781 9.1573 6.37354H9.16759Z";
72
+ const CELL_C_PATH =
73
+ "M1.32755 12.9643C1.67741 12.6397 2.0993 12.3938 2.60351 12.4233C3.29294 12.4626 3.97208 13.0233 3.68396 14.4987C3.39584 15.9741 4.40426 17.0659 2.97395 17.8331C2.39771 18.1479 1.47161 18.02 1.00856 17.5577C-0.113045 16.4266 0.216235 14.0069 1.31726 12.9741L1.32755 12.9643Z";
74
+ const CELL_C_HIGHLIGHT_PATH =
75
+ "M1.87342 14.9118C1.54414 14.8921 1.24573 15.3249 1.19428 15.8954C1.15312 16.456 1.3795 16.938 1.70878 16.9577C2.03806 16.9774 2.33647 16.5446 2.38792 15.9741C2.42908 15.4134 2.2027 14.9314 1.87342 14.9118Z";
76
+ const CELL_D_PATH =
77
+ "M7.93389 4.55365C7.16214 5.39955 5.90676 6.0389 5.03211 5.79299C4.57935 5.66512 4.18833 5.45857 3.94137 5.06512C3.00498 3.57988 4.87776 0.50119 6.60648 0.048731C7.32678 -0.138154 8.20143 0.206108 8.56158 0.845452C9.18927 1.9766 8.69535 3.70775 7.93389 4.55365Z";
78
+ const CELL_D_HIGHLIGHT_PATH =
79
+ "M5.96831 1.60318C5.72135 1.43597 5.27888 1.64253 4.99076 2.04581C4.70264 2.45892 4.67177 2.92122 4.92902 3.07859C5.17598 3.24581 5.61845 3.03925 5.90657 2.63597C6.19469 2.23269 6.22556 1.76056 5.96831 1.60318Z";
@@ -0,0 +1,29 @@
1
+ import { DEFAULT_STROKE_WIDTH } from "../constants";
2
+ import type { OrganSvgProps } from "../types";
3
+
4
+ export function Constitutional({ style, colorScale, isActive = false, className }: OrganSvgProps) {
5
+ const fill = style?.fill ?? "none";
6
+ const defaultColor = colorScale[100];
7
+ const activeColor = colorScale[200];
8
+ const defaultStroke = isActive ? activeColor : defaultColor;
9
+ const stroke = style?.strokeWidth ? style.stroke : defaultStroke;
10
+
11
+ return (
12
+ <g className={className} data-organ="constitutional">
13
+ {/* Invisible outline for interaction */}
14
+ <use
15
+ href="#hpo-body-image"
16
+ fill="none"
17
+ stroke="transparent"
18
+ strokeWidth={Math.max(style?.strokeWidth || DEFAULT_STROKE_WIDTH, 8)}
19
+ />
20
+ {/* Skin path */}
21
+ <use
22
+ href="#hpo-body-image"
23
+ fill={fill}
24
+ stroke={stroke}
25
+ strokeWidth={style?.strokeWidth || DEFAULT_STROKE_WIDTH}
26
+ />
27
+ </g>
28
+ );
29
+ }