Skip to content

Commit

Permalink
fishing-map/vessel-profile-insights (#2832)
Browse files Browse the repository at this point in the history
  • Loading branch information
j8seangel authored Sep 19, 2024
2 parents ba72af6 + 4ab0db0 commit cf57a80
Show file tree
Hide file tree
Showing 61 changed files with 1,898 additions and 478 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -226,12 +226,6 @@ export const selectActiveVesselGroupDataviews = createSelector(
(dataviews): UrlDataviewInstance[] => dataviews?.filter((d) => d.config?.visible)
)

export const selectVesselGroupReportDataview = createSelector(
[selectActiveVesselGroupDataviews, selectReportVesselGroupId],
(dataviews, reportVesselGroupId): UrlDataviewInstance | undefined =>
dataviews?.find(({ vesselGroup }) => vesselGroup?.id === reportVesselGroupId)
)

export const selectDetectionsMergedDataviewId = createSelector(
[selectActiveDetectionsDataviews],
(dataviews): string => {
Expand Down
26 changes: 11 additions & 15 deletions apps/fishing-map/features/vessel-group-report/VesselGroupReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,24 @@ import {
useTimebarVesselGroupConnect,
useTimebarVisualisationConnect,
} from 'features/timebar/timebar.hooks'
import { selectVesselGroupReportDataview } from 'features/dataviews/selectors/dataviews.selectors'
import VGREvents from 'features/vessel-group-report/events/VGREvents'
import { useFetchVesselGroupReport } from './vessel-group-report.hooks'
import {
selectVesselGroupReportData,
selectVesselGroupReportStatus,
} from './vessel-group-report.slice'
import VesselGroupReportError from './VesselGroupReportError'
import { selectVGRData, selectVGRStatus } from './vessel-group-report.slice'
import VesselGroupReportTitle from './VesselGroupReportTitle'
import VesselGroupReportVessels from './vessels/VesselGroupReportVessels'
import { selectVesselGroupReportSection } from './vessel-group.config.selectors'
import { selectVGRSection } from './vessel-group.config.selectors'
import VesselGroupReportInsights from './insights/VGRInsights'
import { selectVGRDataview } from './vessel-group-report.selectors'

function VesselGroupReport() {
const { t } = useTranslation()
const { dispatchQueryParams } = useLocationConnect()
const fetchVesselGroupReport = useFetchVesselGroupReport()
const vesselGroupId = useSelector(selectReportVesselGroupId)
const vesselGroup = useSelector(selectVesselGroupReportData)!
const reportStatus = useSelector(selectVesselGroupReportStatus)
const reportSection = useSelector(selectVesselGroupReportSection)
const reportDataview = useSelector(selectVesselGroupReportDataview)
const vesselGroup = useSelector(selectVGRData)!
const reportStatus = useSelector(selectVGRStatus)
const reportSection = useSelector(selectVGRSection)
const reportDataview = useSelector(selectVGRDataview)
const { dispatchTimebarVisualisation } = useTimebarVisualisationConnect()
const { dispatchTimebarSelectedVGId } = useTimebarVesselGroupConnect()

Expand All @@ -52,7 +49,7 @@ function VesselGroupReport() {

const changeTab = useCallback(
(tab: Tab<VesselGroupReportSection>) => {
dispatchQueryParams({ vesselGroupReportSection: tab.id })
dispatchQueryParams({ vGRSection: tab.id })
trackEvent({
category: TrackCategory.VesselGroupReport,
action: `click_${tab.id}_tab`,
Expand All @@ -70,9 +67,8 @@ function VesselGroupReport() {
},
{
id: 'insights',
title: t('common.areas', 'Areas'),
disabled: true,
content: <p>Coming soon</p>,
title: t('common.insights', 'Insights'),
content: <VesselGroupReportInsights />,
},
{
id: 'activity',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@ import { Trans, useTranslation } from 'react-i18next'
import { AsyncReducerStatus } from 'utils/async-slice'
import LocalStorageLoginLink from 'routes/LoginLink'
import styles from './VesselGroupReport.module.css'
import {
selectVesselGroupReportError,
selectVesselGroupReportStatus,
} from './vessel-group-report.slice'
import { selectVGRError, selectVGRStatus } from './vessel-group-report.slice'

function VesselGroupReportError() {
const { t } = useTranslation()
const reportStatus = useSelector(selectVesselGroupReportStatus)
const reportError = useSelector(selectVesselGroupReportError)
const reportStatus = useSelector(selectVGRStatus)
const reportError = useSelector(selectVGRError)

if (reportStatus !== AsyncReducerStatus.Error) {
return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { Choice, ChoiceOption } from '@globalfishingwatch/ui-components'
import { useLocationConnect } from 'routes/routes.hook'
import { selectVesselGroupReportStatus } from 'features/vessel-group-report/vessel-group-report.slice'
import { selectVGRStatus } from 'features/vessel-group-report/vessel-group-report.slice'
import { AsyncReducerStatus } from 'utils/async-slice'
import { VGREventsSubsection } from 'features/vessel-groups/vessel-groups.types'
import { selectVGREventsSubsection } from '../vessel-group.config.selectors'

function VesselGroupReportEventsSubsectionSelector() {
const { t } = useTranslation()
const { dispatchQueryParams } = useLocationConnect()
const vesselGroupReportStatus = useSelector(selectVesselGroupReportStatus)
const vesselGroupReportStatus = useSelector(selectVGRStatus)
const subsection = useSelector(selectVGREventsSubsection)
const loading = vesselGroupReportStatus === AsyncReducerStatus.Loading
const options: ChoiceOption<VGREventsSubsection>[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import I18nNumber from 'features/i18n/i18nNumber'
import { useLocationConnect } from 'routes/routes.hook'
import { selectTimeRange } from 'features/app/selectors/app.timebar.selectors'
import { REPORT_SHOW_MORE_VESSELS_PER_PAGE, REPORT_VESSELS_PER_PAGE } from 'data/config'
import { selectVesselGroupReportData } from 'features/vessel-group-report/vessel-group-report.slice'
import { selectVGRData } from 'features/vessel-group-report/vessel-group-report.slice'
import { formatInfoField } from 'utils/info'
import {
selectVGREventsVessels,
Expand All @@ -21,7 +21,7 @@ import styles from '../vessels/VesselGroupReportVesselsTableFooter.module.css'
export default function VesselGroupReportVesselsTableFooter() {
const { t } = useTranslation()
const { dispatchQueryParams } = useLocationConnect()
const vesselGroup = useSelector(selectVesselGroupReportData)
const vesselGroup = useSelector(selectVGRData)
const allVessels = useSelector(selectVGREventsVessels)
const reportVesselFilter = useSelector(selectVGREventsVesselFilter)
const pagination = useSelector(selectVGREventsVesselsPagination)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
selectVesselGroupEventsVessels,
VesselGroupEventsVesselsParams,
} from 'queries/vessel-group-events-stats-api'
import { selectVesselGroupReportData } from 'features/vessel-group-report/vessel-group-report.slice'
import { selectVGRData } from 'features/vessel-group-report/vessel-group-report.slice'
import { getSearchIdentityResolved, getVesselId } from 'features/vessel/vessel.utils'
import { selectTimeRange } from 'features/app/selectors/app.timebar.selectors'
import { selectReportVesselGroupId } from 'routes/routes.selectors'
Expand Down Expand Up @@ -45,7 +45,7 @@ export const selectVGREventsVesselsData = createSelector(
)

export const selectVGREventsVessels = createSelector(
[selectVGREventsVesselsData, selectVesselGroupReportData],
[selectVGREventsVesselsData, selectVGRData],
(data, vesselGroup) => {
if (!data || !vesselGroup) {
return
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useTranslation } from 'react-i18next'
import { useGetVesselGroupInsightQuery } from 'queries/vessel-insight-api'
import { useSelector } from 'react-redux'
import { ParsedAPIError } from '@globalfishingwatch/api-client'
import InsightError from 'features/vessel/insights/InsightErrorMessage'
import DataTerminology from 'features/vessel/identity/DataTerminology'
import { selectFetchVesselGroupReportCoverageParams } from '../vessel-group-report.selectors'
import styles from './VGRInsights.module.css'
import VesselGroupReportInsightCoverageGraph from './VGRInsightCoverageGraph'

const VesselGroupReportInsightCoveragePlaceholder = () => {
// TODO graph bar placeholder
return <div style={{ width: '20rem' }} className={styles.loadingPlaceholder} />
}

const VesselGroupReportInsightCoverage = () => {
const { t } = useTranslation()
const fetchParams = useSelector(selectFetchVesselGroupReportCoverageParams)
const { data, error, isLoading } = useGetVesselGroupInsightQuery(fetchParams)

return (
<div id="vessel-group-coverage" className={styles.insightContainer}>
<div className={styles.insightTitle}>
<label className="experimental">{t('vessel.insights.coverage', 'AIS Coverage')}</label>
<DataTerminology
size="tiny"
type="default"
title={t('vessel.insights.coverage', 'AIS Coverage')}
terminologyKey="insightsCoverage"
/>
</div>
{isLoading ? (
<VesselGroupReportInsightCoveragePlaceholder />
) : error ? (
<InsightError error={error as ParsedAPIError} />
) : data?.coverage && data?.coverage?.length > 0 ? (
<VesselGroupReportInsightCoverageGraph data={data.coverage} />
) : null}
</div>
)
}

export default VesselGroupReportInsightCoverage
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
.graph {
width: 100%;
height: 30rem;
margin-block: var(--space-S);
}

.graph tspan {
text-transform: uppercase;
font: var(--font-XS);
line-height: 1.2rem;
fill: var(--color-secondary-blue);
}

.tooltipContainer {
background-color: var(--color-white);
border: var(--border);
padding: var(--space-S);
}

.axisLabel {
cursor: pointer;
}

.tooltipRow {
display: flex;
align-items: center;
}

.tooltipLabel {
color: var(--color-secondary-blue);
}

.graph tspan.info {
text-transform: lowercase;
font-family: serif;
font-style: italic;
font-weight: 600;
}

.graph :global(.recharts-tooltip-cursor) {
fill: var(--color-terthiary-blue);
}

.bar {
transition: fill 300ms linear;
}

.graph :global(.recharts-bar-rectangle):nth-child(1) {
opacity: 0.3;
}

.graph :global(.recharts-bar-rectangle):nth-child(2) {
opacity: 0.5;
}

.graph :global(.recharts-bar-rectangle):nth-child(3) {
opacity: 0.7;
}

.graph :global(.recharts-bar-rectangle):nth-child(4) {
opacity: 0.85;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { Fragment, useMemo } from 'react'
import { useSelector } from 'react-redux'
import { BarChart, Bar, XAxis, ResponsiveContainer, LabelList } from 'recharts'
import { groupBy } from 'es-toolkit'
import { VesselGroupInsightResponse } from '@globalfishingwatch/api-types'
import { COLOR_PRIMARY_BLUE } from 'features/app/app.config'
import { selectVGRDataview } from '../vessel-group-report.selectors'
import styles from './VGRInsightCoverageGraph.module.css'

const CustomTick = (props: any) => {
const { x, y, payload } = props

return (
<text transform={`translate(${x},${y - 3})`}>
<tspan textAnchor="middle" x="0" dy={12}>
{payload.value}
</tspan>
</text>
)
}

type VesselGroupReportCoverageGraphData = {
name: string
value: number
}

const COVERAGE_GRAPH_BUCKETS: Record<string, number> = {
'<20%': 20,
'20-40%': 40,
'40-60%': 60,
'60-80%': 80,
'>80%': 100,
}
const CoverageGraphBuckets = Object.keys(COVERAGE_GRAPH_BUCKETS)
function parseCoverageGraphValueBucket(value: number) {
return (
CoverageGraphBuckets.find((key) => value < COVERAGE_GRAPH_BUCKETS[key]) ||
CoverageGraphBuckets[CoverageGraphBuckets.length - 1]
)
}

function parseCoverageGraphData(
data: VesselGroupInsightResponse['coverage']
): VesselGroupReportCoverageGraphData[] {
if (!data) return []
const dataByCoverage = data.map((d) => ({
name: d.vesselId,
value: parseCoverageGraphValueBucket(d.percentage),
}))
const groupedDataByCoverage = groupBy(dataByCoverage, (entry) => entry.value!)
return Object.keys(COVERAGE_GRAPH_BUCKETS).map((key) => ({
name: key,
value: groupedDataByCoverage[key]?.length || 0,
}))
}

export default function VesselGroupReportInsightCoverageGraph({
data,
}: {
data: VesselGroupInsightResponse['coverage']
}) {
const dataGrouped = useMemo(() => parseCoverageGraphData(data), [data])
const reportDataview = useSelector(selectVGRDataview)
return (
<Fragment>
<div className={styles.graph} data-test="report-vessels-graph">
{dataGrouped && (
<ResponsiveContainer width="100%" height="100%">
<BarChart
width={500}
height={300}
data={dataGrouped}
margin={{
top: 15,
right: 0,
left: 0,
bottom: 0,
}}
>
<Bar
className={styles.bar}
dataKey="value"
fill={reportDataview?.config?.color || COLOR_PRIMARY_BLUE}
>
<LabelList position="top" valueAccessor={(entry: any) => entry.value} />
</Bar>
<XAxis
dataKey="name"
interval="equidistantPreserveStart"
tickLine={false}
minTickGap={-1000}
tick={<CustomTick />}
tickMargin={0}
/>
</BarChart>
</ResponsiveContainer>
)}
</div>
</Fragment>
)
}
Loading

0 comments on commit cf57a80

Please sign in to comment.