Skip to content

Commit

Permalink
Merge pull request #25 from Vizzuality/SKY30-29-fe-implement-the-mari…
Browse files Browse the repository at this point in the history
…ne-conservation-coverage-widget

[SKY30-29][SKY30-52] Marine Conservation Coverage widget
  • Loading branch information
SARodrigues authored Oct 24, 2023
2 parents e080e7e + e428db7 commit 607d15c
Show file tree
Hide file tree
Showing 13 changed files with 1,099 additions and 494 deletions.
8 changes: 6 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"d3-format": "^3.1.0",
"date-fns": "^2.30.0",
"jotai": "2.4.3",
"lodash-es": "^4.17.21",
"lucide-react": "^0.274.0",
"mapbox-gl": "2.15.0",
"next": "13.5.6",
Expand All @@ -52,6 +54,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-map-gl": "7.1.6",
"recharts": "^2.9.0",
"rooks": "7.14.1",
"tailwind-merge": "^1.14.0",
"tailwindcss": "3.2.7",
Expand All @@ -61,8 +64,9 @@
"@types/google.analytics": "0.0.42",
"@types/mapbox__mapbox-gl-draw": "^1.4.2",
"@types/node": "18.15.0",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"@types/recharts": "^1.8.26",
"@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0",
"autoprefixer": "10.4.14",
Expand Down
150 changes: 150 additions & 0 deletions frontend/src/components/charts/conservation-chart/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { useMemo } from 'react';

import {
ComposedChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
ResponsiveContainer,
Cell,
// Tooltip,
ReferenceLine,
Line,
} from 'recharts';

import { cn } from '@/lib/utils';

import ChartLegend from './legend';

// import ChartTooltip from './tooltip';

type ConservationChartProps = {
className?: string;
data: {
year?: number;
percentage: number;
protectedArea: number;
totalArea: number;
active?: boolean;
future?: boolean;
}[];
};

const ConservationChart: React.FC<ConservationChartProps> = ({ className, data }) => {
const firstYearData = data[0];
const lastYearData = data[data?.length - 1];
const activeYearData = data.find(({ active }) => active);
const xAxisTicks = [firstYearData.year, activeYearData.year, lastYearData.year];

const historicalLineData = [
{ year: firstYearData.year, percentage: firstYearData.percentage },
{ year: activeYearData.year + 0.5, percentage: activeYearData.percentage },
];

const projectedPercentage = useMemo(() => {
const numHistoricalYears = activeYearData.year - firstYearData.year;
const numProjectedYears = lastYearData.year - activeYearData.year;

const projectedPercentageChange =
((activeYearData.percentage - firstYearData.percentage) / numHistoricalYears) *
numProjectedYears;

return activeYearData.percentage + projectedPercentageChange;
}, [
activeYearData.percentage,
activeYearData.year,
firstYearData.percentage,
firstYearData.year,
lastYearData.year,
]);

const projectedLineData = [
{ year: activeYearData.year + 0.5, percentage: activeYearData.percentage },
{ year: lastYearData.year, percentage: projectedPercentage },
];

return (
<div className={cn(className, 'text-xs text-black')}>
<ResponsiveContainer>
<ComposedChart data={data}>
<CartesianGrid vertical={false} strokeDasharray="3 3" />
<ReferenceLine
xAxisId={1}
y={30}
label={{ position: 'insideBottomLeft', value: '30x30 Target', fill: '#FD8E28' }}
stroke="#FD8E28"
strokeDasharray="3 3"
/>
<ReferenceLine
xAxisId={1}
x={firstYearData.year - 0.4}
label={{ position: 'insideTopLeft', value: 'Historical', fill: '#000' }}
stroke="#000"
/>
<ReferenceLine
xAxisId={1}
x={activeYearData.year + 0.4}
label={{ position: 'insideTopLeft', value: 'Future Projection', fill: '#000' }}
stroke="#000"
/>
<XAxis
xAxisId={1}
type="number"
dataKey="year"
ticks={xAxisTicks}
domain={[firstYearData.year - 0.4, lastYearData.year]}
/>
<XAxis
xAxisId={2}
type="number"
dataKey="year"
hide={true}
domain={[firstYearData.year, lastYearData.year]}
/>
<YAxis
domain={[0, 55]}
ticks={[0, 15, 30, 45, 55]}
tickFormatter={(value) => `${value}%`}
/>
{/*
// TODO: Investigate tooltip
// Tooltip does not play nice when the Line charts are used (no payload)
<Tooltip content={<ChartTooltip />} />
*/}
<Line
xAxisId={2}
type="monotone"
data={historicalLineData}
strokeWidth={2}
dataKey="percentage"
stroke="#4879FF"
dot={false}
/>
<Line
xAxisId={2}
type="monotone"
data={projectedLineData}
strokeWidth={2}
strokeDasharray="4 4"
dataKey="percentage"
stroke="#4879FF"
dot={false}
/>
<Bar dataKey="percentage" xAxisId={1}>
{data.map((entry, index) => (
<Cell
stroke="black"
fill={entry?.active ? '#4879FF' : 'transparent'}
key={`cell-${index}`}
/>
))}
</Bar>
</ComposedChart>
</ResponsiveContainer>
<ChartLegend />
</div>
);
};

export default ConservationChart;
16 changes: 16 additions & 0 deletions frontend/src/components/charts/conservation-chart/legend/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const ChartLegend = () => {
return (
<div className="ml-8 mt-2 flex justify-between gap-3">
<span className="inline-flex items-center gap-3">
<span className="block h-[2px] w-10 border-b-2 border-blue"></span>
<span>Historical Trend</span>
</span>
<span className="inline-flex items-center gap-3">
<span className="block h-[2px] w-10 border-b-2 border-dashed border-blue"></span>
<span>Future Projection</span>
</span>
</div>
);
};

