Add boxplot

This commit is contained in:
wjsjwr 2025-07-26 14:28:03 +08:00
parent 37692d5775
commit 1514130adc
5 changed files with 728 additions and 16 deletions

View File

@ -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)

View File

@ -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",

View File

@ -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",

View File

@ -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) {
@ -145,12 +198,21 @@ const GeneView: React.FC = () => {
<div className="section">
<h2>3D </h2>
<p style={{ marginBottom: "1.5rem", color: "var(--text-secondary)" }}>
{selectedStage}
{selectedStage}
</p>
{/* 双可视化区域 */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "2rem", minHeight: "500px" }}>
{/* 三可视化区域 */}
<div
className="three-column-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>
@ -201,6 +263,86 @@ 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>
@ -217,7 +359,11 @@ const GeneView: React.FC = () => {
</div>
<div>
<h3>🎨 </h3>
<p></p>
<p>便</p>
</div>
<div>
<h3>📈 线</h3>
<p>线25%-75%线</p>
</div>
</div>
</div>

View File

@ -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;
@ -774,3 +788,31 @@ button:disabled {
grid-template-columns: 1fr;
}
}
/* ===== 响应式网格布局 ===== */
@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;
}
}