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
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
4d25b60
Create component for configuring categorical markers
jernestmyers Jun 6, 2023
2416577
Wire up new table for barplot categorical marker configs
jernestmyers Jun 6, 2023
37c7151
Add CategoricalMarkerConfigurationTable to donut config menu
jernestmyers Jun 6, 2023
e038f2e
Clean up types related to allValues overlay data
jernestmyers Jun 6, 2023
4d03d4a
Add a barplot marker preview for categorical vars
jernestmyers Jun 6, 2023
1d3c312
Ensure 'All other values' is at end of selectedValues and clean up so…
jernestmyers Jun 6, 2023
7e49adb
Add donut marker preview for categorical marker config
jernestmyers Jun 7, 2023
a5188f7
Display histogram in marker config menu for continuous vars
jernestmyers Jun 8, 2023
ebd1ea0
Merge branch 'main' into 190-advanced-map-marker-config
jernestmyers Jun 8, 2023
50bf469
Remove some ts-ignores
jernestmyers Jun 8, 2023
64ce841
Merge branch 'main' into 190-advanced-map-marker-config
jernestmyers Jun 9, 2023
aad6f1b
Wire up marker preview for continuous vars
jernestmyers Jun 12, 2023
5181895
Clean up some of the marker preview logic
jernestmyers Jun 12, 2023
784f218
Change from allValuesSorted to just allValues
jernestmyers Jun 12, 2023
3ed3ca2
Remove ts-ignores from advanced marker config work
jernestmyers Jun 12, 2023
c5c37be
Use standalone markers for all previews and add some styling
jernestmyers Jun 12, 2023
fa7daad
Tweak selectedValues logic and add documentation
jernestmyers Jun 13, 2023
d4706b5
Change type name for getValues function params
jernestmyers Jun 13, 2023
9e99c84
Wire up selectAll and deselectAll handlers
jernestmyers Jun 14, 2023
dd8049a
Add indeterminate state to (de)selectAll toggle
jernestmyers Jun 14, 2023
5516fcd
Wire up marker binning method options for continuous vars
jernestmyers Jun 22, 2023
cf08fa6
Wire up log scale toggle for map markers
jernestmyers Jun 22, 2023
e089aee
Clean up map marker binningMethod typing
jernestmyers Jun 22, 2023
25a3747
Add binning controls to pie marker config menu
jernestmyers Jun 22, 2023
dee48f1
Change menu verbiage from Map Type to Configure Map
jernestmyers Jun 23, 2023
50762c7
Clean up unused imports and replace hardcoded value with defined cons…
jernestmyers Jun 23, 2023
647f47e
Add not-yet-functional counts toggle to marker config menu
jernestmyers Jun 23, 2023
b1f71ae
Overlay warning if user selects too many values for categorical vars …
jernestmyers Jun 26, 2023
b991dae
Merge main and resolve conflicts
jernestmyers Jun 26, 2023
0f08b40
Fix paths to coreui
jernestmyers Jun 26, 2023
f634684
Improve naming and add number formatting
jernestmyers Jun 27, 2023
6334e9b
Remove allValues property and revert its related logic
jernestmyers Jun 27, 2023
2b4e966
Refactor categorical values logic and implement filtered/visible toggle
jernestmyers Jun 27, 2023
80faf09
Remove ts-ignores
jernestmyers Jun 27, 2023
0ba4e3e
Add log scale to categorical bar plot marker previews
jernestmyers Jun 28, 2023
3247e0a
Fix bug when deselecting values where 'All other values' label is irr…
jernestmyers Jun 28, 2023
923fbf5
Implement column sorting and preserve sort state between filtered/vis…
jernestmyers Jun 28, 2023
121d986
Refactor useStandaloneMapMarkers interface to remove isMarkerPreview …
jernestmyers Jun 28, 2023
0c6840e
Clean up and document code
jernestmyers Jun 28, 2023
8338e5a
Simplify logic and improve documentation
jernestmyers Jun 29, 2023
cdb2aa3
Guard against TooManySelectionOverlay displaying erroneously and add …
jernestmyers Jun 29, 2023
faff8cf
Remove magic number, improve logic and improve documentation
jernestmyers Jun 29, 2023
69d5cf9
Fix bug in onMultipleRowSelect callback re: UNSELECTED_TOKEN value
jernestmyers Jun 29, 2023
cbdd30c
Clean up selectedValues type
jernestmyers Jun 29, 2023
87d8baa
Abstract uncontrolledSelections logic into a hook
jernestmyers Jun 30, 2023
b95bc83
Merge branch 'main' into 190-advanced-map-marker-config
jernestmyers Jul 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/libs/eda/src/lib/core/api/DataClient/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,12 @@ export const BinDefinitions = array(
})
);

