-
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
Conversation
} | ||
|
||
// TODO: generalize this and PieMarkerConfigMenu into MarkerConfigurationMenu. Lots of code repitition... |
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.
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.
Perhaps I'll change it to a "maybe do" 😆
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.
🙃
Just wanted to derp by the water color cooler 👋 . I've been painting recently... but with acrylics..
Reviewing this now. #279 seems important as an "up next"? |
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.
I think the big nut to crack is the checkbox column heading behaviour in CategoricalMarkerConfigurationTable
At the moment it seems to behave erratically. With SCORE S. mansoni "occupation" it will create an endless loop (or at least for the number of items). If "Peasant" is unselected first, then the very last value "Transporter" gets selected.
With low cardinality variables it seems to deselect the last value only.
Some options:
- somehow hack the Mesa/DataTable component to disable the heading checkbox (I looked already there's no easy prop to do this)
- keep the heading checkbox but make the first click deselect all (also requires some hacking I think) - and when it is clicked again, don't show endless warnings.
- gate off the value selection behind [:heavy_check_mark: apply] [:x: cancel] buttons, and leave the table implementation alone
I think something is a bit off with the handleSelection
and handleDeselection
handlers but @dmfalke probably understands quicker. They are called in a forEach
which explains the multiple alerts.
selectAllRows() {
const { rows, options, eventHandlers } = this.props;
const { isRowSelected } = options;
const { onRowSelect, onMultipleRowSelect } = eventHandlers;
const unselectedRows = rows.filter((row) => !isRowSelected(row));
if (onMultipleRowSelect) return onMultipleRowSelect(unselectedRows);
return unselectedRows.forEach(onRowSelect);
}
from SelectionCell.jsx
Here, onRowSelect
is the handleSelection
handler we pass in.
Unless perhaps we can use the However these are only applied to unselected and selected rows respectively. I think we'd need our putative |
- apply deselectAll handler if indeterminate checkbox is clicked
The latest push makes use of
Decisions that remain:
|
@@ -272,28 +283,110 @@ function MapAnalysisImpl(props: ImplProps) { | |||
[markerConfigurations, setMarkerConfigurations] | |||
); | |||
|
|||
const filtersIncludingViewport = useMemo(() => { |
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 above allVisibleCategoricalValues
.
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.
This is looking and working great.
If you choose not to follow any of my suggestions please make tickets for them.
I'll come back laters to the io-ts query I made in appState.ts
...es/libs/eda/src/lib/map/analysis/MarkerConfiguration/CategoricalMarkerConfigurationTable.tsx
Outdated
Show resolved
Hide resolved
...es/libs/eda/src/lib/map/analysis/MarkerConfiguration/CategoricalMarkerConfigurationTable.tsx
Outdated
Show resolved
Hide resolved
const unfilteredDistributionResponse = | ||
await subsettingClient.getDistribution( | ||
studyId, | ||
overlayEntity.id, | ||
overlayVariable.id, | ||
{ | ||
valueSpec: 'count', | ||
filters: [], | ||
} | ||
); |
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.
It's not urgent - could be a follow-up ticket - but I think these unfiltered values+counts should be cached somehow.
I can't think of a simple drop-in way to do this. My suggestion would involve a new hook called from MapAnalysis that generates unfilteredValues
and pass this as yet another prop into getCategoricalValues
. This isn't nice though. It unnecessarily exposes things that are rightly "hidden" here in this function.
I'm sure @dmfalke will have a wise suggestion.
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.
I don't have any suggestions right now, but I will think about it some more.
packages/libs/eda/src/lib/map/analysis/utils/categoricalValues.ts
Outdated
Show resolved
Hide resolved
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 | ||
data={finalData} | ||
markerLabel={kFormatter(finalData.reduce((p, c) => p + c.value, 0))} | ||
{...sharedStandaloneMarkerProperties} | ||
/> | ||
); | ||
} else { | ||
return ( | ||
<ChartMarkerStandalone | ||
data={finalData} | ||
markerLabel={mFormatter(finalData.reduce((p, c) => p + c.value, 0))} | ||
dependentAxisLogScale={ | ||
activeMarkerConfiguration && | ||
'dependentAxisLogScale' in activeMarkerConfiguration | ||
? activeMarkerConfiguration.dependentAxisLogScale | ||
: false | ||
} | ||
{...sharedStandaloneMarkerProperties} | ||
/> | ||
); | ||
} | ||
}, [previewMarkerData]); |
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.
It may be possible to move the logic for generating continuousMarkerPreview
inside the *MarkerConfigurationMenu
components (as a utility function, I imagine). It looks like all but one (geoConfig
) of the props needed to make previewMarkerData
are already passed to the *MarkerConfigurationMenu
components, so it wouldn't involve a lot of "prop bloat". In fact we'd be removing one prop (continuousMarkerPreview
) and adding geoConfig
, so breaking even!
It would clean things up a bit at this outer level.
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.
I wonder if this should wait for #106?
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.
This is all looking really good. I reviewed the code and made a couple of suggestions. Feel free to comment if you disagree.
I will continue to think about the best way caching calls to the distribution service, but I think we should make it a follow-up task.
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 | ||
data={finalData} | ||
markerLabel={kFormatter(finalData.reduce((p, c) => p + c.value, 0))} | ||
{...sharedStandaloneMarkerProperties} | ||
/> | ||
); | ||
} else { | ||
return ( | ||
<ChartMarkerStandalone | ||
data={finalData} | ||
markerLabel={mFormatter(finalData.reduce((p, c) => p + c.value, 0))} | ||
dependentAxisLogScale={ | ||
activeMarkerConfiguration && | ||
'dependentAxisLogScale' in activeMarkerConfiguration | ||
? activeMarkerConfiguration.dependentAxisLogScale | ||
: false | ||
} | ||
{...sharedStandaloneMarkerProperties} | ||
/> | ||
); | ||
} | ||
}, [previewMarkerData]); |
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.
I wonder if this should wait for #106?
useEffect(() => { | ||
if (overlayConfiguration?.overlayType !== 'categorical') return; | ||
setUncontrolledSelections(new Set(overlayConfiguration?.overlayValues)); | ||
}, [overlayConfiguration?.overlayValues, overlayConfiguration?.overlayType]); |
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.
You can remove the useEffect
here. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes.
useEffect(() => { | |
if (overlayConfiguration?.overlayType !== 'categorical') return; | |
setUncontrolledSelections(new Set(overlayConfiguration?.overlayValues)); | |
}, [overlayConfiguration?.overlayValues, overlayConfiguration?.overlayType]); | |
if (overlayConfiguration?.overlayType === 'categorical') { | |
setUncontrolledSelections(new Set(overlayConfiguration?.overlayValues)); | |
} |
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.
This suggestion results in an infinite loop, which is why I think I reached for the useEffect
. But it does appear that the "Better" suggestion from that section of the React docs works. Pretty clunky looking though!
const [previousOverlays, setPreviousOverlays] = useState(overlayConfiguration?.overlayValues);
if (
previousOverlays !== overlayConfiguration?.overlayValues &&
overlayConfiguration?.overlayType === 'categorical'
) {
setUncontrolledSelections(new Set(overlayConfiguration?.overlayValues));
setPreviousOverlays(overlayConfiguration.overlayValues);
}
Thoughts on this?
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.
Ah, yes, I see. This is more verbose, but also more efficient. Since this logic will be used in at least two places, maybe extract it into a hook. You can also add the initialization of uncontrolledSelections
in the hook and call it useUncontrolledSelections
. That will make it easier for the reader to ignore the implementation details, if they want to.
packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/PieMarkerConfigurationMenu.tsx
Outdated
Show resolved
Hide resolved
const unfilteredDistributionResponse = | ||
await subsettingClient.getDistribution( | ||
studyId, | ||
overlayEntity.id, | ||
overlayVariable.id, | ||
{ | ||
valueSpec: 'count', | ||
filters: [], | ||
} | ||
); |
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.
I don't have any suggestions right now, but I will think about it some more.
🎉
…On Wed, 5 Jul 2023, 17:47 jeremy, ***@***.***> wrote:
Merged #285 <#285> into
main.
—
Reply to this email directly, view it on GitHub
<#285 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACLLHY5PWBQJOSAVUDCNQLXOWLDHANCNFSM6AAAAAAY56EKWY>
.
You are receiving this because your review was requested.Message ID:
***@***.***>
|
Resolves #190
I'm most unsure still about how I'm handling the
continuousMarkerPreview
data. Not certain this is a great approach, or if the data is being displayed as we're expecting it to display. Aside from that, I have some concerns about code duplication in the different marker config menus but I fear that extracting that duplication would create a lot more code inMapAnalysis
where things would get passed into a generic marker config menu component.Then there are UX/UI decisions to be made.