Compare commits
2 Commits
37692d5775
...
93e0eae353
| Author | SHA1 | Date | |
|---|---|---|---|
| 93e0eae353 | |||
| 1514130adc |
@ -94,5 +94,49 @@ def get_cell_types():
|
||||
]
|
||||
return jsonify({"stage": stage, "cells": result})
|
||||
|
||||
@app.route("/api/gene_dist")
|
||||
def get_gene_distribution():
|
||||
gene = request.args.get("gene")
|
||||
stage = request.args.get("stage")
|
||||
adata = load_adata(stage)
|
||||
|
||||
if not adata:
|
||||
return jsonify({"error": f"Stage '{stage}' not found"}), 404
|
||||
if gene not in adata.var_names:
|
||||
return jsonify({"error": f"Gene '{gene}' not found in {stage}"}), 404
|
||||
|
||||
# 查找细胞类型列,按常见的列名优先级查找
|
||||
cell_type_columns = ['cell_type', 'celltype', 'cluster', 'annotation', 'cell_types', 'clusters']
|
||||
cell_type_col = None
|
||||
|
||||
for col in cell_type_columns:
|
||||
if col in adata.obs.columns:
|
||||
cell_type_col = col
|
||||
break
|
||||
|
||||
if cell_type_col is None:
|
||||
return jsonify({"error": "No cell type information found"}), 404
|
||||
|
||||
# 读取基因表达值
|
||||
expr = adata[:, gene].X
|
||||
expr = expr.toarray().flatten() if hasattr(expr, "toarray") else expr.flatten()
|
||||
|
||||
# 读取细胞类型
|
||||
cell_types = adata.obs[cell_type_col].values
|
||||
|
||||
# 按细胞类型分组表达值
|
||||
distribution = {}
|
||||
for cell_type, expression in zip(cell_types, expr):
|
||||
cell_type_str = str(cell_type)
|
||||
if cell_type_str not in distribution:
|
||||
distribution[cell_type_str] = []
|
||||
distribution[cell_type_str].append(float(expression))
|
||||
|
||||
return jsonify({
|
||||
"gene": gene,
|
||||
"stage": stage,
|
||||
"distribution": distribution
|
||||
})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True)
|
||||
|
||||
479
embryo-frontend/package-lock.json
generated
479
embryo-frontend/package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "embryo-frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@nivo/boxplot": "^0.99.0",
|
||||
"axios": "^1.10.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"react": "^19.1.0",
|
||||
@ -479,6 +480,268 @@
|
||||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@nivo/annotations": {
|
||||
"version": "0.99.0",
|
||||
"resolved": "https://registry.npmjs.org/@nivo/annotations/-/annotations-0.99.0.tgz",
|
||||
"integrity": "sha512-jCuuXPbvpaqaz4xF7k5dv0OT2ubn5Nt0gWryuTe/8oVsC/9bzSuK8bM9vBty60m9tfO+X8vUYliuaCDwGksC2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nivo/colors": "0.99.0",
|
||||
"@nivo/core": "0.99.0",
|
||||
"@nivo/theming": "0.99.0",
|
||||
"@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nivo/axes": {
|
||||
"version": "0.99.0",
|
||||
"resolved": "https://registry.npmjs.org/@nivo/axes/-/axes-0.99.0.tgz",
|
||||
"integrity": "sha512-3KschnmEL0acRoa7INSSOSEFwJLm54aZwSev7/r8XxXlkgRBriu6ReZy/FG0wfN+ljZ4GMvx+XyIIf6kxzvrZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nivo/core": "0.99.0",
|
||||
"@nivo/scales": "0.99.0",
|
||||
"@nivo/text": "0.99.0",
|
||||
"@nivo/theming": "0.99.0",
|
||||
"@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0",
|
||||
"@types/d3-format": "^1.4.1",
|
||||
"@types/d3-time-format": "^2.3.1",
|
||||
"d3-format": "^1.4.4",
|
||||
"d3-time-format": "^3.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nivo/boxplot": {
|
||||
"version": "0.99.0",
|
||||
"resolved": "https://registry.npmjs.org/@nivo/boxplot/-/boxplot-0.99.0.tgz",
|
||||
"integrity": "sha512-1XD5ILCBbmA8ghtUBg0gMaXNQGJgky0yLrcjGCfq6yqNLwscsn2rSKKbCtH9HMUTRrkb0iqkhoAzbX1cvsTe+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nivo/annotations": "0.99.0",
|
||||
"@nivo/axes": "0.99.0",
|
||||
"@nivo/colors": "0.99.0",
|
||||
"@nivo/core": "0.99.0",
|
||||
"@nivo/legends": "0.99.0",
|
||||
"@nivo/scales": "0.99.0",
|
||||
"@nivo/theming": "0.99.0",
|
||||
"@nivo/tooltip": "0.99.0",
|
||||
"@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0",
|
||||
"@types/d3-scale": "^4.0.8",
|
||||
"@types/d3-shape": "^3.1.6",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-shape": "^3.2.0",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nivo/colors": {
|
||||
"version": "0.99.0",
|
||||
"resolved": "https://registry.npmjs.org/@nivo/colors/-/colors-0.99.0.tgz",
|
||||
"integrity": "sha512-hyYt4lEFIfXOUmQ6k3HXm3KwhcgoJpocmoGzLUqzk7DzuhQYJo+4d5jIGGU0N/a70+9XbHIdpKNSblHAIASD3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nivo/core": "0.99.0",
|
||||
"@nivo/theming": "0.99.0",
|
||||
"@types/d3-color": "^3.0.0",
|
||||
"@types/d3-scale": "^4.0.8",
|
||||
"@types/d3-scale-chromatic": "^3.0.0",
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-scale-chromatic": "^3.0.0",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nivo/core": {
|
||||
"version": "0.99.0",
|
||||
"resolved": "https://registry.npmjs.org/@nivo/core/-/core-0.99.0.tgz",
|
||||
"integrity": "sha512-olCItqhPG3xHL5ei+vg52aB6o+6S+xR2idpkd9RormTTUniZb8U2rOdcQojOojPY5i9kVeQyLFBpV4YfM7OZ9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nivo/theming": "0.99.0",
|
||||
"@nivo/tooltip": "0.99.0",
|
||||
"@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0",
|
||||
"@types/d3-shape": "^3.1.6",
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-format": "^1.4.4",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-scale-chromatic": "^3.0.0",
|
||||
"d3-shape": "^3.2.0",
|
||||
"d3-time-format": "^3.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"use-debounce": "^10.0.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/nivo/donate"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nivo/legends": {
|
||||
"version": "0.99.0",
|
||||
"resolved": "https://registry.npmjs.org/@nivo/legends/-/legends-0.99.0.tgz",
|
||||
"integrity": "sha512-P16FjFqNceuTTZphINAh5p0RF0opu3cCKoWppe2aRD9IuVkvRm/wS5K1YwMCxDzKyKh5v0AuTlu9K6o3/hk8hA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nivo/colors": "0.99.0",
|
||||
"@nivo/core": "0.99.0",
|
||||
"@nivo/text": "0.99.0",
|
||||
"@nivo/theming": "0.99.0",
|
||||
"@types/d3-scale": "^4.0.8",
|
||||
"d3-scale": "^4.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nivo/scales": {
|
||||
"version": "0.99.0",
|
||||
"resolved": "https://registry.npmjs.org/@nivo/scales/-/scales-0.99.0.tgz",
|
||||
"integrity": "sha512-g/2K4L6L8si6E2BWAHtFVGahtDKbUcO6xHJtlIZMwdzaJc7yB16EpWLK8AfI/A42KadLhJSJqBK3mty+c7YZ+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-scale": "^4.0.8",
|
||||
"@types/d3-time": "^1.1.1",
|
||||
"@types/d3-time-format": "^3.0.0",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-time": "^1.0.11",
|
||||
"d3-time-format": "^3.0.0",
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@nivo/scales/node_modules/@types/d3-time-format": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-3.0.4.tgz",
|
||||
"integrity": "sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@nivo/text": {
|
||||
"version": "0.99.0",
|
||||
"resolved": "https://registry.npmjs.org/@nivo/text/-/text-0.99.0.tgz",
|
||||
"integrity": "sha512-ho3oZpAZApsJNjsIL5WJSAdg/wjzTBcwo1KiHBlRGUmD+yUWO8qp7V+mnYRhJchwygtRVALlPgZ/rlcW2Xr/MQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nivo/core": "0.99.0",
|
||||
"@nivo/theming": "0.99.0",
|
||||
"@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nivo/theming": {
|
||||
"version": "0.99.0",
|
||||
"resolved": "https://registry.npmjs.org/@nivo/theming/-/theming-0.99.0.tgz",
|
||||
"integrity": "sha512-KvXlf0nqBzh/g2hAIV9bzscYvpq1uuO3TnFN3RDXGI72CrbbZFTGzprPju3sy/myVsauv+Bb+V4f5TZ0jkYKRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nivo/tooltip": {
|
||||
"version": "0.99.0",
|
||||
"resolved": "https://registry.npmjs.org/@nivo/tooltip/-/tooltip-0.99.0.tgz",
|
||||
"integrity": "sha512-weoEGR3xAetV4k2P6k96cdamGzKQ5F2Pq+uyDaHr1P3HYArM879Pl+x+TkU0aWjP6wgUZPx/GOBiV1Hb1JxIqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nivo/core": "0.99.0",
|
||||
"@nivo/theming": "0.99.0",
|
||||
"@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/animated": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.1.tgz",
|
||||
"integrity": "sha512-BGL3hA66Y8Qm3KmRZUlfG/mFbDPYajgil2/jOP0VXf2+o2WPVmcDps/eEgdDqgf5Pv9eBbyj7LschLMuSjlW3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-spring/shared": "~10.0.1",
|
||||
"@react-spring/types": "~10.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/core": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-10.0.1.tgz",
|
||||
"integrity": "sha512-KaMMsN1qHuVTsFpg/5ajAVye7OEqhYbCq0g4aKM9bnSZlDBBYpO7Uf+9eixyXN8YEbF+YXaYj9eoWDs+npZ+sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~10.0.1",
|
||||
"@react-spring/shared": "~10.0.1",
|
||||
"@react-spring/types": "~10.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-spring/donate"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/rafz": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-10.0.1.tgz",
|
||||
"integrity": "sha512-UrzG/d6Is+9i0aCAjsjWRqIlFFiC4lFqFHrH63zK935z2YDU95TOFio4VKGISJ5SG0xq4ULy7c1V3KU+XvL+Yg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-spring/shared": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-10.0.1.tgz",
|
||||
"integrity": "sha512-KR2tmjDShPruI/GGPfAZOOLvDgkhFseabjvxzZFFggJMPkyICLjO0J6mCIoGtdJSuHywZyc4Mmlgi+C88lS00g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-spring/rafz": "~10.0.1",
|
||||
"@react-spring/types": "~10.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/types": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-10.0.1.tgz",
|
||||
"integrity": "sha512-Fk1wYVAKL+ZTYK+4YFDpHf3Slsy59pfFFvnnTfRjQQFGlyIo4VejPtDs3CbDiuBjM135YztRyZjIH2VbycB+ZQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-spring/web": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/web/-/web-10.0.1.tgz",
|
||||
"integrity": "sha512-FgQk02OqFrYyJBTTnBTWAU0WPzkHkKXauc6aeexcvATvLapUxwnfGuLlsLYF8BYjEVfkivPT04ziAue6zyRBtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~10.0.1",
|
||||
"@react-spring/core": "~10.0.1",
|
||||
"@react-spring/shared": "~10.0.1",
|
||||
"@react-spring/types": "~10.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.45.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz",
|
||||
@ -766,6 +1029,69 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-format": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz",
|
||||
"integrity": "sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.4.tgz",
|
||||
"integrity": "sha512-JIvy2HjRInE+TXOmIGN5LCmeO0hkFZx5f9FZ7kiN+D+YTcc8pptsiLiuHsvwxwC7VVKmJ2ExHUgNlAiV7vQM9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-time-format": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.4.tgz",
|
||||
"integrity": "sha512-xdDXbpVO74EvadI3UDxjxTdR6QIxm1FKzEA/+F8tL4GWWUg/hgvBqf6chql64U5A9ZUGWo7pEu4eNlyLwbKdhg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@ -900,6 +1226,122 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz",
|
||||
"integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-interpolate": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale/node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
|
||||
"integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz",
|
||||
"integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 2"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@ -1180,6 +1622,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@ -1360,6 +1817,16 @@
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-virtualized-auto-sizer": {
|
||||
"version": "1.0.26",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz",
|
||||
"integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.45.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz",
|
||||
@ -1459,6 +1926,18 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/use-debounce": {
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.5.tgz",
|
||||
"integrity": "sha512-Q76E3lnIV+4YT9AHcrHEHYmAd9LKwUAbPXDm7FlqVGDHiSOhX3RDjT8dm0AxbJup6WgOb1YEcKyCr11kBJR5KQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz",
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
"vite": "^7.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nivo/boxplot": "^0.99.0",
|
||||
"axios": "^1.10.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"react": "^19.1.0",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
|
||||
@ -12,11 +12,20 @@ interface Point {
|
||||
interface PointCloudProps {
|
||||
data: Point[];
|
||||
isCategorical?: boolean; // 是否为分类数据(细胞类型)
|
||||
autoRotate?: boolean; // 是否自动旋转
|
||||
}
|
||||
|
||||
const PointCloud: React.FC<PointCloudProps> = ({ data, isCategorical = false }) => {
|
||||
const PointCloud: React.FC<PointCloudProps> = ({ data, isCategorical = false, autoRotate = false }) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const rendererRef = useRef<THREE.WebGLRenderer | undefined>(undefined);
|
||||
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
|
||||
const sceneRef = useRef<THREE.Scene | null>(null);
|
||||
const geometryRef = useRef<THREE.BufferGeometry | null>(null);
|
||||
const materialRef = useRef<THREE.PointsMaterial | null>(null);
|
||||
const animationIdRef = useRef<number | null>(null);
|
||||
const controlsRef = useRef<any>(null);
|
||||
const [isSpinning, setIsSpinning] = useState(autoRotate);
|
||||
const isSpinningRef = useRef(autoRotate);
|
||||
const checkboxId = useRef(`spin-checkbox-${Math.random().toString(36).substring(2, 11)}`).current;
|
||||
|
||||
// 为细胞类型生成颜色映射
|
||||
const generateCellTypeColors = (cellTypes: string[]) => {
|
||||
@ -39,8 +48,32 @@ const PointCloud: React.FC<PointCloudProps> = ({ data, isCategorical = false })
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || data.length === 0) return;
|
||||
|
||||
// 确保ref与当前状态同步
|
||||
isSpinningRef.current = isSpinning;
|
||||
|
||||
// 清理之前的资源
|
||||
if (animationIdRef.current !== null) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
animationIdRef.current = null;
|
||||
}
|
||||
|
||||
if (rendererRef.current) {
|
||||
rendererRef.current.dispose();
|
||||
rendererRef.current = null;
|
||||
}
|
||||
|
||||
if (geometryRef.current) {
|
||||
geometryRef.current.dispose();
|
||||
geometryRef.current = null;
|
||||
}
|
||||
|
||||
if (materialRef.current) {
|
||||
materialRef.current.dispose();
|
||||
materialRef.current = null;
|
||||
}
|
||||
|
||||
// 清除旧渲染内容
|
||||
if (containerRef.current.firstChild) {
|
||||
while (containerRef.current.firstChild) {
|
||||
containerRef.current.removeChild(containerRef.current.firstChild);
|
||||
}
|
||||
|
||||
@ -49,16 +82,20 @@ const PointCloud: React.FC<PointCloudProps> = ({ data, isCategorical = false })
|
||||
|
||||
// 初始化场景、相机、渲染器
|
||||
const scene = new THREE.Scene();
|
||||
sceneRef.current = scene; // 存储scene引用
|
||||
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
|
||||
camera.position.set(0, 0, 30);
|
||||
camera.position.set(-15, 15, -12); // 从左下后方观察,使Z轴指向右上方
|
||||
camera.lookAt(0, 0, 0); // 确保相机朝向原点
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setSize(width, height);
|
||||
rendererRef.current = renderer;
|
||||
containerRef.current.appendChild(renderer.domElement);
|
||||
|
||||
// 控件
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controlsRef.current = controls;
|
||||
|
||||
// 坐标轴辅助
|
||||
const axesHelper = new THREE.AxesHelper(10);
|
||||
@ -66,6 +103,7 @@ const PointCloud: React.FC<PointCloudProps> = ({ data, isCategorical = false })
|
||||
|
||||
// 点云数据
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometryRef.current = geometry;
|
||||
const positions: number[] = [];
|
||||
const colors: number[] = [];
|
||||
const sizes: number[] = [];
|
||||
@ -111,6 +149,7 @@ const PointCloud: React.FC<PointCloudProps> = ({ data, isCategorical = false })
|
||||
size: 0.5,
|
||||
sizeAttenuation: true,
|
||||
});
|
||||
materialRef.current = material;
|
||||
|
||||
const points = new THREE.Points(geometry, material);
|
||||
scene.add(points);
|
||||
@ -144,7 +183,13 @@ const PointCloud: React.FC<PointCloudProps> = ({ data, isCategorical = false })
|
||||
|
||||
// 渲染循环
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate);
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
|
||||
// 自动旋转逻辑 - 使用 ref 来避免重新创建场景
|
||||
if (isSpinningRef.current && autoRotate) {
|
||||
scene.rotation.z += 0.002; // 围绕Z轴更慢速旋转
|
||||
}
|
||||
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
@ -182,14 +227,123 @@ const PointCloud: React.FC<PointCloudProps> = ({ data, isCategorical = false })
|
||||
|
||||
// 清理
|
||||
return () => {
|
||||
renderer.dispose();
|
||||
geometry.dispose();
|
||||
material.dispose();
|
||||
renderer.domElement.removeEventListener("click", onClick);
|
||||
};
|
||||
}, [data, isCategorical]);
|
||||
// 停止动画循环
|
||||
if (animationIdRef.current !== null) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
animationIdRef.current = null;
|
||||
}
|
||||
|
||||
return <div ref={containerRef} className="point-cloud-container" />;
|
||||
// 移除事件监听器
|
||||
if (rendererRef.current && rendererRef.current.domElement) {
|
||||
rendererRef.current.domElement.removeEventListener("click", onClick);
|
||||
}
|
||||
|
||||
// 清理控制器
|
||||
if (controlsRef.current) {
|
||||
controlsRef.current.dispose();
|
||||
controlsRef.current = null;
|
||||
}
|
||||
|
||||
// 释放Three.js资源
|
||||
if (rendererRef.current) {
|
||||
rendererRef.current.dispose();
|
||||
rendererRef.current = null;
|
||||
}
|
||||
|
||||
if (geometryRef.current) {
|
||||
geometryRef.current.dispose();
|
||||
geometryRef.current = null;
|
||||
}
|
||||
|
||||
if (materialRef.current) {
|
||||
materialRef.current.dispose();
|
||||
materialRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [data, isCategorical, autoRotate]);
|
||||
|
||||
// 监听autoRotate prop变化,重置旋转状态
|
||||
useEffect(() => {
|
||||
setIsSpinning(autoRotate);
|
||||
isSpinningRef.current = autoRotate;
|
||||
}, [autoRotate]);
|
||||
|
||||
// 监听isSpinning状态变化,只更新ref,不重新创建场景
|
||||
useEffect(() => {
|
||||
isSpinningRef.current = isSpinning;
|
||||
}, [isSpinning]);
|
||||
|
||||
// 组件卸载时的最终清理
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (animationIdRef.current !== null) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
}
|
||||
if (controlsRef.current) {
|
||||
controlsRef.current.dispose();
|
||||
}
|
||||
if (rendererRef.current) {
|
||||
rendererRef.current.dispose();
|
||||
}
|
||||
if (geometryRef.current) {
|
||||
geometryRef.current.dispose();
|
||||
}
|
||||
if (materialRef.current) {
|
||||
materialRef.current.dispose();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
<div ref={containerRef} className="point-cloud-container" />
|
||||
{autoRotate === true && (
|
||||
<div className="spin-control" style={{
|
||||
position: 'absolute',
|
||||
top: '15px',
|
||||
left: '15px',
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
color: 'white',
|
||||
padding: '10px 15px',
|
||||
borderRadius: '8px',
|
||||
border: '2px solid #fff',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
||||
pointerEvents: 'auto'
|
||||
}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={checkboxId}
|
||||
checked={isSpinning}
|
||||
onChange={(e) => setIsSpinning(e.target.checked)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
accentColor: '#4ECDC4'
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor={checkboxId}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
color: 'white',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
Auto Spin
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PointCloud;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { ResponsiveBoxPlot } from "@nivo/boxplot";
|
||||
import PointCloud from "../components/PointCloud";
|
||||
import "../style.css";
|
||||
|
||||
@ -7,6 +8,7 @@ const GeneView: React.FC = () => {
|
||||
const [gene, setGene] = useState("");
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [cellData, setCellData] = useState<any[]>([]);
|
||||
const [distributionData, setDistributionData] = useState<any>({ data: [], colorObj: {}, cellTypes: [] });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cellLoading, setCellLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
@ -14,6 +16,23 @@ const GeneView: React.FC = () => {
|
||||
const [availableGenes, setAvailableGenes] = useState<string[]>([]);
|
||||
const [selectedStage, setSelectedStage] = useState("CS7");
|
||||
|
||||
// 为细胞类型生成颜色映射(与 PointCloud 组件保持一致)
|
||||
const generateCellTypeColors = (cellTypes: string[]) => {
|
||||
const colorMap = new Map<string, string>();
|
||||
const colors = [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
|
||||
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9',
|
||||
'#F8C471', '#82E0AA', '#F1948A', '#85CDCE', '#D7BDE2',
|
||||
'#A9DFBF', '#F9E79F', '#D5A6BD', '#AED6F1', '#E8DAEF'
|
||||
];
|
||||
|
||||
cellTypes.forEach((type, index) => {
|
||||
colorMap.set(type, colors[index % colors.length]);
|
||||
});
|
||||
|
||||
return colorMap;
|
||||
};
|
||||
|
||||
// 动态获取某个阶段的基因列表
|
||||
useEffect(() => {
|
||||
if (!selectedStage) return;
|
||||
@ -58,19 +77,53 @@ const GeneView: React.FC = () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
setData([]);
|
||||
setDistributionData({ data: [], colorObj: {}, cellTypes: [] });
|
||||
|
||||
try {
|
||||
const res = await axios.get("http://localhost:5000/api/gene_expression", {
|
||||
params: {
|
||||
gene: gene.trim(),
|
||||
stage: selectedStage,
|
||||
},
|
||||
});
|
||||
// 同时请求基因表达和分布数据
|
||||
const [expressionRes, distributionRes] = await Promise.all([
|
||||
axios.get("http://localhost:5000/api/gene_expression", {
|
||||
params: {
|
||||
gene: gene.trim(),
|
||||
stage: selectedStage,
|
||||
},
|
||||
}),
|
||||
axios.get("http://localhost:5000/api/gene_dist", {
|
||||
params: {
|
||||
gene: gene.trim(),
|
||||
stage: selectedStage,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (res.data.expression.length === 0) {
|
||||
if (expressionRes.data.expression.length === 0) {
|
||||
setError("No expression data found.");
|
||||
} else {
|
||||
setData(res.data.expression);
|
||||
setData(expressionRes.data.expression);
|
||||
|
||||
// 转换分布数据为 nivo boxplot 格式 - 需要扁平化为原始数据点
|
||||
const boxplotData: any[] = [];
|
||||
const cellTypes = Object.keys(distributionRes.data.distribution).sort();
|
||||
const colorMap = generateCellTypeColors(cellTypes);
|
||||
|
||||
Object.entries(distributionRes.data.distribution).forEach(
|
||||
([cellType, values]: [string, any]) => {
|
||||
values.forEach((value: number) => {
|
||||
boxplotData.push({
|
||||
group: cellType,
|
||||
value: value,
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// 创建颜色对象映射
|
||||
const colorObj: { [key: string]: string } = {};
|
||||
cellTypes.forEach(cellType => {
|
||||
colorObj[cellType] = colorMap.get(cellType) || '#CCCCCC';
|
||||
});
|
||||
|
||||
setDistributionData({ data: boxplotData, colorObj, cellTypes });
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.response && err.response.data?.error) {
|
||||
@ -93,64 +146,71 @@ const GeneView: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="page-content">
|
||||
<div className="section">
|
||||
<h2>基因表达查询</h2>
|
||||
|
||||
{/* 阶段选择 */}
|
||||
<div className="stage-selector">
|
||||
<label>
|
||||
发育阶段:
|
||||
<select
|
||||
value={selectedStage}
|
||||
onChange={(e) => {
|
||||
setSelectedStage(e.target.value);
|
||||
setGene("");
|
||||
setData([]);
|
||||
setError("");
|
||||
}}
|
||||
>
|
||||
<option value="CS7">CS7 - 早期发育阶段</option>
|
||||
<option value="CS8">CS8 - 中期发育阶段</option>
|
||||
<option value="CS9">CS9 - 后期发育阶段</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 基因选择 + 按钮 */}
|
||||
<div className="gene-selector">
|
||||
<label htmlFor="gene-select" className="gene-label">目标基因:</label>
|
||||
{/* 基因表达查询控制器 */}
|
||||
<div className="query-controls" style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "2rem",
|
||||
flexWrap: "wrap",
|
||||
marginBottom: "2rem"
|
||||
}}>
|
||||
<label>
|
||||
发育阶段:
|
||||
<select
|
||||
id="gene-select"
|
||||
aria-label="选择基因"
|
||||
value={gene}
|
||||
onChange={(e) => setGene(e.target.value)}
|
||||
className="gene-select"
|
||||
value={selectedStage}
|
||||
onChange={(e) => {
|
||||
setSelectedStage(e.target.value);
|
||||
setGene("");
|
||||
setData([]);
|
||||
setError("");
|
||||
}}
|
||||
>
|
||||
<option value="">-- 请选择基因 --</option>
|
||||
{availableGenes.map((g) => (
|
||||
<option key={g} value={g}>
|
||||
{g}
|
||||
</option>
|
||||
))}
|
||||
<option value="CS7">CS7 - 早期发育阶段</option>
|
||||
<option value="CS8">CS8 - 中期发育阶段</option>
|
||||
<option value="CS9">CS9 - 后期发育阶段</option>
|
||||
</select>
|
||||
<button onClick={handleSearch} disabled={!gene || loading}>
|
||||
{loading ? "加载中..." : "查询表达"}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
<label htmlFor="gene-select">目标基因:</label>
|
||||
<select
|
||||
id="gene-select"
|
||||
aria-label="选择基因"
|
||||
value={gene}
|
||||
onChange={(e) => setGene(e.target.value)}
|
||||
>
|
||||
<option value="">-- 请选择基因 --</option>
|
||||
{availableGenes.map((g) => (
|
||||
<option key={g} value={g}>
|
||||
{g}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button onClick={handleSearch} disabled={!gene || loading}>
|
||||
{loading ? "加载中..." : "查询表达"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h2>3D 可视化</h2>
|
||||
<p style={{ marginBottom: "1.5rem", color: "var(--text-secondary)" }}>
|
||||
下方展示了{selectedStage}发育阶段的细胞类型分布(左)和所选基因的表达模式(右)。
|
||||
点击任意数据点可查看详细信息。
|
||||
</p>
|
||||
{/* 错误提示 */}
|
||||
{error && <div className="error-message" style={{ marginBottom: "2rem" }}>{error}</div>}
|
||||
|
||||
{/* 双可视化区域 */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "2rem", minHeight: "500px" }}>
|
||||
{/* 3D 可视化说明 */}
|
||||
<p className="flat-section-description">
|
||||
下方展示了{selectedStage}发育阶段的细胞类型分布(左)、所选基因的表达模式(中)和基因在各细胞类型中的表达分布(右)。
|
||||
点击任意数据点可查看详细信息。
|
||||
</p>
|
||||
|
||||
{/* 三可视化区域 */}
|
||||
<div
|
||||
className="three-column-grid visualization-grid"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "minmax(400px, 1fr) minmax(400px, 1fr) minmax(450px, 1fr)",
|
||||
gap: "2rem",
|
||||
minHeight: "600px",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
{/* 细胞类型可视化 */}
|
||||
<div className="visualization-container">
|
||||
<h3 style={{ marginBottom: "1rem", textAlign: "center" }}>细胞类型分布</h3>
|
||||
@ -181,7 +241,7 @@ const GeneView: React.FC = () => {
|
||||
<div className="visualization-container">
|
||||
<h3 style={{ marginBottom: "1rem", textAlign: "center" }}>基因表达模式</h3>
|
||||
{data.length > 0 ? (
|
||||
<PointCloud data={data} />
|
||||
<PointCloud data={data} autoRotate={true} />
|
||||
) : (
|
||||
<div className="no-data-message">
|
||||
{loading ? (
|
||||
@ -201,8 +261,87 @@ const GeneView: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 基因分布箱线图 */}
|
||||
<div className="visualization-container">
|
||||
<h3 style={{ marginBottom: "1rem", textAlign: "center" }}>基因表达分布</h3>
|
||||
{distributionData.data && distributionData.data.length > 0 ? (
|
||||
<div style={{ height: "480px" }}>
|
||||
<ResponsiveBoxPlot
|
||||
data={distributionData.data}
|
||||
margin={{ top: 40, right: 100, bottom: 80, left: 140 }}
|
||||
minValue="auto"
|
||||
maxValue="auto"
|
||||
padding={0.2}
|
||||
enableGridX={true}
|
||||
enableGridY={false}
|
||||
gridXValues={3}
|
||||
layout="horizontal"
|
||||
axisTop={null}
|
||||
axisRight={null}
|
||||
axisBottom={{
|
||||
tickSize: 5,
|
||||
tickPadding: 12,
|
||||
tickRotation: 0,
|
||||
legend: "表达值",
|
||||
legendPosition: "middle",
|
||||
legendOffset: 60,
|
||||
format: (value: number) => value.toFixed(2),
|
||||
tickValues: 3,
|
||||
}}
|
||||
axisLeft={{
|
||||
tickSize: 5,
|
||||
tickPadding: 10,
|
||||
tickRotation: 0,
|
||||
legend: "细胞类型",
|
||||
legendPosition: "middle",
|
||||
legendOffset: -120,
|
||||
format: (value: string) => value.length > 12 ? `${value.substring(0, 12)}...` : value,
|
||||
}}
|
||||
colors={(boxData: any) => {
|
||||
// 根据 group 获取颜色
|
||||
return distributionData.colorObj[boxData.group] || '#CCCCCC';
|
||||
}}
|
||||
borderRadius={2}
|
||||
borderWidth={2}
|
||||
borderColor={{
|
||||
from: "color",
|
||||
modifiers: [["darker", 0.3]],
|
||||
}}
|
||||
medianWidth={2}
|
||||
medianColor={{
|
||||
from: "color",
|
||||
modifiers: [["darker", 0.3]],
|
||||
}}
|
||||
whiskerEndSize={0.6}
|
||||
whiskerColor={{
|
||||
from: "color",
|
||||
modifiers: [["darker", 0.3]],
|
||||
}}
|
||||
motionConfig="gentle"
|
||||
legends={[]}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="no-data-message">
|
||||
{loading ? (
|
||||
<div>
|
||||
<div style={{ fontSize: "2rem", marginBottom: "1rem" }}>🔄</div>
|
||||
<p>正在加载分布数据...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div style={{ fontSize: "2rem", marginBottom: "1rem" }}>📈</div>
|
||||
<p>请选择目标基因以查看表达分布</p>
|
||||
<p style={{ fontSize: "0.9rem", color: "var(--text-secondary)" }}>
|
||||
箱线图显示各细胞类型中的表达分布
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h2>使用说明</h2>
|
||||
@ -217,7 +356,11 @@ const GeneView: React.FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<h3>🎨 颜色编码</h3>
|
||||
<p>左侧:不同颜色代表不同细胞类型。右侧:颜色代表基因表达强度,红色表示高表达,蓝色表示低表达。</p>
|
||||
<p>左侧和右侧:相同颜色代表相同细胞类型,保持一致性便于对比。中间:颜色代表基因表达强度,红色表示高表达,蓝色表示低表达。</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>📈 箱线图解读</h3>
|
||||
<p>箱线图显示四分位数分布:箱体表示25%-75%范围,中线为中位数,胡须显示数据范围,点表示异常值。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -115,9 +115,22 @@ body {
|
||||
|
||||
/* ===== 页面容器 ===== */
|
||||
.page-container {
|
||||
max-width: 1200px;
|
||||
max-width: 1800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.page-container {
|
||||
max-width: 1200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-container {
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
@ -436,7 +449,8 @@ button:disabled {
|
||||
|
||||
.visualization-container {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
height: 550px;
|
||||
min-width: 400px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.75rem;
|
||||
position: relative;
|
||||
@ -730,6 +744,58 @@ button:disabled {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* ===== 查询控制器样式 ===== */
|
||||
.query-controls {
|
||||
padding: 1.5rem;
|
||||
background-color: var(--background-accent);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.query-controls label {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.query-controls select {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
min-width: 200px;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.query-controls select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(44, 82, 130, 0.1);
|
||||
}
|
||||
|
||||
.query-controls button {
|
||||
padding: 0.5rem 1.5rem;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.query-controls button:hover:not(:disabled) {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.query-controls button:disabled {
|
||||
background-color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ===== 响应式设计 ===== */
|
||||
@media (max-width: 768px) {
|
||||
.nav-container {
|
||||
@ -774,3 +840,70 @@ button:disabled {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.query-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
.query-controls select {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.query-controls button {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 响应式网格布局 ===== */
|
||||
@media (max-width: 1200px) {
|
||||
.three-column-grid {
|
||||
grid-template-columns: 1fr 1fr !important;
|
||||
grid-template-rows: auto auto auto;
|
||||
}
|
||||
|
||||
.three-column-grid > div:first-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.visualization-container {
|
||||
min-width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.three-column-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 1.5rem !important;
|
||||
}
|
||||
|
||||
.visualization-container {
|
||||
height: 450px;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 扁平化标题样式 ===== */
|
||||
.flat-section-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.flat-section-description {
|
||||
margin-bottom: 2rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.visualization-grid {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user