🎯 Goal
Implement API GET /analytics/distribution để trả về dữ liệu tỷ lệ phân bổ nghiên cứu phục vụ cho các chart trên dashboard Analytics.
API này dùng cho 2 khối UI chính:
- Research Landscape: biểu đồ khối hình chữ nhật thể hiện tỷ lệ theo lĩnh vực/ngành nghiên cứu.
- Impact Quartiles: biểu đồ vòng tròn tỷ lệ thể hiện phân bổ theo nhóm tác động/phân hạng chất lượng ấn phẩm.
API cần hỗ trợ lọc theo project_id.
Từ project_id, hệ thống sẽ lấy các phạm vi nghiên cứu mà project đang theo dõi, ví dụ:
Sau đó dùng các thông tin này để lọc dữ liệu bài báo trước khi tính tỷ lệ phân bổ.
🖼️ UI Mapping
1. Research Landscape
Dùng để hiển thị tỷ lệ phân bổ theo lĩnh vực/ngành nghiên cứu.
Ví dụ:
[
{ "name": "Biotech", "percentage": 42 },
{ "name": "A.I.", "percentage": 28 },
{ "name": "Materials", "percentage": 15 }
]
2. Impact Quartiles
Dùng để hiển thị tỷ lệ phân bổ theo nhóm tác động hoặc journal quartile.
Ví dụ:
[
{ "name": "Q1", "percentage": 45 },
{ "name": "Q2", "percentage": 30 },
{ "name": "Q3", "percentage": 15 },
{ "name": "Q4", "percentage": 10 }
]
📡 Endpoint
GET /analytics/distribution
📥 Query Parameters
| Param |
Type |
Required |
Description |
project_id |
string / uuid |
Yes |
ID của project cần lấy dữ liệu distribution |
distribution_type |
string |
No |
Loại phân bổ cần lấy. Giá trị đề xuất: sector, impact_quartile |
subject_area |
string |
No |
Lọc hẹp thêm theo subject area cụ thể |
keywords |
string[] / comma-separated |
No |
Lọc hẹp thêm theo danh sách keyword |
from_year |
number |
No |
Năm bắt đầu lọc dữ liệu |
to_year |
number |
No |
Năm kết thúc lọc dữ liệu |
✅ Example Requests
1. Fetch default distribution by project
GET /analytics/distribution?project_id=PROJECT_ID
2. Fetch Research Landscape sector distribution
GET /analytics/distribution?project_id=PROJECT_ID&distribution_type=sector
3. Fetch Impact Quartiles distribution
GET /analytics/distribution?project_id=PROJECT_ID&distribution_type=impact_quartile
4. Fetch distribution with subject area filter
GET /analytics/distribution?project_id=PROJECT_ID&distribution_type=sector&subject_area=Computer Science
5. Fetch distribution with keywords filter
GET /analytics/distribution?project_id=PROJECT_ID&distribution_type=sector&keywords=AI,Machine Learning,RAG
6. Fetch distribution with year range
GET /analytics/distribution?project_id=PROJECT_ID&distribution_type=impact_quartile&from_year=2021&to_year=2026
🧾 Response Contract
{
"code": 200,
"message": "Fetch distribution successfully",
"data": [
{
"name": "Biotech",
"percentage": 42
},
{
"name": "A.I.",
"percentage": 28
},
{
"name": "Materials",
"percentage": 15
}
]
}
🧠 Business Logic
1. Project-based filtering
API bắt buộc nhận project_id.
Từ project_id, hệ thống cần lấy các scope nghiên cứu mà project đang theo dõi:
Sau đó dùng scope này để lọc dataset bài báo toàn cầu.
Ví dụ project đang theo dõi:
{
"project_id": "P001",
"subject_areas": ["Artificial Intelligence", "Data Science"],
"keywords": ["LLM", "RAG", "Machine Learning"]
}
API chỉ tính distribution dựa trên các bài báo match với subject area hoặc keywords của project.
2. Distribution type
API cần hỗ trợ ít nhất 2 loại distribution:
| distribution_type |
Purpose |
UI |
sector |
Phân bổ theo lĩnh vực/ngành nghiên cứu |
Research Landscape |
impact_quartile |
Phân bổ theo nhóm tác động hoặc journal quartile |
Impact Quartiles |
Nếu client không truyền distribution_type, mặc định dùng:
3. Filter priority
Nếu client chỉ truyền:
API tự động lấy toàn bộ subject_area và keywords của project để lọc.
Nếu client truyền thêm:
API sẽ lọc hẹp hơn trong phạm vi project.
Ví dụ:
GET /analytics/distribution?project_id=1&distribution_type=sector&subject_area=AI&keywords=LLM,RAG
Kết quả chỉ tính những bài báo:
- Thuộc phạm vi project
1
- Thuộc subject area
AI
- Có keyword liên quan đến
LLM hoặc RAG
4. Percentage calculation
Sau khi filter dữ liệu, API cần group dữ liệu theo distribution_type.
Với sector
Group theo lĩnh vực/ngành nghiên cứu.
Ví dụ field có thể dùng:
subject_area
sector
category
Với impact_quartile
Group theo nhóm tác động hoặc journal quartile.
Ví dụ field có thể dùng:
quartile
impact_quartile
journal_quartile
5. Percentage formula
Công thức tính:
percentage = round((group_count / total_count) * 100)
Yêu cầu:
- Tổng
percentage nên xấp xỉ 100
- Không có
null
- Không có
NaN
- Nếu cần làm tròn, item cuối có thể được điều chỉnh để tổng chính xác bằng
100
Ví dụ:
[
{ "name": "A.I.", "percentage": 50 },
{ "name": "Biotech", "percentage": 30 },
{ "name": "Materials", "percentage": 20 }
]
6. Sorting rule
Response nên sort giảm dần theo percentage.
Ví dụ:
[
{ "name": "Biotech", "percentage": 42 },
{ "name": "A.I.", "percentage": 28 },
{ "name": "Materials", "percentage": 15 }
]
📦 Data Source - Phase 1
Trong phase 1, dùng mock dataset in-memory.
Mock dataset nên có field tối thiểu:
{
article_id: string;
year: number;
subject_area: string;
sector: string;
keywords: string[];
impact_quartile: string;
}
Hoặc nếu mock theo aggregate sẵn:
{
name: string;
count: number;
distribution_type: "sector" | "impact_quartile";
subject_area: string;
keywords: string[];
year: number;
}
⚠️ Edge Cases
1. Project not found
Nếu project_id không tồn tại:
{
"code": 404,
"message": "Project not found",
"data": null
}
2. Project has no subject area / keywords
Nếu project chưa có scope theo dõi:
{
"code": 200,
"message": "Fetch distribution successfully",
"data": []
}
3. Empty filtered dataset
Nếu filter xong không có bài báo phù hợp:
{
"code": 200,
"message": "Fetch distribution successfully",
"data": []
}
4. Invalid distribution_type
Nếu distribution_type không hợp lệ:
{
"code": 400,
"message": "Invalid distribution type",
"data": null
}
Allowed values:
5. Invalid year range
Nếu from_year > to_year:
{
"code": 400,
"message": "Invalid year range",
"data": null
}
6. Missing group value
Nếu record thiếu field để group, ví dụ thiếu sector hoặc impact_quartile:
- Không làm API lỗi
- Có thể bỏ qua record đó
- Không trả về item có
name: null
🧪 Acceptance Criteria
🧪 Test Cases
TC-01: Fetch default distribution by project_id only
Request
GET /analytics/distribution?project_id=1
Expected
- API lấy subject areas và keywords của project
- Mặc định dùng
distribution_type=sector
- Filter dữ liệu theo project scope
- Tính percentage theo sector
- Response sort giảm dần theo percentage
TC-02: Fetch sector distribution
Request
GET /analytics/distribution?project_id=1&distribution_type=sector
Expected
- Group dữ liệu theo sector hoặc subject area
- Mỗi item có
name và percentage
- Tổng percentage xấp xỉ
100
TC-03: Fetch impact quartile distribution
Request
GET /analytics/distribution?project_id=1&distribution_type=impact_quartile
Expected
- Group dữ liệu theo impact quartile hoặc journal quartile
- Ví dụ:
Q1, Q2, Q3, Q4
- FE render được vòng tròn tỷ lệ trực tiếp
TC-04: Fetch distribution with subject_area filter
Request
GET /analytics/distribution?project_id=1&distribution_type=sector&subject_area=Artificial Intelligence
Expected
- Chỉ lấy dữ liệu thuộc subject area được truyền vào
- Không trả về dữ liệu ngoài scope project
- Data vẫn tính percentage đúng
TC-05: Fetch distribution with keywords filter
Request
GET /analytics/distribution?project_id=1&distribution_type=sector&keywords=LLM,RAG
Expected
- Chỉ lấy dữ liệu có keyword match với
LLM hoặc RAG
- Không có item null
- Không có percentage null hoặc NaN
TC-06: Fetch distribution with year range
Request
GET /analytics/distribution?project_id=1&distribution_type=impact_quartile&from_year=2021&to_year=2026
Expected
- Chỉ lấy dữ liệu trong khoảng năm 2021 đến 2026
- Group theo impact quartile sau khi filter
- Không trả về data ngoài khoảng năm
TC-07: Project not found
Request
GET /analytics/distribution?project_id=999
Expected
{
"code": 404,
"message": "Project not found",
"data": null
}
TC-08: Empty filtered dataset
Request
GET /analytics/distribution?project_id=1&keywords=unknown-keyword
Expected
{
"code": 200,
"message": "Fetch distribution successfully",
"data": []
}
TC-09: Invalid distribution_type
Request
GET /analytics/distribution?project_id=1&distribution_type=unknown
Expected
{
"code": 400,
"message": "Invalid distribution type",
"data": null
}
TC-10: Missing group value in mock data
Input mock data
[
{
"article_id": "A001",
"year": 2024,
"subject_area": "Artificial Intelligence",
"sector": "A.I.",
"keywords": ["LLM"],
"impact_quartile": "Q1"
},
{
"article_id": "A002",
"year": 2024,
"subject_area": "Artificial Intelligence",
"sector": null,
"keywords": ["RAG"],
"impact_quartile": "Q2"
}
]
Expected với distribution_type=sector
{
"code": 200,
"message": "Fetch distribution successfully",
"data": [
{
"name": "A.I.",
"percentage": 100
}
]
}
📌 Implementation Notes
Nên tách logic thành các phần riêng:
Controller
-> validate query params
Service
-> get project tracking scope
-> filter distribution dataset
-> group by selected distribution type
-> calculate percentage
-> normalize percentage
-> build response contract
Gợi ý function:
getProjectTrackingScope(projectId)
filterDistributionDataset(dataset, filters)
groupDistributionData(filteredDataset, distributionType)
calculateDistributionPercentage(groupedData)
normalizePercentage(distributionItems)
buildDistributionResponse(distributionItems)
📦 Suggested Mock Data
const mockDistributionData = [
{
article_id: "A001",
year: 2024,
subject_area: "Artificial Intelligence",
sector: "A.I.",
keywords: ["LLM", "Machine Learning"],
impact_quartile: "Q1"
},
{
article_id: "A002",
year: 2024,
subject_area: "Biotechnology",
sector: "Biotech",
keywords: ["Genomics"],
impact_quartile: "Q2"
},
{
article_id: "A003",
year: 2025,
subject_area: "Materials Science",
sector: "Materials",
keywords: ["Nanomaterials"],
impact_quartile: "Q3"
}
];
🚀 Future Improvements
- Thay mock dataset bằng DB aggregation
- Thêm
distribution_type=country
- Thêm
distribution_type=institution
- Thêm
distribution_type=publisher
- Thêm filter theo region/country
- Cho FE truyền
limit để lấy top N nhóm lớn nhất
- Cache response bằng Redis theo key:
analytics:distribution:{project_id}:{distribution_type}:{subject_area}:{keywords}:{from_year}:{to_year}
- Tối ưu query bằng index cho:
project_id
year
subject_area
keyword
sector
impact_quartile
🎯 Goal
Implement API
GET /analytics/distributionđể trả về dữ liệu tỷ lệ phân bổ nghiên cứu phục vụ cho các chart trên dashboard Analytics.API này dùng cho 2 khối UI chính:
API cần hỗ trợ lọc theo
project_id.Từ
project_id, hệ thống sẽ lấy các phạm vi nghiên cứu mà project đang theo dõi, ví dụ:subject_areakeywordsSau đó dùng các thông tin này để lọc dữ liệu bài báo trước khi tính tỷ lệ phân bổ.
🖼️ UI Mapping
1. Research Landscape
Dùng để hiển thị tỷ lệ phân bổ theo lĩnh vực/ngành nghiên cứu.
Ví dụ:
[ { "name": "Biotech", "percentage": 42 }, { "name": "A.I.", "percentage": 28 }, { "name": "Materials", "percentage": 15 } ]2. Impact Quartiles
Dùng để hiển thị tỷ lệ phân bổ theo nhóm tác động hoặc journal quartile.
Ví dụ:
[ { "name": "Q1", "percentage": 45 }, { "name": "Q2", "percentage": 30 }, { "name": "Q3", "percentage": 15 }, { "name": "Q4", "percentage": 10 } ]📡 Endpoint
📥 Query Parameters
project_iddistribution_typesector,impact_quartilesubject_areakeywordsfrom_yearto_year✅ Example Requests
1. Fetch default distribution by project
2. Fetch Research Landscape sector distribution
3. Fetch Impact Quartiles distribution
4. Fetch distribution with subject area filter
5. Fetch distribution with keywords filter
6. Fetch distribution with year range
🧾 Response Contract
{ "code": 200, "message": "Fetch distribution successfully", "data": [ { "name": "Biotech", "percentage": 42 }, { "name": "A.I.", "percentage": 28 }, { "name": "Materials", "percentage": 15 } ] }🧠 Business Logic
1. Project-based filtering
API bắt buộc nhận
project_id.Từ
project_id, hệ thống cần lấy các scope nghiên cứu mà project đang theo dõi:Sau đó dùng scope này để lọc dataset bài báo toàn cầu.
Ví dụ project đang theo dõi:
{ "project_id": "P001", "subject_areas": ["Artificial Intelligence", "Data Science"], "keywords": ["LLM", "RAG", "Machine Learning"] }API chỉ tính distribution dựa trên các bài báo match với subject area hoặc keywords của project.
2. Distribution type
API cần hỗ trợ ít nhất 2 loại distribution:
sectorimpact_quartileNếu client không truyền
distribution_type, mặc định dùng:3. Filter priority
Nếu client chỉ truyền:
project_idAPI tự động lấy toàn bộ
subject_areavàkeywordscủa project để lọc.Nếu client truyền thêm:
API sẽ lọc hẹp hơn trong phạm vi project.
Ví dụ:
Kết quả chỉ tính những bài báo:
1AILLMhoặcRAG4. Percentage calculation
Sau khi filter dữ liệu, API cần group dữ liệu theo
distribution_type.Với
sectorGroup theo lĩnh vực/ngành nghiên cứu.
Ví dụ field có thể dùng:
Với
impact_quartileGroup theo nhóm tác động hoặc journal quartile.
Ví dụ field có thể dùng:
5. Percentage formula
Công thức tính:
percentage = round((group_count / total_count) * 100)Yêu cầu:
percentagenên xấp xỉ100nullNaN100Ví dụ:
[ { "name": "A.I.", "percentage": 50 }, { "name": "Biotech", "percentage": 30 }, { "name": "Materials", "percentage": 20 } ]6. Sorting rule
Response nên sort giảm dần theo
percentage.Ví dụ:
[ { "name": "Biotech", "percentage": 42 }, { "name": "A.I.", "percentage": 28 }, { "name": "Materials", "percentage": 15 } ]📦 Data Source - Phase 1
Trong phase 1, dùng mock dataset in-memory.
Mock dataset nên có field tối thiểu:
Hoặc nếu mock theo aggregate sẵn:
1. Project not found
Nếu
project_idkhông tồn tại:{ "code": 404, "message": "Project not found", "data": null }2. Project has no subject area / keywords
Nếu project chưa có scope theo dõi:
{ "code": 200, "message": "Fetch distribution successfully", "data": [] }3. Empty filtered dataset
Nếu filter xong không có bài báo phù hợp:
{ "code": 200, "message": "Fetch distribution successfully", "data": [] }4. Invalid distribution_type
Nếu
distribution_typekhông hợp lệ:{ "code": 400, "message": "Invalid distribution type", "data": null }Allowed values:
5. Invalid year range
Nếu
from_year > to_year:{ "code": 400, "message": "Invalid year range", "data": null }6. Missing group value
Nếu record thiếu field để group, ví dụ thiếu
sectorhoặcimpact_quartile:name: null🧪 Acceptance Criteria
project_idsubject_areavàkeywordsmà project đang theo dõidistribution_type=sectordistribution_type=impact_quartiledistribution_type, mặc định dùngsectorsubject_areakeywordsfrom_yearvàto_yearnamevàpercentagepercentageluôn là numbernulltrong responseNaNtrong responsepercentagedata: [], không làm API lỗi100ms🧪 Test Cases
TC-01: Fetch default distribution by project_id only
Request
Expected
distribution_type=sectorTC-02: Fetch sector distribution
Request
Expected
namevàpercentage100TC-03: Fetch impact quartile distribution
Request
Expected
Q1,Q2,Q3,Q4TC-04: Fetch distribution with subject_area filter
Request
Expected
TC-05: Fetch distribution with keywords filter
Request
Expected
LLMhoặcRAGTC-06: Fetch distribution with year range
Request
Expected
TC-07: Project not found
Request
Expected
{ "code": 404, "message": "Project not found", "data": null }TC-08: Empty filtered dataset
Request
Expected
{ "code": 200, "message": "Fetch distribution successfully", "data": [] }TC-09: Invalid distribution_type
Request
Expected
{ "code": 400, "message": "Invalid distribution type", "data": null }TC-10: Missing group value in mock data
Input mock data
[ { "article_id": "A001", "year": 2024, "subject_area": "Artificial Intelligence", "sector": "A.I.", "keywords": ["LLM"], "impact_quartile": "Q1" }, { "article_id": "A002", "year": 2024, "subject_area": "Artificial Intelligence", "sector": null, "keywords": ["RAG"], "impact_quartile": "Q2" } ]Expected với
distribution_type=sector{ "code": 200, "message": "Fetch distribution successfully", "data": [ { "name": "A.I.", "percentage": 100 } ] }📌 Implementation Notes
Nên tách logic thành các phần riêng:
Gợi ý function:
📦 Suggested Mock Data
🚀 Future Improvements
distribution_type=countrydistribution_type=institutiondistribution_type=publisherlimitđể lấy top N nhóm lớn nhấtanalytics:distribution:{project_id}:{distribution_type}:{subject_area}:{keywords}:{from_year}:{to_year}project_idyearsubject_areakeywordsectorimpact_quartile