export type AllValuesDefinition = TypeOf<typeof AllValuesDefinition>;
export const AllValuesDefinition = type({
label: string,
count: number,
});

export type OverlayConfig = TypeOf<typeof OverlayConfig>;
export const OverlayConfig = intersection([
type({
Expand All @@ -777,6 +783,7 @@ export const OverlayConfig = intersection([
type({
overlayType: literal('categorical'),
overlayValues: array(string),
allValuesSorted: array(AllValuesDefinition),
}),
type({
overlayType: literal('continuous'),
Expand Down
44 changes: 33 additions & 11 deletions packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';

import {
AnalysisState,
CategoricalVariableDataShape,
DEFAULT_ANALYSIS_NAME,
EntityDiagram,
OverlayConfig,
Expand Down Expand Up @@ -274,17 +275,26 @@ function MapAnalysisImpl(props: ImplProps) {
// 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
if (
activeMarkerConfiguration?.selectedValues &&
activeMarkerConfiguration?.allValues
) {
return {
overlayType: CategoricalVariableDataShape.is(
overlayVariable?.dataShape
)
? 'categorical'
: 'continuous',
overlayVariable: {
variableId: overlayVariable?.id,
entityId: overlayEntity?.id,
},
overlayValues: activeMarkerConfiguration.selectedValues,
allValuesSorted: activeMarkerConfiguration.allValues,
} as OverlayConfig;
}

return getDefaultOverlayConfig({
studyId,
filters,
Expand All @@ -300,6 +310,8 @@ function MapAnalysisImpl(props: ImplProps) {
overlayVariable,
studyId,
subsettingClient,
activeMarkerConfiguration?.selectedValues,
activeMarkerConfiguration?.allValues,
])
);

Expand Down Expand Up @@ -503,6 +515,11 @@ function MapAnalysisImpl(props: ImplProps) {
}
toggleStarredVariable={toggleStarredVariable}
constraints={markerVariableConstraints}
overlayConfiguration={activeOverlayConfig.value}
overlayVariable={overlayVariable}
subsettingClient={subsettingClient}
studyId={studyId}
filters={filters}
/>
) : (
<></>
Expand All @@ -528,6 +545,11 @@ function MapAnalysisImpl(props: ImplProps) {
toggleStarredVariable={toggleStarredVariable}
configuration={activeMarkerConfiguration}
constraints={markerVariableConstraints}
overlayConfiguration={activeOverlayConfig.value}
overlayVariable={overlayVariable}
subsettingClient={subsettingClient}
studyId={studyId}
filters={filters}
/>
) : (
<></>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { H6 } from '@veupathdb/coreui';
import { useCallback } from 'react';
import {
InputVariables,
Props as InputVariablesProps,
} from '../../../core/components/visualizations/InputVariables';
import RadioButtonGroup from '@veupathdb/components/lib/components/widgets/RadioButtonGroup';
import { VariableDescriptor } from '../../../core/types/variable';
import { VariablesByInputName } from '../../../core/utils/data-element-constraints';
import { BinDefinitions } from '../../../core';
import {
usePromise,
AllValuesDefinition,
OverlayConfig,
Variable,
Filter,
} from '../../../core';
import { CategoricalMarkerConfigurationTable } from './CategoricalMarkerConfigurationTable';
import { MarkerPreview } from './MarkerPreview';
import Barplot from '@veupathdb/components/lib/plots/Barplot';
import { SubsettingClient } from '../../../core/api';

interface MarkerConfiguration<T extends string> {
type: T;
Expand All @@ -15,8 +25,9 @@ interface MarkerConfiguration<T extends string> {
export interface BarPlotMarkerConfiguration
extends MarkerConfiguration<'barplot'> {
selectedVariable: VariableDescriptor;
selectedValues: BinDefinitions | undefined;
selectedValues: OverlayConfig['overlayValues'] | undefined;
selectedPlotMode: 'count' | 'proportion';
allValues: AllValuesDefinition[] | undefined;
}

interface Props
Expand All @@ -26,16 +37,73 @@ interface Props
> {
onChange: (configuration: BarPlotMarkerConfiguration) => void;
configuration: BarPlotMarkerConfiguration;
overlayConfiguration: OverlayConfig | undefined;
overlayVariable: Variable | undefined;
subsettingClient: SubsettingClient;
studyId: string;
filters: Filter[] | undefined;
}

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


export function BarPlotMarkerConfigurationMenu({
entities,
onChange,
starredVariables,
toggleStarredVariable,
configuration,
constraints,
overlayConfiguration,
overlayVariable,
subsettingClient,
studyId,
filters,
}: Props) {
const barplotData = usePromise(
useCallback(async () => {
if (
!overlayVariable ||
overlayConfiguration?.overlayType !== 'continuous' ||
!('distributionDefaults' in overlayVariable)
)
return;
const binSpec = {
displayRangeMin:
overlayVariable.distributionDefaults.rangeMin +
(overlayVariable.type === 'date' ? 'T00:00:00Z' : ''),
displayRangeMax:
overlayVariable.distributionDefaults.rangeMax +
(overlayVariable.type === 'date' ? 'T00:00:00Z' : ''),
binWidth: overlayVariable.distributionDefaults.binWidth,
// @ts-ignore
binUnits: overlayVariable.distributionDefaults.binUnits,
};
const distributionResponse = await subsettingClient.getDistribution(
studyId,
configuration.selectedVariable.entityId,
configuration.selectedVariable.variableId,
{
valueSpec: 'count',
filters: filters ?? [],
// @ts-ignore
binSpec,
}
);
return {
name: '',
value: distributionResponse.histogram.map((d) => d.value),
label: distributionResponse.histogram.map((d) => d.binLabel),
showValues: false,
};
}, [
overlayVariable,
overlayConfiguration?.overlayType,
subsettingClient,
filters,
configuration.selectedVariable,
])
);

function handleInputVariablesOnChange(selection: VariablesByInputName) {
if (!selection.overlayVariable) {
console.error(
Expand All @@ -59,6 +127,19 @@ export function BarPlotMarkerConfigurationMenu({

return (
<div>
<MarkerPreview data={overlayConfiguration} markerType="barplot" />
<RadioButtonGroup
containerStyles={{
marginTop: 20,
}}
label="Y-axis"
selectedOption={configuration.selectedPlotMode || 'count'}
options={['count', 'proportion']}
optionLabels={['Count', 'Proportion']}
buttonColor={'primary'}
margins={['0em', '0', '0', '1em']}
onOptionSelected={handlePlotModeSelection}
/>
<p
style={{
paddingLeft: 7,
Expand All @@ -80,18 +161,23 @@ export function BarPlotMarkerConfigurationMenu({
toggleStarredVariable={toggleStarredVariable}
constraints={constraints}
/>
<RadioButtonGroup
containerStyles={{
marginTop: 20,
}}
label="Y-axis"
selectedOption={configuration.selectedPlotMode || 'count'}
options={['count', 'proportion']}
optionLabels={['Count', 'Proportion']}
buttonColor={'primary'}
margins={['0em', '0', '0', '1em']}
onOptionSelected={handlePlotModeSelection}
/>
{overlayConfiguration?.overlayType === 'categorical' && (
<CategoricalMarkerConfigurationTable
overlayConfiguration={overlayConfiguration}
configuration={configuration}
// @ts-ignore
onChange={onChange}
/>
)}
{overlayConfiguration?.overlayType === 'continuous' &&
barplotData.value && (
<Barplot
data={{ series: [barplotData.value] }}
barLayout="overlay"
showValues={false}
showIndependentAxisTickLabel={false}
/>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import Mesa from '@veupathdb/wdk-client/lib/Components/Mesa';
import { OverlayConfig, AllValuesDefinition } from '../../../core';
import { Tooltip } from '@veupathdb/components/lib/components/widgets/Tooltip';
import { BarPlotMarkerConfiguration } from './BarPlotMarkerConfigurationMenu';
import { PieMarkerConfiguration } from './PieMarkerConfigurationMenu';
import { ColorPaletteDefault } from '@veupathdb/components/lib/types/plots';

type Props = {
overlayConfiguration: OverlayConfig;
onChange: (
configuration: BarPlotMarkerConfiguration | PieMarkerConfiguration
) => void;
configuration: BarPlotMarkerConfiguration | PieMarkerConfiguration;
};

export function CategoricalMarkerConfigurationTable({
overlayConfiguration,
configuration,
onChange,
}: Props) {
if (overlayConfiguration.overlayType !== 'categorical') return <></>;
const { overlayType, overlayValues, allValuesSorted } = overlayConfiguration;
const selected = new Set(overlayValues);
const totalCount = allValuesSorted.reduce(
(prev, curr) => prev + curr.count,
0
);

function handleSelection(data: AllValuesDefinition) {
if (
overlayValues.length <= ColorPaletteDefault.length - 1 &&
overlayType === 'categorical'
) {
if (selected.has(data.label)) return;
const lastItem = [...overlayValues].pop() ?? ''; // TEMP until better solution
onChange({
...configuration,
selectedValues: overlayValues
.slice(0, overlayValues.length - 1)
.concat(data.label, lastItem),
allValues: allValuesSorted,
});
} else {
alert(`Only ${ColorPaletteDefault.length - 1} values can be selected`);
}
}

function handleDeselection(data: AllValuesDefinition) {
if (overlayType === 'categorical')
onChange({
...configuration,
selectedValues: overlayValues.filter((val) => val !== data.label),
allValues: allValuesSorted,
});
}

const tableState = {
options: {
isRowSelected: (value: AllValuesDefinition) => selected.has(value.label),
},
eventHandlers: {
onRowSelect: handleSelection,
onRowDeselect: handleDeselection,
},
actions: [],
rows: allValuesSorted as AllValuesDefinition[],
columns: [
{
key: 'values',
name: 'Values',
renderCell: (data: { row: AllValuesDefinition }) => (
<>{data.row.label}</>
),
},
{
key: 'counts',
name: 'Counts',
renderCell: (data: { row: AllValuesDefinition }) => (
<>{data.row.count}</>
),
},
{
key: 'distribution',
name: 'Distribution',
renderCell: (data: { row: AllValuesDefinition }) => (
<Distribution count={data.row.count} filteredTotal={totalCount} />
),
},
],
};
return <Mesa state={tableState} />;
}

type DistributionProps = {
count: number;
filteredTotal: number;
};

function Distribution({ count, filteredTotal }: DistributionProps) {
return (
<Tooltip title={`${count} out of ${filteredTotal}`}>
<div
style={{
width: '99%',
position: 'relative',
cursor: 'pointer',
}}
>
<div
style={{
width: (count / filteredTotal) * 100 + '%',
backgroundColor: 'black',
minWidth: 1,
position: 'absolute',
height: '1em',
}}
></div>
</div>
</Tooltip>
);
}
Loading