Skip to content

Commit 9af1de2

Browse files
authored
Merge pull request #151 from MetaCell/feature/x_hierarchy
X hierarchy feature
2 parents b047ce4 + 5ce96cf commit 9af1de2

18 files changed

Lines changed: 1777 additions & 125 deletions

File tree

applications/sckanner/backend/sckanner/management/commands/update_knowledge_statements.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,24 @@ class JsonData(BaseModel):
5555
class Command(BaseCommand):
5656
help = "Fetch and update knowledge statements from an external server"
5757

58+
def add_arguments(self, parser):
59+
parser.add_argument(
60+
'--a_b_via_c_json_url',
61+
type=str,
62+
help='URL to the A-B-via-C JSON file',
63+
)
64+
5865
def handle(self, *args, **kwargs):
5966
self.stdout.write("Starting the ingestion process...")
60-
61-
# Step 1: Fetch raw JSON from external source
62-
raw_data_url = "https://raw.githubusercontent.com/smtifahim/SCKAN-Apps/master/sckan-explorer/json/a-b-via-c-2.json"
63-
self.stdout.write(f"Fetching data from {raw_data_url}...")
64-
response = requests.get(raw_data_url)
67+
a_b_via_c_json_url = kwargs.get('a_b_via_c_json_url')
68+
69+
# Step 1: Fetch raw JSON from URL
70+
if not a_b_via_c_json_url:
71+
# Fallback to hardcoded URL (for backward compatibility)
72+
a_b_via_c_json_url = "https://raw.githubusercontent.com/smtifahim/SCKAN-Apps/refs/heads/master/sckan-explorer/json/sckanner-data/hierarchy/sckanner-hierarchy.json"
73+
74+
self.stdout.write(f"Fetching data from {a_b_via_c_json_url}...")
75+
response = requests.get(a_b_via_c_json_url)
6576
response.raise_for_status()
6677
raw_data = JsonData(**response.json())
6778

@@ -125,4 +136,4 @@ def update_database(self, detailed_data):
125136
# Insert new data
126137
KnowledgeStatement.objects.bulk_create(
127138
[KnowledgeStatement(data=entry) for entry in detailed_data]
128-
)
139+
)

applications/sckanner/backend/sckanner/migrations/data/composer_ingestion_script.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,25 @@ def fetch_paginated_data(population_ids: list[str], stdout=None):
123123
return detailed_data
124124

125125

126-
def get_statements(version="", stdout=None):
126+
def get_statements(version="", stdout=None, a_b_via_c_json_file_path=None):
127127
try:
128-
# Step 1: Fetch raw JSON from external source
129-
raw_data_url = "https://raw.githubusercontent.com/smtifahim/SCKAN-Apps/master/sckan-explorer/json/a-b-via-c-2.json"
130-
if stdout:
131-
stdout.write(f"Fetching data from {raw_data_url}...\n")
132-
response = requests.get(raw_data_url)
133-
response.raise_for_status()
134-
raw_data = JsonData(**response.json())
128+
# Step 1: Load raw JSON from file or external source
129+
if a_b_via_c_json_file_path:
130+
# Load from provided file path
131+
if stdout:
132+
stdout.write(f"Loading data from file: {a_b_via_c_json_file_path}...\n")
133+
import json
134+
with open(a_b_via_c_json_file_path, 'r') as f:
135+
raw_json = json.load(f)
136+
raw_data = JsonData(**raw_json)
137+
else:
138+
# Fallback to hardcoded URL (for backward compatibility)
139+
raw_data_url = "https://raw.githubusercontent.com/smtifahim/SCKAN-Apps/refs/heads/master/sckan-explorer/json/sckanner-data/hierarchy/sckanner-hierarchy.json"
140+
if stdout:
141+
stdout.write(f"Fetching data from {raw_data_url}...\n")
142+
response = requests.get(raw_data_url)
143+
response.raise_for_status()
144+
raw_data = JsonData(**response.json())
135145