export default ChartLegend;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { useMemo } from 'react';

import { format } from 'd3-format';

const ChartTooltip = ({ active, payload }) => {
const { percentage, year, protectedArea, totalArea, future } = payload[0]?.payload || {};

const formattedAreas = useMemo(() => {
return {
protected: format(',.2r')(protectedArea),
total: format(',.2r')(totalArea),
};
}, [protectedArea, totalArea]);

if (!active || !payload?.length) return null;

return (
<div className="flex flex-col gap-px rounded-md border border-black bg-white p-4 text-sm">
<span>Year: {year}</span>
{!future && (
<>
<span>Protection percentage: {percentage}%</span>
<span>
Protected area: {formattedAreas.protected} km<sup>2</sup>
</span>
<span>
Total area: {formattedAreas.total} km<sup>2</sup>
</span>
</>
)}
</div>
);
};

export default ChartTooltip;
10 changes: 8 additions & 2 deletions frontend/src/components/charts/horizontal-bar-chart/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useMemo } from 'react';

import { format } from 'd3-format';

import { cn } from '@/lib/utils';

const DEFAULT_BAR_COLOR = '#1E1E1E';
Expand All @@ -24,20 +26,24 @@ const HorizontalBarChart: React.FC<HorizontalBarChartProps> = ({ className, data
}, []);

const protectedAreaPercentage = useMemo(() => {
return ((protectedArea * 100) / totalArea).toFixed(1);
return format('.2r')((protectedArea * 100) / totalArea);
}, [totalArea, protectedArea]);

const barFillPercentage = useMemo(() => {
return Math.round((protectedArea * DEFAULT_MAX_PERCENTAGE) / totalArea);
}, [protectedArea, totalArea]);

const formattedArea = useMemo(() => {
return format(',.2r')(totalArea);
}, [totalArea]);

return (
<div className={cn(className)}>
<div className="flex justify-end text-3xl font-bold">{protectedAreaPercentage}%</div>
<div className="flex justify-between text-xs">
<span>{title} (i)</span>
<span>
of {totalArea} km<sup>2</sup>
of {formattedArea} km<sup>2</sup>
</span>
</div>
<div className="relative my-2 flex h-3">
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/widget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ type WidgetProps = {

const Widget: React.FC<PropsWithChildren<WidgetProps>> = ({ title, lastUpdated, children }) => {
return (
<div>
<div>
<div className="py-4 px-4 md:px-8">
<div className="pt-2">
<h2 className="font-sans text-xl font-bold">{title}</h2>
<span className="text-xs">Data last updated: {lastUpdated}</span>
</div>
Expand Down
22 changes: 14 additions & 8 deletions frontend/src/containers/data-tool/sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ const DataToolSidebar: React.FC = () => {
const [sidebarOpen, setSidebarOpen] = useState(true);

return (
<Collapsible open={sidebarOpen} onOpenChange={setSidebarOpen}>
<Collapsible
className="h-full overflow-hidden"
open={sidebarOpen}
onOpenChange={setSidebarOpen}
>
<CollapsibleTrigger asChild>
<Button
type="button"
Expand All @@ -32,13 +36,15 @@ const DataToolSidebar: React.FC = () => {
<span className="sr-only">Toggle sidebar</span>
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="relative top-0 left-0 z-20 h-full w-[430px] flex-shrink-0 bg-white py-4 fill-mode-none data-[state=closed]:animate-out-absolute data-[state=open]:animate-in-absolute data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left">
<div className="border-b border-black px-8 py-2">
<h1 className="text-5xl font-black">{location.name}</h1>
<LocationSelector className="my-2" />
</div>
<div className="px-8 py-4">
<Widgets />
<CollapsibleContent className="relative top-0 left-0 z-20 h-full flex-shrink-0 bg-white fill-mode-none data-[state=closed]:animate-out-absolute data-[state=open]:animate-in-absolute data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left md:w-[430px]">
<div className="h-full w-full overflow-y-scroll">
<div className="border-b border-black px-4 pt-4 pb-2 md:px-8">
<h1 className="text-5xl font-black">{location.name}</h1>
<LocationSelector className="my-2" />
</div>
<div className="h-full">
<Widgets />
</div>
</div>
</CollapsibleContent>
</Collapsible>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const HabitatWidget: React.FC<HabitatWidgetProps> = ({ location }) => {
code: location.code,
},
},
'pagination[limit]': -1,
});

const habitatStatsData = habitatStatsResponse?.data;
Expand All @@ -44,7 +45,7 @@ const HabitatWidget: React.FC<HabitatWidgetProps> = ({ location }) => {
}, [habitatStatsData]);

// If there is no data for the widget, do not display it.
if (!widgetChartData.length) return null;
if (!widgetChartData?.length) return null;

return (
<Widget
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/containers/data-tool/sidebar/widgets/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { useAtomValue } from 'jotai';
import { locationAtom } from '@/store/location';

import HabitatWidget from './habitat';
import MarineConservationWidget from './marine-conservation';

const DataToolWidgets: React.FC = () => {
const location = useAtomValue(locationAtom);

return (
<div className="flex flex-col font-mono">
<div className="flex flex-col divide-y-[1px] divide-black font-mono">
<MarineConservationWidget location={location} />
<HabitatWidget location={location} />
{/* <HabitatWidget location={location} /> */}
</div>
);
};
Expand Down
Loading

0 comments on commit 607d15c

Please sign in to comment.