diff --git a/django_project/analysis/views.py b/django_project/analysis/views.py index dcc74b8d..f8db5f94 100644 --- a/django_project/analysis/views.py +++ b/django_project/analysis/views.py @@ -12,7 +12,10 @@ class UserAnalysisResultsViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] def perform_create(self, serializer): - serializer.save(created_by=self.request.user) + serializer.save( + created_by=self.request.user, + analysis_results=self.request.data.get('analysis_results') + ) @action(detail=False, methods=['get']) def fetch_analysis_results(self, request): diff --git a/django_project/dashboard/admin.py b/django_project/dashboard/admin.py index c533a5cf..e4ae343b 100644 --- a/django_project/dashboard/admin.py +++ b/django_project/dashboard/admin.py @@ -4,18 +4,34 @@ @admin.register(Dashboard) class DashboardAdmin(admin.ModelAdmin): - list_display = ('uuid', 'privacy_type', 'created_at', 'updated_at') + list_display = ( + 'uuid', + 'privacy_type', + 'created_at', + 'updated_at', + 'linked_analysis_results' + ) list_filter = ('privacy_type', 'created_at', 'updated_at') - search_fields = ('uuid',) - filter_horizontal = ('organisations', 'groups', 'users') + search_fields = ('uuid', 'title') + filter_horizontal = ( + 'organisations', + 'groups', + 'users', + 'analysis_results' + ) readonly_fields = ('created_at', 'updated_at') fieldsets = ( (None, { - 'fields': ('privacy_type',) + 'fields': ('privacy_type', 'title') }), ('Associations', { - 'fields': ('organisations', 'groups', 'users') + 'fields': ( + 'organisations', + 'groups', + 'users', + 'analysis_results' + ) }), ('Configuration', { 'fields': ('config',) @@ -24,3 +40,15 @@ class DashboardAdmin(admin.ModelAdmin): 'fields': ('created_at', 'updated_at') }), ) + + def linked_analysis_results(self, obj): + """ + Display the associated UserAnalysisResults in a human-readable format. + """ + results = obj.analysis_results.all() + return ( + ', '.join([str(result) for result in results]) + if results else "None" + ) + + linked_analysis_results.short_description = "Linked Analysis Results" diff --git a/django_project/frontend/src/components/ChartCard.tsx b/django_project/frontend/src/components/ChartCard.tsx index f98ca51f..29df1dac 100644 --- a/django_project/frontend/src/components/ChartCard.tsx +++ b/django_project/frontend/src/components/ChartCard.tsx @@ -5,83 +5,8 @@ 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, - }, - }; - }); - } - +import { RenderResult } from "./DashboardCharts/CombinedCharts"; +import { Analysis } from "../store/analysisSlice"; interface ChartCardProps { @@ -112,7 +37,6 @@ const ChartCard: React.FC = ({ config, className }) => { // Check if config.chartType is "chart" or "map" const isChart = config.config.preference === "chart"; - console.log('config ',config.config) const getChartComponent = () => { @@ -126,20 +50,33 @@ const ChartCard: React.FC = ({ config, className }) => { ); } - - // If chart type is defined, render corresponding chart component - const data = purifyApiData(config.analysisResults); - - - switch (chartType) { - case "bar": - return ; - case "pie": - return ; - default: - return ; + + try { + + // switch (chartType) { + // case "bar": + // return ; + // case "pie": + // return ; + // default: + // return ; + // } + return + } catch (error) { + console.error("Error processing data:", error); + + // Fallback card in case of invalid data + return ( + + + Data Processing Error + + We couldn't process the data for this chart. Please check the data source or try again later. + + ); } }; + const handleResize = (event: any, { size }: any) => { setCardWidth(size.width); @@ -163,7 +100,7 @@ const ChartCard: React.FC = ({ config, className }) => { return ( -
+
= ({ config, className }) => { onResizeStop={handleResize} resizeHandles={["se", "s", "e"]} > - + + @@ -200,7 +138,9 @@ const ChartCard: React.FC = ({ config, className }) => { + + {/* Settings Modal */} setSettingsOpen(false)}> @@ -244,9 +184,9 @@ const ChartCard: React.FC = ({ config, className }) => { value={chartType} onChange={(e) => setChartType(e.target.value)} > - - - + + +
+
); }; diff --git a/django_project/frontend/src/components/DashboardCharts/CombinedCharts.tsx b/django_project/frontend/src/components/DashboardCharts/CombinedCharts.tsx new file mode 100644 index 00000000..7a1d9c3f --- /dev/null +++ b/django_project/frontend/src/components/DashboardCharts/CombinedCharts.tsx @@ -0,0 +1,256 @@ +import React from 'react'; +import { Box, Center, Spinner, Table, Text } from "@chakra-ui/react"; +import { Analysis } from '../../store/analysisSlice'; +import { Bar, Line } from "react-chartjs-2"; +import { CategoryScale } from "chart.js"; +import Chart from "chart.js/auto"; +import ChartDataLabels from "chartjs-plugin-datalabels"; +import {FeatureCollection} from "geojson"; +import 'chartjs-adapter-date-fns'; + + +Chart.register(CategoryScale); + +interface Props { + analysis: Analysis; +} + +export function BarChart({ analysis }: Props) { + // Extracting data for the chart + + const jsonData = analysis.results[0]; + + let labels: number[] = [jsonData.features[0].properties.year]; + if (jsonData.features.length > 1) { + labels.push(jsonData.features[jsonData.features.length -1].properties.year); + } + const name1 = jsonData.features[0].properties.Name; + const name2 = jsonData.features.length > 1 ? jsonData.features[1].properties.Name : null; + + const dataBar1 = jsonData.features + .filter((feature:any) => feature.properties.Name === name1) + .map((feature:any) => feature.properties[analysis.data.variable]); + + let chartData:any = { + labels, + datasets: [ + { + label: name1, + data: dataBar1, + backgroundColor: "blue" + } + ], + }; + + if (name2 !== null && name1 != name2) { + const dataBar2 = jsonData.features + .filter((feature:any) => feature.properties.Name === name2) + .map((feature:any) => feature.properties[analysis.data.variable]); + + chartData.datasets.push({ + label: name2, + data: dataBar2, + backgroundColor: "red" + }); + } + + const options:any = { + responsive: true, + plugins: { + legend: { + position: "top", + }, + title: { + display: false, + }, + }, + scales: { + y: { + beginAtZero: true, + }, + } + }; + + return +} + +export function LineChart({ analysis }: Props) { + // Extracting data for the chart + + const jsonData = analysis.results[1]; + + const name1 = jsonData.features[0].properties.Name; + const name2 = jsonData.features[1].properties.Name; + const labels: number[] = jsonData.features + .filter((feature:any) => feature.properties.Name === name1) + .map((feature:any) => feature.properties.date); + + const data1 = jsonData.features + .filter((feature:any) => feature.properties.Name === name1) + .map((feature:any) => feature.properties[analysis.data.variable]); + + let chartData:any = { + labels, + datasets: [ + { + label: name1, + data: data1, + backgroundColor: "blue" + } + ], + }; + + if (name1 != name2) { + const data2 = jsonData.features + .filter((feature:any) => feature.properties.Name === name2) + .map((feature:any) => feature.properties[analysis.data.variable]); + + chartData.datasets.push({ + label: name2, + data: data2, + backgroundColor: "red" + }); + } + + const options:any = { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + type: 'timeseries', + title: { + display: false + }, + ticks: { + callback: function (value: any, index: number, ticks: any) { + const currentLabel = new Date(value).getFullYear(); + const previousLabel = index > 0 ? new Date(ticks[index - 1].value).getFullYear() : null; + return currentLabel !== previousLabel ? currentLabel : ''; + }, + } + }, + y: { + title: { + display: false + }, + }, + }, + plugins: { + legend: { + position: 'top', + }, + }, + }; + + return +} + +function SpatialBarChart({ analysis }: Props) { + const featureCollection: FeatureCollection = analysis.results; + + const labels: string[] = featureCollection.features.map((feature) => feature.properties['Name']) + let chartData:any = { + labels, + datasets: [ + { + label: '% difference to reference area', + data: featureCollection.features.map((feature) => feature.properties["mean"]), + backgroundColor: "blue" + } + ], + }; + + const options:any = { + responsive: true, + plugins: { + legend: { + position: "top", + }, + title: { + display: true, + }, + subtitle: { + display: true, + text: 'Feature (labeled by', + position: 'bottom' + } + }, + scales: { + y: { + beginAtZero: true, + title: { + display: true, + text: 'mean' + } + }, + } + }; + + return +} + + +export function RenderBaseline({ analysis }: Props) { + const keys = Object.keys(analysis.results.columns) + return + + + + { + keys.map( + (column: string) => + ) + } + + { + analysis.results.features.map((feature: any) => { + const properties = feature.properties; + return + + { + keys.map( + (column: string) => + ) + } + + }) + } +
Name{column}
{properties.Name} + {properties[column]} +
+
+} + +export function RenderTemporal({ analysis }: Props) { + return ( + + + + + + + + + ); +} + +export function RenderSpatial({ analysis }: Props) { + return + Relative % difference in {analysis.data.variable} between your reference area and selected camp/s: + + +} + + +export function RenderResult({ analysis }: Props) { + switch (analysis.data.analysisType) { + case "Baseline": + return + case "Temporal": + return + case "Spatial": + return + default: + return null + } +} diff --git a/django_project/frontend/src/pages/AnalysisResults/index.tsx b/django_project/frontend/src/pages/AnalysisResults/index.tsx index 58903870..e7ae75e6 100644 --- a/django_project/frontend/src/pages/AnalysisResults/index.tsx +++ b/django_project/frontend/src/pages/AnalysisResults/index.tsx @@ -182,6 +182,7 @@ export default function AnalysisResults() { {loading && Loading...} + {!loading && !analysisData?.length && No analysis data available.} {error && {error}} {/* Analysis Cards Section */} @@ -195,11 +196,27 @@ export default function AnalysisResults() { > {analysisData?.map((analysis: any, index: number) => { // Generate a meaningful title - const projectName = analysis?.analysis_results?.results?.features[0]?.properties?.Project || "Unknown Project"; - const locationName = analysis?.analysis_results?.results?.features[0]?.properties?.Name || "Unknown Location"; + const analysisResults = analysis?.analysis_results?.results; + const features = analysisResults?.length ? analysisResults[0]?.features : []; + + // Extract properties safely + const projectName = features?.[0]?.properties?.Project || "Unknown Project"; + const locationName = features?.[0]?.properties?.Name || "Coordinates Location"; const analysisType = analysis?.analysis_results?.data?.analysisType || "Analysis"; - // const communityId = analysis?.analysis_results?.data?.community || "Unknown Community"; - const meaningfulTitle = `${analysisType} Analysis of ${locationName} in the ${projectName} Landscape.`; + + // Extract coordinates safely + const latitude = analysis?.analysis_results?.data?.latitude; + const longitude = analysis?.analysis_results?.data?.longitude; + + // Generate a meaningful title + const meaningfulTitle = locationName === "Coordinates Location" && projectName === "Unknown Project" + ? `${analysisType} Results from Area (${latitude}, ${longitude})` + : projectName === "Unknown Project" + ? `${analysisType} Analysis of ${locationName} in the Area (${latitude}, ${longitude})` + : `${analysisType} Analysis of ${locationName} in the ${projectName} Landscape.`; + + + return ( diff --git a/django_project/frontend/src/pages/Dashboard/index.tsx b/django_project/frontend/src/pages/Dashboard/index.tsx index 341a30ad..d023d787 100644 --- a/django_project/frontend/src/pages/Dashboard/index.tsx +++ b/django_project/frontend/src/pages/Dashboard/index.tsx @@ -64,6 +64,7 @@ const DashboardPage: React.FC = () => { })); // Set the state to pass down to the chart cards + setChartsConfig(updatedChartsConfig); } }, [loading, dashboardData]); @@ -192,11 +193,12 @@ const DashboardPage: React.FC = () => { {chartsConfig.map((config, index) => ( { try { const response = await axios.get('/user_analysis_results/fetch_analysis_results/'); - console.log(response.data.data) return response.data; } catch (error) { return rejectWithValue('Error fetching analysis results'); @@ -37,7 +36,10 @@ export const saveAnalysis = createAsyncThunk( 'analysis/saveAnalysis', async (data: any, { rejectWithValue }) => { try { - const response = await axios.post('/user_analysis_results/save_analysis_results/', data); + const payload = { + analysis_results: data, + }; + const response = await axios.post('/user_analysis_results/save_analysis_results/', payload); return response.data; } catch (error) { return rejectWithValue('Error saving analysis results');