136146
# Step 2: Extract population IDs
137147
if stdout:
@@ -149,7 +159,7 @@ def get_statements(version="", stdout=None):
149159
# --- NOTE: ONLY FOR TESTING LOCALLY ---
150160

151161
for population_id in batched(population_ids, KNOWLEDGE_STATEMENTS_BATCH_SIZE):
152-
detailed_data.extend(fetch_paginated_data(list(population_id)))
162+
detailed_data.extend(fetch_paginated_data(list(population_id), stdout))
153163

154164
if stdout:
155165
stdout.write(f"Ingestion process completed successfully! Total statements: {len(detailed_data)}\n")

applications/sckanner/backend/sckanner/services/ingestion/connectivity_statement_adapter.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,15 @@ def _parse_and_validate_statements(
4040

4141
# Get statements from the uploaded module
4242
if hasattr(module, "get_statements") and callable(module.get_statements):
43-
statements = module.get_statements(self.snapshot.version)
43+
# Pass the a_b_via_c_json_file path if available
44+
a_b_via_c_json_file_path = None
45+
if self.snapshot.a_b_via_c_json_file:
46+
a_b_via_c_json_file_path = self.snapshot.a_b_via_c_json_file.path
47+
48+
statements = module.get_statements(
49+
self.snapshot.version,
50+
a_b_via_c_json_file_path=a_b_via_c_json_file_path
51+
)
4452
try:
4553
# Validate the statements against the schema
4654
current_path = os.path.dirname(os.path.abspath(__file__))

applications/sckanner/frontend/src/App.tsx

Lines changed: 117 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState, useRef } from 'react';
1+
import React, { useEffect, useState, useRef, useCallback } from 'react';
22
import { useDispatch, useStore } from 'react-redux';
33
import { Box } from '@mui/material';
44
import { getLayoutManagerInstance } from '@metacell/geppetto-meta-client/common/layout/LayoutManager';
@@ -28,6 +28,7 @@ import {
2828
fetchKnowledgeStatements,
2929
fetchMajorNerves,
3030
fetchOrderJson,
31+
fetchEndorgansOrder,
3132
} from './services/fetchService.ts';
3233
import { getUniqueMajorNerves } from './services/filterValuesService.ts';
3334
import {
@@ -37,10 +38,10 @@ import {
3738
} from './models/explorer.ts';
3839
import {
3940
getHierarchicalNodes,
40-
getOrgans,
41-
} from './services/hierarchyService.ts';
41+
getOrgansAndTargetSystems,
42+
} from './services/hierarchyService';
4243
import ReactGA from 'react-ga4';
43-
import { Datasnapshot, OrderJson } from './models/json.ts';
44+
import { Datasnapshot, OrderJson, NerveResponse } from './models/json.ts';
4445
import { useDataContext } from './context/DataContext.ts';
4546
import LoadingOverlay from './components/common/LoadingOverlay.tsx';
4647
import ErrorModal from './components/common/ErrorModal.tsx';
@@ -99,6 +100,12 @@ const AppContent = () => {
99100
Record<string, HierarchicalNode>
100101
>({});
101102
const [organs, setOrgans] = useState<Record<string, Organ>>({});
103+
const [targetSystems, setTargetSystems] = useState<Record<string, Organ[]>>(
104+
{},
105+
);
106+
const [targetSystemNames, setTargetSystemNames] = useState<
107+
Record<string, string>
108+
>({});
102109
const [majorNerves, setMajorNerves] = useState<Set<string>>();
103110
const [knowledgeStatements, setKnowledgeStatements] = useState<
104111
Record<string, KnowledgeStatement>
@@ -108,6 +115,7 @@ const AppContent = () => {
108115
useState<string>('');
109116
const [hasValidatedInitialURL, setHasValidatedInitialURL] = useState(false);
110117
const [isDataLoading, setIsDataLoading] = useState(false);
118+
const [hasCriticalError, setHasCriticalError] = useState(false);
111119
const [fetchError, setFetchError] = useState<{
112120
show: boolean;
113121
message: string;
@@ -120,6 +128,9 @@ const AppContent = () => {
120128
});
121129
const previousDatasnaphshot = useRef<string>('');
122130
const [orderData, setOrderData] = useState<OrderJson>({});
131+
const [endorgansOrder, setEndorgansOrder] = useState<
132+
Record<string, string[]>
133+
>({});
123134
const [searchParams] = useSearchParams();
124135
const [urlState, setUrlState] = useState<URLState>({
125136
datasnapshot: null,
@@ -146,40 +157,106 @@ const AppContent = () => {
146157
dispatch(addWidget(connectionsWidget()));
147158
}, [LayoutComponent, dispatch]);
148159

149-
const fetchJSONAndSetHierarchicalNodes = (
150-
datasnapshot: Datasnapshot,
151-
orderData: OrderJson,
152-
) => {
153-
fetchJSON(datasnapshot.a_b_via_c_json_file).then((jsonData) => {
154-
setHierarchicalNodes(getHierarchicalNodes(jsonData, orderData));
155-
setOrgans(getOrgans(jsonData));
156-
});
157-
};
160+
const fetchJSONAndSetHierarchicalNodes = useCallback(
161+
(datasnapshot: Datasnapshot, orderData: OrderJson) => {
162+
fetchJSON(datasnapshot.a_b_via_c_json_file).then((jsonData) => {
163+
setHierarchicalNodes(getHierarchicalNodes(jsonData, orderData));
164+
const { organs, targetSystems, targetSystemNames } =
165+
getOrgansAndTargetSystems(jsonData, endorgansOrder);
166+
setOrgans(organs);
167+
setTargetSystems(targetSystems);
168+
setTargetSystemNames(targetSystemNames);
169+
});
170+
},
171+
[endorgansOrder],
172+
);
158173

159174
useEffect(() => {
160175
const fetchInitialData = async () => {
176+
const failedResources: string[] = [];
177+
161178
try {
162-
const [orderDataFetched, majorNervesData, datasnapshots] =
163-
await Promise.all([
164-
fetchOrderJson(),
165-
fetchMajorNerves(),
166-
fetchDatasnapshots(),
167-
]);
179+
const results = await Promise.allSettled([
180+
fetchOrderJson().catch((err) => {
181+
failedResources.push('Organ hierarchy order (GitHub)');
182+
throw err;
183+
}),
184+
fetchMajorNerves().catch((err) => {
185+
failedResources.push('Major nerves data (GitHub)');
186+
throw err;
187+
}),
188+
fetchDatasnapshots().catch((err) => {
189+
failedResources.push('Data snapshots (API)');
190+
throw err;
191+
}),
192+
fetchEndorgansOrder().catch((err) => {
193+
failedResources.push('End organs hierarchy (GitHub)');
194+
throw err;
195+
}),
196+
]);
197+
198+
// Check if any fetch failed
199+
const failures = results.filter(
200+
(result) => result.status === 'rejected',
201+
);
202+
203+
if (failures.length > 0) {
204+
const errorDetails = failures
205+
.map((failure, index) => {
206+
if (failure.status === 'rejected') {
207+
const resourceNames = [
208+
'Organ hierarchy order (GitHub)',
209+
'Major nerves data (GitHub)',
210+
'Data snapshots (API)',
211+
'End organs hierarchy (GitHub)',
212+
];
213+
return `• ${resourceNames[index]}: ${failure.reason instanceof Error ? failure.reason.message : String(failure.reason)}`;
214+
}
215+
return '';
216+
})
217+
.filter(Boolean)
218+
.join('\n');
219+
220+
setHasCriticalError(true);
221+
setFetchError({
222+
show: true,
223+
title: 'Resources Unavailable',
224+
message:
225+
'The resources needed to start the application are not available at the moment. Please try again later or contact support if the problem persists.',
226+
details: `Failed to fetch:\n${errorDetails}`,
227+
});
228+
return; // Don't proceed with partial data
229+
}
230+
231+
// All fetches succeeded, extract the data
232+
const successfulResults = results.map((result) =>
233+
result.status === 'fulfilled' ? result.value : null,
234+
);
235+
236+
const orderDataFetched = successfulResults[0] as OrderJson;
237+
const majorNervesData = successfulResults[1] as NerveResponse;
238+
const datasnaphotsData = successfulResults[2] as Datasnapshot[];
239+
const endorgansOrderFetched = successfulResults[3] as Record<
240+
string,
241+
string[]
242+
>;
168243

169244
setOrderData(orderDataFetched);
170245
setMajorNerves(getUniqueMajorNerves(majorNervesData));
171-
setdatasnapshots(datasnapshots);
246+
setdatasnapshots(datasnaphotsData);
247+
setEndorgansOrder(endorgansOrderFetched);
172248
} catch (error) {
173-
console.error('Failed to fetch initial data:', error);
249+
// This catch is a fallback for unexpected errors
250+
console.error('Unexpected error during initial data fetch:', error);
251+
setHasCriticalError(true);
174252
setFetchError({
175253
show: true,
176-
title: 'Data Loading Error',
254+
title: 'Resources Unavailable',
177255
message:
178-
'Failed to load initial application data. Please refresh the page.',
256+
'The resources needed to start the application are not available at the moment. Please try again later.',
179257
details:
180258
error instanceof Error ? error.message : 'Unknown error occurred',
181259
});
182-
setMajorNerves(undefined);
183260
}
184261
};
185262

@@ -241,10 +318,20 @@ const AppContent = () => {
241318
const selectedSnapshotObj = datasnapshots.find(
242319
(ds: Datasnapshot) => ds.id === parseInt(selectedDatasnaphshot),
243320
);
244-
if (selectedSnapshotObj && Object.keys(orderData).length > 0) {
321+
if (
322+
selectedSnapshotObj &&
323+
Object.keys(orderData).length > 0 &&
324+
Object.keys(endorgansOrder).length > 0
325+
) {
245326
fetchJSONAndSetHierarchicalNodes(selectedSnapshotObj, orderData);
246327
}
247-
}, [selectedDatasnaphshot, orderData, datasnapshots]);
328+
}, [
329+
selectedDatasnaphshot,
330+
orderData,
331+
datasnapshots,
332+
endorgansOrder,
333+
fetchJSONAndSetHierarchicalNodes,
334+
]);
248335

249336
useEffect(() => {
250337
if (Object.keys(hierarchicalNodes).length > 0 && selectedDatasnaphshot) {
@@ -324,7 +411,7 @@ const AppContent = () => {
324411
Object.keys(knowledgeStatements).length == 0,
325412
];
326413

327-
const isLoading = loadingConditions.some(Boolean);
414+
const isLoading = !hasCriticalError && loadingConditions.some(Boolean);
328415
const loadingProgress =
329416
(loadingConditions.filter((c) => !c).length / loadingConditions.length) *
330417
100;
@@ -363,7 +450,10 @@ const AppContent = () => {
363450
majorNerves={majorNerves ? majorNerves : new Set<string>()}
364451
hierarchicalNodes={hierarchicalNodes}
365452
organs={organs}
453+
targetSystems={targetSystems}
454+
targetSystemNames={targetSystemNames}
366455
knowledgeStatements={knowledgeStatements}
456+
endorgansOrder={endorgansOrder}
367457
>
368458
<Box>
369459
<Header

applications/sckanner/frontend/src/components/Connections.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,13 @@ function Connections() {
629629
<HeatmapGrid
630630
yAxis={filteredYAxis}
631631
setYAxis={setYAxis}
632-
xAxis={reorderedAxis}
632+
xAxis={reorderedAxis.map((label) => ({
633+
id: label,
634+
label: label,
635+
children: [],
636+
expanded: false,
637+
}))}
638+
setXAxis={() => {}} // No-op for secondary heatmap
633639
onCellClick={handleCellClick}
634640
selectedCell={selectedCell}
635641
setSelectedCell={setSelectedCell}

0 commit comments

Comments
 (0)