-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement advanced map marker configuration menu #285
Changes from all commits
4d25b60
2416577
37c7151
e038f2e
4d03d4a
1d3c312
7e49adb
a5188f7
ebd1ea0
50bf469
64ce841
aad6f1b
5181895
784f218
3ed3ca2
c5c37be
fa7daad
d4706b5
9e99c84
dd8049a
5516fcd
cf08fa6
e089aee
25a3747
dee48f1
50762c7
647f47e
b1f71ae
b991dae
0f08b40
f634684
6334e9b
2b4e966
80faf09
0ba4e3e
3247e0a
923fbf5
121d986
0c6840e
8338e5a
cdb2aa3
faff8cf
69d5cf9
cbdd30c
87d8baa
b95bc83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,9 @@ | ||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; | ||
|
||
import { | ||
AllValuesDefinition, | ||
AnalysisState, | ||
CategoricalVariableDataShape, | ||
DEFAULT_ANALYSIS_NAME, | ||
EntityDiagram, | ||
OverlayConfig, | ||
|
@@ -85,8 +87,17 @@ import { DraggablePanel } from '@veupathdb/coreui/lib/components/containers'; | |
import { TabbedDisplayProps } from '@veupathdb/coreui/lib/components/grids/TabbedDisplay'; | ||
import { GeoConfig } from '../../core/types/geoConfig'; | ||
import Banner from '@veupathdb/coreui/lib/components/banners/Banner'; | ||
import DonutMarkerComponent from '@veupathdb/components/lib/map/DonutMarker'; | ||
import ChartMarkerComponent from '@veupathdb/components/lib/map/ChartMarker'; | ||
import DonutMarkerComponent, { | ||
DonutMarkerProps, | ||
DonutMarkerStandalone, | ||
} from '@veupathdb/components/lib/map/DonutMarker'; | ||
import ChartMarkerComponent, { | ||
ChartMarkerProps, | ||
ChartMarkerStandalone, | ||
} from '@veupathdb/components/lib/map/ChartMarker'; | ||
import { sharedStandaloneMarkerProperties } from './MarkerConfiguration/CategoricalMarkerPreview'; | ||
import { mFormatter, kFormatter } from '../../core/utils/big-number-formatters'; | ||
import { getCategoricalValues } from './utils/categoricalValues'; | ||
|
||
enum MapSideNavItemLabels { | ||
Download = 'Download', | ||
|
@@ -96,7 +107,7 @@ enum MapSideNavItemLabels { | |
Share = 'Share', | ||
StudyDetails = 'View Study Details', | ||
MyAnalyses = 'My Analyses', | ||
MapType = 'Map Type', | ||
ConfigureMap = 'Configure Map', | ||
} | ||
|
||
enum MarkerTypeLabels { | ||
|
@@ -272,28 +283,110 @@ function MapAnalysisImpl(props: ImplProps) { | |
[markerConfigurations, setMarkerConfigurations] | ||
); | ||
|
||
const filtersIncludingViewport = useMemo(() => { | ||
const viewportFilters = appState.boundsZoomLevel | ||
? filtersFromBoundingBox( | ||
appState.boundsZoomLevel.bounds, | ||
{ | ||
variableId: geoConfig.latitudeVariableId, | ||
entityId: geoConfig.entity.id, | ||
}, | ||
{ | ||
variableId: geoConfig.longitudeVariableId, | ||
entityId: geoConfig.entity.id, | ||
} | ||
) | ||
: []; | ||
return [ | ||
...(props.analysisState.analysis?.descriptor.subset.descriptor ?? []), | ||
...viewportFilters, | ||
]; | ||
}, [ | ||
appState.boundsZoomLevel, | ||
geoConfig.entity.id, | ||
geoConfig.latitudeVariableId, | ||
geoConfig.longitudeVariableId, | ||
props.analysisState.analysis?.descriptor.subset.descriptor, | ||
]); | ||
|
||
const allFilteredCategoricalValues = usePromise( | ||
useCallback(async (): Promise<AllValuesDefinition[] | undefined> => { | ||
/** | ||
* We only need this data for categorical vars, so we can return early if var isn't categorical | ||
*/ | ||
if ( | ||
!overlayVariable || | ||
!CategoricalVariableDataShape.is(overlayVariable.dataShape) | ||
) | ||
return; | ||
return getCategoricalValues({ | ||
overlayEntity, | ||
subsettingClient, | ||
studyId, | ||
overlayVariable, | ||
filters, | ||
}); | ||
}, [overlayEntity, overlayVariable, subsettingClient, studyId, filters]) | ||
); | ||
|
||
const allVisibleCategoricalValues = usePromise( | ||
useCallback(async (): Promise<AllValuesDefinition[] | undefined> => { | ||
/** | ||
* Return early if: | ||
* - overlay var isn't categorical | ||
* - "Show counts for" toggle isn't set to 'visible' | ||
*/ | ||
if ( | ||
!overlayVariable || | ||
!CategoricalVariableDataShape.is(overlayVariable.dataShape) || | ||
activeMarkerConfiguration?.selectedCountsOption !== 'visible' | ||
) | ||
return; | ||
|
||
return getCategoricalValues({ | ||
overlayEntity, | ||
subsettingClient, | ||
studyId, | ||
overlayVariable, | ||
filters: filtersIncludingViewport, | ||
}); | ||
}, [ | ||
overlayEntity, | ||
overlayVariable, | ||
subsettingClient, | ||
studyId, | ||
filtersIncludingViewport, | ||
activeMarkerConfiguration?.selectedCountsOption, | ||
]) | ||
); | ||
|
||
// If the variable or filters have changed on the active marker config | ||
// get the default overlay config. | ||
const activeOverlayConfig = usePromise( | ||
useCallback(async (): Promise<OverlayConfig | undefined> => { | ||
// TODO Use `selectedValues` to generate the overlay config. Something like this: | ||
// if (activeMarkerConfiguration?.selectedValues) { | ||
// return { | ||
// overlayType: CategoryVariableDataShape.is(overlayVariable?.dataShape) ? 'categorical' : 'continuous', | ||
// overlayVariable: { | ||
// variableId: overlayVariable.id, | ||
// entityId: overlayEntity.id, | ||
// }, | ||
// overlayValues: activeMarkerConfiguration.selectedValues | ||
// } as OverlayConfig | ||
// } | ||
// Use `selectedValues` to generate the overlay config for categorical variables | ||
if ( | ||
activeMarkerConfiguration?.selectedValues && | ||
CategoricalVariableDataShape.is(overlayVariable?.dataShape) | ||
) { | ||
return { | ||
overlayType: 'categorical', | ||
overlayVariable: { | ||
variableId: overlayVariable?.id, | ||
entityId: overlayEntity?.id, | ||
}, | ||
overlayValues: activeMarkerConfiguration.selectedValues, | ||
} as OverlayConfig; | ||
} | ||
|
||
return getDefaultOverlayConfig({ | ||
studyId, | ||
filters, | ||
overlayVariable, | ||
overlayEntity, | ||
dataClient, | ||
subsettingClient, | ||
binningMethod: activeMarkerConfiguration?.binningMethod, | ||
}); | ||
}, [ | ||
dataClient, | ||
|
@@ -302,6 +395,8 @@ function MapAnalysisImpl(props: ImplProps) { | |
overlayVariable, | ||
studyId, | ||
subsettingClient, | ||
activeMarkerConfiguration?.selectedValues, | ||
activeMarkerConfiguration?.binningMethod, | ||
]) | ||
); | ||
|
||
|
@@ -333,9 +428,73 @@ function MapAnalysisImpl(props: ImplProps) { | |
selectedOverlayVariable: activeMarkerConfiguration?.selectedVariable, | ||
overlayConfig: activeOverlayConfig.value, | ||
outputEntityId: outputEntity?.id, | ||
//TO DO: maybe dependentAxisLogScale | ||
dependentAxisLogScale: | ||
activeMarkerConfiguration && | ||
'dependentAxisLogScale' in activeMarkerConfiguration | ||
? activeMarkerConfiguration.dependentAxisLogScale | ||
: false, | ||
}); | ||
|
||
const { markersData: previewMarkerData } = useStandaloneMapMarkers({ | ||
boundsZoomLevel: undefined, | ||
geoConfig: geoConfig, | ||
studyId, | ||
filters, | ||
markerType, | ||
selectedOverlayVariable: activeMarkerConfiguration?.selectedVariable, | ||
overlayConfig: activeOverlayConfig.value, | ||
outputEntityId: outputEntity?.id, | ||
}); | ||
|
||
const continuousMarkerPreview = useMemo(() => { | ||
if (!previewMarkerData || !previewMarkerData.length) return; | ||
const initialDataObject = previewMarkerData[0].data.map((data) => ({ | ||
label: data.label, | ||
value: 0, | ||
...(data.color ? { color: data.color } : {}), | ||
})); | ||
const typedData = | ||
markerType === 'pie' | ||
? ([...previewMarkerData] as DonutMarkerProps[]) | ||
: ([...previewMarkerData] as ChartMarkerProps[]); | ||
const finalData = typedData.reduce( | ||
(prevData, currData) => | ||
currData.data.map((data, index) => ({ | ||
label: data.label, | ||
value: data.value + prevData[index].value, | ||
...('color' in prevData[index] | ||
? { color: prevData[index].color } | ||
: 'color' in data | ||
? { color: data.color } | ||
: {}), | ||
})), | ||
initialDataObject | ||
); | ||
if (markerType === 'pie') { | ||
return ( | ||
<DonutMarkerStandalone | ||
jernestmyers marked this conversation as resolved.
Show resolved
Hide resolved
|
||
data={finalData} | ||
markerLabel={kFormatter(finalData.reduce((p, c) => p + c.value, 0))} | ||
{...sharedStandaloneMarkerProperties} | ||
/> | ||
); | ||
} else { | ||
return ( | ||
<ChartMarkerStandalone | ||
data={finalData} | ||
jernestmyers marked this conversation as resolved.
Show resolved
Hide resolved
|
||
markerLabel={mFormatter(finalData.reduce((p, c) => p + c.value, 0))} | ||
dependentAxisLogScale={ | ||
activeMarkerConfiguration && | ||
'dependentAxisLogScale' in activeMarkerConfiguration | ||
? activeMarkerConfiguration.dependentAxisLogScale | ||
: false | ||
} | ||
{...sharedStandaloneMarkerProperties} | ||
/> | ||
); | ||
} | ||
}, [previewMarkerData]); | ||
Comment on lines
+438
to
+496
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may be possible to move the logic for generating It would clean things up a bit at this outer level. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if this should wait for #106? |
||
|
||
const markers = useMemo( | ||
() => | ||
markersData?.map((markerProps) => | ||
|
@@ -467,21 +626,21 @@ function MapAnalysisImpl(props: ImplProps) { | |
const sideNavigationButtonConfigurationObjects: SideNavigationItemConfigurationObject[] = | ||
[ | ||
{ | ||
labelText: MapSideNavItemLabels.MapType, | ||
labelText: MapSideNavItemLabels.ConfigureMap, | ||
icon: <EditLocation />, | ||
isExpandable: true, | ||
subMenuConfig: [ | ||
{ | ||
// concatenating the parent and subMenu labels creates a unique ID | ||
id: MapSideNavItemLabels.MapType + MarkerTypeLabels.pie, | ||
id: MapSideNavItemLabels.ConfigureMap + MarkerTypeLabels.pie, | ||
labelText: MarkerTypeLabels.pie, | ||
icon: <DonutMarker style={{ height: '1.25em' }} />, | ||
onClick: () => setActiveMarkerConfigurationType('pie'), | ||
isActive: activeMarkerConfigurationType === 'pie', | ||
}, | ||
{ | ||
// concatenating the parent and subMenu labels creates a unique ID | ||
id: MapSideNavItemLabels.MapType + MarkerTypeLabels.barplot, | ||
id: MapSideNavItemLabels.ConfigureMap + MarkerTypeLabels.barplot, | ||
labelText: MarkerTypeLabels.barplot, | ||
icon: <BarPlotMarker style={{ height: '1.25em' }} />, | ||
onClick: () => setActiveMarkerConfigurationType('barplot'), | ||
|
@@ -516,6 +675,18 @@ function MapAnalysisImpl(props: ImplProps) { | |
} | ||
toggleStarredVariable={toggleStarredVariable} | ||
constraints={markerVariableConstraints} | ||
overlayConfiguration={activeOverlayConfig.value} | ||
overlayVariable={overlayVariable} | ||
subsettingClient={subsettingClient} | ||
studyId={studyId} | ||
filters={filters} | ||
allFilteredCategoricalValues={ | ||
allFilteredCategoricalValues.value | ||
} | ||
allVisibleCategoricalValues={ | ||
allVisibleCategoricalValues.value | ||
} | ||
continuousMarkerPreview={continuousMarkerPreview} | ||
/> | ||
) : ( | ||
<></> | ||
|
@@ -541,6 +712,18 @@ function MapAnalysisImpl(props: ImplProps) { | |
toggleStarredVariable={toggleStarredVariable} | ||
configuration={activeMarkerConfiguration} | ||
constraints={markerVariableConstraints} | ||
overlayConfiguration={activeOverlayConfig.value} | ||
overlayVariable={overlayVariable} | ||
subsettingClient={subsettingClient} | ||
studyId={studyId} | ||
filters={filters} | ||
allFilteredCategoricalValues={ | ||
allFilteredCategoricalValues.value | ||
} | ||
allVisibleCategoricalValues={ | ||
allVisibleCategoricalValues.value | ||
} | ||
continuousMarkerPreview={continuousMarkerPreview} | ||
/> | ||
) : ( | ||
<></> | ||
|
@@ -793,7 +976,7 @@ function MapAnalysisImpl(props: ImplProps) { | |
|
||
function isMapTypeSubMenuItemSelected() { | ||
const mapTypeSideNavObject = sideNavigationButtonConfigurationObjects.find( | ||
(navObject) => navObject.labelText === MapSideNavItemLabels.MapType | ||
(navObject) => navObject.labelText === MapSideNavItemLabels.ConfigureMap | ||
); | ||
if ( | ||
mapTypeSideNavObject && | ||
|
@@ -826,7 +1009,7 @@ function MapAnalysisImpl(props: ImplProps) { | |
MarkerTypeLabels[appState.activeMarkerConfigurationType] | ||
) | ||
return ( | ||
MapSideNavItemLabels.MapType + | ||
MapSideNavItemLabels.ConfigureMap + | ||
MarkerTypeLabels[appState.activeMarkerConfigurationType] | ||
); | ||
|
||
|
@@ -840,32 +1023,6 @@ function MapAnalysisImpl(props: ImplProps) { | |
|
||
const toggleStarredVariable = useToggleStarredVariable(analysisState); | ||
|
||
const filtersIncludingViewport = useMemo(() => { | ||
const viewportFilters = appState.boundsZoomLevel | ||
? filtersFromBoundingBox( | ||
appState.boundsZoomLevel.bounds, | ||
{ | ||
variableId: geoConfig.latitudeVariableId, | ||
entityId: geoConfig.entity.id, | ||
}, | ||
{ | ||
variableId: geoConfig.longitudeVariableId, | ||
entityId: geoConfig.entity.id, | ||
} | ||
) | ||
: []; | ||
return [ | ||
...(props.analysisState.analysis?.descriptor.subset.descriptor ?? []), | ||
...viewportFilters, | ||
]; | ||
}, [ | ||
appState.boundsZoomLevel, | ||
geoConfig.entity.id, | ||
geoConfig.latitudeVariableId, | ||
geoConfig.longitudeVariableId, | ||
props.analysisState.analysis?.descriptor.subset.descriptor, | ||
]); | ||
|
||
const [sideNavigationIsExpanded, setSideNavigationIsExpanded] = | ||
useState<boolean>(true); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NOTE: No logic/code has changed for
filtersIncludingViewport
but I had to move its initialization aboveallVisibleCategoricalValues
.