diff --git a/Homework2/Dashboard.png b/Homework2/Dashboard.png new file mode 100644 index 0000000..41c2f54 Binary files /dev/null and b/Homework2/Dashboard.png differ diff --git a/Homework2/vkosuri/React-Template/README.md b/Homework2/vkosuri/React-Template/README.md new file mode 100644 index 0000000..765052f --- /dev/null +++ b/Homework2/vkosuri/React-Template/README.md @@ -0,0 +1,26 @@ +# More about the Framework + + +This is a template in React and TypeScript. React has a steeper learning curve than Vue.js because it requires understanding JSX syntax and concepts like hooks and component lifecycle. + +If you want to use React but not with TypeScript, just remove any type specifications from the `Example.tsx`, `Notes.tsx`, and `NotesWithReducer.tsx`. You can always refer to `VanillaJS-Template/example.js` for this migration. + + +## Files You Have to Care about + +`package.json` is where we manage the libraries we installed. Besides this, most of the files you can ignore, but **the files under `./src/` are your concern**. + +* `./src/main.tsx` is the root script file for React that instatinates our single page application. +* `./src/App.tsx` is the root file for all **development** needs and is also where we manage the layout and load in components. +* `./src/types.ts` is usually where we declare our customized types if you're planning to use it. +* `./src/stores/` is where we manage the stores if you're planning to use it. The store is responsible for global state management. +* `./src/components/` is where we create the components. You may have multiple components depends on your design. + * `Example.tsx` shows how to read `.csv` and `.json`, how component size is being watched, how a bar chart is created, and how the component updates if there are any changes. + * `Notes.tsx` shows the difference of **state** and **prop**, how to use MUI, and how a local state updates based on interaction. + * `NotesWithReducer.tsx` is equivalent to `Notes.tsx`, excepts it uses store called reducer. + +## Libraries Installed in this Framework + * D3.js v7 for visualization + * [axios](https://axios-http.com/docs/intro) for API. + * [Material UI](https://mui.com/material-ui/getting-started/) for UI components. + * [lodash](https://lodash.com/) for utility functions in JavaScript. diff --git a/Homework2/vkosuri/React-Template/data/Student Mental health.csv b/Homework2/vkosuri/React-Template/data/Student Mental health.csv new file mode 100644 index 0000000..0d75ad4 --- /dev/null +++ b/Homework2/vkosuri/React-Template/data/Student Mental health.csv @@ -0,0 +1,102 @@ +Timestamp,Choose your gender,Age,What is your course?,Your current year of Study,What is your CGPA?,Marital status,Do you have Depression?,Do you have Anxiety?,Do you have Panic attack?,Did you seek any specialist for a treatment? +8/7/2020 12:02,Female,18,Engineering,year 1,3.00 - 3.49,No,Yes,No,Yes,No +8/7/2020 12:04,Male,21,Islamic education,year 2,3.00 - 3.49,No,No,Yes,No,No +8/7/2020 12:05,Male,19,BIT,Year 1,3.00 - 3.49,No,Yes,Yes,Yes,No +8/7/2020 12:06,Female,22,Laws,year 3,3.00 - 3.49,Yes,Yes,No,No,No +8/7/2020 12:13,Male,23,Mathemathics,year 4,3.00 - 3.49,No,No,No,No,No +8/7/2020 12:31,Male,19,Engineering,Year 2,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 12:32,Female,23,Pendidikan islam,year 2,3.50 - 4.00 ,Yes,Yes,No,Yes,No +8/7/2020 12:33,Female,18,BCS,year 1,3.50 - 4.00,No,No,Yes,No,No +8/7/2020 12:35,Female,19,Human Resources,Year 2,2.50 - 2.99,No,No,No,No,No +8/7/2020 12:39,Male,18,Irkhs,year 1,3.50 - 4.00,No,No,Yes,Yes,No +8/7/2020 12:39,Female,20,Psychology,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 12:39,Female,24,Engineering,Year 3,3.50 - 4.00,Yes,Yes,No,No,No +8/7/2020 12:40,Female,18,BCS,year 1,3.00 - 3.49,No,Yes,No,No,No +8/7/2020 12:41,Male,19,Engineering,year 1,3.00 - 3.49,No,No,No,No,No +8/7/2020 12:43,Female,18,KENMS,Year 2,3.50 - 4.00,No,No,Yes,No,No +8/7/2020 12:43,Male,24,BCS,Year 3,3.50 - 4.00,No,No,No,No,No +8/7/2020 12:46,Female,24,Accounting ,year 3,3.00 - 3.49,No,No,No,No,No +8/7/2020 12:52,Female,24,ENM,year 4,3.00 - 3.49,Yes,Yes,Yes,Yes,No +8/7/2020 13:05,Female,20,BIT,Year 2,3.50 - 4.00,No,No,Yes,No,No +8/7/2020 13:07,Female,18,Marine science,year 2,3.50 - 4.00,Yes,Yes,Yes,Yes,No +8/7/2020 13:12,Female,19,Engineering,year 1,3.00 - 3.49,No,No,No,Yes,No +8/7/2020 13:13,Female,18,KOE,Year 2,3.00 - 3.49,No,No,No,No,No +8/7/2020 13:13,Female,24,BCS,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 13:15,Female,24,Engineering,year 1,3.00 - 3.49,No,No,No,No,No +8/7/2020 13:17,Female,23,BCS,Year 3,3.50 - 4.00,No,Yes,Yes,Yes,No +8/7/2020 13:29,Female,18,Banking Studies,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 13:35,Female,19,Engineering,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 13:41,Male,18,Engineering,Year 2,3.00 - 3.49,Yes,Yes,Yes,No,No +8/7/2020 13:58,Female,24,BIT,Year 3,3.50 - 4.00,Yes,Yes,Yes,Yes,Yes +8/7/2020 14:05,Female,24,BCS,year 4,3.50 - 4.00,No,No,No,No,No +8/7/2020 14:27,Female,23,Business Administration,Year 2,3.00 - 3.49,No,No,No,No,No +8/7/2020 14:29,Male,18,BCS,year 2,3.00 - 3.49,No,No,No,No,No +8/7/2020 14:29,Male,19,BCS,year 1,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 14:31,Male,18,BCS,Year 2,3.50 - 4.00,Yes,Yes,Yes,No,Yes +8/7/2020 14:41,Female,19,BIT,year 1,3.00 - 3.49,No,Yes,Yes,Yes,No +8/7/2020 14:43,Female,18,Engineering,year 1,2.00 - 2.49,No,No,No,No,No +8/7/2020 14:43,Female,18,Law,Year 3,3.00 - 3.49,No,Yes,Yes,No,No +8/7/2020 14:45,Female,19,BIT,year 1,2.50 - 2.99,No,Yes,Yes,Yes,No +8/7/2020 14:47,Female,18,KIRKHS,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 14:56,Female,24,Engineering,Year 2,2.50 - 2.99,Yes,Yes,No,Yes,Yes +8/7/2020 14:57,Female,24,BIT,Year 3,3.00 - 3.49,No,No,Yes,No,No +8/7/2020 14:57,Female,22,Engineering,year 4,3.50 - 4.00,No,No,No,No,No +8/7/2020 14:58,Female,20,Usuluddin ,year 2,3.00 - 3.49,No,Yes,No,No,No +8/7/2020 15:07,Male,,BIT,year 1,0 - 1.99,No,No,No,No,No +8/7/2020 15:08,Male,23,TAASL,year 2,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 15:09,Male,18,BCS,year 1,3.50 - 4.00,No,No,Yes,Yes,No +8/7/2020 15:12,Female,19,Engineering,year 1,3.50 - 4.00,No,No,Yes,No,No +8/7/2020 15:14,Female,18,Engine,year 4,3.50 - 4.00,No,No,No,No,No +8/7/2020 15:14,Male,24,BCS,year 2,3.00 - 3.49,No,Yes,No,No,No +8/7/2020 15:18,Female,24,BCS,year 3,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 15:27,Female,23,ALA,year 1,2.50 - 2.99,Yes,Yes,No,Yes,Yes +8/7/2020 15:37,Female,18,BCS,year 2,3.50 - 4.00,No,No,Yes,No,No +8/7/2020 15:47,Female,19,Biomedical science,year 3,3.00 - 3.49,No,No,No,No,No +8/7/2020 15:48,Female,20,koe,year 3,3.00 - 3.49,Yes,Yes,Yes,Yes,No +8/7/2020 15:57,Female,19,BCS,year 1,3.50 - 4.00,No,Yes,No,Yes,Yes +8/7/2020 15:58,Male,21,BCS,year 1,3.00 - 3.49,No,No,No,No,No +8/7/2020 16:08,Male,23,Kirkhs,Year 3,3.50 - 4.00,No,No,No,No,No +8/7/2020 16:21,Female,20,BENL,Year 3,3.00 - 3.49,No,Yes,Yes,No,No +8/7/2020 16:22,Female,18,BCS,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 16:34,Female,23,Benl,year 1,3.00 - 3.49,No,No,No,No,No +8/7/2020 16:34,Female,18,IT,Year 3,3.00 - 3.49,No,No,No,Yes,No +8/7/2020 16:53,Female,19,BCS,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 17:05,Female,18,CTS,Year 1,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 17:37,Female,24,engin,year 1,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 17:46,Female,24,Engine,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 17:50,Female,23,Econs,year 1,3.50 - 4.00,No,Yes,Yes,No,No +8/7/2020 18:10,Female,18,KOE,Year 3,3.00 - 3.49,No,No,Yes,No,No +8/7/2020 18:11,Male,19,MHSC,Year 3,3.00 - 3.49,Yes,Yes,No,Yes,No +8/7/2020 19:05,Female,18,Malcom,year 1,3.50 - 4.00,No,Yes,No,No,No +8/7/2020 19:32,Female,24,Kop,year 4,3.00 - 3.49,No,No,Yes,No,No +8/7/2020 20:36,Female,24,Biomedical science,year 1,3.00 - 3.49,No,No,No,No,No +8/7/2020 21:21,Female,18,Laws,Year 3,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 22:35,Female,19,BIT,Year 3,3.00 - 3.49,Yes,Yes,No,No,No +9/7/2020 6:57,Male,18,Biomedical science,year 1,0 - 1.99,No,No,No,No,No +9/7/2020 11:43,Male,24,BIT,Year 3,3.50 - 4.00,No,No,Yes,No,No +9/7/2020 11:57,Female,24,KOE,year 1,3.50 - 4.00,No,No,Yes,Yes,No +9/7/2020 13:15,Female,23,Engineering,year 1,3.00 - 3.49,No,Yes,No,No,No +9/7/2020 18:24,Female,18,Human Sciences ,Year 2,3.00 - 3.49,No,No,No,Yes,No +13/07/2020 10:07:32,Female,19,Biotechnology,Year 3,0 - 1.99,No,No,No,No,No +13/07/2020 10:10:30,Female,18,Engineering,year 4,3.50 - 4.00,No,No,No,No,No +13/07/2020 10:11:26,Female,24,Communication ,Year 2,3.50 - 4.00,Yes,Yes,Yes,Yes,No +13/07/2020 10:12:18,Female,24,Diploma Nursing,year 2,3.50 - 4.00,No,No,No,No,No +13/07/2020 10:12:26,Female,19,Engineering,year 1,3.00 - 3.49,No,Yes,Yes,No,No +13/07/2020 10:12:28,Female,19,Pendidikan Islam ,Year 2,3.00 - 3.49,No,No,No,No,No +13/07/2020 10:14:46,Male,23,Radiography,year 1,3.00 - 3.49,No,No,No,No,No +13/07/2020 10:33:47,Female,18,psychology,year 1,3.50 - 4.00,No,Yes,Yes,No,Yes +13/07/2020 10:34:08,Female,19,Fiqh fatwa ,Year 3,3.00 - 3.49,No,No,No,No,No +13/07/2020 11:46:13,Female,18,psychology,year 1,3.50 - 4.00,No,Yes,Yes,Yes,No +13/07/2020 11:49:02,Male,24,BIT,year 1,3.00 - 3.49,No,No,Yes,No,No +13/07/2020 11:54:58,Male,24,Engineering,Year 2,2.00 - 2.49,No,No,No,Yes,No +13/07/2020 13:57:11,Female,23,DIPLOMA TESL,Year 3,3.50 - 4.00,No,No,No,Yes,No +13/07/2020 14:38:12,Male,18,Koe,Year 2,3.00 - 3.49,No,No,Yes,No,No +13/07/2020 14:48:05,Female,19,KOE,year 2,3.00 - 3.49,Yes,Yes,No,No,No +13/07/2020 16:15:13,Female,18,BENL,year 1,3.00 - 3.49,No,Yes,No,No,No +13/07/2020 17:30:44,Female,24,Fiqh,Year 3,0 - 1.99,No,No,No,Yes,No +13/07/2020 19:08:32,Female,18,Islamic Education,year 1,3.50 - 4.00,No,No,No,No,No +13/07/2020 19:56:49,Female,21,BCS,year 1,3.50 - 4.00,No,No,Yes,No,No +13/07/2020 21:21:42,Male,18,Engineering,Year 2,3.00 - 3.49,No,Yes,Yes,No,No +13/07/2020 21:22:56,Female,19,Nursing ,Year 3,3.50 - 4.00,Yes,Yes,No,Yes,No +13/07/2020 21:23:57,Female,23,Pendidikan Islam,year 4,3.50 - 4.00,No,No,No,No,No +18/07/2020 20:16:21,Male,20,Biomedical science,Year 2,3.00 - 3.49,No,No,No,No,No diff --git a/Homework2/vkosuri/React-Template/index.html b/Homework2/vkosuri/React-Template/index.html new file mode 100644 index 0000000..a4a4009 --- /dev/null +++ b/Homework2/vkosuri/React-Template/index.html @@ -0,0 +1,21 @@ + + + + + + + Vite + React + TS + + + +
+ + + diff --git a/Homework2/vkosuri/React-Template/package.json b/Homework2/vkosuri/React-Template/package.json new file mode 100644 index 0000000..3603f6b --- /dev/null +++ b/Homework2/vkosuri/React-Template/package.json @@ -0,0 +1,38 @@ +{ + "name": "react-template", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.13.3", + "@emotion/styled": "^11.13.0", + "@mui/material": "^5.16.7", + "@types/d3": "^7.4.3", + "@types/lodash": "^4.17.7", + "@types/styled-components": "^5.1.34", + "axios": "^1.2.1", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "lodash": "^4.17.21", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "styled-components": "^6.1.13", + "usehooks-ts": "^3.1.0" + }, + "devDependencies": { + "@types/d3-sankey": "^0.12.4", + "@types/material-ui": "^0.21.17", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "globals": "^15.9.0", + "typescript": "^5.5.3", + "vite": "^5.4.1" + } +} diff --git a/Homework2/vkosuri/React-Template/src/App.tsx b/Homework2/vkosuri/React-Template/src/App.tsx new file mode 100644 index 0000000..8fe7d9b --- /dev/null +++ b/Homework2/vkosuri/React-Template/src/App.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import BarChart from './components/BarChart'; +import PieChart from './components/PieChart'; +import SankeyChart from './components/SankeyChart'; +import './style.css'; + +function App() { + return ( +
+ {/* Header - 5% */} +
+

Student Mental Health Dashboard - Analysis of mental health conditions among students

+
+ + {/* Main Content - 95% */} +
+ {/* Sankey Diagram - 61% */} +
+
+

Student Mental Health Overview: Sankey Diagram

+
+ +
+
+
+ + {/* Bottom Section - 34% total (18% each, Bar Chart height reduced by half) */} +
+ {/* Bar Chart - Height reduced by 1/2 */} +
+

Mental Health by Academic Performance: Bar Chart

+
+ +
+
+ + {/* Pie Chart - Normal height */} +
+

Mental Health by Gender: Pie Chart

+
+ +
+
+
+
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/Homework2/vkosuri/React-Template/src/components/BarChart.tsx b/Homework2/vkosuri/React-Template/src/components/BarChart.tsx new file mode 100644 index 0000000..1b73b0b --- /dev/null +++ b/Homework2/vkosuri/React-Template/src/components/BarChart.tsx @@ -0,0 +1,224 @@ +import React, { useEffect, useRef } from 'react'; +import * as d3 from 'd3'; +import Grid from '@mui/material/Grid'; + +interface MentalHealthData { + CGPA: string; + depressionCount: number; + anxietyCount: number; + panicAttackCount: number; + noIssuesCount: number; +} + +const CHART_MARGINS = { top: 45, right: 170, bottom: 60, left: 102 }; +const CGPA_ORDER = ["0 - 1.99", "2.00 - 2.49", "2.50 - 2.99", "3.00 - 3.49", "3.50 - 4.00"]; +const COLORS = ['#87CEFA', '#9370DB', '#FF6347', '#32CD32']; +const CHART_WIDTH = 1190; +const CHART_HEIGHT = 388; + +const BarChart: React.FC = () => { + const svgRef = useRef(null); + + useEffect(() => { + const fetchAndRenderData = async () => { + try { + // Load and process data + const csvData = await d3.csv('/data/Student Mental health.csv', d => ({ + CGPA: d["What is your CGPA?"].trim(), + depression: d["Do you have Depression?"], + anxiety: d["Do you have Anxiety?"], + panicAttack: d["Do you have Panic attack?"] + })); + + const cgpaGroups = d3.groups(csvData, (d: any) => d.CGPA); + const data: MentalHealthData[] = cgpaGroups.map(([CGPA, students]) => ({ + CGPA, + depressionCount: students.filter((d: any) => d.depression === 'Yes').length, + anxietyCount: students.filter((d: any) => d.anxiety === 'Yes').length, + panicAttackCount: students.filter((d: any) => d.panicAttack === 'Yes').length, + noIssuesCount: students.filter((d: any) => + d.depression === 'No' && d.anxiety === 'No' && d.panicAttack === 'No' + ).length + })); + + // Clear any existing SVG content + d3.select(svgRef.current).selectAll("*").remove(); + + // Set up the SVG + const svg = d3.select(svgRef.current) + .attr('width', CHART_WIDTH) + .attr('height', CHART_HEIGHT) + .attr('viewBox', `0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`) + .attr('preserveAspectRatio', 'xMidYMid meet'); + + const innerWidth = CHART_WIDTH - CHART_MARGINS.left - CHART_MARGINS.right; + const innerHeight = CHART_HEIGHT - CHART_MARGINS.top - CHART_MARGINS.bottom; + + const chart = svg.append('g') + .attr('transform', `translate(${CHART_MARGINS.left}, ${CHART_MARGINS.top})`); + + // Create scales with adjusted y-axis + const xScale = d3.scaleBand() + .domain(CGPA_ORDER) + .range([0, innerWidth]) + .padding(0.2); + + // Calculate the maximum stack height from the data + const stack = d3.stack() + .keys(['depressionCount', 'anxietyCount', 'panicAttackCount', 'noIssuesCount']); + + const stackedData = stack(data); + const yMax = d3.max(stackedData[stackedData.length - 1], d => d[1]) || 0; + + // Updated y-axis scale with calculated maximum plus 10% padding + const yScale = d3.scaleLinear() + .domain([0, yMax * 1.1]) + .range([innerHeight, 0]) + .nice(); + + // Add axes with matched font styles + chart.append('g') + .attr('transform', `translate(0, ${innerHeight})`) + .call(d3.axisBottom(xScale)) + .style('color', 'white') + .selectAll('text') + .style('fill', 'white') + .style('font-size', '18px') + .style('font-weight', 'bold'); + + // Create y-axis with appropriate number of ticks + const yAxis = d3.axisLeft(yScale) + .ticks(8) + .tickFormat(d => d.toString()); + + chart.append('g') + .call(yAxis) + .style('color', 'white') + .selectAll('text') + .style('fill', 'white') + .style('font-size', '18px') + .style('font-weight', 'bold'); + + // Add axis labels + chart.append('text') + .attr('x', innerWidth / 2) + .attr('y', innerHeight + 45) + .attr('text-anchor', 'middle') + .style('fill', 'white') + .style('font-size', '20px') + .style('font-weight', 'bold') + .text('CGPA Ranges'); + + chart.append('text') + .attr('transform', 'rotate(-90)') + .attr('x', -innerHeight / 2) + .attr('y', -75) + .attr('text-anchor', 'middle') + .style('fill', 'white') + .style('font-size', '20px') + .style('font-weight', 'bold') + .text('Number of Students'); + + // Create layers with labels + const layers = chart.selectAll('g.layer') + .data(stackedData) + .enter() + .append('g') + .attr('class', 'layer') + .attr('fill', (d, i) => COLORS[i]); + + // Add rectangles for each segment + layers.selectAll('rect') + .data(d => d) + .enter() + .append('rect') + .attr('x', d => xScale(d.data.CGPA)!) + .attr('y', d => yScale(d[1])) + .attr('height', d => yScale(d[0]) - yScale(d[1])) + .attr('width', xScale.bandwidth()); + + // Add text labels for counts + layers.selectAll('text') + .data(d => d) + .enter() + .append('text') + .attr('x', d => xScale(d.data.CGPA)! + xScale.bandwidth() / 2) + .attr('y', d => { + const height = yScale(d[0]) - yScale(d[1]); + return yScale(d[1]) + height / 2; + }) + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'middle') + .style('fill', 'black') + .style('font-size', '14px') + .style('font-weight', 'bold') + .text(d => { + const value = d[1] - d[0]; + return value > 0 ? value : ''; + }); + + // Add title + svg.append('text') + .attr('x', CHART_WIDTH / 2) + .attr('y', 25) + .attr('text-anchor', 'middle') + .style('fill', 'white') + .style('font-size', '28px') + .style('font-weight', 'bold') + .text('Mental Health Issues Distribution Across CGPA Ranges'); + + // Add legend + const legend = svg.append('g') + .attr('transform', `translate(${CHART_WIDTH - 160}, ${CHART_MARGINS.top})`); + + const legendItems = [ + 'Depression', + 'Anxiety', + 'Panic Attack', + 'No Issues' + ]; + + legendItems.forEach((item, i) => { + const g = legend.append('g') + .attr('transform', `translate(0, ${i * 30})`); + + g.append('rect') + .attr('width', 15) + .attr('height', 15) + .attr('fill', COLORS[i]) + .attr('stroke', '#000000') + .attr('stroke-width', 1); + + g.append('text') + .attr('x', 25) + .attr('y', 12) + .style('fill', 'white') + .style('font-size', '20px') + .style('font-weight', 'bold') + .text(item); + }); + + } catch (error) { + console.error('Error loading data:', error); + } + }; + + fetchAndRenderData(); + }, []); + + return ( +
+ +
+ ); +}; + +export default BarChart; \ No newline at end of file diff --git a/Homework2/vkosuri/React-Template/src/components/PieChart.tsx b/Homework2/vkosuri/React-Template/src/components/PieChart.tsx new file mode 100644 index 0000000..e79bba3 --- /dev/null +++ b/Homework2/vkosuri/React-Template/src/components/PieChart.tsx @@ -0,0 +1,201 @@ +import React, { useEffect, useRef, useState } from 'react'; +import * as d3 from 'd3'; + +interface MentalHealthCount { + condition: string; + count: number; + percentage: number; +} + +interface GenderData { + male: MentalHealthCount[]; + female: MentalHealthCount[]; +} + +const PieChart: React.FC = (): JSX.Element => { + const svgRef = useRef(null); + const [data, setData] = useState({ male: [], female: [] }); + + useEffect(() => { + const fetchData = async () => { + try { + const csvData = await d3.csv('/data/Student Mental health.csv'); + + const processDataForGender = (gender: string) => { + const genderData = csvData.filter(d => d["Choose your gender"].trim() === gender); + const total = genderData.length; + + const counts = { + depression: genderData.filter(d => d["Do you have Depression?"] === "Yes").length, + anxiety: genderData.filter(d => d["Do you have Anxiety?"] === "Yes").length, + panic: genderData.filter(d => d["Do you have Panic attack?"] === "Yes").length + }; + + const noIssues = genderData.filter(d => + d["Do you have Depression?"] === "No" && + d["Do you have Anxiety?"] === "No" && + d["Do you have Panic attack?"] === "No" + ).length; + + return [ + { condition: "Depression", count: counts.depression, percentage: (counts.depression / total) * 100 }, + { condition: "Anxiety", count: counts.anxiety, percentage: (counts.anxiety / total) * 100 }, + { condition: "Panic Attack", count: counts.panic, percentage: (counts.panic / total) * 100 }, + { condition: "No Mental Issues", count: noIssues, percentage: (noIssues / total) * 100 } + ].sort((a, b) => a.percentage - b.percentage); // Sort by percentage in ascending order + }; + + setData({ + male: processDataForGender("Male"), + female: processDataForGender("Female") + }); + } catch (error) { + console.error('Error loading CSV data:', error); + } + }; + + fetchData(); + }, []); + + useEffect(() => { + if (!data.male.length || !data.female.length) return; + + const container = d3.select(svgRef.current).node()?.parentElement; + if (!container) return; + + const width = container.clientWidth; + const height = container.clientHeight; + const radius = Math.min(width / 6, height / 3); + + const svg = d3.select(svgRef.current); + svg.selectAll('*').remove(); + + svg.attr('width', width) + .attr('height', height) + .attr('viewBox', `0 0 ${width} ${height}`) + .attr('preserveAspectRatio', 'xMidYMid meet'); + + svg.append('text') + .attr('x', width / 2) + .attr('y', 25) + .attr('text-anchor', 'middle') + .style('fill', 'white') + .style('font-size', '20px') + .style('font-weight', 'bold') + .text('Mental Health Issues Distribution Across Gender'); + + const colorScale = d3.scaleOrdinal() + .domain(['Depression', 'Anxiety', 'Panic Attack', 'No Mental Issues']) + .range(['#87CEFA', '#9370DB', '#FF6347', '#32CD32']); + + const createPieChart = (data: MentalHealthCount[], centerX: number, title: string) => { + const g = svg.append('g') + .attr('transform', `translate(${centerX}, ${height / 2})`); + + const pie = d3.pie() + .value(d => d.percentage) + .sort(null); // Remove default sorting to maintain our manual sort + + const arc = d3.arc>() + .innerRadius(0) + .outerRadius(radius); + + g.selectAll('path') + .data(pie(data)) + .enter() + .append('path') + .attr('d', arc) + .attr('fill', d => colorScale(d.data.condition)) + .attr('stroke', '#000000') + .attr('stroke-width', 2); + + g.selectAll('text.percentage') + .data(pie(data)) + .enter() + .append('text') + .attr('class', 'percentage') + .attr('transform', d => { + const [x, y] = arc.centroid(d); + return `translate(${x}, ${y})`; + }) + .attr('text-anchor', 'middle') + .attr('dy', '0.35em') + .style('fill', '#000000') + .style('font-size', '12px') + .style('font-weight', 'bold') + .text(d => `${Math.round(d.data.percentage)}%`); + + g.append('text') + .attr('class', 'title') + .attr('y', radius + 30) + .attr('text-anchor', 'middle') + .style('fill', 'white') + .style('font-size', '16px') + .style('font-weight', 'bold') + .text(title); + + const total = data.reduce((sum, d) => sum + d.count, 0); + g.append('text') + .attr('class', 'total') + .attr('y', radius + 55) + .attr('text-anchor', 'middle') + .style('fill', 'white') + .style('font-size', '14px') + .text(`Total: ${total} students`); + }; + + createPieChart(data.female, width * 0.3, 'Female'); + createPieChart(data.male, width * 0.7, 'Male'); + + // Get conditions in order of appearance for legend (using female data as reference) + const legendData = data.female.map(d => d.condition); + + const legend = svg.append('g') + .attr('transform', `translate(${width * 0.1 - 132 + 75.6}, ${height/2 - 100 + 56.7})`); + + const legendSpacing = 30; + + legendData.forEach((item, i) => { + const legendItem = legend.append('g') + .attr('transform', `translate(0, ${i * legendSpacing})`); + + legendItem.append('rect') + .attr('width', 15) + .attr('height', 15) + .attr('fill', colorScale(item)) + .attr('stroke', '#000000') + .attr('stroke-width', 1); + + legendItem.append('text') + .attr('x', 25) + .attr('y', 12) + .style('fill', 'white') + .style('font-size', '14px') + .style('font-weight', 'bold') + .text(item); + }); + + }, [data]); + + return ( +
+ +
+ ); +}; + +export default PieChart; \ No newline at end of file diff --git a/Homework2/vkosuri/React-Template/src/components/SankeyChart.tsx b/Homework2/vkosuri/React-Template/src/components/SankeyChart.tsx new file mode 100644 index 0000000..977681f --- /dev/null +++ b/Homework2/vkosuri/React-Template/src/components/SankeyChart.tsx @@ -0,0 +1,328 @@ +import React, { useEffect, useRef } from 'react'; +import * as d3 from 'd3'; +import { + sankey, + sankeyLinkHorizontal, + SankeyNode, + SankeyLink +} from 'd3-sankey'; + +interface NodeData { + name: string; + category: string; + index?: number; + x0?: number; + x1?: number; + y0?: number; + y1?: number; + value?: number; +} + +interface LinkData { + source: number | NodeData; + target: number | NodeData; + value: number; + width?: number; +} + +type SankeyNodeExtended = Required>; +type SankeyLinkExtended = Required>; + +const SankeyDiagram: React.FC = () => { + const svgRef = useRef(null); + + useEffect(() => { + const fetchData = async () => { + try { + const data = await d3.csv('/data/Student Mental health.csv'); + + const width = 5760; + const height = 1344; + const margin = { + top: 225.39, + right: 640, + bottom: 240, + left: 640 + }; + + // Process data for Sankey diagram + const nodes: NodeData[] = [ + { name: "18 or younger", category: "age" }, + { name: "19", category: "age" }, + { name: "20", category: "age" }, + { name: "21", category: "age" }, + { name: "22", category: "age" }, + { name: "23", category: "age" }, + { name: "24+", category: "age" }, + { name: "Depression", category: "condition" }, + { name: "Anxiety", category: "condition" }, + { name: "Panic Attack", category: "condition" }, + { name: "No Mental Issues", category: "condition" }, + { name: "Sought Treatment", category: "treatment" }, + { name: "No Treatment", category: "treatment" } + ]; + + const links: LinkData[] = []; + + // Process each student's data + data.forEach(d => { + const ageGroup = getAgeGroup(d["Age"]); + if (!ageGroup) return; + + const ageIndex = nodes.findIndex(n => n.name === ageGroup); + const hasCondition = d["Do you have Depression?"] === "Yes" || + d["Do you have Anxiety?"] === "Yes" || + d["Do you have Panic attack?"] === "Yes"; + + // Age to Conditions links + if (d["Do you have Depression?"] === "Yes") { + addOrUpdateLink(links, ageIndex, 7); + } + if (d["Do you have Anxiety?"] === "Yes") { + addOrUpdateLink(links, ageIndex, 8); + } + if (d["Do you have Panic attack?"] === "Yes") { + addOrUpdateLink(links, ageIndex, 9); + } + if (!hasCondition) { + addOrUpdateLink(links, ageIndex, 10); + } + + // Conditions to Treatment links + const soughtTreatment = d["Did you seek any specialist for a treatment?"] === "Yes"; + if (hasCondition) { + if (d["Do you have Depression?"] === "Yes") { + addOrUpdateLink(links, 7, soughtTreatment ? 11 : 12); + } + if (d["Do you have Anxiety?"] === "Yes") { + addOrUpdateLink(links, 8, soughtTreatment ? 11 : 12); + } + if (d["Do you have Panic attack?"] === "Yes") { + addOrUpdateLink(links, 9, soughtTreatment ? 11 : 12); + } + } + if (!hasCondition) { + addOrUpdateLink(links, 10, 12); + } + }); + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + svg.attr("width", width) + .attr("height", height) + .attr("viewBox", `0 0 ${width} ${height}`) + .attr("preserveAspectRatio", "xMidYMid meet"); + + const sankeyGenerator = sankey() + .nodeWidth(400) + .nodePadding(45) + .extent([[margin.left, margin.top], [width - margin.right, height - margin.bottom]]); + + sankeyGenerator.nodeSort((a, b) => { + if (a.category === 'age' && b.category === 'age') { + const getAgeValue = (name: string) => { + if (name === "18 or younger") return 18; + if (name === "24+") return 24; + return parseInt(name); + }; + return getAgeValue(a.name) - getAgeValue(b.name); + } + return 0; + }); + + const { nodes: sankeyNodes, links: sankeyLinks } = sankeyGenerator({ + nodes: nodes.map((d, i) => ({ ...d, index: i })), + links: links + }); + + // Color scales + const ageColorScale = d3.scaleOrdinal() + .domain(["18 or younger", "19", "20", "21", "22", "23", "24+"]) + .range(['#9c27b0', '#7e57c2', '#5c6bc0', '#42a5f5', '#26c6da', '#26a69a', '#2e7d32']); + + const conditionColors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4']; + const treatmentColors = ['#00C853', '#FF5252']; + + const getNodeColor = (node: SankeyNodeExtended) => { + if (node.category === 'age') { + return ageColorScale(node.name); + } else if (node.category === 'condition') { + return conditionColors[node.index - 7]; + } else { + return treatmentColors[node.index - 11]; + } + }; + + // Add links + const linkGroup = svg.append("g") + .attr("class", "links") + .selectAll("path") + .data(sankeyLinks) + .enter() + .append("path") + .attr("d", sankeyLinkHorizontal()) + .style("fill", "none") + .style("stroke", (d) => { + const sourceNode = d.source as SankeyNodeExtended; + return d3.rgb(getNodeColor(sourceNode)).darker(0.3).toString(); + }) + .style("stroke-width", d => Math.max(2, (d as SankeyLinkExtended).width)) + .style("stroke-opacity", 0.4); + + // Add nodes + const nodeGroup = svg.append("g") + .attr("class", "nodes") + .selectAll("g") + .data(sankeyNodes) + .enter() + .append("g"); + + nodeGroup.append("rect") + .attr("x", d => (d as SankeyNodeExtended).x0!) + .attr("y", d => (d as SankeyNodeExtended).y0!) + .attr("height", d => (d as SankeyNodeExtended).y1! - (d as SankeyNodeExtended).y0!) + .attr("width", d => (d as SankeyNodeExtended).x1! - (d as SankeyNodeExtended).x0!) + .style("fill", d => getNodeColor(d as SankeyNodeExtended)) + .style("stroke", "#000") + .style("stroke-width", 2); + + // Add node labels + nodeGroup.append("text") + .attr("x", d => { + const node = d as SankeyNodeExtended; + return (node.x0! + node.x1!) / 2; + }) + .attr("y", d => { + const node = d as SankeyNodeExtended; + return (node.y1! + node.y0!) / 2; + }) + .attr("dy", "0.35em") + .attr("text-anchor", "middle") + .attr("fill", "white") + .style("font-size", "40px") + .style("font-weight", "bold") + .text(d => `${(d as SankeyNodeExtended).name} (${(d as SankeyNodeExtended).value})`); + + // Add category labels + const categories = [ + { text: "Age Groups", x: margin.left + 400, y: margin.top - 68 }, + { text: "Mental Health Conditions", x: width/2, y: margin.top - 68 }, + { text: "Treatment Status", x: width - margin.right - 400, y: margin.top - 68 } + ]; + + svg.selectAll(".category-label") + .data(categories) + .enter() + .append("text") + .attr("x", d => d.x) + .attr("y", d => d.y) + .attr("text-anchor", "middle") + .attr("fill", "white") + .style("font-size", "72px") + .style("font-weight", "bold") + .text(d => d.text); + + // Add title + svg.append("text") + .attr("x", width / 2) + .attr("y", margin.top / 2 - 113.39) + .attr("text-anchor", "middle") + .attr("fill", "white") + .style("font-size", "86px") + .style("font-weight", "bold") + .text("Student Mental Health Flow Analysis"); + + // Add legend with reduced spacing + const legendData = [ + { category: "Age Groups", colors: ageColorScale.range(), labels: ageColorScale.domain() }, + { category: "Conditions", colors: conditionColors, labels: ["Depression", "Anxiety", "Panic Attack", "No Mental Issues"] }, + { category: "Treatment", colors: treatmentColors, labels: ["Sought Treatment", "No Treatment"] } + ]; + + const cmToPixels = 37.795275591; + const legendYOffset = 3 * cmToPixels; + + const legendGroup = svg.append("g") + .attr("class", "legend") + .attr("transform", `translate(${margin.left - 113.39 - 188.98 - 188.98}, ${height - margin.bottom + 60 + legendYOffset})`); + + let xOffset = 0; + legendData.forEach((categoryData, categoryIndex) => { + // Add category label with slant + legendGroup.append("text") + .attr("x", xOffset) + .attr("y", -20) + .attr("transform", `rotate(-45, ${xOffset}, -20)`) + .attr("fill", "white") + .style("font-size", "72px") + .style("font-weight", "bold") + .text(categoryData.category); + + const itemGroup = legendGroup.append("g") + .attr("transform", `translate(${xOffset}, 20)`); + + // Reduced spacing for labels + const labelSpacing = categoryData.category === "Treatment" ? 200 : 300; // Reduced from 300/500 + + categoryData.colors.forEach((color, i) => { + const g = itemGroup.append("g") + .attr("transform", `translate(${i * labelSpacing}, 0)`); + + g.append("rect") + .attr("width", 40) + .attr("height", 40) + .attr("fill", color) + .attr("stroke", "white") + .attr("stroke-width", 2); + + g.append("text") + .attr("x", 60) + .attr("y", 20) + .attr("transform", "rotate(-45, 60, 20)") + .attr("fill", "white") + .style("font-size", "64px") + .text(categoryData.labels[i]); + }); + + // Reduced spacing between categories + xOffset += (categoryData.colors.length * labelSpacing) + 200; // Reduced from 400 + }); + + } catch (error) { + console.error('Error loading CSV data:', error); + } + }; + + fetchData(); + }, []); + + return ( +
+ +
+ ); +}; + +const getAgeGroup = (age: string) => { + const ageNum = parseInt(age); + if (isNaN(ageNum)) return null; + if (ageNum <= 18) return "18 or younger"; + if (ageNum === 19) return "19"; + if (ageNum === 20) return "20"; + if (ageNum === 21) return "21"; + if (ageNum === 22) return "22"; + if (ageNum === 23) return "23"; + return "24+"; +}; + +const addOrUpdateLink = (links: LinkData[], source: number, target: number) => { + const existingLink = links.find(l => + (typeof l.source === 'number' ? l.source : l.source.index) === source && + (typeof l.target === 'number' ? l.target : l.target.index) === target + ); + if (existingLink) existingLink.value++; + else links.push({ source, target, value: 1 }); +}; + +export default SankeyDiagram; \ No newline at end of file diff --git a/Homework2/vkosuri/React-Template/src/main.tsx b/Homework2/vkosuri/React-Template/src/main.tsx new file mode 100644 index 0000000..87e2790 --- /dev/null +++ b/Homework2/vkosuri/React-Template/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.tsx' +import './style.css' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/Homework2/vkosuri/React-Template/src/stores/Reducer.ts b/Homework2/vkosuri/React-Template/src/stores/Reducer.ts new file mode 100644 index 0000000..0160617 --- /dev/null +++ b/Homework2/vkosuri/React-Template/src/stores/Reducer.ts @@ -0,0 +1,25 @@ +// Define action types +export const ACTIONS = { INCREMENT: 'increment' }; + +interface Action{ + type: string; +} + +interface State{ + count: number; +} + +// Define initial state +const initialState: State = { count: 0 }; + +// reducer function +export const reducer = (state: State, action: Action) => { + switch (action.type) { + case ACTIONS.INCREMENT: + return { count: state.count + 1 }; + default: + return state; + } +} + +export type { State, Action }; \ No newline at end of file diff --git a/Homework2/vkosuri/React-Template/src/style.css b/Homework2/vkosuri/React-Template/src/style.css new file mode 100644 index 0000000..cf4f9f0 --- /dev/null +++ b/Homework2/vkosuri/React-Template/src/style.css @@ -0,0 +1,202 @@ +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body, html { + background-color: #000; + color: #fff; + overflow: hidden; + font-family: 'Roboto', sans-serif; + height: 100%; +} + +/* Dashboard container */ +.dashboard { + width: 100vw; + height: 100vh; + background-color: #000; + display: flex; + flex-direction: column; + padding: 5px; +} + +/* Header - exactly 5% */ +.dashboard-header { + height: 5%; + display: flex; + align-items: center; + justify-content: center; + padding: 5px; +} + +.dashboard-header h1 { + font-size: 1.3rem; + color: #fff; + font-weight: normal; + white-space: nowrap; + text-align: center; + letter-spacing: 0.5px; + opacity: 0.95; +} + +/* Main layout */ +.dashboard-layout { + height: 95%; + display: flex; + flex-direction: column; + gap: 5px; +} + +/* Top section - Sankey (61%) */ +.top-section { + height: 61%; + min-height: 61%; +} + +.top-section .chart-container { + width: 100%; + height: 100%; + padding: 10px 20px; /* More horizontal padding for Sankey */ +} + +/* Bottom section - Bar and Pie (18% each) */ +.bottom-section { + height: 34%; /* 18% + 18% = 34% total for bottom section */ + display: flex; + gap: 5px; +} + +/* Chart containers */ +.chart-container { + background-color: #000; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + padding: 10px; + height: 100%; + display: flex; + flex-direction: column; +} + +/* Half width for bottom charts */ +.half-width { + width: 50%; +} + +/* Chart titles */ +.chart-container h2 { + font-size: 0.9rem; + color: #fff; + margin-bottom: 8px; + font-weight: normal; + white-space: nowrap; + opacity: 0.9; +} + +/* Chart wrappers */ +.chart-wrapper { + flex: 1; + min-height: 0; + position: relative; +} + +/* Specific wrapper styles */ +.sankey-wrapper { + width: 100%; + height: 100%; +} + +.bar-wrapper { + height: 50% !important; /* Force half height */ + margin: auto 0; /* Center vertically */ +} + +.pie-wrapper { + height: 100%; +} + +/* SVG styling */ +svg { + width: 100%; + height: 100%; + background-color: #000; +} + +/* Chart axis and text colors */ +.axis text { + fill: #fff; +} + +.axis line, +.axis path { + stroke: rgba(255, 255, 255, 0.2); +} + +/* Legend styling */ +.legend text { + fill: #fff; + font-size: 0.8rem; +} + +/* Remove scrollbars */ +::-webkit-scrollbar { + display: none; +} + +/* Ensure consistent black background */ +.chart-container, +.dashboard, +body, +html { + background-color: #000; +} + +/* Bottom section specific styles */ +.bottom-section .chart-container { + padding: 10px; +} + +/* Chart specific text styles */ +.chart-title { + fill: #fff; + font-size: 0.9rem; +} + +.axis-label { + fill: #fff; + font-size: 0.8rem; +} + +/* Sankey specific styles */ +.sankey-node text { + fill: #fff; + font-size: 0.8rem; +} + +.sankey-link { + fill: none; + stroke-opacity: 0.3; +} + +/* Responsive font sizes */ +@media (max-width: 1280px) { + .dashboard-header h1 { + font-size: 1.1rem; + } + + .chart-container h2 { + font-size: 0.8rem; + } +} + +@media (min-width: 1920px) { + .dashboard-header h1 { + font-size: 1.5rem; + } + + .chart-container h2 { + font-size: 1rem; + } +} \ No newline at end of file diff --git a/Homework2/vkosuri/React-Template/src/types.ts b/Homework2/vkosuri/React-Template/src/types.ts new file mode 100644 index 0000000..ba503fc --- /dev/null +++ b/Homework2/vkosuri/React-Template/src/types.ts @@ -0,0 +1,21 @@ +// Global types and interfaces are stored here. +export interface Margin { + readonly left: number; + readonly right: number; + readonly top: number; + readonly bottom: number; +} + +export interface ComponentSize { + width: number; + height: number; +} + +export interface Point { + readonly posX: number; + readonly posY: number; +} + +export interface Bar{ + readonly value: number; +} \ No newline at end of file diff --git a/Homework2/vkosuri/React-Template/src/vite-env.d.ts b/Homework2/vkosuri/React-Template/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/Homework2/vkosuri/React-Template/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/Homework2/vkosuri/React-Template/tsconfig.json b/Homework2/vkosuri/React-Template/tsconfig.json new file mode 100644 index 0000000..3b32131 --- /dev/null +++ b/Homework2/vkosuri/React-Template/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable", "es6"], + "module": "ESNext", + "skipLibCheck": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + }, + "include": ["src", "vite.config.ts"] +} diff --git a/Homework2/vkosuri/React-Template/vite.config.ts b/Homework2/vkosuri/React-Template/vite.config.ts new file mode 100644 index 0000000..198ec17 --- /dev/null +++ b/Homework2/vkosuri/React-Template/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + server: { + port: 3000, + }, + plugins: [react()], +}) diff --git a/Homework3/Interactive dashboard.png b/Homework3/Interactive dashboard.png new file mode 100644 index 0000000..d6f44db Binary files /dev/null and b/Homework3/Interactive dashboard.png differ diff --git a/Homework3/vkosuri/React-Template/README.md b/Homework3/vkosuri/React-Template/README.md new file mode 100644 index 0000000..765052f --- /dev/null +++ b/Homework3/vkosuri/React-Template/README.md @@ -0,0 +1,26 @@ +# More about the Framework + + +This is a template in React and TypeScript. React has a steeper learning curve than Vue.js because it requires understanding JSX syntax and concepts like hooks and component lifecycle. + +If you want to use React but not with TypeScript, just remove any type specifications from the `Example.tsx`, `Notes.tsx`, and `NotesWithReducer.tsx`. You can always refer to `VanillaJS-Template/example.js` for this migration. + + +## Files You Have to Care about + +`package.json` is where we manage the libraries we installed. Besides this, most of the files you can ignore, but **the files under `./src/` are your concern**. + +* `./src/main.tsx` is the root script file for React that instatinates our single page application. +* `./src/App.tsx` is the root file for all **development** needs and is also where we manage the layout and load in components. +* `./src/types.ts` is usually where we declare our customized types if you're planning to use it. +* `./src/stores/` is where we manage the stores if you're planning to use it. The store is responsible for global state management. +* `./src/components/` is where we create the components. You may have multiple components depends on your design. + * `Example.tsx` shows how to read `.csv` and `.json`, how component size is being watched, how a bar chart is created, and how the component updates if there are any changes. + * `Notes.tsx` shows the difference of **state** and **prop**, how to use MUI, and how a local state updates based on interaction. + * `NotesWithReducer.tsx` is equivalent to `Notes.tsx`, excepts it uses store called reducer. + +## Libraries Installed in this Framework + * D3.js v7 for visualization + * [axios](https://axios-http.com/docs/intro) for API. + * [Material UI](https://mui.com/material-ui/getting-started/) for UI components. + * [lodash](https://lodash.com/) for utility functions in JavaScript. diff --git a/Homework3/vkosuri/React-Template/data/Student Mental health.csv b/Homework3/vkosuri/React-Template/data/Student Mental health.csv new file mode 100644 index 0000000..0d75ad4 --- /dev/null +++ b/Homework3/vkosuri/React-Template/data/Student Mental health.csv @@ -0,0 +1,102 @@ +Timestamp,Choose your gender,Age,What is your course?,Your current year of Study,What is your CGPA?,Marital status,Do you have Depression?,Do you have Anxiety?,Do you have Panic attack?,Did you seek any specialist for a treatment? +8/7/2020 12:02,Female,18,Engineering,year 1,3.00 - 3.49,No,Yes,No,Yes,No +8/7/2020 12:04,Male,21,Islamic education,year 2,3.00 - 3.49,No,No,Yes,No,No +8/7/2020 12:05,Male,19,BIT,Year 1,3.00 - 3.49,No,Yes,Yes,Yes,No +8/7/2020 12:06,Female,22,Laws,year 3,3.00 - 3.49,Yes,Yes,No,No,No +8/7/2020 12:13,Male,23,Mathemathics,year 4,3.00 - 3.49,No,No,No,No,No +8/7/2020 12:31,Male,19,Engineering,Year 2,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 12:32,Female,23,Pendidikan islam,year 2,3.50 - 4.00 ,Yes,Yes,No,Yes,No +8/7/2020 12:33,Female,18,BCS,year 1,3.50 - 4.00,No,No,Yes,No,No +8/7/2020 12:35,Female,19,Human Resources,Year 2,2.50 - 2.99,No,No,No,No,No +8/7/2020 12:39,Male,18,Irkhs,year 1,3.50 - 4.00,No,No,Yes,Yes,No +8/7/2020 12:39,Female,20,Psychology,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 12:39,Female,24,Engineering,Year 3,3.50 - 4.00,Yes,Yes,No,No,No +8/7/2020 12:40,Female,18,BCS,year 1,3.00 - 3.49,No,Yes,No,No,No +8/7/2020 12:41,Male,19,Engineering,year 1,3.00 - 3.49,No,No,No,No,No +8/7/2020 12:43,Female,18,KENMS,Year 2,3.50 - 4.00,No,No,Yes,No,No +8/7/2020 12:43,Male,24,BCS,Year 3,3.50 - 4.00,No,No,No,No,No +8/7/2020 12:46,Female,24,Accounting ,year 3,3.00 - 3.49,No,No,No,No,No +8/7/2020 12:52,Female,24,ENM,year 4,3.00 - 3.49,Yes,Yes,Yes,Yes,No +8/7/2020 13:05,Female,20,BIT,Year 2,3.50 - 4.00,No,No,Yes,No,No +8/7/2020 13:07,Female,18,Marine science,year 2,3.50 - 4.00,Yes,Yes,Yes,Yes,No +8/7/2020 13:12,Female,19,Engineering,year 1,3.00 - 3.49,No,No,No,Yes,No +8/7/2020 13:13,Female,18,KOE,Year 2,3.00 - 3.49,No,No,No,No,No +8/7/2020 13:13,Female,24,BCS,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 13:15,Female,24,Engineering,year 1,3.00 - 3.49,No,No,No,No,No +8/7/2020 13:17,Female,23,BCS,Year 3,3.50 - 4.00,No,Yes,Yes,Yes,No +8/7/2020 13:29,Female,18,Banking Studies,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 13:35,Female,19,Engineering,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 13:41,Male,18,Engineering,Year 2,3.00 - 3.49,Yes,Yes,Yes,No,No +8/7/2020 13:58,Female,24,BIT,Year 3,3.50 - 4.00,Yes,Yes,Yes,Yes,Yes +8/7/2020 14:05,Female,24,BCS,year 4,3.50 - 4.00,No,No,No,No,No +8/7/2020 14:27,Female,23,Business Administration,Year 2,3.00 - 3.49,No,No,No,No,No +8/7/2020 14:29,Male,18,BCS,year 2,3.00 - 3.49,No,No,No,No,No +8/7/2020 14:29,Male,19,BCS,year 1,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 14:31,Male,18,BCS,Year 2,3.50 - 4.00,Yes,Yes,Yes,No,Yes +8/7/2020 14:41,Female,19,BIT,year 1,3.00 - 3.49,No,Yes,Yes,Yes,No +8/7/2020 14:43,Female,18,Engineering,year 1,2.00 - 2.49,No,No,No,No,No +8/7/2020 14:43,Female,18,Law,Year 3,3.00 - 3.49,No,Yes,Yes,No,No +8/7/2020 14:45,Female,19,BIT,year 1,2.50 - 2.99,No,Yes,Yes,Yes,No +8/7/2020 14:47,Female,18,KIRKHS,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 14:56,Female,24,Engineering,Year 2,2.50 - 2.99,Yes,Yes,No,Yes,Yes +8/7/2020 14:57,Female,24,BIT,Year 3,3.00 - 3.49,No,No,Yes,No,No +8/7/2020 14:57,Female,22,Engineering,year 4,3.50 - 4.00,No,No,No,No,No +8/7/2020 14:58,Female,20,Usuluddin ,year 2,3.00 - 3.49,No,Yes,No,No,No +8/7/2020 15:07,Male,,BIT,year 1,0 - 1.99,No,No,No,No,No +8/7/2020 15:08,Male,23,TAASL,year 2,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 15:09,Male,18,BCS,year 1,3.50 - 4.00,No,No,Yes,Yes,No +8/7/2020 15:12,Female,19,Engineering,year 1,3.50 - 4.00,No,No,Yes,No,No +8/7/2020 15:14,Female,18,Engine,year 4,3.50 - 4.00,No,No,No,No,No +8/7/2020 15:14,Male,24,BCS,year 2,3.00 - 3.49,No,Yes,No,No,No +8/7/2020 15:18,Female,24,BCS,year 3,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 15:27,Female,23,ALA,year 1,2.50 - 2.99,Yes,Yes,No,Yes,Yes +8/7/2020 15:37,Female,18,BCS,year 2,3.50 - 4.00,No,No,Yes,No,No +8/7/2020 15:47,Female,19,Biomedical science,year 3,3.00 - 3.49,No,No,No,No,No +8/7/2020 15:48,Female,20,koe,year 3,3.00 - 3.49,Yes,Yes,Yes,Yes,No +8/7/2020 15:57,Female,19,BCS,year 1,3.50 - 4.00,No,Yes,No,Yes,Yes +8/7/2020 15:58,Male,21,BCS,year 1,3.00 - 3.49,No,No,No,No,No +8/7/2020 16:08,Male,23,Kirkhs,Year 3,3.50 - 4.00,No,No,No,No,No +8/7/2020 16:21,Female,20,BENL,Year 3,3.00 - 3.49,No,Yes,Yes,No,No +8/7/2020 16:22,Female,18,BCS,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 16:34,Female,23,Benl,year 1,3.00 - 3.49,No,No,No,No,No +8/7/2020 16:34,Female,18,IT,Year 3,3.00 - 3.49,No,No,No,Yes,No +8/7/2020 16:53,Female,19,BCS,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 17:05,Female,18,CTS,Year 1,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 17:37,Female,24,engin,year 1,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 17:46,Female,24,Engine,year 1,3.50 - 4.00,No,No,No,No,No +8/7/2020 17:50,Female,23,Econs,year 1,3.50 - 4.00,No,Yes,Yes,No,No +8/7/2020 18:10,Female,18,KOE,Year 3,3.00 - 3.49,No,No,Yes,No,No +8/7/2020 18:11,Male,19,MHSC,Year 3,3.00 - 3.49,Yes,Yes,No,Yes,No +8/7/2020 19:05,Female,18,Malcom,year 1,3.50 - 4.00,No,Yes,No,No,No +8/7/2020 19:32,Female,24,Kop,year 4,3.00 - 3.49,No,No,Yes,No,No +8/7/2020 20:36,Female,24,Biomedical science,year 1,3.00 - 3.49,No,No,No,No,No +8/7/2020 21:21,Female,18,Laws,Year 3,3.50 - 4.00,No,No,No,Yes,No +8/7/2020 22:35,Female,19,BIT,Year 3,3.00 - 3.49,Yes,Yes,No,No,No +9/7/2020 6:57,Male,18,Biomedical science,year 1,0 - 1.99,No,No,No,No,No +9/7/2020 11:43,Male,24,BIT,Year 3,3.50 - 4.00,No,No,Yes,No,No +9/7/2020 11:57,Female,24,KOE,year 1,3.50 - 4.00,No,No,Yes,Yes,No +9/7/2020 13:15,Female,23,Engineering,year 1,3.00 - 3.49,No,Yes,No,No,No +9/7/2020 18:24,Female,18,Human Sciences ,Year 2,3.00 - 3.49,No,No,No,Yes,No +13/07/2020 10:07:32,Female,19,Biotechnology,Year 3,0 - 1.99,No,No,No,No,No +13/07/2020 10:10:30,Female,18,Engineering,year 4,3.50 - 4.00,No,No,No,No,No +13/07/2020 10:11:26,Female,24,Communication ,Year 2,3.50 - 4.00,Yes,Yes,Yes,Yes,No +13/07/2020 10:12:18,Female,24,Diploma Nursing,year 2,3.50 - 4.00,No,No,No,No,No +13/07/2020 10:12:26,Female,19,Engineering,year 1,3.00 - 3.49,No,Yes,Yes,No,No +13/07/2020 10:12:28,Female,19,Pendidikan Islam ,Year 2,3.00 - 3.49,No,No,No,No,No +13/07/2020 10:14:46,Male,23,Radiography,year 1,3.00 - 3.49,No,No,No,No,No +13/07/2020 10:33:47,Female,18,psychology,year 1,3.50 - 4.00,No,Yes,Yes,No,Yes +13/07/2020 10:34:08,Female,19,Fiqh fatwa ,Year 3,3.00 - 3.49,No,No,No,No,No +13/07/2020 11:46:13,Female,18,psychology,year 1,3.50 - 4.00,No,Yes,Yes,Yes,No +13/07/2020 11:49:02,Male,24,BIT,year 1,3.00 - 3.49,No,No,Yes,No,No +13/07/2020 11:54:58,Male,24,Engineering,Year 2,2.00 - 2.49,No,No,No,Yes,No +13/07/2020 13:57:11,Female,23,DIPLOMA TESL,Year 3,3.50 - 4.00,No,No,No,Yes,No +13/07/2020 14:38:12,Male,18,Koe,Year 2,3.00 - 3.49,No,No,Yes,No,No +13/07/2020 14:48:05,Female,19,KOE,year 2,3.00 - 3.49,Yes,Yes,No,No,No +13/07/2020 16:15:13,Female,18,BENL,year 1,3.00 - 3.49,No,Yes,No,No,No +13/07/2020 17:30:44,Female,24,Fiqh,Year 3,0 - 1.99,No,No,No,Yes,No +13/07/2020 19:08:32,Female,18,Islamic Education,year 1,3.50 - 4.00,No,No,No,No,No +13/07/2020 19:56:49,Female,21,BCS,year 1,3.50 - 4.00,No,No,Yes,No,No +13/07/2020 21:21:42,Male,18,Engineering,Year 2,3.00 - 3.49,No,Yes,Yes,No,No +13/07/2020 21:22:56,Female,19,Nursing ,Year 3,3.50 - 4.00,Yes,Yes,No,Yes,No +13/07/2020 21:23:57,Female,23,Pendidikan Islam,year 4,3.50 - 4.00,No,No,No,No,No +18/07/2020 20:16:21,Male,20,Biomedical science,Year 2,3.00 - 3.49,No,No,No,No,No diff --git a/Homework3/vkosuri/React-Template/index.html b/Homework3/vkosuri/React-Template/index.html new file mode 100644 index 0000000..a4a4009 --- /dev/null +++ b/Homework3/vkosuri/React-Template/index.html @@ -0,0 +1,21 @@ + + + + + + + Vite + React + TS + + + +
+ + + diff --git a/Homework3/vkosuri/React-Template/package.json b/Homework3/vkosuri/React-Template/package.json new file mode 100644 index 0000000..3603f6b --- /dev/null +++ b/Homework3/vkosuri/React-Template/package.json @@ -0,0 +1,38 @@ +{ + "name": "react-template", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.13.3", + "@emotion/styled": "^11.13.0", + "@mui/material": "^5.16.7", + "@types/d3": "^7.4.3", + "@types/lodash": "^4.17.7", + "@types/styled-components": "^5.1.34", + "axios": "^1.2.1", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "lodash": "^4.17.21", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "styled-components": "^6.1.13", + "usehooks-ts": "^3.1.0" + }, + "devDependencies": { + "@types/d3-sankey": "^0.12.4", + "@types/material-ui": "^0.21.17", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "globals": "^15.9.0", + "typescript": "^5.5.3", + "vite": "^5.4.1" + } +} diff --git a/Homework3/vkosuri/React-Template/src/App.tsx b/Homework3/vkosuri/React-Template/src/App.tsx new file mode 100644 index 0000000..95e26bc --- /dev/null +++ b/Homework3/vkosuri/React-Template/src/App.tsx @@ -0,0 +1,225 @@ +import React, { useState, useCallback } from 'react'; +import BarChart from './components/BarChart'; +import PieChart from './components/PieChart'; +import SankeyChart from './components/SankeyChart'; +import { ChartProps, FilterState, HoverState, NodeData, SankeyProps } from './types'; +import './style.css'; + +interface AppProps {} + +const App: React.FC = () => { + const [filters, setFilters] = useState({ + selectedAge: null, + selectedCondition: null, + selectedTreatment: null + }); + + const [hoverState, setHoverState] = useState({ + element: null + }); + + const [isTransitioning, setIsTransitioning] = useState(false); + const [activeView, setActiveView] = useState<'sankey' | 'bar' | 'pie'>('sankey'); + + const handleSankeySelect = useCallback((node: NodeData): void => { + setIsTransitioning(true); + + setFilters(prev => { + const newFilters = { ...prev }; + if (node.category === 'age') { + newFilters.selectedAge = prev.selectedAge === node.name ? null : node.name; + setActiveView('bar'); + } else if (node.category === 'condition') { + newFilters.selectedCondition = prev.selectedCondition === node.name ? null : node.name; + setActiveView('pie'); + } else if (node.category === 'treatment') { + newFilters.selectedTreatment = prev.selectedTreatment === node.name ? null : node.name; + setActiveView('bar'); + } + return newFilters; + }); + + setTimeout(() => setIsTransitioning(false), 500); + }, []); + + const handleHover = useCallback(( + element: NonNullable | null, + event: React.MouseEvent + ): void => { + if (element) { + setHoverState({ + element: { + ...element, + coordinates: { x: event.clientX, y: event.clientY } + } + }); + } else { + setHoverState({ element: null }); + } + }, []); + + const commonChartProps: Omit = { + filters, + onElementHover: handleHover, + isTransitioning + }; + + const sankeyProps: SankeyProps = { + ...commonChartProps, + onNodeSelect: handleSankeySelect + }; + + const resetFilters = useCallback((): void => { + setIsTransitioning(true); + setFilters({ + selectedAge: null, + selectedCondition: null, + selectedTreatment: null + }); + setActiveView('sankey'); + setTimeout(() => setIsTransitioning(false), 500); + }, []); + + const renderFilterTag = useCallback(( + type: keyof FilterState, + label: string, + value: string | null + ): JSX.Element | null => { + if (!value) return null; + + return ( +
{ + handleHover({ + type: type.replace('selected', '').toLowerCase() as any, + name: value, + value: 0, + coordinates: { x: 0, y: 0 } + }, {} as React.MouseEvent) + }} + onMouseLeave={() => handleHover(null, {} as React.MouseEvent)} + > + {label}: + {value} + +
+ ); + }, [handleHover]); + + return ( +
+ {/* Header - 5% */} +
+

Student Mental Health Dashboard - Analysis of mental health conditions among students

+ +
+ {(filters.selectedAge || filters.selectedCondition || filters.selectedTreatment) && ( + <> + {renderFilterTag('selectedAge', 'Age', filters.selectedAge)} + {renderFilterTag('selectedCondition', 'Condition', filters.selectedCondition)} + {renderFilterTag('selectedTreatment', 'Treatment', filters.selectedTreatment)} + + + + )} + + {activeView !== 'sankey' && ( + + )} +
+
+ + {/* Main Content - 95% */} +
+ {/* Sankey or Active Chart - 61% */} +
+
+

+ {activeView === 'sankey' ? 'Student Mental Health Overview: Sankey Diagram' : + activeView === 'bar' ? 'Treatment Distribution Analysis' : + 'Mental Health Condition Distribution'} +

+
+ {activeView === 'sankey' && } + {activeView === 'bar' && } + {activeView === 'pie' && } +
+
+
+ + {/* Bottom Section - 34% total */} + {activeView === 'sankey' && ( +
+
+

Mental Health by Academic Performance: Bar Chart

+
+ +
+
+
+

Mental Health by Gender: Pie Chart

+
+ +
+
+
+ )} +
+ + {/* Tooltip */} + {hoverState.element && ( +
+
+
+ {hoverState.element.name} +
+
+ {hoverState.element.value && ( +
+ Count: + {hoverState.element.value} +
+ )} + {hoverState.element.percentage && ( +
+ Percentage: + {hoverState.element.percentage.toFixed(1)}% +
+ )} +
+
+
+ )} +
+ ); +}; + +export default App; \ No newline at end of file diff --git a/Homework3/vkosuri/React-Template/src/components/BarChart.tsx b/Homework3/vkosuri/React-Template/src/components/BarChart.tsx new file mode 100644 index 0000000..407decd --- /dev/null +++ b/Homework3/vkosuri/React-Template/src/components/BarChart.tsx @@ -0,0 +1,325 @@ +import React, { useEffect, useRef } from 'react'; +import * as d3 from 'd3'; +import { ChartProps } from '../types'; + +const CHART_MARGINS = { top: 45, right: 170, bottom: 60, left: 102 }; +const AGE_GROUPS = ["18 or younger", "19", "20", "21", "22", "23", "24+"]; +const COLORS = { + soughtTreatment: '#00C853', + noTreatment: '#FF5252' +}; +const CHART_WIDTH = 1190; +const CHART_HEIGHT = 388; + +const getAgeGroup = (age: string): string | null => { + const ageNum = parseInt(age); + if (isNaN(ageNum)) return null; + if (ageNum <= 18) return "18 or younger"; + if (ageNum === 19) return "19"; + if (ageNum === 20) return "20"; + if (ageNum === 21) return "21"; + if (ageNum === 22) return "22"; + if (ageNum === 23) return "23"; + return "24+"; +}; + +const BarChart: React.FC = ({ filters, onElementHover, isTransitioning }) => { + const svgRef = useRef(null); + + useEffect(() => { + const fetchAndRenderData = async () => { + try { + const data = await d3.csv('/data/Student Mental health.csv'); + + // Process data matching Sankey chart logic + const processedData = data.reduce((acc: any, row) => { + const ageGroup = getAgeGroup(row.Age); + if (!ageGroup) return acc; + + if (!acc[ageGroup]) { + acc[ageGroup] = { + ageGroup, + soughtTreatment: 0, + noTreatment: 0, + totalConditions: 0 + }; + } + + const hasDepression = row["Do you have Depression?"] === "Yes"; + const hasAnxiety = row["Do you have Anxiety?"] === "Yes"; + const hasPanicAttack = row["Do you have Panic attack?"] === "Yes"; + const hasCondition = hasDepression || hasAnxiety || hasPanicAttack; + const soughtTreatment = row["Did you seek any specialist for a treatment?"] === "Yes"; + + if (!hasCondition) { + acc[ageGroup].noTreatment++; + } else { + if (hasDepression) { + if (soughtTreatment) acc[ageGroup].soughtTreatment++; + else acc[ageGroup].noTreatment++; + } + if (hasAnxiety) { + if (soughtTreatment) acc[ageGroup].soughtTreatment++; + else acc[ageGroup].noTreatment++; + } + if (hasPanicAttack) { + if (soughtTreatment) acc[ageGroup].soughtTreatment++; + else acc[ageGroup].noTreatment++; + } + } + + return acc; + }, {}); + + const chartData = AGE_GROUPS.map(age => ({ + ageGroup: age, + soughtTreatment: processedData[age]?.soughtTreatment || 0, + noTreatment: processedData[age]?.noTreatment || 0, + total: (processedData[age]?.soughtTreatment || 0) + (processedData[age]?.noTreatment || 0) + })); + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + svg.attr('width', CHART_WIDTH) + .attr('height', CHART_HEIGHT) + .attr('viewBox', `0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`) + .attr('preserveAspectRatio', 'xMidYMid meet'); + + const innerWidth = CHART_WIDTH - CHART_MARGINS.left - CHART_MARGINS.right; + const innerHeight = CHART_HEIGHT - CHART_MARGINS.top - CHART_MARGINS.bottom; + + const chart = svg.append('g') + .attr('transform', `translate(${CHART_MARGINS.left}, ${CHART_MARGINS.top})`); + + const xScale = d3.scaleBand() + .domain(AGE_GROUPS) + .range([0, innerWidth]) + .padding(0.2); + + // Adjust yMax based on selected treatment type + let yMax; + if (filters?.selectedTreatment === 'Sought Treatment') { + yMax = d3.max(chartData, d => d.soughtTreatment) || 0; + } else if (filters?.selectedTreatment === 'No Treatment') { + yMax = d3.max(chartData, d => d.noTreatment) || 0; + } else { + yMax = d3.max(chartData, d => d.total) || 0; + } + + const yScale = d3.scaleLinear() + .domain([0, yMax]) + .range([innerHeight, 0]) + .nice(); + + // Add the bars for each age group + chartData.forEach(d => { + const x = xScale(d.ageGroup); + if (x === undefined) return; + + // Only show No Treatment bars if no filter or explicitly selected + if ((filters?.selectedTreatment === 'No Treatment' || !filters?.selectedTreatment) && d.noTreatment > 0) { + const height = innerHeight - yScale(d.noTreatment); + chart.append('rect') + .attr('x', x) + .attr('y', yScale(d.noTreatment)) + .attr('width', xScale.bandwidth()) + .attr('height', height) + .attr('fill', COLORS.noTreatment) + .style('cursor', 'pointer') + .on('mouseenter', (event) => { + d3.select(event.target) + .style('opacity', 0.8) + .style('stroke', 'white') + .style('stroke-width', 2); + + onElementHover({ + type: 'bar', + name: d.ageGroup, + category: 'No Treatment', + value: d.noTreatment, + percentage: (d.noTreatment / d.total) * 100, + coordinates: { x: event.pageX, y: event.pageY } + }, event); + }) + .on('mouseleave', (event) => { + d3.select(event.target) + .style('opacity', 1) + .style('stroke', 'none'); + onElementHover(null, event); + }); + + if (!filters?.selectedTreatment || filters?.selectedTreatment === 'No Treatment') { + chart.append('text') + .attr('x', x + xScale.bandwidth() / 2) + .attr('y', yScale(d.noTreatment) + height / 2) + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'middle') + .style('fill', 'white') + .style('font-size', '14px') + .style('font-weight', 'bold') + .text(d.noTreatment); + } + } + + // Only show Sought Treatment bars if no filter or explicitly selected + if ((filters?.selectedTreatment === 'Sought Treatment' || !filters?.selectedTreatment) && d.soughtTreatment > 0) { + const baseY = filters?.selectedTreatment === 'Sought Treatment' ? innerHeight : yScale(d.noTreatment); + const height = filters?.selectedTreatment === 'Sought Treatment' + ? innerHeight - yScale(d.soughtTreatment) + : yScale(d.noTreatment) - yScale(d.total); + const barY = filters?.selectedTreatment === 'Sought Treatment' + ? yScale(d.soughtTreatment) + : yScale(d.total); + + chart.append('rect') + .attr('x', x) + .attr('y', barY) + .attr('width', xScale.bandwidth()) + .attr('height', height) + .attr('fill', COLORS.soughtTreatment) + .style('cursor', 'pointer') + .on('mouseenter', (event) => { + d3.select(event.target) + .style('opacity', 0.8) + .style('stroke', 'white') + .style('stroke-width', 2); + + onElementHover({ + type: 'bar', + name: d.ageGroup, + category: 'Sought Treatment', + value: d.soughtTreatment, + percentage: (d.soughtTreatment / d.total) * 100, + coordinates: { x: event.pageX, y: event.pageY } + }, event); + }) + .on('mouseleave', (event) => { + d3.select(event.target) + .style('opacity', 1) + .style('stroke', 'none'); + onElementHover(null, event); + }); + + if (!filters?.selectedTreatment || filters?.selectedTreatment === 'Sought Treatment') { + chart.append('text') + .attr('x', x + xScale.bandwidth() / 2) + .attr('y', barY + height / 2) + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'middle') + .style('fill', 'white') + .style('font-size', '14px') + .style('font-weight', 'bold') + .text(d.soughtTreatment); + } + } + }); + + // X-axis with straight labels + chart.append('g') + .attr('transform', `translate(0, ${innerHeight})`) + .call(d3.axisBottom(xScale)) + .style('color', 'white') + .selectAll('text') + .style('fill', 'white') + .style('font-size', '18px') + .style('font-weight', 'bold') + .style('text-anchor', 'middle'); + + // Y-axis + chart.append('g') + .call(d3.axisLeft(yScale).ticks(5)) + .style('color', 'white') + .selectAll('text') + .style('fill', 'white') + .style('font-size', '18px') + .style('font-weight', 'bold'); + + // Title + svg.append('text') + .attr('x', CHART_WIDTH / 2) + .attr('y', 25) + .attr('text-anchor', 'middle') + .style('fill', 'white') + .style('font-size', '28px') + .style('font-weight', 'bold') + .style('opacity', isTransitioning ? 0.5 : 1) + .text('Treatment Status Distribution by Age Group'); + + // X-axis label + chart.append('text') + .attr('x', innerWidth / 2) + .attr('y', innerHeight + 45) + .attr('text-anchor', 'middle') + .style('fill', 'white') + .style('font-size', '20px') + .style('font-weight', 'bold') + .text('Age Groups'); + + // Y-axis label + chart.append('text') + .attr('transform', 'rotate(-90)') + .attr('x', -innerHeight / 2) + .attr('y', -75) + .attr('text-anchor', 'middle') + .style('fill', 'white') + .style('font-size', '20px') + .style('font-weight', 'bold') + .text('Number of Students'); + + // Show legend only when no specific treatment is selected + if (!filters?.selectedTreatment) { + const legend = svg.append('g') + .attr('transform', `translate(${CHART_WIDTH - 160}, ${CHART_MARGINS.top})`); + + const legendItems = [ + { key: 'soughtTreatment', label: 'Sought Treatment' }, + { key: 'noTreatment', label: 'No Treatment' } + ]; + + legendItems.forEach((item, i) => { + const g = legend.append('g') + .attr('transform', `translate(0, ${i * 30})`) + .style('cursor', 'pointer'); + + g.append('rect') + .attr('width', 15) + .attr('height', 15) + .attr('fill', COLORS[item.key as keyof typeof COLORS]) + .attr('stroke', '#000000') + .attr('stroke-width', 1); + + g.append('text') + .attr('x', 25) + .attr('y', 12) + .style('fill', 'white') + .style('font-size', '24px') // Increased from 20px to 24px + .style('font-weight', 'bold') + .text(item.label); + }); + } + + } catch (error) { + console.error('Error loading data:', error); + } + }; + + fetchAndRenderData(); + }, [filters, onElementHover, isTransitioning]); + + return ( +
+ +
+ ); +}; + +export default BarChart; \ No newline at end of file diff --git a/Homework3/vkosuri/React-Template/src/components/PieChart.tsx b/Homework3/vkosuri/React-Template/src/components/PieChart.tsx new file mode 100644 index 0000000..c90643f --- /dev/null +++ b/Homework3/vkosuri/React-Template/src/components/PieChart.tsx @@ -0,0 +1,282 @@ +import React, { useEffect, useRef, useMemo } from 'react'; +import * as d3 from 'd3'; +import { ChartProps, FilterState } from '../types'; + +// Interfaces and Types +interface MentalHealthCount { + condition: string; + count: number; + percentage: number; +} + +type D3Selection = d3.Selection; +type D3Transition = d3.Transition; +type D3PieArcDatum = d3.PieArcDatum; + +interface D3Event extends MouseEvent { + currentTarget: SVGPathElement | SVGGElement; +} + +// Constants +const COLORS = { + Depression: '#FF69B4', // Pink + Anxiety: '#FFA500', // Orange + 'Panic Attack': '#9370DB', // Purple + 'No Mental Issues': '#808080' // Gray +}; + +const TRANSITION_DURATION = 750; + +// Helper Functions +const getAgeGroup = (age: string | undefined, selectedAge: string | null): boolean => { + if (!age || selectedAge === null) return true; + const ageNum = parseInt(age); + if (isNaN(ageNum)) return false; + + switch (selectedAge) { + case "18 or younger": + return ageNum <= 18; + case "24+": + return ageNum >= 24; + default: + return age === selectedAge; + } +}; + +// Main Component +const PieChart: React.FC = ({ filters, onElementHover, isTransitioning }) => { + const svgRef = useRef(null); + const prevDataRef = useRef<{ male: MentalHealthCount[]; female: MentalHealthCount[]; } | null>(null); + + // Data Processing Logic + const processData = useMemo(() => (csvData: d3.DSVRowString[]) => { + const processDataForGender = (gender: string) => { + let filteredData = csvData.filter(d => d["Choose your gender"]?.trim() === gender); + filteredData = filteredData.filter(d => getAgeGroup(d.Age, filters.selectedAge)); + + const total = filteredData.length; + if (total === 0) return []; + + const counts = { + Depression: filteredData.filter(d => d["Do you have Depression?"] === "Yes").length, + Anxiety: filteredData.filter(d => d["Do you have Anxiety?"] === "Yes").length, + "Panic Attack": filteredData.filter(d => d["Do you have Panic attack?"] === "Yes").length, + "No Mental Issues": filteredData.filter(d => + d["Do you have Depression?"] === "No" && + d["Do you have Anxiety?"] === "No" && + d["Do you have Panic attack?"] === "No" + ).length + }; + + return Object.entries(counts) + .map(([condition, count]) => ({ + condition, + count, + percentage: total > 0 ? (count / total) * 100 : 0 + })) + .filter(d => d.count > 0); + }; + + return { + male: processDataForGender("Male"), + female: processDataForGender("Female") + }; + }, [filters]); + + // Chart Rendering Logic + useEffect(() => { + const renderChart = async () => { + try { + const csvData = await d3.csv('/data/Student Mental health.csv'); + const data = processData(csvData); + const prevData = prevDataRef.current; + prevDataRef.current = data; + + const container = d3.select(svgRef.current).node()?.parentElement; + if (!container) return; + + const width = container.clientWidth; + const height = container.clientHeight; + const radius = Math.min(width / 6, height / 3); + + const svg = d3.select(svgRef.current); + svg.selectAll('*').remove(); + + svg.attr('width', width) + .attr('height', height) + .attr('viewBox', `0 0 ${width} ${height}`) + .attr('preserveAspectRatio', 'xMidYMid meet') + .style('opacity', isTransitioning ? 0 : 1) + .transition() + .duration(300) + .style('opacity', 1); + + svg.append('text') + .attr('x', width / 2) + .attr('y', 25) + .attr('text-anchor', 'middle') + .style('fill', 'white') + .style('font-size', '16px') + .style('font-weight', 'bold') + .text('Mental Health Issues Distribution Across Gender'); + + const pie = d3.pie() + .value(d => d.percentage) + .sort((a, b) => a.percentage - b.percentage); + + const arc = d3.arc() + .innerRadius(0) + .outerRadius(radius); + + const createPieChart = (pieData: MentalHealthCount[], centerX: number, title: string) => { + const g = svg.append('g') + .attr('transform', `translate(${centerX}, ${height / 2})`) as unknown as D3Selection; + + const segments = g.selectAll('path') + .data(pie(pieData)) + .join( + enter => enter.append('path') + .attr('d', arc) + .style('opacity', d => filters.selectedCondition === null ? 1 : + d.data.condition === filters.selectedCondition ? 1 : 0.2) + .style('fill', d => COLORS[d.data.condition as keyof typeof COLORS]) + .style('stroke', '#000000') + .style('stroke-width', 2) + .style('cursor', 'pointer'), + update => update, + exit => exit.remove() + ); + + segments + .on('mouseenter', (event: MouseEvent, d: D3PieArcDatum) => { + const target = event.currentTarget as SVGPathElement; + d3.select(target) + .transition() + .duration(200) + .attr('transform', 'scale(1.05)'); + + onElementHover({ + type: 'pie', + name: d.data.condition, + value: d.data.count, + percentage: d.data.percentage, + coordinates: { x: event.pageX, y: event.pageY } + }, event as unknown as React.MouseEvent); + }) + .on('mouseleave', (event: MouseEvent) => { + d3.select(event.currentTarget as SVGPathElement) + .transition() + .duration(200) + .attr('transform', 'scale(1)'); + + onElementHover(null, event as unknown as React.MouseEvent); + }); + + g.selectAll('text.percentage') + .data(pie(pieData)) + .join('text') + .attr('class', 'percentage') + .attr('transform', d => `translate(${arc.centroid(d)})`) + .style('text-anchor', 'middle') + .style('fill', '#000000') + .style('font-size', '12px') + .style('font-weight', 'bold') + .style('opacity', d => filters.selectedCondition === null ? 1 : + d.data.condition === filters.selectedCondition ? 1 : 0.2) + .text(d => `${Math.round(d.data.percentage)}%`); + + g.append('text') + .attr('class', 'title') + .attr('x', radius + 10) + .attr('y', 0) + .attr('text-anchor', 'start') + .style('fill', 'white') + .style('font-size', '16px') + .style('font-weight', 'bold') + .text(title); + + // Add count and percentage for selected condition + if (filters.selectedCondition) { + const selectedData = pieData.find(d => d.condition === filters.selectedCondition); + if (selectedData) { + g.append('text') + .attr('class', 'selected-info') + .attr('x', radius + 10) + .attr('y', 30) + .attr('text-anchor', 'start') + .style('fill', 'white') + .style('font-size', '14px') + .text(`${filters.selectedCondition}: ${selectedData.count} (${selectedData.percentage.toFixed(1)}%)`); + } + } + + const total = pieData.reduce((sum, d) => sum + d.count, 0); + g.append('text') + .attr('class', 'total') + .attr('y', radius + 30) + .attr('text-anchor', 'middle') + .style('fill', 'white') + .style('font-size', '14px') + .text(`Total: ${total} students`); + }; + + createPieChart(data.female, width * 0.3, 'Female'); + createPieChart(data.male, width * 0.7, 'Male'); + + const legendData = Object.entries(COLORS); + const legend = svg.append('g') + .attr('transform', `translate(20, ${height * 0.3})`); + + legendData.forEach(([condition, color], i) => { + const g = legend.append('g') + .attr('transform', `translate(0, ${i * 30})`) + .style('cursor', 'pointer') + .style('opacity', filters.selectedCondition === null ? 1 : + condition === filters.selectedCondition ? 1 : 0.2); + + g.append('rect') + .attr('width', 15) + .attr('height', 15) + .attr('fill', color) + .attr('stroke', '#000000') + .attr('stroke-width', 1); + + g.append('text') + .attr('x', 25) + .attr('y', 12) + .style('fill', 'white') + .style('font-size', '14px') + .style('font-weight', 'bold') + .text(condition); + }); + + } catch (error) { + console.error('Error loading CSV data:', error); + } + }; + + renderChart(); + }, [filters, onElementHover, isTransitioning, processData]); + + return ( +
+ +
+ ); +}; + +export default PieChart; \ No newline at end of file diff --git a/Homework3/vkosuri/React-Template/src/components/SankeyChart.tsx b/Homework3/vkosuri/React-Template/src/components/SankeyChart.tsx new file mode 100644 index 0000000..efa89ce --- /dev/null +++ b/Homework3/vkosuri/React-Template/src/components/SankeyChart.tsx @@ -0,0 +1,457 @@ +import React, { useEffect, useRef } from 'react'; +import * as d3 from 'd3'; +import { sankey, sankeyLinkHorizontal, SankeyNode, SankeyLink } from 'd3-sankey'; +import { + SankeyProps, + NodeData, + LinkData, + ChartDimensions, + Point +} from '../types'; + + +interface SankeyNodeExtended extends Required> {} +interface SankeyLinkExtended extends Required> {} + + +interface D3Event extends MouseEvent { + currentTarget: SVGPathElement | SVGGElement; +} + + +const getAgeGroup = (age: string): string | null => { + const ageNum = parseInt(age); + if (isNaN(ageNum)) return null; + if (ageNum <= 18) return "18 or younger"; + if (ageNum === 19) return "19"; + if (ageNum === 20) return "20"; + if (ageNum === 21) return "21"; + if (ageNum === 22) return "22"; + if (ageNum === 23) return "23"; + return "24+"; +}; + + +const addOrUpdateLink = (links: LinkData[], source: number, target: number): void => { + const existingLink = links.find(l => + (typeof l.source === 'number' ? l.source : l.source.index) === source && + (typeof l.target === 'number' ? l.target : l.target.index) === target + ); + if (existingLink) { + existingLink.value++; + } else { + links.push({ source, target, value: 1 }); + } +}; + + +const SankeyChart: React.FC = ({ + onNodeSelect, + onElementHover, + filters, + isTransitioning +}) => { + const svgRef = useRef(null); + + + useEffect(() => { + const fetchData = async () => { + try { + const data = await d3.csv('/data/Student Mental health.csv'); + const width = 5760; + const height = 1344; + const margin = { + top: 149.80, + right: 640, + bottom: 240, + left: 840 // Increased to move diagram right + }; + + + const nodes: NodeData[] = [ + { name: "18 or younger", category: "age" }, + { name: "19", category: "age" }, + { name: "20", category: "age" }, + { name: "21", category: "age" }, + { name: "22", category: "age" }, + { name: "23", category: "age" }, + { name: "24+", category: "age" }, + { name: "Depression", category: "condition" }, + { name: "Anxiety", category: "condition" }, + { name: "Panic Attack", category: "condition" }, + { name: "No Mental Issues", category: "condition" }, + { name: "Sought Treatment", category: "treatment" }, + { name: "No Treatment", category: "treatment" } + ]; + + + const links: LinkData[] = []; + + + data.forEach(d => { + const ageGroup = getAgeGroup(d["Age"]); + if (!ageGroup) return; + + const ageIndex = nodes.findIndex(n => n.name === ageGroup); + const hasCondition = d["Do you have Depression?"] === "Yes" || + d["Do you have Anxiety?"] === "Yes" || + d["Do you have Panic attack?"] === "Yes"; + + + if (d["Do you have Depression?"] === "Yes") { + addOrUpdateLink(links, ageIndex, 7); + } + if (d["Do you have Anxiety?"] === "Yes") { + addOrUpdateLink(links, ageIndex, 8); + } + if (d["Do you have Panic attack?"] === "Yes") { + addOrUpdateLink(links, ageIndex, 9); + } + if (!hasCondition) { + addOrUpdateLink(links, ageIndex, 10); + } + + + const soughtTreatment = d["Did you seek any specialist for a treatment?"] === "Yes"; + if (hasCondition) { + if (d["Do you have Depression?"] === "Yes") { + addOrUpdateLink(links, 7, soughtTreatment ? 11 : 12); + } + if (d["Do you have Anxiety?"] === "Yes") { + addOrUpdateLink(links, 8, soughtTreatment ? 11 : 12); + } + if (d["Do you have Panic attack?"] === "Yes") { + addOrUpdateLink(links, 9, soughtTreatment ? 11 : 12); + } + } + if (!hasCondition) { + addOrUpdateLink(links, 10, 12); + } + }); + + + const ageColorScale = d3.scaleSequential() + .domain([0, 6]) + .interpolator(d3.interpolate("#1a4c7c", "#90caf9")); + + + const conditionColors = { + "Depression": "#FF69B4", + "Anxiety": "#FFA500", + "Panic Attack": "#9370DB", + "No Mental Issues": "#808080" + }; + + + const treatmentColors = { + "Sought Treatment": "#00C853", + "No Treatment": "#FF5252" + }; + + + const getNodeColor = (node: SankeyNodeExtended): string => { + if (node.category === 'age') { + return ageColorScale(nodes.findIndex(n => n.name === node.name)); + } else if (node.category === 'condition') { + return conditionColors[node.name as keyof typeof conditionColors]; + } else { + return treatmentColors[node.name as keyof typeof treatmentColors]; + } + }; + + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + svg.attr("width", width) + .attr("height", height) + .attr("viewBox", `0 0 ${width} ${height}`) + .attr("preserveAspectRatio", "xMidYMid meet"); + + + svg.append("text") + .attr("x", width / 2) + .attr("y", margin.top / 2 - 75.59) + .attr("text-anchor", "middle") + .attr("fill", "white") + .style("font-size", "72px") + .style("font-weight", "bold") + .text("Student Mental Health Flow Analysis"); + + + const sectionTitles = ["Age Groups", "Mental Health Conditions", "Treatment Status"]; + const sectionWidth = (width - margin.left - margin.right) / 3; + + sectionTitles.forEach((title, i) => { + svg.append("text") + .attr("x", margin.left + 200 + (sectionWidth / 2) + (i * sectionWidth)) // Added 200 to move section titles right + .attr("y", margin.top - 50) + .attr("text-anchor", "middle") + .attr("fill", "white") + .style("font-size", "54px") + .style("font-weight", "bold") + .text(title); + }); + + + const zoom = d3.zoom() + .scaleExtent([0.5, 2]) + .on('zoom', (event) => { + svg.selectAll('g') + .transition() + .duration(200) + .attr('transform', event.transform); + }); + + + svg.call(zoom); + + + const sankeyGenerator = sankey() + .nodeWidth(400) + .nodePadding(45) + .extent([[margin.left + 200, margin.top], [width - margin.right, height - margin.bottom]]); + + + sankeyGenerator.nodeSort((a, b) => { + if (a.category === 'age' && b.category === 'age') { + const getAgeValue = (name: string) => { + if (name === "18 or younger") return 18; + if (name === "24+") return 24; + return parseInt(name); + }; + return getAgeValue(a.name) - getAgeValue(b.name); + } + return 0; + }); + + + const { nodes: sankeyNodes, links: sankeyLinks } = sankeyGenerator({ + nodes: nodes.map((d, i) => ({ ...d, index: i })), + links: links + }); + + + const linkGroup = svg.append("g") + .attr("class", "links") + .selectAll("path") + .data(sankeyLinks) + .enter() + .append("path") + .attr("d", sankeyLinkHorizontal()) + .attr("class", "sankey-link") + .style("fill", "none") + .style("stroke", (d) => { + const sourceNode = d.source as SankeyNodeExtended; + return d3.rgb(getNodeColor(sourceNode)).darker(0.3).toString(); + }) + .style("stroke-width", d => Math.max(2, (d as SankeyLinkExtended).width)) + .style("stroke-opacity", 0.4) + .style("cursor", "pointer") + .on("mouseenter", (event: D3Event, d: SankeyLinkExtended) => { + d3.selectAll(".sankey-link") + .transition() + .duration(300) + .style("stroke-opacity", 0.1); + + + d3.select(event.currentTarget) + .transition() + .duration(300) + .style("stroke-opacity", 0.8); + + + const source = d.source as SankeyNodeExtended; + const target = d.target as SankeyNodeExtended; + + + onElementHover({ + type: 'link', + name: `${source.name} → ${target.name}`, + value: d.value, + coordinates: { x: event.pageX, y: event.pageY } + }, event as unknown as React.MouseEvent); + }) + .on("mouseleave", (event: D3Event) => { + d3.selectAll(".sankey-link") + .transition() + .duration(300) + .style("stroke-opacity", 0.4); + + + onElementHover(null, event as unknown as React.MouseEvent); + }); + + + const nodeGroup = svg.append("g") + .attr("class", "nodes") + .selectAll("g") + .data(sankeyNodes) + .enter() + .append("g") + .attr("class", "node-group") + .style("cursor", "pointer") + .on("click", (event: D3Event, d: SankeyNodeExtended) => { + onNodeSelect(d); + }) + .on("mouseenter", (event: D3Event, d: SankeyNodeExtended) => { + const connectedLinks = sankeyLinks.filter(link => + (link.source as SankeyNodeExtended).index === d.index || + (link.target as SankeyNodeExtended).index === d.index + ); + + + d3.selectAll(".sankey-link") + .transition() + .duration(300) + .style("stroke-opacity", l => + connectedLinks.includes(l as any) ? 0.8 : 0.1 + ); + + + onElementHover({ + type: 'node', + category: d.category, + name: d.name, + value: d.value!, + coordinates: { x: event.pageX, y: event.pageY } + }, event as unknown as React.MouseEvent); + }) + .on("mouseleave", (event: D3Event) => { + d3.selectAll(".sankey-link") + .transition() + .duration(300) + .style("stroke-opacity", 0.4); + + + onElementHover(null, event as unknown as React.MouseEvent); + }); + + + nodeGroup.append("rect") + .attr("x", d => (d as SankeyNodeExtended).x0!) + .attr("y", d => (d as SankeyNodeExtended).y0!) + .attr("height", d => (d as SankeyNodeExtended).y1! - (d as SankeyNodeExtended).y0!) + .attr("width", d => (d as SankeyNodeExtended).x1! - (d as SankeyNodeExtended).x0!) + .style("fill", d => getNodeColor(d as SankeyNodeExtended)) + .style("stroke", "#000") + .style("stroke-width", 2); + + + nodeGroup.append("text") + .attr("x", d => { + const node = d as SankeyNodeExtended; + return (node.x0! + node.x1!) / 2; + }) + .attr("y", d => { + const node = d as SankeyNodeExtended; + return (node.y1! + node.y0!) / 2; + }) + .attr("dy", "0.35em") + .attr("text-anchor", "middle") + .attr("fill", "white") + .style("font-size", "40px") + .style("font-weight", "bold") + .text(d => `${(d as SankeyNodeExtended).name} (${(d as SankeyNodeExtended).value})`); + + + // Updated legend positioning and styling + const legendGroups = [ + { + title: "Age Groups", + items: nodes.filter(n => n.category === "age").map((n, i) => ({ + color: ageColorScale(i), + label: n.name + })) + }, + { + title: "Mental Health Conditions", + items: nodes.filter(n => n.category === "condition").map(n => ({ + color: conditionColors[n.name as keyof typeof conditionColors], + label: n.name + })) + }, + { + title: "Treatment Status", + items: nodes.filter(n => n.category === "treatment").map(n => ({ + color: treatmentColors[n.name as keyof typeof treatmentColors], + label: n.name + })) + } + ]; + + + // Adjusted starting position and spacing + let currentY = margin.top - 20; // Moved up more + const xPosition = 124.41; + const itemSpacing = 80; // Increased spacing between items + const groupSpacing = 60; // Increased spacing between groups + + + legendGroups.forEach((group, groupIndex) => { + const legendGroup = svg.append("g") + .attr("class", "legend-group") + .attr("transform", `translate(${xPosition}, ${currentY})`); + + + // Add group title with larger font + legendGroup.append("text") + .attr("x", 150) + .attr("y", 0) + .attr("text-anchor", "middle") + .attr("fill", "white") + .style("font-size", "56px") // Increased from 48px + .style("font-weight", "bold") + .text(group.title); + + + // Add legend items + const items = legendGroup.selectAll(".legend-item") + .data(group.items) + .enter() + .append("g") + .attr("class", "legend-item") + .attr("transform", (d, i) => `translate(0, ${i * itemSpacing + 50})`); // Increased spacing + + + // Add colored rectangles (slightly larger) + items.append("rect") + .attr("width", 55) // Increased from 50 + .attr("height", 55) // Increased from 50 + .attr("rx", 4) + .attr("fill", d => d.color); + + + // Add labels with larger font + items.append("text") + .attr("x", 75) + .attr("y", 40) + .attr("fill", "white") + .style("font-size", "44px") // Increased from 36px + .text(d => d.label); + + + // Update currentY for next group + currentY += (group.items.length * itemSpacing) + groupSpacing + 60; + }); + + + } catch (error) { + console.error('Error loading CSV data:', error); + } + }; + + + fetchData(); +}, [filters, onNodeSelect, onElementHover]); + + +return ( +
+ +
+); +}; + + +export default SankeyChart; diff --git a/Homework3/vkosuri/React-Template/src/main.tsx b/Homework3/vkosuri/React-Template/src/main.tsx new file mode 100644 index 0000000..87e2790 --- /dev/null +++ b/Homework3/vkosuri/React-Template/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.tsx' +import './style.css' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/Homework3/vkosuri/React-Template/src/stores/Reducer.ts b/Homework3/vkosuri/React-Template/src/stores/Reducer.ts new file mode 100644 index 0000000..0160617 --- /dev/null +++ b/Homework3/vkosuri/React-Template/src/stores/Reducer.ts @@ -0,0 +1,25 @@ +// Define action types +export const ACTIONS = { INCREMENT: 'increment' }; + +interface Action{ + type: string; +} + +interface State{ + count: number; +} + +// Define initial state +const initialState: State = { count: 0 }; + +// reducer function +export const reducer = (state: State, action: Action) => { + switch (action.type) { + case ACTIONS.INCREMENT: + return { count: state.count + 1 }; + default: + return state; + } +} + +export type { State, Action }; \ No newline at end of file diff --git a/Homework3/vkosuri/React-Template/src/style.css b/Homework3/vkosuri/React-Template/src/style.css new file mode 100644 index 0000000..f0dbf07 --- /dev/null +++ b/Homework3/vkosuri/React-Template/src/style.css @@ -0,0 +1,280 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body, html { + background-color: #000; + color: #fff; + overflow: hidden; + font-family: 'Roboto', sans-serif; + height: 100%; +} + +/* Dashboard container */ +.dashboard { + width: 100vw; + height: 100vh; + background-color: #000; + display: flex; + flex-direction: column; + padding: 5px; +} + +/* Header - exactly 5% */ +.dashboard-header { + height: 5%; + display: flex; + align-items: center; + justify-content: center; + padding: 5px; +} + +.dashboard-header h1 { + font-size: 1.3rem; + color: #fff; + font-weight: normal; + white-space: nowrap; + text-align: center; + letter-spacing: 0.5px; + opacity: 0.95; +} + +/* Active Filters Section */ +.active-filters { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: center; + justify-content: center; +} + +.filter-tag { + display: inline-flex; + align-items: center; + background: rgba(255, 255, 255, 0.1); + padding: 4px 8px; + border-radius: 4px; + font-size: 0.8rem; +} + +.filter-tag:hover { + background: rgba(255, 255, 255, 0.2); +} + +.tag-label { + color: rgba(255, 255, 255, 0.7); + margin-right: 4px; +} + +.tag-value { + font-weight: 500; +} + +.remove-tag { + background: none; + border: none; + color: rgba(255, 255, 255, 0.6); + margin-left: 6px; + cursor: pointer; + padding: 0 4px; +} + +.remove-tag:hover { + color: #fff; +} + +/* Main layout */ +.dashboard-layout { + height: 95%; + display: flex; + flex-direction: column; + gap: 5px; +} + +/* Top section - Sankey (61%) */ +.top-section { + height: 61%; + min-height: 61%; +} + +.top-section .chart-container { + width: 100%; + height: 100%; + padding: 10px 20px; +} + +/* Bottom section - Bar and Pie (34% total) */ +.bottom-section { + height: 34%; + display: flex; + gap: 5px; +} + +/* Chart containers */ +.chart-container { + background-color: #000; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + padding: 10px; + height: 100%; + display: flex; + flex-direction: column; +} + +/* Half width for bottom charts */ +.half-width { + width: 50%; +} + +/* Chart titles */ +.chart-container h2 { + font-size: 0.9rem; + color: #fff; + margin-bottom: 8px; + font-weight: normal; + white-space: nowrap; + opacity: 0.9; +} + +/* Chart wrappers */ +.chart-wrapper { + flex: 1; + min-height: 0; + position: relative; +} + +/* Specific wrapper styles */ +.sankey-wrapper { + width: 100%; + height: 100%; +} + +.bar-wrapper { + height: 50% !important; + margin: auto 0; +} + +.pie-wrapper { + height: 100%; +} + +/* SVG styling */ +svg { + width: 100%; + height: 100%; + background-color: #000; +} + +/* Navigation buttons */ +.back-to-overview { + background: rgba(255, 255, 255, 0.1); + border: none; + color: #fff; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; +} + +.back-to-overview:hover { + background: rgba(255, 255, 255, 0.2); +} + +/* Tooltip */ +.tooltip { + position: fixed; + background-color: rgba(0, 0, 0, 0.95); + border: 1px solid rgba(255, 255, 255, 0.2); + color: white; + padding: 12px; + border-radius: 6px; + pointer-events: none; + font-size: 0.8rem; + z-index: 1000; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.tooltip-header { + margin-bottom: 8px; + padding-bottom: 4px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.tooltip-body { + display: flex; + flex-direction: column; + gap: 4px; +} + +.stat-row { + display: flex; + justify-content: space-between; + gap: 20px; +} + +/* Chart axis and text colors */ +.axis text { + fill: #fff; +} + +.axis line, .axis path { + stroke: rgba(255, 255, 255, 0.2); +} + +/* Legend styling */ +.legend text { + fill: #fff; + font-size: 0.8rem; +} + +/* Sankey specific styles */ +.sankey-node text { + fill: #fff; + font-size: 0.8rem; +} + +.sankey-link { + fill: none; + stroke-opacity: 0.3; + transition: stroke-opacity 0.3s ease; +} + +/* Interactive elements */ +.interactive-element { + cursor: pointer; + transition: opacity 0.3s ease; +} + +/* Transitions */ +.transitioning { + opacity: 0.5; + pointer-events: none; +} + +/* Remove scrollbars */ +::-webkit-scrollbar { + display: none; +} + +/* Responsive font sizes */ +@media (max-width: 1280px) { + .dashboard-header h1 { + font-size: 1.1rem; + } + + .chart-container h2 { + font-size: 0.8rem; + } +} + +@media (min-width: 1920px) { + .dashboard-header h1 { + font-size: 1.5rem; + } + + .chart-container h2 { + font-size: 1rem; + } +} \ No newline at end of file diff --git a/Homework3/vkosuri/React-Template/src/types.ts b/Homework3/vkosuri/React-Template/src/types.ts new file mode 100644 index 0000000..bf460ce --- /dev/null +++ b/Homework3/vkosuri/React-Template/src/types.ts @@ -0,0 +1,232 @@ +// Base layout interfaces +export interface Margin { + readonly left: number; + readonly right: number; + readonly top: number; + readonly bottom: number; +} + +export interface ComponentSize { + width: number; + height: number; +} + +export interface Point { + readonly posX: number; + readonly posY: number; +} + +export interface Bar { + readonly value: number; +} + +// Filter and Hover State Management +export interface FilterState { + selectedAge: string | null; + selectedCondition: string | null; + selectedTreatment: string | null; +} + +export interface HoverState { + element: { + type: 'node' | 'link' | 'bar' | 'pie'; + category?: string; + name: string; + value: number; + percentage?: number; + coordinates: { + x: number; + y: number; + }; + details?: Record; + } | null; +} + +// Data Structures +export interface StudentData { + Age: string; + "Choose your gender": string; + "What is your course?": string; + "Your current year of Study": string; + "What is your CGPA?": string; + "Do you have Depression?": string; + "Do you have Anxiety?": string; + "Do you have Panic attack?": string; + "Did you seek any specialist for a treatment?": string; +} + +export interface ProcessedData { + sankeyData: { + nodes: NodeData[]; + links: LinkData[]; + }; + barData: EnhancedBar[]; + pieData: { + male: PieSegment[]; + female: PieSegment[]; + }; + rawData: StudentData[]; +} + +// Sankey Chart Types +export interface NodeData { + name: string; + category: 'age' | 'condition' | 'treatment'; + index?: number; + x0?: number; + x1?: number; + y0?: number; + y1?: number; + value?: number; +} + +export interface LinkData { + source: number | NodeData; + target: number | NodeData; + value: number; + width?: number; +} + +// Bar Chart Types +export interface EnhancedBar extends Bar { + category: string; + label: string; + color: string; + percentage: number; + condition?: string; +} + +// Pie Chart Types +export interface PieSegment { + id: string; + value: number; + label: string; + percentage: number; + color: string; +} + +export interface GenderData { + male: MentalHealthCount[]; + female: MentalHealthCount[]; +} + +export interface MentalHealthCount { + condition: string; + count: number; + percentage: number; +} + +// Component Props +export interface ChartProps { + filters: FilterState; + onElementHover: (element: HoverState['element'] | null, event: React.MouseEvent) => void; + isTransitioning: boolean; +} + +export interface SankeyProps extends ChartProps { + onNodeSelect: (node: NodeData) => void; +} + +// Color Schemes +export interface ColorScheme { + age: { + [key: string]: string; + }; + condition: { + [key: string]: string; + }; + treatment: { + [key: string]: string; + }; +} + +// Chart Configuration +export interface ChartDimensions extends ComponentSize { + margin: Margin; + boundedWidth: number; + boundedHeight: number; +} + +export interface TooltipConfig { + position: Point; + content: { + title: string; + values: Array<{ + label: string; + value: string | number; + }>; + }; +} + +// Animation Configuration +export interface TransitionConfig { + duration: number; + ease: string; +} + +// Event Handlers +export interface ChartEventHandlers { + onHover?: (element: HoverState['element'] | null, event: React.MouseEvent) => void; + onClick?: (element: any) => void; + onMouseLeave?: () => void; +} + +// Chart Scales +export interface ChartScales { + xScale: any; // d3 scale + yScale: any; // d3 scale + colorScale: any; // d3 scale +} + +// Legend Configuration +export interface LegendConfig { + position: Point; + orientation: 'horizontal' | 'vertical'; + items: Array<{ + label: string; + color: string; + }>; +} + +// Update Triggers +export interface UpdateTriggers { + data?: boolean; + dimensions?: boolean; + scales?: boolean; + filters?: boolean; +} + +// Accessibility Configuration +export interface AccessibilityConfig { + title: string; + description: string; + ariaLabel: string; +} + +// Data Processing Types +export interface DataProcessor { + processData: (data: StudentData[]) => ProcessedData; + filterData: (data: ProcessedData, filters: FilterState) => ProcessedData; +} + +// Chart State +export interface ChartState { + data: ProcessedData | null; + dimensions: ChartDimensions | null; + scales: ChartScales | null; + isLoading: boolean; + error: string | null; +} + +// Theme Configuration +export interface ThemeConfig { + colors: ColorScheme; + fonts: { + primary: string; + secondary: string; + }; + spacing: { + padding: number; + margin: number; + }; +} \ No newline at end of file diff --git a/Homework3/vkosuri/React-Template/src/vite-env.d.ts b/Homework3/vkosuri/React-Template/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/Homework3/vkosuri/React-Template/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/Homework3/vkosuri/React-Template/tsconfig.json b/Homework3/vkosuri/React-Template/tsconfig.json new file mode 100644 index 0000000..3b32131 --- /dev/null +++ b/Homework3/vkosuri/React-Template/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable", "es6"], + "module": "ESNext", + "skipLibCheck": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + }, + "include": ["src", "vite.config.ts"] +} diff --git a/Homework3/vkosuri/React-Template/vite.config.ts b/Homework3/vkosuri/React-Template/vite.config.ts new file mode 100644 index 0000000..198ec17 --- /dev/null +++ b/Homework3/vkosuri/React-Template/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + server: { + port: 3000, + }, + plugins: [react()], +})