🎯 Goal
Implement API GET /analytics/journals/quartiles để trả về dữ liệu phân bố tạp chí theo nhóm Scimago Quartile.
API này phục vụ biểu đồ vòng khuyên Quartile Distribution trong màn hình:
Scientia: Journal Metrics & Performance (V6)
Mục tiêu là cung cấp:
- Tổng số journal trong phạm vi project
- Tỷ lệ phân bố journal theo các nhóm Q1, Q2, Q3, Q4
- Dữ liệu sẵn sàng để FE render donut chart
🖼️ UI Mapping
Screen
Màn hình 4: Scientia: Journal Metrics & Performance (V6)
UI Block
Chart Type
Description
Biểu đồ vòng khuyên hiển thị tỷ lệ tạp chí theo các nhóm phân hạng:
Q1 - High Impact
Q2 - Moderate
Q3 - Standard
Q4 - Developing
Ví dụ UI:
2.4k TOTAL JOURNALS
Q1 (High Impact) 42%
Q2 (Moderate) 35%
Q3 (Standard) 18%
Q4 (Developing) 5%
📡 Endpoint
GET /analytics/journals/quartiles
📥 Query Parameters
| Param |
Type |
Required |
Description |
project_id |
string / number |
Yes |
ID của project cần lấy quartile distribution |
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 bài báo |
to_year |
number |
No |
Năm kết thúc lọc dữ liệu bài báo |
✅ Example Requests
1. Fetch quartile distribution by project only
GET /analytics/journals/quartiles?project_id=PROJECT_ID
2. Fetch quartile distribution with subject area filter
GET /analytics/journals/quartiles?project_id=PROJECT_ID&subject_area=Computer Science
3. Fetch quartile distribution with keywords filter
GET /analytics/journals/quartiles?project_id=PROJECT_ID&keywords=AI,Machine Learning,RAG
4. Fetch quartile distribution with year range
GET /analytics/journals/quartiles?project_id=PROJECT_ID&from_year=2021&to_year=2026
🧾 Response Contract
{
"code": 200,
"message": "Fetch quartile distribution successfully",
"data": {
"totalJournals": 2400,
"distribution": [
{
"group": "Q1 (High Impact)",
"percentage": 42
},
{
"group": "Q2 (Moderate)",
"percentage": 35
}
]
}
}
📌 Response Field Explanation
| Field |
Type |
Description |
totalJournals |
number |
Tổng số journal duy nhất trong filtered dataset |
distribution |
array |
Danh sách tỷ lệ phân bố theo quartile |
distribution[].group |
string |
Tên nhóm quartile hiển thị trên UI |
distribution[].percentage |
number |
Tỷ lệ journal thuộc nhóm đó, tính theo phần trăm |
✅ Expected Full Distribution Format
API nên trả đủ 4 nhóm Q1-Q4 để FE render ổn định:
{
"code": 200,
"message": "Fetch quartile distribution successfully",
"data": {
"totalJournals": 2400,
"distribution": [
{
"group": "Q1 (High Impact)",
"percentage": 42
},
{
"group": "Q2 (Moderate)",
"percentage": 35
},
{
"group": "Q3 (Standard)",
"percentage": 18
},
{
"group": "Q4 (Developing)",
"percentage": 5
}
]
}
}
🧠 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 scope nghiên cứu của project:
- Subject area của project
- Subject categories thuộc subject area đó
- Keywords project đang theo dõi
Sau đó dùng scope này để lọc tập bài báo trước khi tính phân bố journal quartile.
Ví dụ project đang theo dõi:
{
"project_id": 1,
"subject_area": "Artificial Intelligence",
"subject_categories": ["Computer Science Applications", "Artificial Intelligence"],
"keywords": ["LLM", "RAG", "Machine Learning"]
}
API chỉ tính quartile distribution trên các journal có bài báo thuộc scope này.
2. Filter priority
Nếu client chỉ truyền:
API tự động lấy toàn bộ subject area, subject categories 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/journals/quartiles?project_id=1&subject_area=AI&keywords=LLM,RAG
Kết quả chỉ tính quartile distribution trên các journal:
- Có bài báo thuộc project scope
- Có bài báo thuộc subject area
AI
- Có bài báo có keyword liên quan đến
LLM hoặc RAG
3. Journal collection logic
API cần lấy danh sách journal duy nhất từ filtered articles.
Suggested logic:
1. Filter related articles by project scope
2. Join Article -> Issue -> Journal
3. Distinct by Journal.journal_id
4. Join Journal -> Journal_Ranking
5. Get latest quartile data
6. Count journals by quartile
7. Calculate percentages
4. Quartile source
Ưu tiên lấy quartile từ bảng ranking hoặc metrics của journal.
Suggested sources:
Journal_Ranking.quartile
Journal_Ranking.scimago_quartile
Journal.quartile
Tuỳ schema thực tế.
Nếu có nhiều bản ghi ranking theo năm:
Use latest available ranking year
Nếu client truyền to_year:
Use latest ranking year <= to_year
5. Quartile group mapping
API cần chuẩn hóa label quartile như sau:
| Raw Quartile |
Response group |
Q1 |
Q1 (High Impact) |
Q2 |
Q2 (Moderate) |
Q3 |
Q3 (Standard) |
Q4 |
Q4 (Developing) |
Nếu raw quartile không hợp lệ hoặc null:
Ignore journal đó khỏi quartile percentage calculation
Suggested phase 1:
Only count Q1-Q4.
Unknown quartile is excluded.
6. Percentage calculation
Công thức:
percentage = round((journalCountInQuartile / totalJournalsWithValidQuartile) * 100)
Trong đó:
totalJournalsWithValidQuartile = tổng số journal có quartile Q1-Q4
Ví dụ:
Q1 = 42 journals
Q2 = 35 journals
Q3 = 18 journals
Q4 = 5 journals
total = 100
Q1 percentage = 42%
Q2 percentage = 35%
Q3 percentage = 18%
Q4 percentage = 5%
7. Total journals rule
totalJournals nên là số lượng journal duy nhất trong filtered dataset.
Suggested:
totalJournals = count(distinct journal_id)
Nếu có journal không có quartile, totalJournals vẫn có thể bao gồm chúng.
Tuy nhiên, percentage nên tính trên các journal có quartile hợp lệ để tổng Q1-Q4 gần bằng 100%.
Alternative:
totalJournals = totalJournalsWithValidQuartile
Suggested phase 1:
totalJournals = totalJournalsWithValidQuartile
Để UI donut chart nhất quán.
8. Missing quartile group
API nên luôn trả đủ 4 nhóm Q1-Q4.
Nếu một nhóm không có journal:
{
"group": "Q4 (Developing)",
"percentage": 0
}
Rule:
Order cố định:
9. Rounding rule
Suggested phase 1:
percentage = round to integer
Nếu tổng sau rounding không đúng 100 chính xác, có thể chấp nhận sai số nhỏ.
Optional improvement:
Adjust largest group by rounding difference to make total exactly 100.
📦 Data Source
API có thể lấy dữ liệu từ các bảng hiện tại:
Project
Project_Keyword
Subject_Area
Subject_Category
Topic
Sub_Topic
Keyword
Keyword_Article
Article
Issue
Journal
Journal_Ranking
Relevant schema flow:
Project.subject_area -> Subject_Area.subject_area_id
Subject_Category.subject_area_id -> Subject_Area.subject_area_id
Article.primary_topic -> Topic.topic_id
Topic.subject_category_id -> Subject_Category.subject_category_id
Sub_Topic.article_id -> Article.article_id
Sub_Topic.topic_id -> Topic.topic_id
Keyword_Article.article_id -> Article.article_id
Keyword_Article.keyword_id -> Keyword.keyword_id
Project_Keyword.keyword_id -> Keyword.keyword_id
Article.issue_id -> Issue.issue_id
Issue.journal_id -> Journal.journal_id
Journal.journal_id -> Journal_Ranking.journal_id
🧮 Suggested Query Logic
1. Resolve project scope
project_id
-> subject_area
-> subject_category_ids
-> keyword_ids
2. Filter related articles
Article match nếu thỏa mãn ít nhất một trong các điều kiện:
Article.primary_topic thuộc subject_category_ids
OR Article có Sub_Topic thuộc subject_category_ids
OR Article có Keyword_Article thuộc keyword_ids
3. Map articles to journals
Filtered Article
-> Issue
-> Journal
4. Get latest quartile for each journal
Journal
-> latest Journal_Ranking
-> quartile
5. Aggregate distribution
count Q1
count Q2
count Q3
count Q4
calculate percentage
⚠️ 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 scope
Nếu project chưa có subject area hoặc keywords:
{
"code": 200,
"message": "Fetch quartile distribution successfully",
"data": {
"totalJournals": 0,
"distribution": [
{
"group": "Q1 (High Impact)",
"percentage": 0
},
{
"group": "Q2 (Moderate)",
"percentage": 0
},
{
"group": "Q3 (Standard)",
"percentage": 0
},
{
"group": "Q4 (Developing)",
"percentage": 0
}
]
}
}
3. Empty filtered dataset
Nếu filter xong không có bài báo phù hợp:
{
"code": 200,
"message": "Fetch quartile distribution successfully",
"data": {
"totalJournals": 0,
"distribution": [
{
"group": "Q1 (High Impact)",
"percentage": 0
},
{
"group": "Q2 (Moderate)",
"percentage": 0
},
{
"group": "Q3 (Standard)",
"percentage": 0
},
{
"group": "Q4 (Developing)",
"percentage": 0
}
]
}
}
4. Article has no journal
Nếu bài báo không map được journal:
Ignore article đó trong quartile calculation
5. Journal has no quartile
Nếu journal không có quartile:
Ignore journal đó khỏi distribution calculation
Nếu tất cả journal đều không có quartile:
{
"code": 200,
"message": "Fetch quartile distribution successfully",
"data": {
"totalJournals": 0,
"distribution": [
{
"group": "Q1 (High Impact)",
"percentage": 0
},
{
"group": "Q2 (Moderate)",
"percentage": 0
},
{
"group": "Q3 (Standard)",
"percentage": 0
},
{
"group": "Q4 (Developing)",
"percentage": 0
}
]
}
}
6. Invalid year range
Nếu from_year > to_year:
{
"code": 400,
"message": "Invalid year range",
"data": null
}
🧪 Acceptance Criteria
🧪 Test Cases
TC-01: Fetch quartile distribution by project_id only
Request
GET /analytics/journals/quartiles?project_id=1
Expected
- API lấy project scope
- Filter bài báo theo project scope
- Map article sang journal
- Lấy quartile của journal
- Tính percentage Q1-Q4
- Response đúng contract
TC-02: Fetch quartile distribution with subject_area filter
Request
GET /analytics/journals/quartiles?project_id=1&subject_area=Artificial Intelligence
Expected
- Chỉ lấy dữ liệu thuộc subject area được truyền vào
- Không trả dữ liệu ngoài project scope
- Distribution vẫn đủ Q1-Q4
TC-03: Fetch quartile distribution with keywords filter
Request
GET /analytics/journals/quartiles?project_id=1&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-04: Fetch quartile distribution with year range
Request
GET /analytics/journals/quartiles?project_id=1&from_year=2021&to_year=2026
Expected
- Chỉ lấy bài báo trong khoảng năm 2021 đến 2026
- Nếu có ranking theo năm, dùng quartile mới nhất <=
to_year
- Response đúng contract
TC-05: Missing quartile group
Input
Filtered dataset chỉ có journal thuộc Q1 và Q2.
Expected
API vẫn trả đủ Q1-Q4:
{
"totalJournals": 100,
"distribution": [
{
"group": "Q1 (High Impact)",
"percentage": 70
},
{
"group": "Q2 (Moderate)",
"percentage": 30
},
{
"group": "Q3 (Standard)",
"percentage": 0
},
{
"group": "Q4 (Developing)",
"percentage": 0
}
]
}
TC-06: Journal has no quartile
Input
Journal có bài báo phù hợp nhưng không có quartile.
Expected
- Journal đó bị bỏ khỏi distribution calculation
- API không crash
- Response vẫn đúng contract
TC-07: Project not found
Request
GET /analytics/journals/quartiles?project_id=999
Expected
{
"code": 404,
"message": "Project not found",
"data": null
}
TC-08: Empty filtered dataset
Request
GET /analytics/journals/quartiles?project_id=1&keywords=unknown-keyword
Expected
{
"code": 200,
"message": "Fetch quartile distribution successfully",
"data": {
"totalJournals": 0,
"distribution": [
{
"group": "Q1 (High Impact)",
"percentage": 0
},
{
"group": "Q2 (Moderate)",
"percentage": 0
},
{
"group": "Q3 (Standard)",
"percentage": 0
},
{
"group": "Q4 (Developing)",
"percentage": 0
}
]
}
}
TC-09: Invalid year range
Request
GET /analytics/journals/quartiles?project_id=1&from_year=2026&to_year=2021
Expected
{
"code": 400,
"message": "Invalid year range",
"data": null
}
📌 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 related articles
-> map articles to journals
-> get latest journal quartile
-> count quartile groups
-> calculate percentages
-> build response contract
Gợi ý function:
getProjectTrackingScope(projectId)
buildArticleScopeFilter(scope, queryParams)
getFilteredJournalQuartiles(filters)
getLatestJournalQuartile(journalId, toYear)
countQuartileGroups(items)
calculateQuartilePercentages(counts)
buildQuartileDistributionResponse(totalJournals, distribution)
📦 Suggested Mock Data
const mockQuartileDistribution = {
totalJournals: 2400,
distribution: [
{
group: "Q1 (High Impact)",
percentage: 42
},
{
group: "Q2 (Moderate)",
percentage: 35
},
{
group: "Q3 (Standard)",
percentage: 18
},
{
group: "Q4 (Developing)",
percentage: 5
}
]
};
🚀 Future Improvements
- Add raw count per quartile:
{
"group": "Q1 (High Impact)",
"count": 1008,
"percentage": 42
}
- Add
quartile field separately from display label:
{
"quartile": "Q1",
"label": "High Impact",
"percentage": 42
}
- Add
rankingYear
- Add
unknownQuartileCount
- Add
metric_type=scimago|jcr
- Add Redis caching:
analytics:journals:quartiles:{project_id}:{subject_area}:{keywords}:{from_year}:{to_year}
- Optimize query with indexes on:
project_id
publication_year
article_id
issue_id
journal_id
keyword_id
subject_category_id
quartile
🎯 Goal
Implement API
GET /analytics/journals/quartilesđể trả về dữ liệu phân bố tạp chí theo nhóm Scimago Quartile.API này phục vụ biểu đồ vòng khuyên Quartile Distribution trong màn hình:
Mục tiêu là cung cấp:
🖼️ UI Mapping
Screen
UI Block
Chart Type
Description
Biểu đồ vòng khuyên hiển thị tỷ lệ tạp chí theo các nhóm phân hạng:
Ví dụ UI:
📡 Endpoint
📥 Query Parameters
project_idsubject_areakeywordsfrom_yearto_year✅ Example Requests
1. Fetch quartile distribution by project only
2. Fetch quartile distribution with subject area filter
3. Fetch quartile distribution with keywords filter
4. Fetch quartile distribution with year range
🧾 Response Contract
{ "code": 200, "message": "Fetch quartile distribution successfully", "data": { "totalJournals": 2400, "distribution": [ { "group": "Q1 (High Impact)", "percentage": 42 }, { "group": "Q2 (Moderate)", "percentage": 35 } ] } }📌 Response Field Explanation
totalJournalsdistributiondistribution[].groupdistribution[].percentage✅ Expected Full Distribution Format
API nên trả đủ 4 nhóm Q1-Q4 để FE render ổn định:
{ "code": 200, "message": "Fetch quartile distribution successfully", "data": { "totalJournals": 2400, "distribution": [ { "group": "Q1 (High Impact)", "percentage": 42 }, { "group": "Q2 (Moderate)", "percentage": 35 }, { "group": "Q3 (Standard)", "percentage": 18 }, { "group": "Q4 (Developing)", "percentage": 5 } ] } }🧠 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 scope nghiên cứu của project:Sau đó dùng scope này để lọc tập bài báo trước khi tính phân bố journal quartile.
Ví dụ project đang theo dõi:
{ "project_id": 1, "subject_area": "Artificial Intelligence", "subject_categories": ["Computer Science Applications", "Artificial Intelligence"], "keywords": ["LLM", "RAG", "Machine Learning"] }API chỉ tính quartile distribution trên các journal có bài báo thuộc scope này.
2. Filter priority
Nếu client chỉ truyền:
project_idAPI tự động lấy toàn bộ subject area, subject categories 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ụ:
Kết quả chỉ tính quartile distribution trên các journal:
AILLMhoặcRAG3. Journal collection logic
API cần lấy danh sách journal duy nhất từ filtered articles.
Suggested logic:
4. Quartile source
Ưu tiên lấy quartile từ bảng ranking hoặc metrics của journal.
Suggested sources:
Tuỳ schema thực tế.
Nếu có nhiều bản ghi ranking theo năm:
Nếu client truyền
to_year:5. Quartile group mapping
API cần chuẩn hóa label quartile như sau:
Q1Q1 (High Impact)Q2Q2 (Moderate)Q3Q3 (Standard)Q4Q4 (Developing)Nếu raw quartile không hợp lệ hoặc null:
Suggested phase 1:
6. Percentage calculation
Công thức:
percentage = round((journalCountInQuartile / totalJournalsWithValidQuartile) * 100)Trong đó:
Ví dụ:
7. Total journals rule
totalJournalsnên là số lượng journal duy nhất trong filtered dataset.Suggested:
Nếu có journal không có quartile,
totalJournalsvẫn có thể bao gồm chúng.Tuy nhiên, percentage nên tính trên các journal có quartile hợp lệ để tổng Q1-Q4 gần bằng 100%.
Alternative:
Suggested phase 1:
Để UI donut chart nhất quán.
8. Missing quartile group
API nên luôn trả đủ 4 nhóm Q1-Q4.
Nếu một nhóm không có journal:
{ "group": "Q4 (Developing)", "percentage": 0 }Rule:
Order cố định:
9. Rounding rule
Suggested phase 1:
Nếu tổng sau rounding không đúng 100 chính xác, có thể chấp nhận sai số nhỏ.
Optional improvement:
📦 Data Source
API có thể lấy dữ liệu từ các bảng hiện tại:
Relevant schema flow:
🧮 Suggested Query Logic
1. Resolve project scope
2. Filter related articles
Article match nếu thỏa mãn ít nhất một trong các điều kiện:
3. Map articles to journals
4. Get latest quartile for each journal
5. Aggregate distribution
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 scope
Nếu project chưa có subject area hoặc keywords:
{ "code": 200, "message": "Fetch quartile distribution successfully", "data": { "totalJournals": 0, "distribution": [ { "group": "Q1 (High Impact)", "percentage": 0 }, { "group": "Q2 (Moderate)", "percentage": 0 }, { "group": "Q3 (Standard)", "percentage": 0 }, { "group": "Q4 (Developing)", "percentage": 0 } ] } }3. Empty filtered dataset
Nếu filter xong không có bài báo phù hợp:
{ "code": 200, "message": "Fetch quartile distribution successfully", "data": { "totalJournals": 0, "distribution": [ { "group": "Q1 (High Impact)", "percentage": 0 }, { "group": "Q2 (Moderate)", "percentage": 0 }, { "group": "Q3 (Standard)", "percentage": 0 }, { "group": "Q4 (Developing)", "percentage": 0 } ] } }4. Article has no journal
Nếu bài báo không map được journal:
5. Journal has no quartile
Nếu journal không có quartile:
Nếu tất cả journal đều không có quartile:
{ "code": 200, "message": "Fetch quartile distribution successfully", "data": { "totalJournals": 0, "distribution": [ { "group": "Q1 (High Impact)", "percentage": 0 }, { "group": "Q2 (Moderate)", "percentage": 0 }, { "group": "Q3 (Standard)", "percentage": 0 }, { "group": "Q4 (Developing)", "percentage": 0 } ] } }6. Invalid year range
Nếu
from_year > to_year:{ "code": 400, "message": "Invalid year range", "data": null }🧪 Acceptance Criteria
project_idsubject_areakeywordsfrom_yearvàto_yeardatadata.totalJournalsluôn là numberdata.distributionluôn là arraydistributionluôn có đủ 4 nhóm Q1-Q4group,percentagepercentageluôn là numberpercentagenằm trong khoảng0đến100NaNtrong response0100ms🧪 Test Cases
TC-01: Fetch quartile distribution by project_id only
Request
Expected
TC-02: Fetch quartile distribution with subject_area filter
Request
Expected
TC-03: Fetch quartile distribution with keywords filter
Request
Expected
LLMhoặcRAGTC-04: Fetch quartile distribution with year range
Request
Expected
to_yearTC-05: Missing quartile group
Input
Filtered dataset chỉ có journal thuộc Q1 và Q2.
Expected
API vẫn trả đủ Q1-Q4:
{ "totalJournals": 100, "distribution": [ { "group": "Q1 (High Impact)", "percentage": 70 }, { "group": "Q2 (Moderate)", "percentage": 30 }, { "group": "Q3 (Standard)", "percentage": 0 }, { "group": "Q4 (Developing)", "percentage": 0 } ] }TC-06: Journal has no quartile
Input
Journal có bài báo phù hợp nhưng không có quartile.
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 quartile distribution successfully", "data": { "totalJournals": 0, "distribution": [ { "group": "Q1 (High Impact)", "percentage": 0 }, { "group": "Q2 (Moderate)", "percentage": 0 }, { "group": "Q3 (Standard)", "percentage": 0 }, { "group": "Q4 (Developing)", "percentage": 0 } ] } }TC-09: Invalid year range
Request
Expected
{ "code": 400, "message": "Invalid year range", "data": null }📌 Implementation Notes
Nên tách logic thành các phần riêng:
Gợi ý function:
📦 Suggested Mock Data
🚀 Future Improvements
{ "group": "Q1 (High Impact)", "count": 1008, "percentage": 42 }quartilefield separately from display label:{ "quartile": "Q1", "label": "High Impact", "percentage": 42 }rankingYearunknownQuartileCountmetric_type=scimago|jcranalytics:journals:quartiles:{project_id}:{subject_area}:{keywords}:{from_year}:{to_year}project_idpublication_yeararticle_idissue_idjournal_idkeyword_idsubject_category_idquartile