diff --git a/django_project/dashboard/serializers.py b/django_project/dashboard/serializers.py index 7b9ae8c4..dde64bc1 100644 --- a/django_project/dashboard/serializers.py +++ b/django_project/dashboard/serializers.py @@ -1,8 +1,26 @@ +from analysis.serializer import UserAnalysisResultsSerializer from rest_framework import serializers from .models import Dashboard class DashboardSerializer(serializers.ModelSerializer): + owner = serializers.SerializerMethodField() + analysis_results = UserAnalysisResultsSerializer(many=True) + class Meta: model = Dashboard - fields = '__all__' + fields = [ + 'uuid', + 'title', + 'created_by', + 'privacy_type', + 'config', + 'created_at', + 'updated_at', + 'owner', + 'analysis_results', + ] + + def get_owner(self, obj): + user = self.context['request'].user + return obj.created_by == user diff --git a/django_project/frontend/package-lock.json b/django_project/frontend/package-lock.json index b1389613..b5e3e0f6 100644 --- a/django_project/frontend/package-lock.json +++ b/django_project/frontend/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@chakra-ui/icons": "^2.2.4", - "@chakra-ui/react": "^2.8.2", + "@chakra-ui/react": "^2.10.5", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mapbox/mapbox-gl-draw": "^1.5.0", @@ -25,7 +25,7 @@ "@turf/combine": "^7.2.0", "@types/mapbox__mapbox-gl-draw": "^1.4.8", "axios": "^1.7.7", - "chart.js": "^4.3.3", + "chart.js": "^4.4.7", "chartjs-2": "^0.0.1-security", "chartjs-adapter-date-fns": "^3.0.0", "chartjs-chart-error-bars": "^4.3.3", @@ -37,13 +37,15 @@ "mini-css-extract-plugin": "^2.9.2", "pmtiles": "^3.2.1", "react": "^18.3.1", - "react-chartjs-2": "^5.2.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", + "react-draggable": "^4.4.6", "react-helmet": "^6.1.0", "react-icons": "^5.3.0", "react-pro-sidebar": "^1.1.0", "react-redux": "^8.1.3", "react-refresh": "^0.14.2", + "react-resizable": "^3.0.5", "react-router-dom": "6.0.2", "react-router-hash-link": "^2.4.3", "react-scripts": "5.0.1", @@ -62,6 +64,7 @@ "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/react-helmet": "^6.1.11", + "@types/react-resizable": "^3.0.8", "@types/styled-components": "^5.1.34", "clean-webpack-plugin": "^4.0.0", "css-loader": "^6.7.3", @@ -2046,9 +2049,9 @@ } }, "node_modules/@chakra-ui/react": { - "version": "2.10.4", - "resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-2.10.4.tgz", - "integrity": "sha512-XyRWnuZ1Uw7Mlj5pKUGO5/WhnIHP/EOrpy6lGZC1yWlkd0eIfIpYMZ1ALTZx4KPEdbBaes48dgiMT2ROCqLhkA==", + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-2.10.5.tgz", + "integrity": "sha512-wCetfxT1iXWNmAwSLF1d0zyTQOQYswA3YJcw05grY1XEngO6956PScRB0Or5ZQJ6rGMcz5pcUhVgrb3q7AE+gQ==", "dependencies": { "@chakra-ui/hooks": "2.4.3", "@chakra-ui/styled-system": "2.12.1", @@ -4577,6 +4580,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-resizable": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-3.0.8.tgz", + "integrity": "sha512-Pcvt2eGA7KNXldt1hkhVhAgZ8hK41m0mp89mFgQi7LAAEZiaLgm4fHJ5zbJZ/4m2LVaAyYrrRRv1LHDcrGQanA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -6416,6 +6428,14 @@ "node": ">=6" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -14878,6 +14898,19 @@ "react": "^18.3.1" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", @@ -15048,6 +15081,18 @@ } } }, + "node_modules/react-resizable": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", + "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.0.3" + }, + "peerDependencies": { + "react": ">= 16.3" + } + }, "node_modules/react-router": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.0.2.tgz", diff --git a/django_project/frontend/package.json b/django_project/frontend/package.json index 2465fbaa..d57af746 100644 --- a/django_project/frontend/package.json +++ b/django_project/frontend/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@chakra-ui/icons": "^2.2.4", - "@chakra-ui/react": "^2.8.2", + "@chakra-ui/react": "^2.10.5", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mapbox/mapbox-gl-draw": "^1.5.0", @@ -20,7 +20,7 @@ "@turf/combine": "^7.2.0", "@types/mapbox__mapbox-gl-draw": "^1.4.8", "axios": "^1.7.7", - "chart.js": "^4.3.3", + "chart.js": "^4.4.7", "chartjs-2": "^0.0.1-security", "chartjs-adapter-date-fns": "^3.0.0", "chartjs-chart-error-bars": "^4.3.3", @@ -32,13 +32,15 @@ "mini-css-extract-plugin": "^2.9.2", "pmtiles": "^3.2.1", "react": "^18.3.1", - "react-chartjs-2": "^5.2.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", + "react-draggable": "^4.4.6", "react-helmet": "^6.1.0", "react-icons": "^5.3.0", "react-pro-sidebar": "^1.1.0", "react-redux": "^8.1.3", "react-refresh": "^0.14.2", + "react-resizable": "^3.0.5", "react-router-dom": "6.0.2", "react-router-hash-link": "^2.4.3", "react-scripts": "5.0.1", @@ -85,6 +87,7 @@ "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/react-helmet": "^6.1.11", + "@types/react-resizable": "^3.0.8", "@types/styled-components": "^5.1.34", "clean-webpack-plugin": "^4.0.0", "css-loader": "^6.7.3", diff --git a/django_project/frontend/src/components/ChartCard.tsx b/django_project/frontend/src/components/ChartCard.tsx new file mode 100644 index 00000000..f98ca51f --- /dev/null +++ b/django_project/frontend/src/components/ChartCard.tsx @@ -0,0 +1,264 @@ +import React, { useState } from "react"; +import { Card, CardBody, Text, Box, Button, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, Input, Select, VStack } from "@chakra-ui/react"; +import { ResizableBox } from "react-resizable"; +import Draggable from "react-draggable"; +import LineChart from "./DashboardCharts/LineChart"; +import BarChart from "./DashboardCharts/BarChart"; +import PieChart from "./DashboardCharts/PieChart"; + + + function purifyApiData(apiData: any[]) { + if (!Array.isArray(apiData)) { + throw new Error("Input data should be an array."); + } + + return apiData.map((item) => { + const { analysis_results } = item; + + if (!analysis_results || !analysis_results.data || !analysis_results.results) { + throw new Error("Invalid data structure."); + } + + const { + latitude, + community, + landscape, + longitude, + analysisType, + } = analysis_results.data; + + const { + id, + type, + columns, + version, + features, + } = analysis_results.results; + + const purifiedFeatures = features.map((feature: { id: any; type: any; geometry: any; properties: any; }) => { + const { + id: featureId, + type: featureType, + geometry, + properties, + } = feature; + + return { + id: featureId, + type: featureType, + geometry: geometry || {}, + properties: { + EVI: parseFloat(properties.EVI.toFixed(2)), + NDVI: parseFloat(properties.NDVI.toFixed(2)), + Name: properties.Name, + area: parseFloat(properties.area.toFixed(0)), + "SOC kg/m2": parseFloat(properties["SOC kg/m2"].toFixed(2)), + }, + }; + }); + + return { + data: { + latitude, + community, + landscape, + longitude, + analysisType, + }, + results: { + id, + type, + columns: { + EVI: columns.EVI, + NDVI: columns.NDVI, + Name: columns.Name, + area: columns.area, + "SOC kg/m2": columns["SOC kg/m2"], + }, + version, + features: purifiedFeatures, + }, + }; + }); + } + + + +interface ChartCardProps { + config: { + config: { + dashboardName: string; + preference: string; + chartType: string; + title: string; + data: any; + downloadData: string; + owner: boolean; + } + analysisResults: any[]; + + }; + className?: string; +} + +const ChartCard: React.FC = ({ config, className }) => { + const [cardWidth, setCardWidth] = useState(500); + const [cardHeight, setCardHeight] = useState(500); + const [isSettingsOpen, setSettingsOpen] = useState(false); + const [newWidth, setNewWidth] = useState(cardWidth); + const [newHeight, setNewHeight] = useState(cardHeight); + const [chartType, setChartType] = useState(config.config.chartType); + const [dashboardName, setDashboardName] = useState(config.config.dashboardName); + + // Check if config.chartType is "chart" or "map" + const isChart = config.config.preference === "chart"; + console.log('config ',config.config) + + + const getChartComponent = () => { + if (!isChart) { + return ( + + + Coming Soon + + The map feature is not available yet. + + ); + } + + // If chart type is defined, render corresponding chart component + const data = purifyApiData(config.analysisResults); + + + switch (chartType) { + case "bar": + return ; + case "pie": + return ; + default: + return ; + } + }; + + const handleResize = (event: any, { size }: any) => { + setCardWidth(size.width); + setCardHeight(size.height); + }; + + const handleDownload = () => { + const element = document.createElement("a"); + const file = new Blob([config.config.downloadData], { type: "text/plain" }); + element.href = URL.createObjectURL(file); + element.download = "chart-data.txt"; + document.body.appendChild(element); + element.click(); + }; + + const handleSettingsSave = () => { + setCardWidth(newWidth); + setCardHeight(newHeight); + setSettingsOpen(false); + }; + + return ( + +
+ + + + + + {dashboardName} {config.config.owner && "(Owner)"} + + {getChartComponent()} + + + + + + + + + + {/* Settings Modal */} + setSettingsOpen(false)}> + + + Dashboard Settings + + + + + Dashboard Name + setDashboardName(e.target.value)} + placeholder="Enter dashboard name" + /> + + + Width + setNewWidth(Number(e.target.value))} + min={150} + max={500} + /> + + + Height + setNewHeight(Number(e.target.value))} + min={150} + max={500} + /> + + + Chart Type + + + + + + + +
+
+ ); +}; + +export default ChartCard; diff --git a/django_project/frontend/src/components/DashboardCharts/BarChart.tsx b/django_project/frontend/src/components/DashboardCharts/BarChart.tsx new file mode 100644 index 00000000..72bca6eb --- /dev/null +++ b/django_project/frontend/src/components/DashboardCharts/BarChart.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import { Bar } from "react-chartjs-2"; +import { + Chart as ChartJS, + BarElement, + CategoryScale, + LinearScale, + Tooltip, + Legend, +} from "chart.js"; + +ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend); + +type DataObject = { + data: { + latitude: number; + community: string; + landscape: string; + longitude: number; + analysisType: string; + }; + results: { + id: string; + type: string; + columns: { [key: string]: string }; + version: number; + features: { + id: string; + type: string; + geometry?: any[]; + properties: { [key: string]: any }; + }[]; + }; +}; + +type BarChartProps = { + inputData: DataObject; +}; + +const BarChart: React.FC = ({ inputData }) => { + const features = inputData.results.features; + + // Extract property keys for bar chart labels + const propertyKeys = Object.keys(features[0]?.properties || {}); + + // Prepare chart data + const chartData = { + labels: propertyKeys.filter((key) => typeof features[0].properties[key] === "number"), + datasets: features.map((feature, index) => ({ + label: feature.properties.Name || `Feature ${index + 1}`, + data: propertyKeys + .filter((key) => typeof feature.properties[key] === "number") + .map((key) => feature.properties[key]), + backgroundColor: `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor( + Math.random() * 255 + )}, ${Math.floor(Math.random() * 255)}, 0.6)`, + borderColor: "rgba(0, 0, 0, 0.8)", + borderWidth: 1, + })), + }; + + return ( + + ); +}; + +export default BarChart; diff --git a/django_project/frontend/src/components/DashboardCharts/LineChart.tsx b/django_project/frontend/src/components/DashboardCharts/LineChart.tsx new file mode 100644 index 00000000..7a9a0800 --- /dev/null +++ b/django_project/frontend/src/components/DashboardCharts/LineChart.tsx @@ -0,0 +1,104 @@ +import React from "react"; +import { Line } from "react-chartjs-2"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +} from "chart.js"; + +// Register chart.js modules +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend +); + +interface DataObject { + data: { + latitude: number; + community: string; + landscape: string; + longitude: number; + analysisType: string; + }; + results: { + id: string; + type: string; + columns: { [key: string]: string }; + version: number; + features: Array<{ + id: string; + type: string; + geometry: any; + properties: { [key: string]: any }; + }>; + }; +} + +interface Props { + inputData: DataObject; +} + +const LineChart: React.FC = ({ inputData }) => { + const { features, columns } = inputData.results; + + // Extract labels (feature names or IDs) + const labels = features.map((feature, index) => feature.properties.Name || `Feature ${index + 1}`); + + // Dynamically create datasets for each numerical column + const numericKeys = Object.keys(columns).filter( + (key) => columns[key] === "Float" || columns[key] === "Integer" + ); + + const datasets = numericKeys.map((key) => ({ + label: key, // Label for the dataset (e.g., "NDVI", "EVI") + data: features.map((feature) => feature.properties[key] || 0), // Extract data for the key from all features + borderColor: getRandomColor(), + backgroundColor: getRandomColor(0.2), + borderWidth: 2, + fill: true, + })); + + // Chart data configuration + const data = { + labels, // Use feature names or IDs as labels + datasets, // Dynamically generated datasets + }; + + // Chart options + const options = { + responsive: true, + plugins: { + legend: { + position: "top" as const, + }, + title: { + display: true, + text: "Dynamic Feature Metrics Comparison", + }, + }, + }; + + // Utility function to generate random colors + function getRandomColor(alpha = 1): string { + const r = Math.floor(Math.random() * 255); + const g = Math.floor(Math.random() * 255); + const b = Math.floor(Math.random() * 255); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; + } + + return ( + + ); +}; + +export default LineChart; diff --git a/django_project/frontend/src/components/DashboardCharts/PieChart.tsx b/django_project/frontend/src/components/DashboardCharts/PieChart.tsx new file mode 100644 index 00000000..ba6c0535 --- /dev/null +++ b/django_project/frontend/src/components/DashboardCharts/PieChart.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import { Pie } from "react-chartjs-2"; +import { + Chart as ChartJS, + ArcElement, + Tooltip, + Legend, +} from "chart.js"; + +ChartJS.register(ArcElement, Tooltip, Legend); + +type DataObject = { + data: { + latitude: number; + community: string; + landscape: string; + longitude: number; + analysisType: string; + }; + results: { + id: string; + type: string; + columns: { [key: string]: string }; + version: number; + features: { + id: string; + type: string; + geometry?: any; + properties: { [key: string]: any }; + }[]; + }; +}; + +type PieChartProps = { + inputData: DataObject; +}; + +const PieChart: React.FC = ({ inputData }) => { + const features = inputData.results.features; + + // Combine the numeric properties from all features for the pie chart + const numericProperties = features.reduce<{ [key: string]: number }>((acc, feature) => { + Object.entries(feature.properties).forEach(([key, value]) => { + if (typeof value === "number") { + acc[key] = (acc[key] || 0) + value; + } + }); + return acc; + }, {}); + + const chartData = { + labels: Object.keys(numericProperties), + datasets: [ + { + data: Object.values(numericProperties), + backgroundColor: Object.keys(numericProperties).map( + () => + `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor( + Math.random() * 255 + )}, ${Math.floor(Math.random() * 255)}, 0.6)` + ), + borderColor: "rgba(0, 0, 0, 0.8)", + borderWidth: 1, + }, + ], + }; + + return ( + + ); +}; + +export default PieChart; diff --git a/django_project/frontend/src/pages/Dashboard/index.tsx b/django_project/frontend/src/pages/Dashboard/index.tsx index 48be14e1..341a30ad 100644 --- a/django_project/frontend/src/pages/Dashboard/index.tsx +++ b/django_project/frontend/src/pages/Dashboard/index.tsx @@ -21,6 +21,7 @@ import { DrawerContent, DrawerCloseButton, useDisclosure, + Grid, } from "@chakra-ui/react"; import { FaFilter } from "react-icons/fa"; import { ChevronLeftIcon, ChevronRightIcon } from "@chakra-ui/icons"; @@ -29,6 +30,12 @@ import { Helmet } from "react-helmet"; import Footer from "../../components/Footer"; import AnalysisSideBar from "../../components/SideBar/AnalysisSideBar"; import Pagination from "../../components/Pagination"; +import ChartCard from "../../components/ChartCard"; +import { useDispatch, useSelector } from "react-redux"; +import { AppDispatch } from "../../store"; +import { fetchDashboards } from "../../store/dashboardSlice"; + + const DashboardPage: React.FC = () => { const [currentPage, setCurrentPage] = useState(1); @@ -39,18 +46,44 @@ const DashboardPage: React.FC = () => { const { isOpen, onOpen, onClose } = useDisclosure(); const [isAnalysisOpen, setIsAnalysisOpen] = useState(false); const [data, setData] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const dispatch = useDispatch(); + const dashboardData = useSelector((state: any) => state.dashboard.dashboards); + const loading = useSelector((state: any) => state.dashboard.loading); + const error = useSelector((state: any) => state.dashboard.error); + const [resourcesCount, setResourcesCount] = useState(0); + const [chartsConfig, setChartsConfig] = useState([]); + useEffect(() => { + if (!loading && Array.isArray(dashboardData)) { + const updatedChartsConfig = dashboardData.map((dashboard) => ({ + config: dashboard.config, + analysisResults: dashboard.analysis_results, + title: dashboard.title, + uuid: dashboard.uuid, + owner: dashboard.owner, + })); + + // Set the state to pass down to the chart cards + setChartsConfig(updatedChartsConfig); + } + }, [loading, dashboardData]); + + + useEffect(() => { + dispatch(fetchDashboards()); + }, [dispatch]); - const fetchData = () => { - setIsLoading(true); - setData([]); - setIsLoading(false);; - }; + useEffect(() => { - fetchData(); - }, []); + if(!loading){ + console.log(dashboardData) + setResourcesCount(Array.isArray(dashboardData)? dashboardData.length: 0) + } + + }, [loading]); + + const handlePageChange = (page: number) => { if (page >= 1 && page <= totalPages) { @@ -120,7 +153,7 @@ const DashboardPage: React.FC = () => { Filter - 0 resources found + {resourcesCount} resources found. @@ -157,9 +190,26 @@ const DashboardPage: React.FC = () => { + + {chartsConfig.map((config, index) => ( + + ))} + + {/* Cards Section */} - {isLoading ? ( + {loading ? ( Loading... ) : ( data