Skip to content
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

Merged
merged 46 commits into from
Jul 5, 2023

Conversation

jernestmyers
Copy link
Member

@jernestmyers jernestmyers commented Jun 7, 2023

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 in MapAnalysis where things would get passed into a generic marker config menu component.

Then there are UX/UI decisions to be made.

  • how do/should we style the continuous var histograms (we know plotly components aren't the easiest layouts to configure!)
  • do we want values in the marker previews?
  • how should we handle/communicate value selections in the categorical value tables?
  • certainly many more!

}

// TODO: generalize this and PieMarkerConfigMenu into MarkerConfigurationMenu. Lots of code repitition...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

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" 😆

Copy link
Contributor

@adnauseum adnauseum Jun 9, 2023

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..

@jernestmyers jernestmyers marked this pull request as ready for review June 13, 2023 15:48
@bobular
Copy link
Member

bobular commented Jun 14, 2023

Reviewing this now.

#279 seems important as an "up next"?

image

Copy link
Member

@bobular bobular left a 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:

  1. somehow hack the Mesa/DataTable component to disable the heading checkbox (I looked already there's no easy prop to do this)
  2. 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.
  3. 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.

@bobular
Copy link
Member

bobular commented Jun 14, 2023

Unless perhaps we can use the onMultipleRowSelect and onMultipleRowDeselect handlers?

However these are only applied to unselected and selected rows respectively. I think we'd need our putative toggleSelectAll handler to know about selected and unselected rows.

@jernestmyers
Copy link
Member Author

Unless perhaps we can use the onMultipleRowSelect and onMultipleRowDeselect handlers?

However these are only applied to unselected and selected rows respectively. I think we'd need our putative toggleSelectAll handler to know about selected and unselected rows.

The latest push makes use of onMultipleRowSelect and onMultipleRowDeselect. More specifically...

  • onMultipleRowSelect is currently selecting all values, which makes the legend and the markers look quite strange
  • onMultipleRowDeselect sets selectedValue to be only __UNSELECTED__, which renders as All other values in the legend; this handler is also now triggered when the indeterminate-state checkbox is clicked in the header.

Decisions that remain:
How to handle selecting too many values?

  • Should we add intermediate state that can be used to show the boxes as checked while we continue to aggregate any "excess" selections as __UNSELECTED__? We would still need to communicate to the use that, "Hey, we can only select X unique values. All others will be aggregated as All other values."
  • 🤷

@@ -272,28 +283,110 @@ function MapAnalysisImpl(props: ImplProps) {
[markerConfigurations, setMarkerConfigurations]
);

const filtersIncludingViewport = useMemo(() => {
Copy link
Member Author

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.

Copy link
Member

@bobular bobular left a 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

Comment on lines +25 to +34
const unfilteredDistributionResponse =
await subsettingClient.getDistribution(
studyId,
overlayEntity.id,
overlayVariable.id,
{
valueSpec: 'count',
filters: [],
}
);
Copy link
Member

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.

Copy link
Member

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.

Comment on lines +438 to +496
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]);
Copy link
Member

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.

Copy link
Member

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?

packages/libs/eda/src/lib/map/analysis/appState.ts Outdated Show resolved Hide resolved
packages/libs/eda/src/lib/map/analysis/appState.ts Outdated Show resolved Hide resolved
@bobular bobular assigned jernestmyers and unassigned jernestmyers Jun 29, 2023
Copy link
Member

@dmfalke dmfalke left a 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.

Comment on lines +438 to +496
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]);
Copy link
Member

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?

Comment on lines 88 to 91
useEffect(() => {
if (overlayConfiguration?.overlayType !== 'categorical') return;
setUncontrolledSelections(new Set(overlayConfiguration?.overlayValues));
}, [overlayConfiguration?.overlayValues, overlayConfiguration?.overlayType]);
Copy link
Member

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.

Suggested change
useEffect(() => {
if (overlayConfiguration?.overlayType !== 'categorical') return;
setUncontrolledSelections(new Set(overlayConfiguration?.overlayValues));
}, [overlayConfiguration?.overlayValues, overlayConfiguration?.overlayType]);
if (overlayConfiguration?.overlayType === 'categorical') {
setUncontrolledSelections(new Set(overlayConfiguration?.overlayValues));
}

Copy link
Member Author

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?

Copy link
Member

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.

Comment on lines +25 to +34
const unfilteredDistributionResponse =
await subsettingClient.getDistribution(
studyId,
overlayEntity.id,
overlayVariable.id,
{
valueSpec: 'count',
filters: [],
}
);
Copy link
Member

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.

@jernestmyers jernestmyers merged commit 8e25821 into main Jul 5, 2023
1 check passed
@jernestmyers jernestmyers deleted the 190-advanced-map-marker-config branch July 5, 2023 16:47
@bobular
Copy link
Member

bobular commented Jul 6, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Map: Update 'Markers' menu entry to allow more advanced user configuration
4 participants