From 3ff0400d614ca143b8d405cd95fb72eb8ffcacea Mon Sep 17 00:00:00 2001 From: "Dae Kun (DK) Kwon" Date: Thu, 25 May 2023 08:36:33 -0400 Subject: [PATCH 01/56] preliminary bubble marker component --- .../libs/components/src/map/BubbleMarker.tsx | 193 ++++++++++++++++++ .../src/stories/BubbleMarkers.stories.tsx | 56 +++++ 2 files changed, 249 insertions(+) create mode 100755 packages/libs/components/src/map/BubbleMarker.tsx create mode 100644 packages/libs/components/src/stories/BubbleMarkers.stories.tsx diff --git a/packages/libs/components/src/map/BubbleMarker.tsx b/packages/libs/components/src/map/BubbleMarker.tsx new file mode 100755 index 0000000000..4626c0482c --- /dev/null +++ b/packages/libs/components/src/map/BubbleMarker.tsx @@ -0,0 +1,193 @@ +import React from 'react'; +import L from 'leaflet'; +import BoundsDriftMarker, { BoundsDriftMarkerProps } from './BoundsDriftMarker'; + +import { + MarkerScaleAddon, + MarkerScaleDefault, + ContainerStylesAddon, +} from '../types/plots'; + +import { last } from 'lodash'; + +// ts definition for HistogramMarkerSVGProps: need some adjustment but for now, just use bubble marker one +export interface BubbleMarkerProps + extends BoundsDriftMarkerProps, + MarkerScaleAddon { + data: { + //TODO: will bubble size depend on either data.value relatively or backend response? + value: number; + label: string; + color?: string; + }[]; + // isAtomic: add a special thumbtack icon if this is true + isAtomic?: boolean; + onClick?: (event: L.LeafletMouseEvent) => void | undefined; + /** center title/number for marker (defaults to sum of data[].value) */ + markerLabel?: string; + /** cumulative mode: values are expected in order and to **already** be cumulative in nature. + * That is, values 20, 40, 60, 80, 100 would generate five equal-sized segments. The final + * value does not have to be 100. 2,4,6,8,10 would produce the same bubble + * (but with different mouse-overs in the enlarged version.) */ + cumulative?: boolean; +} + +/** + * this is a SVG bubble marker icon + */ +export default function BubbleMarker(props: BubbleMarkerProps) { + const { + html: svgHTML, + size, + markerLabel, + sliceTextOverrides, + } = bubbleMarkerSVGIcon(props); + + // set icon as divIcon + const SVGBubbleIcon: any = L.divIcon({ + className: 'leaflet-canvas-icon', // may need to change this className but just leave it as it for now + iconSize: new L.Point(size, size), // this will make icon to cover up SVG area! + iconAnchor: new L.Point(size / 2, size / 2), // location of topleft corner: this is used for centering of the icon like transform/translate in CSS + html: svgHTML, // divIcon HTML svg code generated above + }); + + // anim check duration exists or not + const duration: number = props.duration ? props.duration : 300; + + return ( + + ); +} + +type BubbleMarkerStandaloneProps = Omit< + BubbleMarkerProps, + | 'id' + | 'position' + | 'bounds' + | 'onClick' + | 'duration' + | 'showPopup' + | 'popupClass' + | 'popupContent' +> & + ContainerStylesAddon; + +export function BubbleMarkerStandalone(props: BubbleMarkerStandaloneProps) { + const { html, size } = bubbleMarkerSVGIcon(props); + // NOTE: the font size and line height would normally come from the .leaflet-container class + // but we won't be using that. You can override these with `containerStyles` if you like. + return ( +
+ ); +} + +function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { + html: string; + size: number; + sliceTextOverrides: string[]; + markerLabel: string; +} { + const scale = props.markerScale ?? MarkerScaleDefault; + const size = 40 * scale; + // set outter white circle size to describe white boundary + const backgroundWhiteCircleRadius = size / 2 + size / 16; + + let svgHTML: string = ''; + + // set drawing area + svgHTML += + ''; // initiate svg marker icon + + // what value corresponds to 360 degrees of the circle? + // regular mode: summation of fullStat.value per marker icon + // cumulative mode: take the last value + const fullPieValue: number = props.cumulative + ? last(props.data)?.value ?? 0 + : props.data + .map((o) => o.value) + .reduce((a, c) => { + return a + c; + }); + + // for display, convert large value with k (e.g., 12345 -> 12k): return original value if less than a criterion + const sumLabel = props.markerLabel ?? String(fullPieValue); + + // draw a larger white-filled circle + svgHTML += + ''; + + // set start point of arc = 0 + let cumulativeSum = 0; + const sliceTextOverrides: string[] = []; + + // create bubbles + props.data.forEach(function (el) { + // if fullPieValue = 0, do not draw arc + if (fullPieValue > 0) { + // compute the ratio of each data to the total number + const thisValue = el.value - cumulativeSum; // subtracts nothing if not in cumulative mode, see below + + if (props.cumulative) + // only sum up in cumulative mode + cumulativeSum += thisValue; + + //TODO: two things to consider: a) bubble size; b) bubble color + svgHTML += + ''; + } + }); + + //TODO: do we need to show total number for bubble marker? + // adding total number text/label and centering it + svgHTML += + '' + + sumLabel + + ''; + + // check isAtomic: draw pushpin if true + if (props.isAtomic) { + let pushPinCode = '🖈'; + svgHTML += + '' + + pushPinCode + + ''; + } + + // closing svg tag + svgHTML += ''; + + return { html: svgHTML, size, sliceTextOverrides, markerLabel: sumLabel }; +} diff --git a/packages/libs/components/src/stories/BubbleMarkers.stories.tsx b/packages/libs/components/src/stories/BubbleMarkers.stories.tsx new file mode 100644 index 0000000000..b1922cd9ad --- /dev/null +++ b/packages/libs/components/src/stories/BubbleMarkers.stories.tsx @@ -0,0 +1,56 @@ +import React, { ReactElement, useState, useCallback, useEffect } from 'react'; +import { Story, Meta } from '@storybook/react/types-6-0'; +// import { action } from '@storybook/addon-actions'; +import { BoundsViewport } from '../map/Types'; +import { BoundsDriftMarkerProps } from '../map/BoundsDriftMarker'; +import { defaultAnimationDuration } from '../map/config/map'; +import { leafletZoomLevelToGeohashLevel } from '../map/utils/leaflet-geohash'; +import { + getSpeciesDonuts, + getSpeciesBasicMarkers, +} from './api/getMarkersFromFixtureData'; + +import { LeafletMouseEvent } from 'leaflet'; +import { Viewport } from '../map/MapVEuMap'; + +// sidebar & legend +import MapVEuMap, { MapVEuMapProps } from '../map/MapVEuMap'; +import geohashAnimation from '../map/animation_functions/geohash'; +import { MouseMode } from '../map/MouseTools'; + +import BubbleMarker, { + BubbleMarkerProps, + BubbleMarkerStandalone, +} from '../map/BubbleMarker'; + +export default { + title: 'Map/Bubble Markers', +} as Meta; + +export const Standalone: Story = () => { + return ( + + ); +}; From e739bb32f154424d936d5fc898f84cbb7de95000 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Thu, 8 Jun 2023 14:42:55 -0400 Subject: [PATCH 02/56] Update bubble spec and design --- .../libs/components/src/map/BubbleMarker.tsx | 116 +++++------------- .../src/stories/BubbleMarkers.stories.tsx | 46 ++++--- 2 files changed, 60 insertions(+), 102 deletions(-) diff --git a/packages/libs/components/src/map/BubbleMarker.tsx b/packages/libs/components/src/map/BubbleMarker.tsx index 4626c0482c..3e22d5d493 100755 --- a/packages/libs/components/src/map/BubbleMarker.tsx +++ b/packages/libs/components/src/map/BubbleMarker.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +// import React from 'react'; import L from 'leaflet'; import BoundsDriftMarker, { BoundsDriftMarkerProps } from './BoundsDriftMarker'; @@ -8,43 +8,29 @@ import { ContainerStylesAddon, } from '../types/plots'; -import { last } from 'lodash'; - // ts definition for HistogramMarkerSVGProps: need some adjustment but for now, just use bubble marker one export interface BubbleMarkerProps extends BoundsDriftMarkerProps, MarkerScaleAddon { data: { //TODO: will bubble size depend on either data.value relatively or backend response? - value: number; - label: string; - color?: string; - }[]; + /** Bubble diameter */ + size: number; + color: string; + }; // isAtomic: add a special thumbtack icon if this is true isAtomic?: boolean; onClick?: (event: L.LeafletMouseEvent) => void | undefined; - /** center title/number for marker (defaults to sum of data[].value) */ - markerLabel?: string; - /** cumulative mode: values are expected in order and to **already** be cumulative in nature. - * That is, values 20, 40, 60, 80, 100 would generate five equal-sized segments. The final - * value does not have to be 100. 2,4,6,8,10 would produce the same bubble - * (but with different mouse-overs in the enlarged version.) */ - cumulative?: boolean; } /** * this is a SVG bubble marker icon */ export default function BubbleMarker(props: BubbleMarkerProps) { - const { - html: svgHTML, - size, - markerLabel, - sliceTextOverrides, - } = bubbleMarkerSVGIcon(props); + const { html: svgHTML, size } = bubbleMarkerSVGIcon(props); // set icon as divIcon - const SVGBubbleIcon: any = L.divIcon({ + const SVGBubbleIcon = L.divIcon({ className: 'leaflet-canvas-icon', // may need to change this className but just leave it as it for now iconSize: new L.Point(size, size), // this will make icon to cover up SVG area! iconAnchor: new L.Point(size / 2, size / 2), // location of topleft corner: this is used for centering of the icon like transform/translate in CSS @@ -59,7 +45,7 @@ export default function BubbleMarker(props: BubbleMarkerProps) { id={props.id} position={props.position} bounds={props.bounds} - icon={SVGBubbleIcon} + icon={SVGBubbleIcon as L.Icon} duration={duration} /> ); @@ -99,83 +85,49 @@ export function BubbleMarkerStandalone(props: BubbleMarkerStandaloneProps) { function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { html: string; size: number; - sliceTextOverrides: string[]; - markerLabel: string; } { const scale = props.markerScale ?? MarkerScaleDefault; - const size = 40 * scale; - // set outter white circle size to describe white boundary - const backgroundWhiteCircleRadius = size / 2 + size / 16; + const size = props.data.size * scale; + const circleRadius = size / 2; let svgHTML: string = ''; // set drawing area svgHTML += - ''; // initiate svg marker icon - - // what value corresponds to 360 degrees of the circle? - // regular mode: summation of fullStat.value per marker icon - // cumulative mode: take the last value - const fullPieValue: number = props.cumulative - ? last(props.data)?.value ?? 0 - : props.data - .map((o) => o.value) - .reduce((a, c) => { - return a + c; - }); + ''; // initiate svg marker icon // for display, convert large value with k (e.g., 12345 -> 12k): return original value if less than a criterion - const sumLabel = props.markerLabel ?? String(fullPieValue); + // const sumLabel = props.markerLabel ?? String(fullPieValue); // draw a larger white-filled circle + // svgHTML += + // ''; + + // create bubble + //TODO: two things to consider: a) bubble size; b) bubble color svgHTML += ''; - - // set start point of arc = 0 - let cumulativeSum = 0; - const sliceTextOverrides: string[] = []; - - // create bubbles - props.data.forEach(function (el) { - // if fullPieValue = 0, do not draw arc - if (fullPieValue > 0) { - // compute the ratio of each data to the total number - const thisValue = el.value - cumulativeSum; // subtracts nothing if not in cumulative mode, see below - - if (props.cumulative) - // only sum up in cumulative mode - cumulativeSum += thisValue; - - //TODO: two things to consider: a) bubble size; b) bubble color - svgHTML += - ''; - } - }); + circleRadius + + '" stroke="green" stroke-width="0" fill="' + + props.data.color + + '" />'; //TODO: do we need to show total number for bubble marker? // adding total number text/label and centering it - svgHTML += - '' + - sumLabel + - ''; + // svgHTML += + // '' + + // sumLabel + + // ''; // check isAtomic: draw pushpin if true if (props.isAtomic) { @@ -189,5 +141,5 @@ function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { // closing svg tag svgHTML += ''; - return { html: svgHTML, size, sliceTextOverrides, markerLabel: sumLabel }; + return { html: svgHTML, size }; } diff --git a/packages/libs/components/src/stories/BubbleMarkers.stories.tsx b/packages/libs/components/src/stories/BubbleMarkers.stories.tsx index b1922cd9ad..9b43a3a324 100644 --- a/packages/libs/components/src/stories/BubbleMarkers.stories.tsx +++ b/packages/libs/components/src/stories/BubbleMarkers.stories.tsx @@ -29,28 +29,34 @@ export default { export const Standalone: Story = () => { return ( - + + + + }} + isAtomic={false} + markerScale={1} + containerStyles={{ margin: '10px' }} + /> +
); }; From 93068429dc8cbb7d7bc6805dc727e64b54222e8e Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Tue, 13 Jun 2023 20:16:06 -0400 Subject: [PATCH 03/56] Get working bubble markers on map --- .../libs/components/src/map/BubbleMarker.tsx | 29 +++++++++------- .../libs/components/src/map/DonutMarker.tsx | 2 ++ .../src/stories/BubbleMarkers.stories.tsx | 33 ++++++++++++------- .../eda/src/lib/core/hooks/mapMarkers.tsx | 3 +- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 29 +++++++++++----- .../analysis/hooks/standaloneMapMarkers.tsx | 16 ++++++++- 6 files changed, 78 insertions(+), 34 deletions(-) diff --git a/packages/libs/components/src/map/BubbleMarker.tsx b/packages/libs/components/src/map/BubbleMarker.tsx index 3e22d5d493..d2a2a4f835 100755 --- a/packages/libs/components/src/map/BubbleMarker.tsx +++ b/packages/libs/components/src/map/BubbleMarker.tsx @@ -7,19 +7,20 @@ import { MarkerScaleDefault, ContainerStylesAddon, } from '../types/plots'; +import { NumberRange } from '../types/general'; // ts definition for HistogramMarkerSVGProps: need some adjustment but for now, just use bubble marker one export interface BubbleMarkerProps extends BoundsDriftMarkerProps, MarkerScaleAddon { data: { - //TODO: will bubble size depend on either data.value relatively or backend response? - /** Bubble diameter */ - size: number; - color: string; - }; + value: number; + label: string; + color?: string; + }[]; // isAtomic: add a special thumbtack icon if this is true isAtomic?: boolean; + dependentAxisRange?: NumberRange | null; // y-axis range for setting global max onClick?: (event: L.LeafletMouseEvent) => void | undefined; } @@ -87,7 +88,12 @@ function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { size: number; } { const scale = props.markerScale ?? MarkerScaleDefault; - const size = props.data.size * scale; + console.log({ dependentAxisRange: props.dependentAxisRange }); + // defined assertion here + const size = + 100 * + (Math.log(props.data[0].value) / Math.log(props.dependentAxisRange!.max)) * + scale; const circleRadius = size / 2; let svgHTML: string = ''; @@ -119,15 +125,16 @@ function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { '" r="' + circleRadius + '" stroke="green" stroke-width="0" fill="' + - props.data.color + + // color is possibly undefined + props.data[0].color + '" />'; //TODO: do we need to show total number for bubble marker? // adding total number text/label and centering it - // svgHTML += - // '' + - // sumLabel + - // ''; + svgHTML += + '' + + props.data[0].value + + ''; // check isAtomic: draw pushpin if true if (props.isAtomic) { diff --git a/packages/libs/components/src/map/DonutMarker.tsx b/packages/libs/components/src/map/DonutMarker.tsx index 1d4b044767..bc79797c4d 100755 --- a/packages/libs/components/src/map/DonutMarker.tsx +++ b/packages/libs/components/src/map/DonutMarker.tsx @@ -9,6 +9,7 @@ import { PiePlotDatum, ContainerStylesAddon, } from '../types/plots'; +import { NumberRange } from '../types/general'; import { last } from 'lodash'; @@ -31,6 +32,7 @@ export interface DonutMarkerProps * value does not have to be 100. 2,4,6,8,10 would produce the same donut * (but with different mouse-overs in the enlarged version.) */ cumulative?: boolean; + dependentAxisRange?: NumberRange | null; // y-axis range for setting global max } // convert to Cartesian coord. toCartesian(centerX, centerY, Radius for arc to draw, arc (radian)) diff --git a/packages/libs/components/src/stories/BubbleMarkers.stories.tsx b/packages/libs/components/src/stories/BubbleMarkers.stories.tsx index 9b43a3a324..14936b3200 100644 --- a/packages/libs/components/src/stories/BubbleMarkers.stories.tsx +++ b/packages/libs/components/src/stories/BubbleMarkers.stories.tsx @@ -31,28 +31,37 @@ export const Standalone: Story = () => { return (
markersData?.map((markerProps) => markerType === 'pie' ? ( - + ) : ( ) @@ -464,6 +466,7 @@ function MapAnalysisImpl(props: ImplProps) { const filteredEntities = uniq(filters?.map((f) => f.entityId)); + //here const sideNavigationButtonConfigurationObjects: SideNavigationItemConfigurationObject[] = [ { @@ -471,14 +474,14 @@ function MapAnalysisImpl(props: ImplProps) { icon: , isExpandable: true, subMenuConfig: [ - { - // concatenating the parent and subMenu labels creates a unique ID - id: MapSideNavItemLabels.MapType + MarkerTypeLabels.pie, - labelText: MarkerTypeLabels.pie, - icon: , - onClick: () => setActiveMarkerConfigurationType('pie'), - isActive: activeMarkerConfigurationType === 'pie', - }, + // { + // // concatenating the parent and subMenu labels creates a unique ID + // id: MapSideNavItemLabels.MapType + MarkerTypeLabels.pie, + // labelText: MarkerTypeLabels.pie, + // icon: , + // onClick: () => setActiveMarkerConfigurationType('pie'), + // isActive: activeMarkerConfigurationType === 'pie', + // }, { // concatenating the parent and subMenu labels creates a unique ID id: MapSideNavItemLabels.MapType + MarkerTypeLabels.barplot, @@ -487,6 +490,14 @@ function MapAnalysisImpl(props: ImplProps) { onClick: () => setActiveMarkerConfigurationType('barplot'), isActive: activeMarkerConfigurationType === 'barplot', }, + { + // concatenating the parent and subMenu labels creates a unique ID + id: MapSideNavItemLabels.MapType + MarkerTypeLabels.bubble, + labelText: MarkerTypeLabels.bubble, + icon: , + onClick: () => setActiveMarkerConfigurationType('pie'), + isActive: activeMarkerConfigurationType === 'pie', + }, ], renderSideNavigationPanel: (apps) => { const markerVariableConstraints = apps diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 635cdd06fa..0e0b8a8b75 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -139,6 +139,7 @@ export function useStandaloneMapMarkers( ? overlayConfig?.overlayValues.map((ov) => ov.binLabel) : undefined; + // here const rawMarkersData = usePromise( useCallback(async () => { // check all required vizConfigs are provided @@ -252,6 +253,8 @@ export function useStandaloneMapMarkers( dependentAxisLogScale ) as NumberRange; + console.log({ defaultDependentAxisRange }); + /** * Merge the overlay data into the basicMarkerData, if available, * and create markers. @@ -316,17 +319,27 @@ export function useStandaloneMapMarkers( }, ]; + // here's what Bob pointed out const count = vocabulary != null // if there's an overlay (all expected use cases) ? overlayValues.reduce((sum, { count }) => (sum = sum + count), 0) : entityCount; // fallback if not + const bubbleData = [ + { + label: reorderedData[0].label, + value: count, + color: + 'color' in reorderedData[0] ? reorderedData[0].color : undefined, + }, + ]; + const commonMarkerProps = { id: geoAggregateValue, key: geoAggregateValue, bounds: bounds, position: position, - data: reorderedData, + data: bubbleData, duration: defaultAnimationDuration, }; @@ -335,6 +348,7 @@ export function useStandaloneMapMarkers( return { ...commonMarkerProps, markerLabel: kFormatter(count), + dependentAxisRange: defaultDependentAxisRange, } as DonutMarkerProps; } default: { From 96ad6e98e6f10dd909f35e49590477957fe5e1ae Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Wed, 14 Jun 2023 18:15:55 -0400 Subject: [PATCH 04/56] Add dedicated bubble marker menu option --- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 58 ++++++++++++--- .../BubbleMarkerConfigurationMenu.tsx | 74 +++++++++++++++++++ .../icons/BubbleMarker.tsx | 49 ++++++++++++ .../icons/BubbleMarkers.tsx | 36 +++++++++ .../MarkerConfiguration/icons/index.ts | 11 ++- .../map/analysis/MarkerConfiguration/index.ts | 2 + .../libs/eda/src/lib/map/analysis/appState.ts | 10 +++ .../analysis/hooks/standaloneMapMarkers.tsx | 33 +++++++-- 8 files changed, 255 insertions(+), 18 deletions(-) create mode 100644 packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx create mode 100644 packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarker.tsx create mode 100644 packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarkers.tsx diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index 62de0c3cf5..d9e8fc26e5 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -70,8 +70,13 @@ import { RecordController } from '@veupathdb/wdk-client/lib/Controllers'; import { BarPlotMarkerConfigurationMenu, PieMarkerConfigurationMenu, + BubbleMarkerConfigurationMenu, } from './MarkerConfiguration'; -import { BarPlotMarker, DonutMarker } from './MarkerConfiguration/icons'; +import { + BarPlotMarker, + DonutMarker, + BubbleMarker, +} from './MarkerConfiguration/icons'; import { leastAncestralEntity } from '../../core/utils/data-element-constraints'; import { getDefaultOverlayConfig } from './utils/defaultOverlayConfig'; import { AllAnalyses } from '../../workspace/AllAnalyses'; @@ -313,6 +318,8 @@ function MapAnalysisImpl(props: ImplProps) { case 'barplot': { return activeMarkerConfiguration?.selectedPlotMode; // count or proportion } + case 'bubble': + return 'bubble'; case 'pie': default: return 'pie'; @@ -342,6 +349,8 @@ function MapAnalysisImpl(props: ImplProps) { () => markersData?.map((markerProps) => markerType === 'pie' ? ( + + ) : markerType === 'bubble' ? ( ) : ( @@ -474,14 +483,14 @@ function MapAnalysisImpl(props: ImplProps) { icon: , isExpandable: true, subMenuConfig: [ - // { - // // concatenating the parent and subMenu labels creates a unique ID - // id: MapSideNavItemLabels.MapType + MarkerTypeLabels.pie, - // labelText: MarkerTypeLabels.pie, - // icon: , - // onClick: () => setActiveMarkerConfigurationType('pie'), - // isActive: activeMarkerConfigurationType === 'pie', - // }, + { + // concatenating the parent and subMenu labels creates a unique ID + id: MapSideNavItemLabels.MapType + MarkerTypeLabels.pie, + labelText: MarkerTypeLabels.pie, + icon: , + onClick: () => setActiveMarkerConfigurationType('pie'), + isActive: activeMarkerConfigurationType === 'pie', + }, { // concatenating the parent and subMenu labels creates a unique ID id: MapSideNavItemLabels.MapType + MarkerTypeLabels.barplot, @@ -494,9 +503,9 @@ function MapAnalysisImpl(props: ImplProps) { // concatenating the parent and subMenu labels creates a unique ID id: MapSideNavItemLabels.MapType + MarkerTypeLabels.bubble, labelText: MarkerTypeLabels.bubble, - icon: , - onClick: () => setActiveMarkerConfigurationType('pie'), - isActive: activeMarkerConfigurationType === 'pie', + icon: , + onClick: () => setActiveMarkerConfigurationType('bubble'), + isActive: activeMarkerConfigurationType === 'bubble', }, ], renderSideNavigationPanel: (apps) => { @@ -557,6 +566,31 @@ function MapAnalysisImpl(props: ImplProps) { <> ), }, + { + type: 'bubble', + displayName: MarkerTypeLabels.bubble, + icon: ( + + ), + configurationMenu: + activeMarkerConfiguration?.type === 'bubble' ? ( + + ) : ( + <> + ), + }, ]; const mapTypeConfigurationMenuTabs: TabbedDisplayProps< diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx new file mode 100644 index 0000000000..1c7869b07a --- /dev/null +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx @@ -0,0 +1,74 @@ +import { + InputVariables, + Props as InputVariablesProps, +} from '../../../core/components/visualizations/InputVariables'; +import { VariableDescriptor } from '../../../core/types/variable'; +import { VariablesByInputName } from '../../../core/utils/data-element-constraints'; + +interface MarkerConfiguration { + type: T; +} +export interface BubbleMarkerConfiguration + extends MarkerConfiguration<'bubble'> { + selectedVariable: VariableDescriptor; + selectedValues: string[] | undefined; +} +interface Props + extends Omit< + InputVariablesProps, + 'onChange' | 'selectedVariables' | 'selectedPlotMode' | 'onPlotSelected' + > { + onChange: (configuration: BubbleMarkerConfiguration) => void; + configuration: BubbleMarkerConfiguration; +} + +// Currently identical to pie marker configuration menu +export function BubbleMarkerConfigurationMenu({ + entities, + configuration, + onChange, + starredVariables, + toggleStarredVariable, + constraints, +}: Props) { + function handleInputVariablesOnChange(selection: VariablesByInputName) { + if (!selection.overlayVariable) { + console.error( + `Expected overlayVariable to be defined but got ${typeof selection.overlayVariable}` + ); + return; + } + + onChange({ + ...configuration, + selectedVariable: selection.overlayVariable, + selectedValues: undefined, + }); + } + + return ( +
+

+ Color: +

+ +
+ ); +} diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarker.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarker.tsx new file mode 100644 index 0000000000..1e9918df34 --- /dev/null +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarker.tsx @@ -0,0 +1,49 @@ +import { SVGProps } from 'react'; +export function BubbleMarker(props: SVGProps) { + return ( + + + + + + + + + + + ); +} diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarkers.tsx new file mode 100644 index 0000000000..754df0f1dd --- /dev/null +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarkers.tsx @@ -0,0 +1,36 @@ +import { SVGProps } from 'react'; +// Currently same as DonutMarkers +export function BubbleMarkers(props: SVGProps) { + return ( + + + + + + + + + + ); +} diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/index.ts b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/index.ts index 48e8b56bf2..91e7469651 100644 --- a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/index.ts +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/index.ts @@ -2,5 +2,14 @@ import { DonutMarker } from './DonutMarker'; import { DonutMarkers } from './DonutMarkers'; import { BarPlotMarker } from './BarPlotMarker'; import { BarPlotMarkers } from './BarPlotMarkers'; +import { BubbleMarker } from './BubbleMarker'; +import { BubbleMarkers } from './BubbleMarkers'; -export { DonutMarker, DonutMarkers, BarPlotMarker, BarPlotMarkers }; +export { + DonutMarker, + DonutMarkers, + BarPlotMarker, + BarPlotMarkers, + BubbleMarker, + BubbleMarkers, +}; diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/index.ts b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/index.ts index d6102c524a..3b67e0b8c1 100644 --- a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/index.ts +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/index.ts @@ -1,9 +1,11 @@ import { BarPlotMarkerConfigurationMenu } from './BarPlotMarkerConfigurationMenu'; import { PieMarkerConfigurationMenu } from './PieMarkerConfigurationMenu'; import { MarkerConfigurationSelector } from './MarkerConfigurationSelector'; +import { BubbleMarkerConfigurationMenu } from './BubbleMarkerConfigurationMenu'; export { MarkerConfigurationSelector, PieMarkerConfigurationMenu, BarPlotMarkerConfigurationMenu, + BubbleMarkerConfigurationMenu, }; diff --git a/packages/libs/eda/src/lib/map/analysis/appState.ts b/packages/libs/eda/src/lib/map/analysis/appState.ts index 7896b8f69d..e2039ee1d5 100644 --- a/packages/libs/eda/src/lib/map/analysis/appState.ts +++ b/packages/libs/eda/src/lib/map/analysis/appState.ts @@ -16,6 +16,7 @@ const LatLngLiteral = t.type({ lat: t.number, lng: t.number }); const MarkerType = t.keyof({ barplot: null, pie: null, + bubble: null, }); export type MarkerConfiguration = t.TypeOf; @@ -35,6 +36,10 @@ export const MarkerConfiguration = t.intersection([ type: t.literal('pie'), selectedValues: t.union([t.array(t.string), t.undefined]), // user-specified selection }), + t.type({ + type: t.literal('bubble'), + selectedValues: t.union([t.array(t.string), t.undefined]), // user-specified selection + }), ]), ]); @@ -110,6 +115,11 @@ export function useAppState(uiStateKey: string, analysisState: AnalysisState) { selectedVariable: defaultVariable, selectedValues: undefined, }, + { + type: 'bubble', + selectedVariable: defaultVariable, + selectedValues: undefined, + }, ], }; setVariableUISettings((prev) => ({ diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 0e0b8a8b75..fcbbac61fc 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -27,6 +27,7 @@ import { useDeepValue } from '../../../core/hooks/immutability'; import { UNSELECTED_DISPLAY_TEXT, UNSELECTED_TOKEN } from '../..'; import { DonutMarkerProps } from '@veupathdb/components/lib/map/DonutMarker'; import { ChartMarkerProps } from '@veupathdb/components/lib/map/ChartMarker'; +import { BubbleMarkerProps } from '@veupathdb/components/lib/map/BubbleMarker'; /** * Provides markers for use in the MapVEuMap component @@ -47,14 +48,18 @@ export interface StandaloneMapMarkersProps { */ overlayConfig: OverlayConfig | undefined; outputEntityId: string | undefined; - markerType: 'count' | 'proportion' | 'pie'; + markerType: 'count' | 'proportion' | 'pie' | 'bubble'; dependentAxisLogScale?: boolean; } // what this hook returns interface MapMarkers { /** the markers */ - markersData: DonutMarkerProps[] | ChartMarkerProps[] | undefined; + markersData: + | DonutMarkerProps[] + | ChartMarkerProps[] + | BubbleMarkerProps[] + | undefined; /** `totalVisibleEntityCount` tells you how many entities are visible at a given viewport. But not necessarily with data for the overlay variable. */ totalVisibleEntityCount: number | undefined; /** This tells you how many entities are on screen that also have data for the overlay variable @@ -86,6 +91,8 @@ export function useStandaloneMapMarkers( dependentAxisLogScale = false, } = props; + console.log({ markerType }); + // these two deepvalue eliminate an unnecessary data request // when switching between pie and bar markers when using the same variable const selectedOverlayVariable = useDeepValue(sov); @@ -178,7 +185,10 @@ export function useStandaloneMapMarkers( longitudeVariable, overlayConfig, outputEntityId, - valueSpec: markerType === 'pie' ? 'count' : markerType, + valueSpec: + markerType === 'pie' || markerType === 'bubble' + ? 'count' + : markerType, viewport: { latitude: { xMin, @@ -192,6 +202,8 @@ export function useStandaloneMapMarkers( }, }; + console.log('here1'); + // now get the data return await dataClient.getStandaloneMapMarkers( 'standalone-map', @@ -213,6 +225,9 @@ export function useStandaloneMapMarkers( ]) ); + console.log('here2'); + console.log({ rawMarkersData }); + const totalVisibleEntityCount: number | undefined = rawMarkersData.value?.mapElements.reduce((acc, curr) => { return acc + curr.entityCount; @@ -339,7 +354,6 @@ export function useStandaloneMapMarkers( key: geoAggregateValue, bounds: bounds, position: position, - data: bubbleData, duration: defaultAnimationDuration, }; @@ -347,13 +361,22 @@ export function useStandaloneMapMarkers( case 'pie': { return { ...commonMarkerProps, + data: reorderedData, markerLabel: kFormatter(count), - dependentAxisRange: defaultDependentAxisRange, } as DonutMarkerProps; } + case 'bubble': { + return { + ...commonMarkerProps, + data: bubbleData, + markerLabel: String(count), + dependentAxisRange: defaultDependentAxisRange, + } as BubbleMarkerProps; + } default: { return { ...commonMarkerProps, + data: reorderedData, markerLabel: mFormatter(count), dependentAxisRange: defaultDependentAxisRange, dependentAxisLogScale, From 0688eec46edcf9263f13b273446ce20cbbae0510 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Tue, 20 Jun 2023 13:00:36 -0400 Subject: [PATCH 05/56] Make Bubble Marker Legend --- .../plotControls/PlotBubbleLegend.tsx | 224 ++++++++++++++++++ .../components/plotControls/PlotLegend.tsx | 8 +- .../libs/components/src/map/BubbleMarker.tsx | 12 +- .../plotControls/PlotLegend.stories.tsx | 37 +++ .../analysis/hooks/standaloneMapMarkers.tsx | 19 ++ 5 files changed, 291 insertions(+), 9 deletions(-) create mode 100644 packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx diff --git a/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx new file mode 100644 index 0000000000..7c12e5794e --- /dev/null +++ b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx @@ -0,0 +1,224 @@ +import React from 'react'; +import { range } from 'd3'; + +// set props for custom legend function +export interface PlotLegendBubbleProps { + legendMax: number; + // legendMin: number; + valueToSizeMapper: (value: number) => number; + // nTicks?: number; // MUST be odd! + // showMissingness?: boolean; +} + +// legend ellipsis function for legend title and legend items (from custom legend work) +// const legendEllipsis = (label: string, ellipsisLength: number) => { +// return (label || '').length > ellipsisLength +// ? (label || '').substring(0, ellipsisLength) + '...' +// : label; +// }; + +// make gradient colorscale legend into a component so it can be more easily incorporated into DK's custom legend if we need +export default function PlotBubbleLegend({ + legendMax, + // legendMin, + valueToSizeMapper, +}: // nTicks = 5, +// showMissingness, +PlotLegendBubbleProps) { + // Declare constants + // const gradientBoxHeight = 150; + // const gradientBoxWidth = 20; + const tickFontSize = '0.8em'; + const legendTextSize = '1.0em'; + const circleStrokeWidth = 3; + const padding = 5; + // const largestDataCircleSize = valueToSizeMapper(legendMax); + const numCircles = 3; + + // the value of the largest circle in the legend will be the smallest power of 10 that's larger than legendMax + const largestCircleValue = Math.pow(10, Math.ceil(Math.log10(legendMax))); + const largestCircleDiameter = valueToSizeMapper(largestCircleValue); + const largestCircleRadius = largestCircleDiameter / 2; + const circleValues = range(numCircles).map( + (i) => largestCircleValue / Math.pow(10, i) + ); + + console.log({ circleValues }); + + const tickLength = largestCircleRadius + 5; + + // Create gradient stop points from the colorscale from values [legendMin TO legendMax] at an arbitrary 50 step resolution + // const numCircles = 3; + // const legendStep = legendMax / (numCircles - 1); + // // const fudge = legendStep / 10; // to get an inclusive range from d3 we have to make a slightly too-large max + // const stopPoints = range(legendStep, legendMax, legendStep).map( + // (value: number, index: number) => { + // const size = valueToSizeMapper(value); + // return ( + // + // ); + // } + // ); + + // let svgHTML: string = ''; + // set drawing area + // svgHTML += + // ''; // initiate svg marker icon + + const BubbleLegendSVG = () => ( + + {circleValues.map((value, i) => { + console.log({ value }); + // const value = legendMax * (i / (numCircles - 1)); + const circleDiameter = valueToSizeMapper(value); + const circleRadius = circleDiameter / 2; + const tickY = + padding + largestCircleDiameter + circleStrokeWidth - circleDiameter; + // const stopPercentage = (i / (numCircles - 1)) * 100; + + return ( + <> + + + + + {value} + + + + ); + })} + + ); + // ''; + + // + // ); + + // }); + + // for display, convert large value with k (e.g., 12345 -> 12k): return original value if less than a criterion + // const sumLabel = props.markerLabel ?? String(fullPieValue); + + // draw a larger white-filled circle + // svgHTML += + // ''; + + // create bubble + //TODO: two things to consider: a) bubble size; b) bubble color + // svgHTML += + // ''; + + //TODO: do we need to show total number for bubble marker? + // adding total number text/label and centering it + // svgHTML += + // '' + + // props.data[0].value + + // ''; + + // // check isAtomic: draw pushpin if true + // if (props.isAtomic) { + // let pushPinCode = '🖈'; + // svgHTML += + // '' + + // pushPinCode + + // ''; + // } + + // // closing svg tag + // svgHTML += ''; + + // Create ticks + // const ticks = range(nTicks).map((a: number) => { + // const location: number = + // gradientBoxHeight - gradientBoxHeight * (a / (nTicks! - 1)); // draw bottom to top + // return ( + // + // + // + // {( + // (a / (nTicks! - 1)) * (legendMax - legendMin) + + // legendMin + // ).toPrecision(3)} + // + // + // ); + // }); + + return ( +
+ +
+ ); +} diff --git a/packages/libs/components/src/components/plotControls/PlotLegend.tsx b/packages/libs/components/src/components/plotControls/PlotLegend.tsx index e847e49068..c61447293a 100755 --- a/packages/libs/components/src/components/plotControls/PlotLegend.tsx +++ b/packages/libs/components/src/components/plotControls/PlotLegend.tsx @@ -4,6 +4,7 @@ import PlotListLegend, { PlotListLegendProps } from './PlotListLegend'; import PlotGradientLegend, { PlotLegendGradientProps, } from './PlotGradientLegend'; +import PlotBubbleLegend, { PlotLegendBubbleProps } from './PlotBubbleLegend'; interface PlotLegendBaseProps extends ContainerStylesAddon { legendTitle?: string; @@ -13,6 +14,7 @@ export type PlotLegendProps = PlotLegendBaseProps & ( | ({ type: 'list' } & PlotListLegendProps) | ({ type: 'colorscale' } & PlotLegendGradientProps) + | ({ type: 'bubble' } & PlotLegendBubbleProps) ); export default function PlotLegend({ @@ -29,7 +31,8 @@ export default function PlotLegend({ {((type === 'list' && ((otherProps as PlotListLegendProps).legendItems.length > 1 || (otherProps as PlotListLegendProps).showOverlayLegend)) || - type === 'colorscale') && ( + type === 'colorscale' || + type === 'bubble') && (
)} + {type === 'bubble' && ( + + )}
)} diff --git a/packages/libs/components/src/map/BubbleMarker.tsx b/packages/libs/components/src/map/BubbleMarker.tsx index d2a2a4f835..6f02446b38 100755 --- a/packages/libs/components/src/map/BubbleMarker.tsx +++ b/packages/libs/components/src/map/BubbleMarker.tsx @@ -10,9 +10,7 @@ import { import { NumberRange } from '../types/general'; // ts definition for HistogramMarkerSVGProps: need some adjustment but for now, just use bubble marker one -export interface BubbleMarkerProps - extends BoundsDriftMarkerProps, - MarkerScaleAddon { +export interface BubbleMarkerProps extends BoundsDriftMarkerProps { data: { value: number; label: string; @@ -21,6 +19,7 @@ export interface BubbleMarkerProps // isAtomic: add a special thumbtack icon if this is true isAtomic?: boolean; dependentAxisRange?: NumberRange | null; // y-axis range for setting global max + valueToSizeMapper: (value: number) => number; onClick?: (event: L.LeafletMouseEvent) => void | undefined; } @@ -87,13 +86,10 @@ function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { html: string; size: number; } { - const scale = props.markerScale ?? MarkerScaleDefault; + // const scale = props.markerScale ?? MarkerScaleDefault; console.log({ dependentAxisRange: props.dependentAxisRange }); // defined assertion here - const size = - 100 * - (Math.log(props.data[0].value) / Math.log(props.dependentAxisRange!.max)) * - scale; + const size = props.valueToSizeMapper(props.data[0].value); const circleRadius = size / 2; let svgHTML: string = ''; diff --git a/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx b/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx index 9fc754a19d..fcc4b7e26c 100755 --- a/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx +++ b/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx @@ -527,6 +527,43 @@ export const GradientPlotLegend = () => { ); }; +export const BubbleMarkerLegend = () => { + const maxValue = 100; + // const scale = 1; + + const valueToSizeMapper = (value: number) => { + // Area scales directly with value + const constant = 100; + const area = value * constant; + const radius = Math.sqrt(area / Math.PI); + + // Radius scales with log_10 of value + // const constant = 20; + // const radius = Math.log10(value) * constant; + + // Radius scales directly with value + // const largestCircleSize = 150; + // const constant = maxValue / largestCircleSize; + // const radius = value * constant; + + return 2 * radius; + }; + + return ( +
+ +
+ ); +}; + // custom legend with histogram export const TestLongLegendItems = () => { // long legend test diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index fcbbac61fc..945f0200ec 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -349,6 +349,24 @@ export function useStandaloneMapMarkers( }, ]; + const bubbleValueToSizeMapper = (value: number) => { + // Area scales directly with value + const constant = 100; + const area = value * constant; + const radius = Math.sqrt(area / Math.PI); + + // Radius scales with log_10 of value + // const constant = 20; + // const radius = Math.log10(value) * constant; + + // Radius scales directly with value + // const largestCircleSize = 150; + // const constant = maxValue / largestCircleSize; + // const radius = value * constant; + + return 2 * radius; + }; + const commonMarkerProps = { id: geoAggregateValue, key: geoAggregateValue, @@ -371,6 +389,7 @@ export function useStandaloneMapMarkers( data: bubbleData, markerLabel: String(count), dependentAxisRange: defaultDependentAxisRange, + valueToSizeMapper: bubbleValueToSizeMapper, } as BubbleMarkerProps; } default: { From d0a5f21681f8fbae51376751e76d9fdba5ae7146 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Tue, 20 Jun 2023 13:25:28 -0400 Subject: [PATCH 06/56] Add missing stuff related to bubble markers --- .../src/stories/BubbleMarkers.stories.tsx | 27 ++++++++++++++++--- .../eda/src/lib/core/hooks/mapMarkers.tsx | 2 +- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 6 +++-- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/libs/components/src/stories/BubbleMarkers.stories.tsx b/packages/libs/components/src/stories/BubbleMarkers.stories.tsx index 14936b3200..0ca4555401 100644 --- a/packages/libs/components/src/stories/BubbleMarkers.stories.tsx +++ b/packages/libs/components/src/stories/BubbleMarkers.stories.tsx @@ -27,6 +27,24 @@ export default { title: 'Map/Bubble Markers', } as Meta; +const valueToSizeMapper = (value: number) => { + // Area scales directly with value + const constant = 100; + const area = value * constant; + const radius = Math.sqrt(area / Math.PI); + + // Radius scales with log_10 of value + // const constant = 20; + // const radius = Math.log10(value) * constant; + + // Radius scales directly with value + // const largestCircleSize = 150; + // const constant = maxValue / largestCircleSize; + // const radius = value * constant; + + return 2 * radius; +}; + export const Standalone: Story = () => { return (
@@ -39,7 +57,8 @@ export const Standalone: Story = () => { }, ]} isAtomic={false} - markerScale={1} + // markerScale={1} + valueToSizeMapper={valueToSizeMapper} containerStyles={{ margin: '10px' }} /> = () => { }, ]} isAtomic={false} - markerScale={1} + // markerScale={1} + valueToSizeMapper={valueToSizeMapper} containerStyles={{ margin: '10px' }} /> = () => { }, ]} isAtomic={false} - markerScale={1} + // markerScale={1} + valueToSizeMapper={valueToSizeMapper} containerStyles={{ margin: '10px' }} />
diff --git a/packages/libs/eda/src/lib/core/hooks/mapMarkers.tsx b/packages/libs/eda/src/lib/core/hooks/mapMarkers.tsx index 2ed5b2cd52..77d9f7385e 100644 --- a/packages/libs/eda/src/lib/core/hooks/mapMarkers.tsx +++ b/packages/libs/eda/src/lib/core/hooks/mapMarkers.tsx @@ -518,7 +518,7 @@ export function useMapMarkers(props: MapMarkersProps): MapMarkers { const MarkerComponent = markerType == null || markerType === 'pie' - ? BubbleMarker + ? DonutMarker : ChartMarker; const count = diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index 2c4b6dac85..3198010f71 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -92,7 +92,9 @@ 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 BubbleMarkerComponent from '@veupathdb/components/lib/map/BubbleMarker'; +import BubbleMarkerComponent, { + BubbleMarkerProps, +} from '@veupathdb/components/lib/map/BubbleMarker'; enum MapSideNavItemLabels { Download = 'Download', @@ -351,7 +353,7 @@ function MapAnalysisImpl(props: ImplProps) { markerType === 'pie' ? ( ) : markerType === 'bubble' ? ( - + ) : ( ) From 910286aedfe179742da8c64b10a1a21728fcfc43 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Tue, 20 Jun 2023 13:58:35 -0400 Subject: [PATCH 07/56] Add bubble marker legend to SAM --- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 84 ++++++++++++++----- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index 3198010f71..1e92dcac59 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -95,6 +95,7 @@ import ChartMarkerComponent from '@veupathdb/components/lib/map/ChartMarker'; import BubbleMarkerComponent, { BubbleMarkerProps, } from '@veupathdb/components/lib/map/BubbleMarker'; +import PlotLegend from '@veupathdb/components/lib/components/plotControls/PlotLegend'; enum MapSideNavItemLabels { Download = 'Download', @@ -1060,25 +1061,70 @@ function MapAnalysisImpl(props: ImplProps) { />
- -
- -
-
+ {(markerType === 'count' || markerType === 'proportion') && ( + +
+ +
+
+ )} + + {markerType === 'bubble' && markersData !== undefined && ( + +
+ markerData.data[0].value ?? 0 + ) + ) + : 0 + } + valueToSizeMapper={ + (markersData as BubbleMarkerProps[])[0] + .valueToSizeMapper + } + containerStyles={{ + border: 'none', + boxShadow: 'none', + padding: 0, + width: 'auto', + maxWidth: 400, + }} + /> +
+
+ )} {/* Date: Tue, 20 Jun 2023 14:52:29 -0400 Subject: [PATCH 08/56] Set max bubble marker size --- .../libs/components/src/map/DonutMarker.tsx | 2 - .../analysis/hooks/standaloneMapMarkers.tsx | 54 ++++++++++++------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/packages/libs/components/src/map/DonutMarker.tsx b/packages/libs/components/src/map/DonutMarker.tsx index 46861bb087..590d63e0b0 100755 --- a/packages/libs/components/src/map/DonutMarker.tsx +++ b/packages/libs/components/src/map/DonutMarker.tsx @@ -9,7 +9,6 @@ import { PiePlotDatum, ContainerStylesAddon, } from '../types/plots'; -import { NumberRange } from '../types/general'; import { last } from 'lodash'; @@ -32,7 +31,6 @@ export interface DonutMarkerProps * value does not have to be 100. 2,4,6,8,10 would produce the same donut * (but with different mouse-overs in the enlarged version.) */ cumulative?: boolean; - dependentAxisRange?: NumberRange | null; // y-axis range for setting global max } // convert to Cartesian coord. toCartesian(centerX, centerY, Radius for arc to draw, arc (radian)) diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 945f0200ec..09abe431b2 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -11,7 +11,7 @@ import { Filter } from '../../../core/types/filter'; import { useDataClient } from '../../../core/hooks/workspace'; import { NumberRange } from '../../../core/types/general'; import { useDefaultAxisRange } from '../../../core/hooks/computeDefaultAxisRange'; -import { isEqual, some } from 'lodash'; +import { isEqual, max, some } from 'lodash'; import { ColorPaletteDefault, gradientSequentialColorscaleMap, @@ -275,6 +275,40 @@ export function useStandaloneMapMarkers( * and create markers. */ const finalMarkersData = useMemo(() => { + const maxOverlayCount = rawMarkersData.value + ? Math.max( + ...rawMarkersData.value.mapElements.map((mapElement) => { + const count = + vocabulary != null // if there's an overlay (all expected use cases) + ? mapElement.overlayValues.reduce( + (sum, { count }) => (sum = sum + count), + 0 + ) + : mapElement.entityCount; + return count; + }) + ) + : 0; + + const bubbleValueToSizeMapper = (value: number) => { + const largestCircleArea = 9000; + + // Area scales directly with value + const constant = largestCircleArea / maxOverlayCount; + const area = value * constant; + const radius = Math.sqrt(area / Math.PI); + + // Radius scales with log_10 of value + // const constant = 20; + // const radius = Math.log10(value) * constant; + + // Radius scales directly with value + // const constant = maxValue / largestCircleSize; + // const radius = value * constant; + + return 2 * radius; + }; + return rawMarkersData.value?.mapElements.map( ({ geoAggregateValue, @@ -349,24 +383,6 @@ export function useStandaloneMapMarkers( }, ]; - const bubbleValueToSizeMapper = (value: number) => { - // Area scales directly with value - const constant = 100; - const area = value * constant; - const radius = Math.sqrt(area / Math.PI); - - // Radius scales with log_10 of value - // const constant = 20; - // const radius = Math.log10(value) * constant; - - // Radius scales directly with value - // const largestCircleSize = 150; - // const constant = maxValue / largestCircleSize; - // const radius = value * constant; - - return 2 * radius; - }; - const commonMarkerProps = { id: geoAggregateValue, key: geoAggregateValue, From 3d0902a2210ccabecade21e993fa8062ec850134 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Wed, 28 Jun 2023 11:09:58 -0400 Subject: [PATCH 09/56] Add outline to bubble markers and update legend tick calculation --- .../plotControls/PlotBubbleLegend.tsx | 36 +++++++++++----- .../libs/components/src/map/BubbleMarker.tsx | 41 +++++++++++-------- .../src/stories/BubbleMarkers.stories.tsx | 8 ++-- .../plotControls/PlotLegend.stories.tsx | 21 ++++++---- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 4 +- .../analysis/hooks/standaloneMapMarkers.tsx | 20 +++++---- 6 files changed, 80 insertions(+), 50 deletions(-) diff --git a/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx index 7c12e5794e..bcdedd9589 100644 --- a/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx +++ b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx @@ -1,11 +1,12 @@ import React from 'react'; import { range } from 'd3'; +import _ from 'lodash'; // set props for custom legend function export interface PlotLegendBubbleProps { legendMax: number; // legendMin: number; - valueToSizeMapper: (value: number) => number; + valueToDiameterMapper: (value: number) => number; // nTicks?: number; // MUST be odd! // showMissingness?: boolean; } @@ -21,7 +22,7 @@ export interface PlotLegendBubbleProps { export default function PlotBubbleLegend({ legendMax, // legendMin, - valueToSizeMapper, + valueToDiameterMapper, }: // nTicks = 5, // showMissingness, PlotLegendBubbleProps) { @@ -32,16 +33,31 @@ PlotLegendBubbleProps) { const legendTextSize = '1.0em'; const circleStrokeWidth = 3; const padding = 5; - // const largestDataCircleSize = valueToSizeMapper(legendMax); + // const largestDataCircleSize = valueToDiameterMapper(legendMax); const numCircles = 3; // the value of the largest circle in the legend will be the smallest power of 10 that's larger than legendMax - const largestCircleValue = Math.pow(10, Math.ceil(Math.log10(legendMax))); - const largestCircleDiameter = valueToSizeMapper(largestCircleValue); - const largestCircleRadius = largestCircleDiameter / 2; - const circleValues = range(numCircles).map( - (i) => largestCircleValue / Math.pow(10, i) + // const largestCircleValue = Math.pow(10, Math.ceil(Math.log10(legendMax))); + // const circleValues = range(numCircles).map( + // (i) => largestCircleValue / Math.pow(10, i) + // ); + + console.log('here9'); + + const legendMaxLog10 = Math.floor(Math.log10(legendMax)); + const largestCircleValue = + legendMax <= 10 + ? legendMax + : (Number(legendMax.toPrecision(1)[0]) + 1) * 10 ** legendMaxLog10; + const circleValues = _.uniq( + range(numCircles) + .map((i) => Math.round(largestCircleValue / 2 ** i)) + .filter((value) => value >= 1) ); + console.log({ legendMax, legendMaxLog10, largestCircleValue, circleValues }); + + const largestCircleDiameter = valueToDiameterMapper(largestCircleValue); + const largestCircleRadius = largestCircleDiameter / 2; console.log({ circleValues }); @@ -53,7 +69,7 @@ PlotLegendBubbleProps) { // // const fudge = legendStep / 10; // to get an inclusive range from d3 we have to make a slightly too-large max // const stopPoints = range(legendStep, legendMax, legendStep).map( // (value: number, index: number) => { - // const size = valueToSizeMapper(value); + // const size = valueToDiameterMapper(value); // return ( // { console.log({ value }); // const value = legendMax * (i / (numCircles - 1)); - const circleDiameter = valueToSizeMapper(value); + const circleDiameter = valueToDiameterMapper(value); const circleRadius = circleDiameter / 2; const tickY = padding + largestCircleDiameter + circleStrokeWidth - circleDiameter; diff --git a/packages/libs/components/src/map/BubbleMarker.tsx b/packages/libs/components/src/map/BubbleMarker.tsx index 6f02446b38..94b2384300 100755 --- a/packages/libs/components/src/map/BubbleMarker.tsx +++ b/packages/libs/components/src/map/BubbleMarker.tsx @@ -19,7 +19,7 @@ export interface BubbleMarkerProps extends BoundsDriftMarkerProps { // isAtomic: add a special thumbtack icon if this is true isAtomic?: boolean; dependentAxisRange?: NumberRange | null; // y-axis range for setting global max - valueToSizeMapper: (value: number) => number; + valueToDiameterMapper: (value: number) => number; onClick?: (event: L.LeafletMouseEvent) => void | undefined; } @@ -27,7 +27,7 @@ export interface BubbleMarkerProps extends BoundsDriftMarkerProps { * this is a SVG bubble marker icon */ export default function BubbleMarker(props: BubbleMarkerProps) { - const { html: svgHTML, size } = bubbleMarkerSVGIcon(props); + const { html: svgHTML, diameter: size } = bubbleMarkerSVGIcon(props); // set icon as divIcon const SVGBubbleIcon = L.divIcon({ @@ -65,7 +65,7 @@ type BubbleMarkerStandaloneProps = Omit< ContainerStylesAddon; export function BubbleMarkerStandalone(props: BubbleMarkerStandaloneProps) { - const { html, size } = bubbleMarkerSVGIcon(props); + const { html, diameter } = bubbleMarkerSVGIcon(props); // NOTE: the font size and line height would normally come from the .leaflet-container class // but we won't be using that. You can override these with `containerStyles` if you like. return ( @@ -73,8 +73,8 @@ export function BubbleMarkerStandalone(props: BubbleMarkerStandaloneProps) { style={{ fontSize: '12px', lineHeight: 1.5, - width: size, - height: size, + width: diameter, + height: diameter, ...props.containerStyles, }} dangerouslySetInnerHTML={{ __html: html }} @@ -84,19 +84,27 @@ export function BubbleMarkerStandalone(props: BubbleMarkerStandaloneProps) { function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { html: string; - size: number; + diameter: number; } { // const scale = props.markerScale ?? MarkerScaleDefault; console.log({ dependentAxisRange: props.dependentAxisRange }); - // defined assertion here - const size = props.valueToSizeMapper(props.data[0].value); - const circleRadius = size / 2; + const diameter = props.valueToDiameterMapper(props.data[0].value); + const radius = diameter / 2; + // set outer white circle size to describe white boundary + const strokeWidth = 2; + const outlineRadius = radius + strokeWidth; let svgHTML: string = ''; // set drawing area svgHTML += - ''; // initiate svg marker icon + ''; // initiate svg marker icon + + console.log('here5'); // for display, convert large value with k (e.g., 12345 -> 12k): return original value if less than a criterion // const sumLabel = props.markerLabel ?? String(fullPieValue); @@ -115,13 +123,14 @@ function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { //TODO: two things to consider: a) bubble size; b) bubble color svgHTML += ''; @@ -144,5 +153,5 @@ function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { // closing svg tag svgHTML += ''; - return { html: svgHTML, size }; + return { html: svgHTML, diameter: diameter }; } diff --git a/packages/libs/components/src/stories/BubbleMarkers.stories.tsx b/packages/libs/components/src/stories/BubbleMarkers.stories.tsx index 0ca4555401..abb4d49ef6 100644 --- a/packages/libs/components/src/stories/BubbleMarkers.stories.tsx +++ b/packages/libs/components/src/stories/BubbleMarkers.stories.tsx @@ -27,7 +27,7 @@ export default { title: 'Map/Bubble Markers', } as Meta; -const valueToSizeMapper = (value: number) => { +const valueToDiameterMapper = (value: number) => { // Area scales directly with value const constant = 100; const area = value * constant; @@ -58,7 +58,7 @@ export const Standalone: Story = () => { ]} isAtomic={false} // markerScale={1} - valueToSizeMapper={valueToSizeMapper} + valueToDiameterMapper={valueToDiameterMapper} containerStyles={{ margin: '10px' }} /> = () => { ]} isAtomic={false} // markerScale={1} - valueToSizeMapper={valueToSizeMapper} + valueToDiameterMapper={valueToDiameterMapper} containerStyles={{ margin: '10px' }} /> = () => { ]} isAtomic={false} // markerScale={1} - valueToSizeMapper={valueToSizeMapper} + valueToDiameterMapper={valueToDiameterMapper} containerStyles={{ margin: '10px' }} /> diff --git a/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx b/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx index fcc4b7e26c..f6911bb270 100755 --- a/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx +++ b/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx @@ -531,22 +531,25 @@ export const BubbleMarkerLegend = () => { const maxValue = 100; // const scale = 1; - const valueToSizeMapper = (value: number) => { + const valueToDiameterMapper = (value: number) => { + // const largestCircleArea = 9000; + const largestCircleDiameter = 150; + // Area scales directly with value - const constant = 100; - const area = value * constant; - const radius = Math.sqrt(area / Math.PI); + // const constant = largestCircleArea / maxOverlayCount; + // const area = value * constant; + // const radius = Math.sqrt(area / Math.PI); // Radius scales with log_10 of value // const constant = 20; // const radius = Math.log10(value) * constant; // Radius scales directly with value - // const largestCircleSize = 150; - // const constant = maxValue / largestCircleSize; - // const radius = value * constant; + const constant = maxValue / largestCircleDiameter; + const diameter = value * constant; - return 2 * radius; + // return 2 * radius; + return diameter; }; return ( @@ -555,7 +558,7 @@ export const BubbleMarkerLegend = () => { type="bubble" legendMax={maxValue} // legendMin={5} - valueToSizeMapper={valueToSizeMapper} + valueToDiameterMapper={valueToDiameterMapper} // pass legend title // nTicks={5} // showMissingness diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index 1e92dcac59..8bff97949b 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -1110,9 +1110,9 @@ function MapAnalysisImpl(props: ImplProps) { ) : 0 } - valueToSizeMapper={ + valueToDiameterMapper={ (markersData as BubbleMarkerProps[])[0] - .valueToSizeMapper + .valueToDiameterMapper } containerStyles={{ border: 'none', diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 09abe431b2..5dcc9d56f7 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -290,23 +290,25 @@ export function useStandaloneMapMarkers( ) : 0; - const bubbleValueToSizeMapper = (value: number) => { - const largestCircleArea = 9000; + const bubbleValueToDiameterMapper = (value: number) => { + // const largestCircleArea = 9000; + const largestCircleDiameter = 90; // Area scales directly with value - const constant = largestCircleArea / maxOverlayCount; - const area = value * constant; - const radius = Math.sqrt(area / Math.PI); + // const constant = largestCircleArea / maxOverlayCount; + // const area = value * constant; + // const radius = Math.sqrt(area / Math.PI); // Radius scales with log_10 of value // const constant = 20; // const radius = Math.log10(value) * constant; // Radius scales directly with value - // const constant = maxValue / largestCircleSize; - // const radius = value * constant; + const scalingFactor = largestCircleDiameter / maxOverlayCount; + const diameter = value * scalingFactor; - return 2 * radius; + // return 2 * radius; + return diameter; }; return rawMarkersData.value?.mapElements.map( @@ -405,7 +407,7 @@ export function useStandaloneMapMarkers( data: bubbleData, markerLabel: String(count), dependentAxisRange: defaultDependentAxisRange, - valueToSizeMapper: bubbleValueToSizeMapper, + valueToDiameterMapper: bubbleValueToDiameterMapper, } as BubbleMarkerProps; } default: { From 29b79a549affcf3a8bff778ebc43358cf269ce36 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Thu, 29 Jun 2023 13:01:19 -0400 Subject: [PATCH 10/56] Fix bubble outline shape and draw smaller bubbles over bigger --- .../components/src/map/BoundsDriftMarker.tsx | 2 ++ .../libs/components/src/map/BubbleMarker.tsx | 25 ++++++++----------- packages/libs/components/src/map/Types.ts | 1 + 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/libs/components/src/map/BoundsDriftMarker.tsx b/packages/libs/components/src/map/BoundsDriftMarker.tsx index 7cdfc9a1d2..7710f57373 100644 --- a/packages/libs/components/src/map/BoundsDriftMarker.tsx +++ b/packages/libs/components/src/map/BoundsDriftMarker.tsx @@ -29,6 +29,7 @@ export default function BoundsDriftMarker({ showPopup, popupContent, popupClass, + zIndexOffset, }: BoundsDriftMarkerProps) { const [displayBounds, setDisplayBounds] = useState(false); const map = useMap(); @@ -195,6 +196,7 @@ export default function BoundsDriftMarker({ mouseout: (e: LeafletMouseEvent) => handleMouseOut(e), dblclick: handleDoubleClick, }} + zIndexOffset={zIndexOffset} {...optionalIconProp} > {displayBounds ? ( diff --git a/packages/libs/components/src/map/BubbleMarker.tsx b/packages/libs/components/src/map/BubbleMarker.tsx index 94b2384300..d882ceffe8 100755 --- a/packages/libs/components/src/map/BubbleMarker.tsx +++ b/packages/libs/components/src/map/BubbleMarker.tsx @@ -47,6 +47,7 @@ export default function BubbleMarker(props: BubbleMarkerProps) { bounds={props.bounds} icon={SVGBubbleIcon as L.Icon} duration={duration} + zIndexOffset={-props.data[0].value * 1000} /> ); } @@ -104,20 +105,18 @@ function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { outlineRadius * 2 + '">'; // initiate svg marker icon - console.log('here5'); - // for display, convert large value with k (e.g., 12345 -> 12k): return original value if less than a criterion // const sumLabel = props.markerLabel ?? String(fullPieValue); // draw a larger white-filled circle - // svgHTML += - // ''; + svgHTML += + ''; // create bubble //TODO: two things to consider: a) bubble size; b) bubble color @@ -127,10 +126,8 @@ function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { '" cy="' + outlineRadius + '" r="' + - outlineRadius + - '" stroke="white" stroke-width="' + - strokeWidth + - '" fill="' + + radius + + '" stroke="white" stroke-width="0" fill="' + props.data[0].color + '" />'; diff --git a/packages/libs/components/src/map/Types.ts b/packages/libs/components/src/map/Types.ts index 18e91ff321..9f69e7273f 100644 --- a/packages/libs/components/src/map/Types.ts +++ b/packages/libs/components/src/map/Types.ts @@ -35,6 +35,7 @@ export interface MarkerProps { height: number; }; }; + zIndexOffset?: number; } export type AnimationFunction = ({ From 2a6acedd3892839016139c61bd61f9d13440e247 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Thu, 29 Jun 2023 13:23:34 -0400 Subject: [PATCH 11/56] Clean up --- .../plotControls/PlotBubbleLegend.tsx | 129 +----------------- .../libs/components/src/map/BubbleMarker.tsx | 8 +- .../src/stories/BubbleMarkers.stories.tsx | 23 +--- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 2 +- .../icons/BubbleMarker.tsx | 2 + .../icons/BubbleMarkers.tsx | 36 ----- .../analysis/hooks/standaloneMapMarkers.tsx | 11 -- 7 files changed, 12 insertions(+), 199 deletions(-) delete mode 100644 packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarkers.tsx diff --git a/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx index bcdedd9589..99d5a46aad 100644 --- a/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx +++ b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx @@ -7,7 +7,7 @@ export interface PlotLegendBubbleProps { legendMax: number; // legendMin: number; valueToDiameterMapper: (value: number) => number; - // nTicks?: number; // MUST be odd! + // nTicks?: number; // showMissingness?: boolean; } @@ -27,13 +27,10 @@ export default function PlotBubbleLegend({ // showMissingness, PlotLegendBubbleProps) { // Declare constants - // const gradientBoxHeight = 150; - // const gradientBoxWidth = 20; const tickFontSize = '0.8em'; - const legendTextSize = '1.0em'; + // const legendTextSize = '1.0em'; const circleStrokeWidth = 3; const padding = 5; - // const largestDataCircleSize = valueToDiameterMapper(legendMax); const numCircles = 3; // the value of the largest circle in the legend will be the smallest power of 10 that's larger than legendMax @@ -42,8 +39,6 @@ PlotLegendBubbleProps) { // (i) => largestCircleValue / Math.pow(10, i) // ); - console.log('here9'); - const legendMaxLog10 = Math.floor(Math.log10(legendMax)); const largestCircleValue = legendMax <= 10 @@ -54,50 +49,22 @@ PlotLegendBubbleProps) { .map((i) => Math.round(largestCircleValue / 2 ** i)) .filter((value) => value >= 1) ); - console.log({ legendMax, legendMaxLog10, largestCircleValue, circleValues }); const largestCircleDiameter = valueToDiameterMapper(largestCircleValue); const largestCircleRadius = largestCircleDiameter / 2; - console.log({ circleValues }); - const tickLength = largestCircleRadius + 5; - // Create gradient stop points from the colorscale from values [legendMin TO legendMax] at an arbitrary 50 step resolution - // const numCircles = 3; - // const legendStep = legendMax / (numCircles - 1); - // // const fudge = legendStep / 10; // to get an inclusive range from d3 we have to make a slightly too-large max - // const stopPoints = range(legendStep, legendMax, legendStep).map( - // (value: number, index: number) => { - // const size = valueToDiameterMapper(value); - // return ( - // - // ); - // } - // ); - - // let svgHTML: string = ''; - // set drawing area - // svgHTML += - // ''; // initiate svg marker icon - - const BubbleLegendSVG = () => ( + return ( {circleValues.map((value, i) => { - console.log({ value }); - // const value = legendMax * (i / (numCircles - 1)); const circleDiameter = valueToDiameterMapper(value); const circleRadius = circleDiameter / 2; const tickY = padding + largestCircleDiameter + circleStrokeWidth - circleDiameter; - // const stopPercentage = (i / (numCircles - 1)) * 100; return ( <> @@ -144,97 +111,7 @@ PlotLegendBubbleProps) { })} ); - // ''; - - // - // ); - - // }); // for display, convert large value with k (e.g., 12345 -> 12k): return original value if less than a criterion // const sumLabel = props.markerLabel ?? String(fullPieValue); - - // draw a larger white-filled circle - // svgHTML += - // ''; - - // create bubble - //TODO: two things to consider: a) bubble size; b) bubble color - // svgHTML += - // ''; - - //TODO: do we need to show total number for bubble marker? - // adding total number text/label and centering it - // svgHTML += - // '' + - // props.data[0].value + - // ''; - - // // check isAtomic: draw pushpin if true - // if (props.isAtomic) { - // let pushPinCode = '🖈'; - // svgHTML += - // '' + - // pushPinCode + - // ''; - // } - - // // closing svg tag - // svgHTML += ''; - - // Create ticks - // const ticks = range(nTicks).map((a: number) => { - // const location: number = - // gradientBoxHeight - gradientBoxHeight * (a / (nTicks! - 1)); // draw bottom to top - // return ( - // - // - // - // {( - // (a / (nTicks! - 1)) * (legendMax - legendMin) + - // legendMin - // ).toPrecision(3)} - // - // - // ); - // }); - - return ( -
- -
- ); } diff --git a/packages/libs/components/src/map/BubbleMarker.tsx b/packages/libs/components/src/map/BubbleMarker.tsx index d882ceffe8..59d716bc7a 100755 --- a/packages/libs/components/src/map/BubbleMarker.tsx +++ b/packages/libs/components/src/map/BubbleMarker.tsx @@ -9,7 +9,7 @@ import { } from '../types/plots'; import { NumberRange } from '../types/general'; -// ts definition for HistogramMarkerSVGProps: need some adjustment but for now, just use bubble marker one +// Don't need some of these props, but have to have them because of the general marker API/type definitions export interface BubbleMarkerProps extends BoundsDriftMarkerProps { data: { value: number; @@ -92,8 +92,8 @@ function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { const diameter = props.valueToDiameterMapper(props.data[0].value); const radius = diameter / 2; // set outer white circle size to describe white boundary - const strokeWidth = 2; - const outlineRadius = radius + strokeWidth; + const outlineWidth = 2; + const outlineRadius = radius + outlineWidth; let svgHTML: string = ''; @@ -119,7 +119,6 @@ function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { '" stroke="green" stroke-width="0" fill="white" />'; // create bubble - //TODO: two things to consider: a) bubble size; b) bubble color svgHTML += ' - - - - - - - - - ); -} diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 5dcc9d56f7..05800ff9fc 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -91,8 +91,6 @@ export function useStandaloneMapMarkers( dependentAxisLogScale = false, } = props; - console.log({ markerType }); - // these two deepvalue eliminate an unnecessary data request // when switching between pie and bar markers when using the same variable const selectedOverlayVariable = useDeepValue(sov); @@ -146,7 +144,6 @@ export function useStandaloneMapMarkers( ? overlayConfig?.overlayValues.map((ov) => ov.binLabel) : undefined; - // here const rawMarkersData = usePromise( useCallback(async () => { // check all required vizConfigs are provided @@ -202,8 +199,6 @@ export function useStandaloneMapMarkers( }, }; - console.log('here1'); - // now get the data return await dataClient.getStandaloneMapMarkers( 'standalone-map', @@ -225,9 +220,6 @@ export function useStandaloneMapMarkers( ]) ); - console.log('here2'); - console.log({ rawMarkersData }); - const totalVisibleEntityCount: number | undefined = rawMarkersData.value?.mapElements.reduce((acc, curr) => { return acc + curr.entityCount; @@ -268,8 +260,6 @@ export function useStandaloneMapMarkers( dependentAxisLogScale ) as NumberRange; - console.log({ defaultDependentAxisRange }); - /** * Merge the overlay data into the basicMarkerData, if available, * and create markers. @@ -370,7 +360,6 @@ export function useStandaloneMapMarkers( }, ]; - // here's what Bob pointed out const count = vocabulary != null // if there's an overlay (all expected use cases) ? overlayValues.reduce((sum, { count }) => (sum = sum + count), 0) From e9891fc004a1439c276fb8d1892edb591ab95aed Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Wed, 5 Jul 2023 18:38:53 -0400 Subject: [PATCH 12/56] Allow analysis backwards compatibility when adding new map types --- .../libs/eda/src/lib/map/analysis/appState.ts | 101 +++++++++++------- 1 file changed, 64 insertions(+), 37 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/appState.ts b/packages/libs/eda/src/lib/map/analysis/appState.ts index 76334e6bd7..df7555d99f 100644 --- a/packages/libs/eda/src/lib/map/analysis/appState.ts +++ b/packages/libs/eda/src/lib/map/analysis/appState.ts @@ -2,7 +2,7 @@ import { getOrElseW } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/function'; import * as t from 'io-ts'; import { isEqual } from 'lodash'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { AnalysisState, useGetDefaultVariableDescriptor, @@ -130,44 +130,71 @@ export function useAppState(uiStateKey: string, analysisState: AnalysisState) { studyMetadata.rootEntity.id ); + const defaultAppState: AppState = useMemo( + () => ({ + viewport: defaultViewport, + mouseMode: 'default', + activeMarkerConfigurationType: 'pie', + markerConfigurations: [ + { + type: 'pie', + selectedVariable: defaultVariable, + selectedValues: undefined, + binningMethod: undefined, + selectedCountsOption: 'filtered', + }, + { + type: 'barplot', + selectedPlotMode: 'count', + selectedVariable: defaultVariable, + selectedValues: undefined, + binningMethod: undefined, + dependentAxisLogScale: false, + selectedCountsOption: 'filtered', + }, + { + type: 'bubble', + selectedVariable: defaultVariable, + selectedValues: undefined, + binningMethod: undefined, + selectedCountsOption: 'filtered', + }, + ], + }), + [defaultVariable] + ); + useEffect(() => { - if (analysis && !appState) { - const defaultAppState: AppState = { - viewport: defaultViewport, - mouseMode: 'default', - activeMarkerConfigurationType: 'pie', - markerConfigurations: [ - { - type: 'pie', - selectedVariable: defaultVariable, - selectedValues: undefined, - binningMethod: undefined, - selectedCountsOption: 'filtered', - }, - { - type: 'barplot', - selectedPlotMode: 'count', - selectedVariable: defaultVariable, - selectedValues: undefined, - binningMethod: undefined, - dependentAxisLogScale: false, - selectedCountsOption: 'filtered', - }, - { - type: 'bubble', - selectedVariable: defaultVariable, - selectedValues: undefined, - binningMethod: undefined, - selectedCountsOption: 'filtered', - }, - ], - }; - setVariableUISettings((prev) => ({ - ...prev, - [uiStateKey]: defaultAppState, - })); + if (analysis) { + if (!appState) { + setVariableUISettings((prev) => ({ + ...prev, + [uiStateKey]: defaultAppState, + })); + } else { + const missingMarkerConfigs = + defaultAppState.markerConfigurations.filter( + (defaultConfig) => + !appState.markerConfigurations.some( + (config) => config.type === defaultConfig.type + ) + ); + + if (missingMarkerConfigs.length > 0) { + setVariableUISettings((prev) => ({ + ...prev, + [uiStateKey]: { + ...appState, + markerConfigurations: [ + ...appState.markerConfigurations, + ...missingMarkerConfigs, + ], + }, + })); + } + } } - }, [analysis, appState, defaultVariable, setVariableUISettings, uiStateKey]); + }, [analysis, appState, setVariableUISettings, uiStateKey, defaultAppState]); function useSetter(key: T) { return useCallback( From 709d5087ac28351caeec85c58437c2dbcbf9a0a6 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Mon, 24 Jul 2023 16:07:13 -0400 Subject: [PATCH 13/56] Fix bugs from merge --- .../MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx | 1 - .../eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx index 55d1585e46..d8a543b3fb 100644 --- a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx @@ -60,7 +60,6 @@ export function BubbleMarkerConfigurationMenu({ Color:

Date: Mon, 24 Jul 2023 17:23:37 -0400 Subject: [PATCH 14/56] independent axis truncation for Histogram Viz --- .../HistogramVisualization.tsx | 40 ++++++++++++++----- .../lib/core/hooks/computeDefaultAxisRange.ts | 7 +++- .../src/lib/core/utils/default-axis-range.ts | 16 +++++--- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/HistogramVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/HistogramVisualization.tsx index f6ab0d355c..74333dc9d6 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/HistogramVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/HistogramVisualization.tsx @@ -562,6 +562,9 @@ function HistogramViz(props: VisualizationProps) { [data] ); + // Note: defaultIndependentRange in the Histogram Viz should keep its initial range + // regardless of the change of the data to ensure the truncation behavior + // Thus, pass an additional prop to useDefaultAxisRange() if Histogram Viz const defaultIndependentRange = useDefaultAxisRange( xAxisVariable, vizConfig.independentAxisValueSpec === 'Full' @@ -572,7 +575,9 @@ function HistogramViz(props: VisualizationProps) { ? undefined : independentAxisMinMax?.max, undefined, - vizConfig.independentAxisValueSpec + vizConfig.independentAxisValueSpec, + // pass true for histogramViz (default is false) + true ); // separate minPosMax from dependentMinPosMax @@ -731,16 +736,29 @@ function HistogramViz(props: VisualizationProps) { truncationConfigIndependentAxisMax, truncationConfigDependentAxisMin, truncationConfigDependentAxisMax, - } = truncationConfig( - { - ...defaultUIState, // using annotated range, NOT the actual data - ...(minPosMax != null && minPosMax.min != null && minPosMax.max != null - ? { dependentAxisRange: minPosMax } - : {}), - }, - vizConfig, - {}, // no overrides - true // use inclusive less than equal for the range min + } = useMemo( + () => + truncationConfig( + { + ...defaultUIState, // using annotated range, NOT the actual data + ...(minPosMax != null && + minPosMax.min != null && + minPosMax.max != null + ? { dependentAxisRange: minPosMax } + : {}), + }, + vizConfig, + {}, // no overrides + true // use inclusive less than equal for the range min + ), + [ + defaultUIState, + dependentMinPosMax, + vizConfig.independentAxisRange, + vizConfig.dependentAxisRange, + vizConfig.independentAxisValueSpec, + vizConfig.dependentAxisValueSpec, + ] ); // axis range control diff --git a/packages/libs/eda/src/lib/core/hooks/computeDefaultAxisRange.ts b/packages/libs/eda/src/lib/core/hooks/computeDefaultAxisRange.ts index 5c9c0c9c27..fa7a0cbb08 100755 --- a/packages/libs/eda/src/lib/core/hooks/computeDefaultAxisRange.ts +++ b/packages/libs/eda/src/lib/core/hooks/computeDefaultAxisRange.ts @@ -22,7 +22,9 @@ export function useDefaultAxisRange( max?: number | string, /** are we using a log scale */ logScale?: boolean, - axisRangeSpec = 'Full' + axisRangeSpec = 'Full', + // check histogramViz + histogramViz: boolean = false ): NumberOrDateRange | undefined { const defaultAxisRange = useMemo(() => { // Check here to make sure number ranges (min, minPos, max) came with number variables @@ -45,7 +47,8 @@ export function useDefaultAxisRange( minPos, max, logScale, - axisRangeSpec + axisRangeSpec, + histogramViz ); // 4 significant figures diff --git a/packages/libs/eda/src/lib/core/utils/default-axis-range.ts b/packages/libs/eda/src/lib/core/utils/default-axis-range.ts index bb1e61823d..1788d28a00 100755 --- a/packages/libs/eda/src/lib/core/utils/default-axis-range.ts +++ b/packages/libs/eda/src/lib/core/utils/default-axis-range.ts @@ -12,14 +12,16 @@ export function numberDateDefaultAxisRange( observedMax: number | string | undefined, /** are we using a log scale */ logScale?: boolean, - axisRangeSpec = 'Full' + axisRangeSpec = 'Full', + histogramViz: boolean = false ): NumberOrDateRange | undefined { if (Variable.is(variable)) { if (variable.type === 'number' || variable.type === 'integer') { const defaults = variable.distributionDefaults; if (logScale && observedMinPos == null) return undefined; // return nothing - there will be no plottable data anyway - // set default range of Custom to be Auto-zoom - return axisRangeSpec === 'Full' + // set default range of Custom to be Auto-zoom and check Histogram Viz + return axisRangeSpec === 'Full' || + (histogramViz && axisRangeSpec === 'Custom') ? { min: logScale && @@ -39,7 +41,7 @@ export function numberDateDefaultAxisRange( (min([ defaults.displayRangeMin ?? 0, defaults.rangeMin, - observedMin as number, + observedMin, ]) as number), max: max([ defaults.displayRangeMax, @@ -56,7 +58,8 @@ export function numberDateDefaultAxisRange( } else if (variable.type === 'date') { const defaults = variable.distributionDefaults; // considering axis range control option such as Full, Auto-zoom, and Custom for date type - return axisRangeSpec === 'Full' + return axisRangeSpec === 'Full' || + (histogramViz && axisRangeSpec === 'Custom') ? defaults.displayRangeMin != null && defaults.displayRangeMax != null ? { min: @@ -126,7 +129,8 @@ export function numberDateDefaultAxisRange( variable.displayRangeMin != null && variable.displayRangeMax != null ) { - return axisRangeSpec === 'Full' + return axisRangeSpec === 'Full' || + (histogramViz && axisRangeSpec === 'Custom') ? { min: logScale ? (observedMinPos as number) From 70f8236875d16cf6bd3f66f4cb96229c1fe7e3ea Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Mon, 24 Jul 2023 21:25:56 -0400 Subject: [PATCH 15/56] Add aggregation input and use bubble endpoint for data --- .../plotControls/PlotBubbleLegend.tsx | 161 ++++----- .../libs/components/src/map/BubbleMarker.tsx | 16 +- .../eda/src/lib/core/api/DataClient/index.ts | 15 + .../eda/src/lib/core/api/DataClient/types.ts | 142 ++++---- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 46 ++- .../BubbleMarkerConfigurationMenu.tsx | 264 ++++++++++++++- .../icons/BubbleMarker.tsx | 74 ++--- .../libs/eda/src/lib/map/analysis/appState.ts | 54 ++- .../analysis/hooks/standaloneMapMarkers.tsx | 309 ++++++++++++------ .../analysis/hooks/standaloneVizPlugins.ts | 22 +- .../analysis/utils/defaultOverlayConfig.ts | 83 +++-- 11 files changed, 858 insertions(+), 328 deletions(-) diff --git a/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx index 99d5a46aad..b9110d725d 100644 --- a/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx +++ b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx @@ -6,7 +6,7 @@ import _ from 'lodash'; export interface PlotLegendBubbleProps { legendMax: number; // legendMin: number; - valueToDiameterMapper: (value: number) => number; + valueToDiameterMapper: ((value: number) => number) | undefined; // nTicks?: number; // showMissingness?: boolean; } @@ -26,91 +26,102 @@ export default function PlotBubbleLegend({ }: // nTicks = 5, // showMissingness, PlotLegendBubbleProps) { - // Declare constants - const tickFontSize = '0.8em'; - // const legendTextSize = '1.0em'; - const circleStrokeWidth = 3; - const padding = 5; - const numCircles = 3; + if (valueToDiameterMapper) { + // Declare constants + const tickFontSize = '0.8em'; + // const legendTextSize = '1.0em'; + const circleStrokeWidth = 3; + const padding = 5; + const numCircles = 3; - // the value of the largest circle in the legend will be the smallest power of 10 that's larger than legendMax - // const largestCircleValue = Math.pow(10, Math.ceil(Math.log10(legendMax))); - // const circleValues = range(numCircles).map( - // (i) => largestCircleValue / Math.pow(10, i) - // ); + // the value of the largest circle in the legend will be the smallest power of 10 that's larger than legendMax + // const largestCircleValue = Math.pow(10, Math.ceil(Math.log10(legendMax))); + // const circleValues = range(numCircles).map( + // (i) => largestCircleValue / Math.pow(10, i) + // ); - const legendMaxLog10 = Math.floor(Math.log10(legendMax)); - const largestCircleValue = - legendMax <= 10 - ? legendMax - : (Number(legendMax.toPrecision(1)[0]) + 1) * 10 ** legendMaxLog10; - const circleValues = _.uniq( - range(numCircles) - .map((i) => Math.round(largestCircleValue / 2 ** i)) - .filter((value) => value >= 1) - ); + const legendMaxLog10 = Math.floor(Math.log10(legendMax)); + const largestCircleValue = + legendMax <= 10 + ? legendMax + : (Number(legendMax.toPrecision(1)[0]) + 1) * 10 ** legendMaxLog10; + const circleValues = _.uniq( + range(numCircles) + .map((i) => Math.round(largestCircleValue / 2 ** i)) + .filter((value) => value >= 1) + ); - const largestCircleDiameter = valueToDiameterMapper(largestCircleValue); - const largestCircleRadius = largestCircleDiameter / 2; + const largestCircleDiameter = valueToDiameterMapper(largestCircleValue); + const largestCircleRadius = largestCircleDiameter / 2; - const tickLength = largestCircleRadius + 5; + const tickLength = largestCircleRadius + 5; - return ( - - {circleValues.map((value, i) => { - const circleDiameter = valueToDiameterMapper(value); - const circleRadius = circleDiameter / 2; - const tickY = - padding + largestCircleDiameter + circleStrokeWidth - circleDiameter; + return ( + + {circleValues.map((value, i) => { + const circleDiameter = valueToDiameterMapper(value); + const circleRadius = circleDiameter / 2; + const tickY = + padding + + largestCircleDiameter + + circleStrokeWidth - + circleDiameter; - return ( - <> - - - + - - {value} - - - - ); - })} - - ); + + + {value} + + + + ); + })} + + ); + } else { + return null; + } // for display, convert large value with k (e.g., 12345 -> 12k): return original value if less than a criterion // const sumLabel = props.markerLabel ?? String(fullPieValue); diff --git a/packages/libs/components/src/map/BubbleMarker.tsx b/packages/libs/components/src/map/BubbleMarker.tsx index 59d716bc7a..88da1ce738 100755 --- a/packages/libs/components/src/map/BubbleMarker.tsx +++ b/packages/libs/components/src/map/BubbleMarker.tsx @@ -11,14 +11,16 @@ import { NumberRange } from '../types/general'; // Don't need some of these props, but have to have them because of the general marker API/type definitions export interface BubbleMarkerProps extends BoundsDriftMarkerProps { - data: { - value: number; - label: string; - color?: string; - }[]; + data: [ + { + value: number; + label: string; + color?: string; + } + ]; // isAtomic: add a special thumbtack icon if this is true isAtomic?: boolean; - dependentAxisRange?: NumberRange | null; // y-axis range for setting global max + // dependentAxisRange?: NumberRange | null; // y-axis range for setting global max valueToDiameterMapper: (value: number) => number; onClick?: (event: L.LeafletMouseEvent) => void | undefined; } @@ -88,7 +90,7 @@ function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { diameter: number; } { // const scale = props.markerScale ?? MarkerScaleDefault; - console.log({ dependentAxisRange: props.dependentAxisRange }); + // console.log({ dependentAxisRange: props.dependentAxisRange }); const diameter = props.valueToDiameterMapper(props.data[0].value); const radius = diameter / 2; // set outer white circle size to describe white boundary diff --git a/packages/libs/eda/src/lib/core/api/DataClient/index.ts b/packages/libs/eda/src/lib/core/api/DataClient/index.ts index 733cc0b161..6f328722f2 100644 --- a/packages/libs/eda/src/lib/core/api/DataClient/index.ts +++ b/packages/libs/eda/src/lib/core/api/DataClient/index.ts @@ -27,6 +27,8 @@ import { MapMarkersOverlayResponse, StandaloneMapMarkersResponse, StandaloneMapMarkersRequestParams, + StandaloneMapBubblesResponse, + StandaloneMapBubblesRequestParams, ContinousVariableMetadataRequestParams, ContinousVariableMetadataResponse, } from './types'; @@ -186,6 +188,19 @@ export default class DataClient extends FetchClientWithCredentials { ); } + // standalone bubble markers + getStandaloneBubbles( + computationName: string, + params: StandaloneMapBubblesRequestParams + ): Promise { + return this.getVisualizationData( + computationName, + 'map-markers/bubbles', + params, + StandaloneMapBubblesResponse + ); + } + // filter-aware continuous overlay variable metadata getContinousVariableMetadata( params: ContinousVariableMetadataRequestParams diff --git a/packages/libs/eda/src/lib/core/api/DataClient/types.ts b/packages/libs/eda/src/lib/core/api/DataClient/types.ts index ffdcd33684..6cae7f27f1 100755 --- a/packages/libs/eda/src/lib/core/api/DataClient/types.ts +++ b/packages/libs/eda/src/lib/core/api/DataClient/types.ts @@ -148,6 +148,13 @@ const plotConfig = intersection([ }), ]); +// to be distinguised from geo-viewports +export type NumericViewport = TypeOf; +const numericViewport = type({ + xMin: string, + xMax: string, +}); + export interface HistogramRequestParams { studyId: string; filters: Filter[]; @@ -164,10 +171,7 @@ export interface HistogramRequestParams { value?: number; units?: TimeUnit; }; - viewport?: { - xMin: string; - xMax: string; - }; + viewport?: NumericViewport; showMissingness?: 'TRUE' | 'FALSE'; }; } @@ -182,13 +186,6 @@ const histogramSummary = type({ max: string, }); -// to be distinguised from geo-viewports -export type NumericViewport = TypeOf; -const numericViewport = type({ - xMin: string, - xMax: string, -}); - export type HistogramConfig = TypeOf; const histogramConfig = intersection([ plotConfig, @@ -408,10 +405,7 @@ export interface LineplotRequestParams { overlayVariable?: VariableDescriptor; facetVariable?: ZeroToTwoVariables; binSpec: BinSpec; - viewport?: { - xMin: string; - xMax: string; - }; + viewport?: NumericViewport; showMissingness?: 'TRUE' | 'FALSE'; valueSpec: 'mean' | 'median' | 'geometricMean' | 'proportion'; errorBars: 'TRUE' | 'FALSE'; @@ -666,6 +660,18 @@ export const BoxplotResponse = intersection([ }), ]); +export type LatLonViewport = TypeOf; +const latLonViewport = type({ + latitude: type({ + xMin: number, + xMax: number, + }), + longitude: type({ + left: number, + right: number, + }), +}); + export interface MapMarkersRequestParams { studyId: string; filters: Filter[]; @@ -674,16 +680,7 @@ export interface MapMarkersRequestParams { geoAggregateVariable: VariableDescriptor; latitudeVariable: VariableDescriptor; longitudeVariable: VariableDescriptor; - viewport: { - latitude: { - xMin: number; - xMax: number; - }; - longitude: { - left: number; - right: number; - }; - }; + viewport: LatLonViewport; }; } @@ -722,16 +719,7 @@ export interface MapMarkersOverlayRequestParams { longitudeVariable: VariableDescriptor; geoAggregateVariable: VariableDescriptor; valueSpec: 'count' | 'proportion'; - viewport: { - latitude: { - xMin: number; - xMax: number; - }; - longitude: { - left: number; - right: number; - }; - }; + viewport: LatLonViewport; }; } @@ -739,16 +727,7 @@ export type MapMarkersOverlayConfig = TypeOf; const mapMarkersOverlayConfig = intersection([ plotConfig, type({ - viewport: type({ - latitude: type({ - xMin: number, - xMax: number, - }), - longitude: type({ - left: number, - right: number, - }), - }), + viewport: latLonViewport, }), partial({ binSpec: BinSpec, @@ -793,12 +772,15 @@ export const AllValuesDefinition = type({ count: number, }); +export type CommonOverlayConfig = TypeOf; +export const CommonOverlayConfig = type({ + overlayVariable: VariableDescriptor, +}); + export type OverlayConfig = TypeOf; export const OverlayConfig = intersection([ - type({ - overlayType: keyof({ categorical: null, continuous: null }), - overlayVariable: VariableDescriptor, - }), + CommonOverlayConfig, + // type({overlayType: keyof({ categorical: null, continuous: null })}), union([ type({ overlayType: literal('categorical'), @@ -811,6 +793,24 @@ export const OverlayConfig = intersection([ ]), ]); +export type BubbleOverlayConfig = TypeOf; +export const BubbleOverlayConfig = intersection([ + CommonOverlayConfig, + type({ + aggregationConfig: union([ + type({ + overlayType: literal('categorical'), + numeratorValues: array(string), + denominatorValues: array(string), + }), + type({ + overlayType: literal('continuous'), + aggregator: keyof({ mean: null, median: null }), + }), + ]), + }), +]); + export interface StandaloneMapMarkersRequestParams { studyId: string; filters: Filter[]; @@ -821,16 +821,7 @@ export interface StandaloneMapMarkersRequestParams { longitudeVariable: VariableDescriptor; overlayConfig?: Omit; valueSpec: 'count' | 'proportion'; - viewport: { - latitude: { - xMin: number; - xMax: number; - }; - longitude: { - left: number; - right: number; - }; - }; + viewport: LatLonViewport; }; } @@ -865,6 +856,39 @@ export const StandaloneMapMarkersResponse = type({ ), }); +export interface StandaloneMapBubblesRequestParams { + studyId: string; + filters: Filter[]; + config: { + outputEntityId: string; + geoAggregateVariable: VariableDescriptor; + latitudeVariable: VariableDescriptor; + longitudeVariable: VariableDescriptor; + overlayConfig?: BubbleOverlayConfig; + valueSpec: 'count'; + viewport: LatLonViewport; + }; +} + +export type StandaloneMapBubblesResponse = TypeOf< + typeof StandaloneMapBubblesResponse +>; +export const StandaloneMapBubblesResponse = type({ + mapElements: array( + type({ + geoAggregateValue: string, + entityCount: number, + overlayValue: number, + avgLat: number, + avgLon: number, + minLat: number, + minLon: number, + maxLat: number, + maxLon: number, + }) + ), +}); + export interface ContinousVariableMetadataRequestParams { studyId: string; filters: Filter[]; diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index 458b5b9656..5facf2e299 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -3,6 +3,7 @@ import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; import { AllValuesDefinition, AnalysisState, + BubbleOverlayConfig, CategoricalVariableDataShape, DEFAULT_ANALYSIS_NAME, EntityDiagram, @@ -372,8 +373,11 @@ function MapAnalysisImpl(props: ImplProps) { // If the variable or filters have changed on the active marker config // get the default overlay config. + // here const activeOverlayConfig = usePromise( - useCallback(async (): Promise => { + useCallback(async (): Promise< + OverlayConfig | BubbleOverlayConfig | undefined + > => { // Use `selectedValues` to generate the overlay config for categorical variables if ( activeMarkerConfiguration?.selectedValues && @@ -396,17 +400,31 @@ function MapAnalysisImpl(props: ImplProps) { overlayEntity, dataClient, subsettingClient, + markerType: activeMarkerConfiguration?.type, binningMethod: activeMarkerConfiguration?.binningMethod, + aggregator: + activeMarkerConfiguration && 'aggregator' in activeMarkerConfiguration + ? activeMarkerConfiguration.aggregator + : undefined, + numeratorValues: + activeMarkerConfiguration && + 'numeratorValues' in activeMarkerConfiguration + ? activeMarkerConfiguration.numeratorValues + : undefined, + denominatorValues: + activeMarkerConfiguration && + 'denominatorValues' in activeMarkerConfiguration + ? activeMarkerConfiguration.denominatorValues + : undefined, }); }, [ - dataClient, - filters, - overlayEntity, + activeMarkerConfiguration, overlayVariable, studyId, + filters, + overlayEntity, + dataClient, subsettingClient, - activeMarkerConfiguration?.selectedValues, - activeMarkerConfiguration?.binningMethod, ]) ); @@ -424,6 +442,8 @@ function MapAnalysisImpl(props: ImplProps) { } })(); + console.log({ activeOverlayConfig }); + const { markersData, pending, @@ -697,7 +717,9 @@ function MapAnalysisImpl(props: ImplProps) { } toggleStarredVariable={toggleStarredVariable} constraints={markerVariableConstraints} - overlayConfiguration={activeOverlayConfig.value} + overlayConfiguration={ + activeOverlayConfig.value as OverlayConfig + } overlayVariable={overlayVariable} subsettingClient={subsettingClient} studyId={studyId} @@ -734,7 +756,9 @@ function MapAnalysisImpl(props: ImplProps) { toggleStarredVariable={toggleStarredVariable} configuration={activeMarkerConfiguration} constraints={markerVariableConstraints} - overlayConfiguration={activeOverlayConfig.value} + overlayConfiguration={ + activeOverlayConfig.value as OverlayConfig + } overlayVariable={overlayVariable} subsettingClient={subsettingClient} studyId={studyId} @@ -1265,8 +1289,10 @@ function MapAnalysisImpl(props: ImplProps) { : 0 } valueToDiameterMapper={ - (markersData as BubbleMarkerProps[])[0] - .valueToDiameterMapper + markersData.length > 0 + ? (markersData as BubbleMarkerProps[])[0] + .valueToDiameterMapper + : undefined } containerStyles={{ border: 'none', diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx index d8a543b3fb..9e7c2048f1 100644 --- a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx @@ -1,17 +1,46 @@ +import { keys } from 'lodash'; import { InputVariables, Props as InputVariablesProps, } from '../../../core/components/visualizations/InputVariables'; +import { useInputStyles } from '../../../core/components/visualizations/inputStyles'; +import { useFindEntityAndVariable } from '../../../core/hooks/workspace'; +import { Variable, VariableTreeNode } from '../../../core/types/study'; import { VariableDescriptor } from '../../../core/types/variable'; import { VariablesByInputName } from '../../../core/utils/data-element-constraints'; +import { + EntityAndVariable, + findEntityAndVariable, +} from '../../../core/utils/study-metadata'; import { SharedMarkerConfigurations } from './PieMarkerConfigurationMenu'; +import { Tooltip } from '@veupathdb/components/lib/components/widgets/Tooltip'; +import SingleSelect from '@veupathdb/coreui/lib/components/inputs/SingleSelect'; +import { ValuePicker } from '../../../core/components/visualizations/implementations/ValuePicker'; +import HelpIcon from '@veupathdb/wdk-client/lib/Components/Icon/HelpIcon'; +import { useEffect } from 'react'; +import { BubbleOverlayConfig } from '../../../core'; + +// // Display names to internal names +// const valueSpecLookup = { +// 'Arithmetic mean': 'mean', +// Median: 'median', +// // 'Geometric mean': 'geometricMean', +// Proportion: 'proportion', // used to be 'Ratio or proportion' hence the lookup rather than simple lowercasing +// } as const; + +const aggregatorOptions = ['mean', 'median'] as const; interface MarkerConfiguration { type: T; } + export interface BubbleMarkerConfiguration extends MarkerConfiguration<'bubble'>, SharedMarkerConfigurations { + // valueSpecConfig: 'Arithmetic mean' | 'Median' | 'Proportion'; + aggregator?: typeof aggregatorOptions[number]; + numeratorValues?: string[]; + denominatorValues?: string[]; // selectedVariable: VariableDescriptor; // selectedValues: string[] | undefined; } @@ -22,17 +51,29 @@ interface Props > { onChange: (configuration: BubbleMarkerConfiguration) => void; configuration: BubbleMarkerConfiguration; + // overlayConfiguration: BubbleOverlayConfig | undefined; } // Currently identical to pie marker configuration menu export function BubbleMarkerConfigurationMenu({ entities, configuration, + // overlayConfiguration, onChange, starredVariables, toggleStarredVariable, constraints, }: Props) { + // const getValueSpec = ( + // variable?: VariableTreeNode + // ): keyof typeof valueSpecLookup => { + // return isSuitableCategoricalVariable(variable) + // ? 'Proportion' + // : configuration.valueSpecConfig === 'Proportion' + // ? 'Arithmetic mean' + // : configuration.valueSpecConfig; + // }; + function handleInputVariablesOnChange(selection: VariablesByInputName) { if (!selection.overlayVariable) { console.error( @@ -41,13 +82,196 @@ export function BubbleMarkerConfigurationMenu({ return; } + // const selectedVariable = findEntityAndVariable( + // entities, + // selection.overlayVariable + // )?.variable; + + // const valueSpec = getValueSpec(selectedVariable); + onChange({ ...configuration, selectedVariable: selection.overlayVariable, - selectedValues: undefined, + numeratorValues: undefined, + denominatorValues: undefined, + // selectedValues: undefined, + // valueSpecConfig: valueSpec, }); } + const selectedVariable = findEntityAndVariable( + entities, + configuration.selectedVariable + )?.variable; + + // useEffect(() => { + // // The first time the component is rendered, check that the valueSpec is + // // correct for the given variable. If not, update it. + // const valueSpec = getValueSpec(selectedVariable); + + // if (configuration.valueSpecConfig !== valueSpec) { + // onChange({ + // ...configuration, + // valueSpecConfig: valueSpec, + // }); + // } + // }, []); + + const categoricalMode = isSuitableCategoricalVariable(selectedVariable); + const classes = useInputStyles(); + + if ( + categoricalMode && + configuration.numeratorValues !== undefined && + configuration.denominatorValues !== undefined + ) { + if ( + !configuration.numeratorValues.every((value) => + configuration.denominatorValues?.includes(value) + ) + ) + throw new Error( + 'To calculate a proportion, all selected numerator values must also be present in the denominator' + ); + } + + const aggregationInputs = ( +
+ {!categoricalMode ? ( +
+ +
+ Function* +
+
+ + onChange({ + ...configuration, + aggregator: value, + }) + } + value={configuration.aggregator} + buttonDisplayContent={configuration.aggregator} + items={aggregatorOptions.map((option) => ({ + value: option, + display: option, + }))} + /> +
+ ) : ( +
+ +
+ Proportion* = +
+
+
+ + onChange({ + ...configuration, + numeratorValues: value, + }) + } + /> +
+
+
+
+
+ + onChange({ + ...configuration, + denominatorValues: value, + }) + } + /> +
+
+ )} +
+ ); + + const aggregationHelp = ( +
+

+ “Mean” and “Median” are y-axis aggregation functions that can only be + used when continuous variables{' '} + are selected for the + y-axis. +

+
    +
  • + Mean = Sum of values for all data points / Number of all data points +
  • +
  • + Median = The middle number in a sorted list of numbers. The median is + a better measure of central tendency than the mean when data are not + normally distributed. +
  • +
+

+ “Proportion” is the only y-axis aggregation function that can be used + when categorical variables are + selected for the y-axis. +

+
    +
  • Proportion = Numerator count / Denominator count
  • +
+

+ The y-axis variable's values that count towards numerator and + denominator must be selected in the two drop-downs. +

+
+ ); + return (

+ + Y-axis aggregation{' '} + {selectedVariable + ? categoricalMode + ? '(categorical Y)' + : '(continuous Y)' + : ''} + + + + ), + order: 75, + content: selectedVariable ? ( + aggregationInputs + ) : ( + + First choose a Y-axis variable. + + ), + }, + ]} entities={entities} selectedVariables={{ overlayVariable: configuration.selectedVariable }} onChange={handleInputVariablesOnChange} @@ -73,3 +322,16 @@ export function BubbleMarkerConfigurationMenu({

); } + +/** + * determine if we are dealing with a categorical variable + */ +function isSuitableCategoricalVariable(variable?: VariableTreeNode): boolean { + return ( + variable != null && + 'dataShape' in variable && + variable.dataShape !== 'continuous' && + variable.vocabulary != null && + variable.distinctValuesCount != null + ); +} diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarker.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarker.tsx index 724d640b3a..3cddd4e6e1 100644 --- a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarker.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarker.tsx @@ -3,49 +3,49 @@ import { SVGProps } from 'react'; // This needs to be redesigned export function BubbleMarker(props: SVGProps) { return ( + // - ); } diff --git a/packages/libs/eda/src/lib/map/analysis/appState.ts b/packages/libs/eda/src/lib/map/analysis/appState.ts index 86bd5e2aa8..66e646bc7e 100644 --- a/packages/libs/eda/src/lib/map/analysis/appState.ts +++ b/packages/libs/eda/src/lib/map/analysis/appState.ts @@ -18,6 +18,14 @@ const MarkerType = t.keyof({ bubble: null, }); +// // Display names to internal names +// export const valueSpecLookup = { +// 'Arithmetic mean': 'mean', +// Median: 'median', +// // 'Geometric mean': 'geometricMean', +// Proportion: 'proportion', // used to be 'Ratio or proportion' hence the lookup rather than simple lowercasing +// } as const; + export type MarkerConfiguration = t.TypeOf; // eslint-disable-next-line @typescript-eslint/no-redeclare export const MarkerConfiguration = t.intersection([ @@ -58,21 +66,33 @@ export const MarkerConfiguration = t.intersection([ t.undefined, ]), }), - t.type({ - type: t.literal('bubble'), - selectedValues: t.union([t.array(t.string), t.undefined]), // user-specified selection - binningMethod: t.union([ - t.literal('equalInterval'), - t.literal('quantile'), - t.literal('standardDeviation'), - t.undefined, - ]), - selectedCountsOption: t.union([ - t.literal('filtered'), - t.literal('visible'), - t.undefined, - ]), - }), + // here + t.intersection([ + t.type({ + type: t.literal('bubble'), + // not needed for bubbles? + selectedValues: t.union([t.array(t.string), t.undefined]), // user-specified selection + // not needed for bubbles + binningMethod: t.union([ + t.literal('equalInterval'), + t.literal('quantile'), + t.literal('standardDeviation'), + t.undefined, + ]), + // valueSpecConfig: t.literal('count'), + // not needed for bubbles? + selectedCountsOption: t.union([ + t.literal('filtered'), + t.literal('visible'), + t.undefined, + ]), + }), + t.partial({ + aggregator: t.union([t.literal('mean'), t.literal('median')]), + numeratorValues: t.union([t.array(t.string), t.undefined]), + denominatorValues: t.union([t.array(t.string), t.undefined]), + }), + ]), ]), ]); @@ -153,6 +173,10 @@ export function useAppState(uiStateKey: string, analysisState: AnalysisState) { selectedVariable: defaultVariable, selectedValues: undefined, binningMethod: undefined, + // valueSpecConfig: 'Arithmetic mean', + aggregator: 'mean', + numeratorValues: undefined, + denominatorValues: undefined, selectedCountsOption: 'filtered', }, ], diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index e32dcbc600..17c2246efc 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -3,7 +3,10 @@ import { usePromise } from '../../../core/hooks/promise'; import { BoundsViewport } from '@veupathdb/components/lib/map/Types'; import { GeoConfig } from '../../../core/types/geoConfig'; import DataClient, { + BubbleOverlayConfig, OverlayConfig, + StandaloneMapBubblesRequestParams, + StandaloneMapBubblesResponse, StandaloneMapMarkersRequestParams, StandaloneMapMarkersResponse, } from '../../../core/api/DataClient'; @@ -66,7 +69,7 @@ export interface StandaloneMapMarkersProps { /** What is the full configuration for that overlay? * This is (sometimes) determined asynchronously from back end requests. */ - overlayConfig: OverlayConfig | undefined; + overlayConfig: OverlayConfig | BubbleOverlayConfig | undefined; outputEntityId: string | undefined; markerType: 'count' | 'proportion' | 'pie' | 'bubble'; dependentAxisLogScale?: boolean; @@ -115,7 +118,11 @@ export function useStandaloneMapMarkers( // when switching between pie and bar markers when using the same variable const selectedOverlayVariable = useDeepValue(sov); const overlayConfig = useDeepValue(oc); - const overlayType = overlayConfig?.overlayType; + const overlayType = overlayConfig + ? 'overlayType' in overlayConfig + ? overlayConfig.overlayType + : overlayConfig.aggregationConfig.overlayType + : undefined; const dataClient: DataClient = useDataClient(); @@ -162,10 +169,24 @@ export function useStandaloneMapMarkers( const rawPromise = usePromise< | { - rawMarkersData: StandaloneMapMarkersResponse; + rawMarkersData: + | StandaloneMapMarkersResponse + | StandaloneMapBubblesResponse; vocabulary: string[] | undefined; } | undefined + // const overlayType = overlayConfig?.overlayType; + // const vocabulary = + // overlayConfig && 'overlayValues' in overlayConfig + // ? overlayType === 'categorical' // switch statement style guide time!! + // ? overlayConfig.overlayValues + // : overlayType === 'continuous' + // ? overlayConfig.overlayValues.map((ov) => ov.binLabel) + // : undefined + // : undefined; + + // const rawMarkersData = usePromise< + // StandaloneMapMarkersResponse | StandaloneMapBubblesResponse | undefined >( useCallback(async () => { // check all required vizConfigs are provided @@ -201,37 +222,120 @@ export function useStandaloneMapMarkers( } : GLOBAL_VIEWPORT; - // now prepare the rest of the request params - const requestParams: StandaloneMapMarkersRequestParams = { - studyId, - filters: filters || [], - config: { - geoAggregateVariable, - latitudeVariable, - longitudeVariable, - overlayConfig, - outputEntityId, - valueSpec: - markerType === 'pie' || markerType === 'bubble' - ? 'count' - : markerType, - viewport, - }, - }; - - // now get and return the data - return { - rawMarkersData: await dataClient.getStandaloneMapMarkers( - 'standalone-map', - requestParams - ), - vocabulary: - overlayType === 'categorical' // switch statement style guide time!! - ? overlayConfig?.overlayValues - : overlayType === 'continuous' - ? overlayConfig?.overlayValues.map((ov) => ov.binLabel) - : undefined, - }; + // // now prepare the rest of the request params + // const requestParams: StandaloneMapMarkersRequestParams = { + // studyId, + // filters: filters || [], + // config: { + // geoAggregateVariable, + // latitudeVariable, + // longitudeVariable, + // overlayConfig, + // outputEntityId, + // valueSpec: + // markerType === 'pie' || markerType === 'bubble' + // ? 'count' + // : markerType, + // viewport, + // }, + // }; + + // // now get and return the data + // return { + // rawMarkersData: await dataClient.getStandaloneMapMarkers( + // 'standalone-map', + // requestParams + // ), + // vocabulary: + // overlayType === 'categorical' // switch statement style guide time!! + // ? overlayConfig?.overlayValues + // : overlayType === 'continuous' + // ? overlayConfig?.overlayValues.map((ov) => ov.binLabel) + // : undefined, + // }; + if (markerType === 'bubble') { + const bubbleOverlayConfig = overlayConfig as + | BubbleOverlayConfig + | undefined; + + if ( + bubbleOverlayConfig && + bubbleOverlayConfig.aggregationConfig.overlayType === 'categorical' && + (bubbleOverlayConfig.aggregationConfig.numeratorValues.length === 0 || + bubbleOverlayConfig.aggregationConfig.denominatorValues.length === + 0) + ) { + return { + rawMarkersData: { + mapElements: [], + }, + vocabulary: undefined, + }; + } + + const requestParams: StandaloneMapBubblesRequestParams = { + studyId, + filters: filters || [], + config: { + geoAggregateVariable, + latitudeVariable, + longitudeVariable, + overlayConfig: bubbleOverlayConfig, + outputEntityId, + // need to get the actual valueSpec instead of just 'count' + valueSpec: 'count', + viewport, + }, + }; + + // now get and return the data + return { + rawMarkersData: await dataClient.getStandaloneBubbles( + 'standalone-map', + requestParams + ), + // vocabulary: + // overlayType === 'categorical' // switch statement style guide time!! + // ? overlayConfig?.overlayValues + // : overlayType === 'continuous' + // ? overlayConfig?.overlayValues.map((ov) => ov.binLabel) + // : undefined, + vocabulary: undefined, + }; + } else { + const standardOverlayConfig = overlayConfig as + | OverlayConfig + | undefined; + const requestParams: StandaloneMapMarkersRequestParams = { + studyId, + filters: filters || [], + config: { + geoAggregateVariable, + latitudeVariable, + longitudeVariable, + overlayConfig: standardOverlayConfig, + outputEntityId, + valueSpec: markerType === 'pie' ? 'count' : markerType, + viewport, + }, + }; + + // now get and return the data + return { + rawMarkersData: await dataClient.getStandaloneMapMarkers( + 'standalone-map', + requestParams + ), + vocabulary: + overlayType === 'categorical' // switch statement style guide time!! + ? (standardOverlayConfig?.overlayValues as string[]) + : overlayType === 'continuous' + ? standardOverlayConfig?.overlayValues.map((ov) => + typeof ov === 'object' ? ov.binLabel : '' + ) + : undefined, + }; + } }, [ studyId, filters, @@ -249,18 +353,26 @@ export function useStandaloneMapMarkers( ]) ); - const totalVisibleEntityCount: number | undefined = - rawPromise.value?.rawMarkersData.mapElements.reduce((acc, curr) => { - return acc + curr.entityCount; - }, 0); + const totalVisibleEntityCount: number | undefined = rawPromise.value + ? ( + rawPromise.value.rawMarkersData.mapElements as Array<{ + entityCount: number; + }> + ).reduce((acc, curr) => { + return acc + curr.entityCount; + }, 0) + : undefined; + + console.log({ rawPromise }); // calculate minPos, max and sum for chart marker dependent axis // assumes the value is a count! (so never negative) const { valueMax, valueMinPos, countSum } = useMemo( () => - rawPromise.value?.rawMarkersData + (markerType === 'count' || markerType === 'proportion') && + rawPromise.value ? rawPromise.value.rawMarkersData.mapElements - .flatMap((el) => el.overlayValues) + .flatMap((el) => ('overlayValues' in el ? el.overlayValues : [])) .reduce( ({ valueMax, valueMinPos, countSum }, elem) => ({ valueMax: Math.max(elem.value, valueMax), @@ -278,7 +390,7 @@ export function useStandaloneMapMarkers( } ) : { valueMax: undefined, valueMinPos: undefined, countSum: undefined }, - [rawPromise.value?.rawMarkersData] + [markerType, rawPromise.value?.rawMarkersData] ); const defaultDependentAxisRange = useDefaultAxisRange( @@ -296,41 +408,42 @@ export function useStandaloneMapMarkers( * and create markers. */ const finalMarkersData = useMemo(() => { - const maxOverlayCount = rawPromise.value?.rawMarkersData - ? Math.max( - ...rawPromise.value.rawMarkersData.mapElements.map((mapElement) => { - const count = - vocabulary != null // if there's an overlay (all expected use cases) - ? mapElement.overlayValues.reduce( - (sum, { count }) => (sum = sum + count), - 0 - ) - : mapElement.entityCount; - return count; - }) - ) - : 0; - - const bubbleValueToDiameterMapper = (value: number) => { - // const largestCircleArea = 9000; - const largestCircleDiameter = 90; - - // Area scales directly with value - // const constant = largestCircleArea / maxOverlayCount; - // const area = value * constant; - // const radius = Math.sqrt(area / Math.PI); - - // Radius scales with log_10 of value - // const constant = 20; - // const radius = Math.log10(value) * constant; - - // Radius scales directly with value - const scalingFactor = largestCircleDiameter / maxOverlayCount; - const diameter = value * scalingFactor; - - // return 2 * radius; - return diameter; - }; + const maxOverlayCount = + markerType === 'bubble' + ? rawPromise.value?.rawMarkersData + ? Math.max( + ...rawPromise.value.rawMarkersData.mapElements.map((mapElement) => + 'overlayValue' in mapElement + ? mapElement.overlayValue + : mapElement.entityCount + ) + ) + : 0 + : undefined; + + const bubbleValueToDiameterMapper = + markerType === 'bubble' && maxOverlayCount + ? (value: number) => { + // const largestCircleArea = 9000; + const largestCircleDiameter = 90; + + // Area scales directly with value + // const constant = largestCircleArea / maxOverlayCount; + // const area = value * constant; + // const radius = Math.sqrt(area / Math.PI); + + // Radius scales with log_10 of value + // const constant = 20; + // const radius = Math.log10(value) * constant; + + // Radius scales directly with value + const scalingFactor = largestCircleDiameter / maxOverlayCount; + const diameter = value * scalingFactor; + + // return 2 * radius; + return diameter; + } + : undefined; return rawPromise.value?.rawMarkersData.mapElements.map( ({ @@ -342,13 +455,15 @@ export function useStandaloneMapMarkers( minLon, maxLat, maxLon, - overlayValues, + ...otherProps }) => { const bounds = { southWest: { lat: minLat, lng: minLon }, northEast: { lat: maxLat, lng: maxLon }, }; const position = { lat: avgLat, lng: avgLon }; + const overlayValues = + 'overlayValues' in otherProps ? otherProps.overlayValues : undefined; const donutData = vocabulary && overlayValues && overlayValues.length @@ -392,21 +507,12 @@ export function useStandaloneMapMarkers( ]; const count = - vocabulary != null // if there's an overlay (all expected use cases) + vocabulary != null && overlayValues // if there's an overlay (all expected use cases) ? overlayValues .filter(({ binLabel }) => vocabulary.includes(binLabel)) .reduce((sum, { count }) => (sum = sum + count), 0) : entityCount; // fallback if not - const bubbleData = [ - { - label: reorderedData[0].label, - value: count, - color: - 'color' in reorderedData[0] ? reorderedData[0].color : undefined, - }, - ]; - const commonMarkerProps = { id: geoAggregateValue, key: geoAggregateValue, @@ -424,11 +530,22 @@ export function useStandaloneMapMarkers( } as DonutMarkerProps; } case 'bubble': { + const bubbleCount = + 'overlayValue' in otherProps + ? otherProps.overlayValue + : entityCount; + const bubbleData = [ + { + label: '', + value: bubbleCount, + }, + ]; + return { ...commonMarkerProps, data: bubbleData, - markerLabel: String(count), - dependentAxisRange: defaultDependentAxisRange, + markerLabel: String(bubbleCount), + // dependentAxisRange: defaultDependentAxisRange, valueToDiameterMapper: bubbleValueToDiameterMapper, } as BubbleMarkerProps; } @@ -446,6 +563,7 @@ export function useStandaloneMapMarkers( ); }, [ rawPromise, + vocabulary, markerType, overlayType, defaultDependentAxisRange, @@ -457,7 +575,7 @@ export function useStandaloneMapMarkers( */ const legendItems: LegendItemsProps[] = useMemo(() => { const vocabulary = rawPromise?.value?.vocabulary; - if (vocabulary == null) return []; + if (vocabulary == null || markerType === 'bubble') return []; return vocabulary.map((label) => ({ label: fixLabelForOtherValues(label), @@ -475,14 +593,19 @@ export function useStandaloneMapMarkers( // has any geo-facet got an array of overlay data // containing at least one element that satisfies label==label hasData: rawPromise.value?.rawMarkersData - ? some(rawPromise.value.rawMarkersData.mapElements, (el) => - el.overlayValues.some((ov) => ov.binLabel === label) + ? some( + rawPromise.value.rawMarkersData.mapElements, + (el) => + // TS says el could potentially be a number, and I don't know why + typeof el === 'object' && + 'overlayValues' in el && + el.overlayValues.some((ov) => ov.binLabel === label) ) : false, group: 1, rank: 1, })); - }, [rawPromise, overlayType]); + }, [markerType, overlayType, rawPromise]); return { markersData: finalMarkersData, diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneVizPlugins.ts b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneVizPlugins.ts index 891d829fa2..3a135165ee 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneVizPlugins.ts +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneVizPlugins.ts @@ -17,7 +17,11 @@ import { scatterplotVisualization } from '../../../core/components/visualization import { lineplotVisualization } from '../../../core/components/visualizations/implementations/LineplotVisualization'; import { barplotVisualization } from '../../../core/components/visualizations/implementations/BarplotVisualization'; import { boxplotVisualization } from '../../../core/components/visualizations/implementations/BoxplotVisualization'; -import { BinDefinitions, OverlayConfig } from '../../../core'; +import { + BinDefinitions, + OverlayConfig, + BubbleOverlayConfig, +} from '../../../core'; import { boxplotRequest } from './plugins/boxplot'; import { barplotRequest } from './plugins/barplot'; import { lineplotRequest } from './plugins/lineplot'; @@ -27,7 +31,7 @@ import { scatterplotRequest } from './plugins/scatterplot'; import LineSVG from '../../../core/components/visualizations/implementations/selectorIcons/LineSVG'; interface Props { - selectedOverlayConfig?: OverlayConfig; + selectedOverlayConfig?: OverlayConfig | BubbleOverlayConfig; } type StandaloneVizOptions = LayoutOptions & OverlayOptions; @@ -47,9 +51,17 @@ export function useStandaloneVizPlugins({ // one object? Because in the pre-SAM world, getOverlayVariable was already // part of this interface. getOverlayVariable: (_) => selectedOverlayConfig?.overlayVariable, - getOverlayType: () => selectedOverlayConfig?.overlayType, + getOverlayType: () => + selectedOverlayConfig + ? 'overlayType' in selectedOverlayConfig + ? selectedOverlayConfig.overlayType + : selectedOverlayConfig.aggregationConfig.overlayType + : undefined, getOverlayVocabulary: () => { - const overlayValues = selectedOverlayConfig?.overlayValues; + const overlayValues = + selectedOverlayConfig && 'overlayValues' in selectedOverlayConfig + ? selectedOverlayConfig.overlayValues + : undefined; if (overlayValues == null) return undefined; if (BinDefinitions.is(overlayValues)) { return overlayValues.map((bin) => bin.binLabel); @@ -74,7 +86,7 @@ export function useStandaloneVizPlugins({ requestFunction: ( props: RequestOptionProps & ExtraProps & { - overlayConfig: OverlayConfig | undefined; + overlayConfig: OverlayConfig | BubbleOverlayConfig | undefined; } ) => RequestParamsType ) { diff --git a/packages/libs/eda/src/lib/map/analysis/utils/defaultOverlayConfig.ts b/packages/libs/eda/src/lib/map/analysis/utils/defaultOverlayConfig.ts index c2618be7da..7e0f7685c9 100644 --- a/packages/libs/eda/src/lib/map/analysis/utils/defaultOverlayConfig.ts +++ b/packages/libs/eda/src/lib/map/analysis/utils/defaultOverlayConfig.ts @@ -2,6 +2,7 @@ import { ColorPaletteDefault } from '@veupathdb/components/lib/types/plots'; import { UNSELECTED_TOKEN } from '../..'; import { BinRange, + BubbleOverlayConfig, CategoricalVariableDataShape, ContinuousVariableDataShape, Filter, @@ -11,6 +12,7 @@ import { } from '../../../core'; import { DataClient, SubsettingClient } from '../../../core/api'; import { MarkerConfiguration } from '../appState'; +import { BubbleMarkerConfiguration } from '../MarkerConfiguration/BubbleMarkerConfigurationMenu'; // This async function fetches the default overlay config. // For continuous variables, this involves calling the filter-aware-metadata/continuous-variable @@ -26,12 +28,16 @@ export interface DefaultOverlayConfigProps { overlayEntity: StudyEntity | undefined; dataClient: DataClient; subsettingClient: SubsettingClient; + markerType?: MarkerConfiguration['type']; binningMethod?: MarkerConfiguration['binningMethod']; + aggregator?: BubbleMarkerConfiguration['aggregator']; + numeratorValues?: BubbleMarkerConfiguration['numeratorValues']; + denominatorValues?: BubbleMarkerConfiguration['denominatorValues']; } export async function getDefaultOverlayConfig( props: DefaultOverlayConfigProps -): Promise { +): Promise { const { studyId, filters, @@ -39,7 +45,11 @@ export async function getDefaultOverlayConfig( overlayEntity, dataClient, subsettingClient, + markerType, binningMethod = 'equalInterval', + aggregator = 'mean', + numeratorValues = [], + denominatorValues = [], } = props; if (overlayVariable != null && overlayEntity != null) { @@ -50,34 +60,55 @@ export async function getDefaultOverlayConfig( if (CategoricalVariableDataShape.is(overlayVariable.dataShape)) { // categorical - const overlayValues = await getMostFrequentValues({ - studyId: studyId, - ...overlayVariableDescriptor, - filters: filters ?? [], - numValues: ColorPaletteDefault.length - 1, - subsettingClient, - }); + if (markerType === 'bubble') { + return { + overlayVariable: overlayVariableDescriptor, + aggregationConfig: { + overlayType: 'categorical', + numeratorValues, + denominatorValues, + }, + }; + } else { + const overlayValues = await getMostFrequentValues({ + studyId: studyId, + ...overlayVariableDescriptor, + filters: filters ?? [], + numValues: ColorPaletteDefault.length - 1, + subsettingClient, + }); - return { - overlayType: 'categorical', - overlayVariable: overlayVariableDescriptor, - overlayValues, - }; + return { + overlayType: 'categorical', + overlayVariable: overlayVariableDescriptor, + overlayValues, + }; + } } else if (ContinuousVariableDataShape.is(overlayVariable.dataShape)) { - // continuous - const overlayBins = await getBinRanges({ - studyId, - ...overlayVariableDescriptor, - filters: filters ?? [], - dataClient, - binningMethod, - }); + if (markerType === 'bubble') { + return { + overlayVariable: overlayVariableDescriptor, + aggregationConfig: { + overlayType: 'continuous', + aggregator, + }, + }; + } else { + // continuous + const overlayBins = await getBinRanges({ + studyId, + ...overlayVariableDescriptor, + filters: filters ?? [], + dataClient, + binningMethod, + }); - return { - overlayType: 'continuous', - overlayValues: overlayBins, - overlayVariable: overlayVariableDescriptor, - }; + return { + overlayType: 'continuous', + overlayValues: overlayBins, + overlayVariable: overlayVariableDescriptor, + }; + } } else { return; } From 8a01b1e867716605a77c4d8942b265cb29e448f9 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Tue, 25 Jul 2023 14:09:21 -0400 Subject: [PATCH 16/56] Use correct value for bubble size --- .../map/analysis/hooks/standaloneMapMarkers.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 17c2246efc..e71960bc9d 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -390,7 +390,7 @@ export function useStandaloneMapMarkers( } ) : { valueMax: undefined, valueMinPos: undefined, countSum: undefined }, - [markerType, rawPromise.value?.rawMarkersData] + [markerType, rawPromise.value] ); const defaultDependentAxisRange = useDefaultAxisRange( @@ -412,10 +412,8 @@ export function useStandaloneMapMarkers( markerType === 'bubble' ? rawPromise.value?.rawMarkersData ? Math.max( - ...rawPromise.value.rawMarkersData.mapElements.map((mapElement) => - 'overlayValue' in mapElement - ? mapElement.overlayValue - : mapElement.entityCount + ...rawPromise.value.rawMarkersData.mapElements.map( + (mapElement) => mapElement.entityCount ) ) : 0 @@ -530,21 +528,17 @@ export function useStandaloneMapMarkers( } as DonutMarkerProps; } case 'bubble': { - const bubbleCount = - 'overlayValue' in otherProps - ? otherProps.overlayValue - : entityCount; const bubbleData = [ { label: '', - value: bubbleCount, + value: entityCount, }, ]; return { ...commonMarkerProps, data: bubbleData, - markerLabel: String(bubbleCount), + markerLabel: String(entityCount), // dependentAxisRange: defaultDependentAxisRange, valueToDiameterMapper: bubbleValueToDiameterMapper, } as BubbleMarkerProps; From 4942c67112a96bf210fe261d2ec5812e45523362 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Tue, 25 Jul 2023 14:41:03 -0400 Subject: [PATCH 17/56] Reintroduce appState backwards compatibility logic --- .../libs/eda/src/lib/map/analysis/appState.ts | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/appState.ts b/packages/libs/eda/src/lib/map/analysis/appState.ts index 66e646bc7e..5049aaf315 100644 --- a/packages/libs/eda/src/lib/map/analysis/appState.ts +++ b/packages/libs/eda/src/lib/map/analysis/appState.ts @@ -185,11 +185,34 @@ export function useAppState(uiStateKey: string, analysisState: AnalysisState) { ); useEffect(() => { - if (analysis && !appState) { - setVariableUISettings((prev) => ({ - ...prev, - [uiStateKey]: defaultAppState, - })); + if (analysis) { + if (!appState) { + setVariableUISettings((prev) => ({ + ...prev, + [uiStateKey]: defaultAppState, + })); + } else { + const missingMarkerConfigs = + defaultAppState.markerConfigurations.filter( + (defaultConfig) => + !appState.markerConfigurations.some( + (config) => config.type === defaultConfig.type + ) + ); + + if (missingMarkerConfigs.length > 0) { + setVariableUISettings((prev) => ({ + ...prev, + [uiStateKey]: { + ...appState, + markerConfigurations: [ + ...appState.markerConfigurations, + ...missingMarkerConfigs, + ], + }, + })); + } + } } }, [analysis, appState, setVariableUISettings, uiStateKey, defaultAppState]); From c49a557ee8bb848f91b2df73c2913867b5ad011e Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Tue, 25 Jul 2023 22:27:10 -0400 Subject: [PATCH 18/56] Use bubble legend endpoint for bubble sizes --- .../libs/components/src/map/BubbleMarker.tsx | 138 +++++++++--------- .../eda/src/lib/core/api/DataClient/index.ts | 14 ++ .../eda/src/lib/core/api/DataClient/types.ts | 64 +++++--- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 4 +- .../analysis/hooks/standaloneMapMarkers.tsx | 106 ++++++++++---- 5 files changed, 207 insertions(+), 119 deletions(-) diff --git a/packages/libs/components/src/map/BubbleMarker.tsx b/packages/libs/components/src/map/BubbleMarker.tsx index 88da1ce738..17993485a8 100755 --- a/packages/libs/components/src/map/BubbleMarker.tsx +++ b/packages/libs/components/src/map/BubbleMarker.tsx @@ -11,17 +11,16 @@ import { NumberRange } from '../types/general'; // Don't need some of these props, but have to have them because of the general marker API/type definitions export interface BubbleMarkerProps extends BoundsDriftMarkerProps { - data: [ - { - value: number; - label: string; - color?: string; - } - ]; + data: { + value: number; + label: string; + color?: string; + }[]; // isAtomic: add a special thumbtack icon if this is true isAtomic?: boolean; // dependentAxisRange?: NumberRange | null; // y-axis range for setting global max - valueToDiameterMapper: (value: number) => number; + // Marker won't be shown if there's no mapper function + valueToDiameterMapper?: (value: number) => number; onClick?: (event: L.LeafletMouseEvent) => void | undefined; } @@ -29,6 +28,9 @@ export interface BubbleMarkerProps extends BoundsDriftMarkerProps { * this is a SVG bubble marker icon */ export default function BubbleMarker(props: BubbleMarkerProps) { + console.log({ props }); + console.log('here'); + const { html: svgHTML, diameter: size } = bubbleMarkerSVGIcon(props); // set icon as divIcon @@ -89,66 +91,70 @@ function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): { html: string; diameter: number; } { - // const scale = props.markerScale ?? MarkerScaleDefault; - // console.log({ dependentAxisRange: props.dependentAxisRange }); - const diameter = props.valueToDiameterMapper(props.data[0].value); - const radius = diameter / 2; - // set outer white circle size to describe white boundary - const outlineWidth = 2; - const outlineRadius = radius + outlineWidth; - - let svgHTML: string = ''; - - // set drawing area - svgHTML += - ''; // initiate svg marker icon - - // for display, convert large value with k (e.g., 12345 -> 12k): return original value if less than a criterion - // const sumLabel = props.markerLabel ?? String(fullPieValue); - - // draw a larger white-filled circle - svgHTML += - ''; - - // create bubble - svgHTML += - ''; - - //TODO: do we need to show total number for bubble marker? - // adding total number text/label and centering it - svgHTML += - '' + - props.data[0].value + - ''; - - // check isAtomic: draw pushpin if true - if (props.isAtomic) { - let pushPinCode = '🖈'; + if (props.valueToDiameterMapper) { + // const scale = props.markerScale ?? MarkerScaleDefault; + // console.log({ dependentAxisRange: props.dependentAxisRange }); + const diameter = props.valueToDiameterMapper(props.data[0].value); + const radius = diameter / 2; + // set outer white circle size to describe white boundary + const outlineWidth = 2; + const outlineRadius = radius + outlineWidth; + + let svgHTML: string = ''; + + // set drawing area + svgHTML += + ''; // initiate svg marker icon + + // for display, convert large value with k (e.g., 12345 -> 12k): return original value if less than a criterion + // const sumLabel = props.markerLabel ?? String(fullPieValue); + + // draw a larger white-filled circle + svgHTML += + ''; + + // create bubble svgHTML += - '' + - pushPinCode + + ''; + + //TODO: do we need to show total number for bubble marker? + // adding total number text/label and centering it + svgHTML += + '' + + props.data[0].value + ''; - } - svgHTML += ''; + // check isAtomic: draw pushpin if true + if (props.isAtomic) { + let pushPinCode = '🖈'; + svgHTML += + '' + + pushPinCode + + ''; + } - return { html: svgHTML, diameter: diameter }; + svgHTML += ''; + + return { html: svgHTML, diameter: diameter }; + } else { + return { html: '', diameter: 0 }; + } } diff --git a/packages/libs/eda/src/lib/core/api/DataClient/index.ts b/packages/libs/eda/src/lib/core/api/DataClient/index.ts index 6f328722f2..a69b4b797a 100644 --- a/packages/libs/eda/src/lib/core/api/DataClient/index.ts +++ b/packages/libs/eda/src/lib/core/api/DataClient/index.ts @@ -31,6 +31,8 @@ import { StandaloneMapBubblesRequestParams, ContinousVariableMetadataRequestParams, ContinousVariableMetadataResponse, + StandaloneMapBubblesLegendRequestParams, + StandaloneMapBubblesLegendResponse, } from './types'; export default class DataClient extends FetchClientWithCredentials { @@ -201,6 +203,18 @@ export default class DataClient extends FetchClientWithCredentials { ); } + getStandaloneBubblesLegend( + computationName: string, + params: StandaloneMapBubblesLegendRequestParams + ): Promise { + return this.getVisualizationData( + computationName, + 'map-markers/bubbles/legend', + params, + StandaloneMapBubblesLegendResponse + ); + } + // filter-aware continuous overlay variable metadata getContinousVariableMetadata( params: ContinousVariableMetadataRequestParams diff --git a/packages/libs/eda/src/lib/core/api/DataClient/types.ts b/packages/libs/eda/src/lib/core/api/DataClient/types.ts index 6cae7f27f1..e069f71e6c 100755 --- a/packages/libs/eda/src/lib/core/api/DataClient/types.ts +++ b/packages/libs/eda/src/lib/core/api/DataClient/types.ts @@ -772,14 +772,11 @@ export const AllValuesDefinition = type({ count: number, }); -export type CommonOverlayConfig = TypeOf; -export const CommonOverlayConfig = type({ - overlayVariable: VariableDescriptor, -}); - export type OverlayConfig = TypeOf; export const OverlayConfig = intersection([ - CommonOverlayConfig, + type({ + overlayVariable: VariableDescriptor, + }), // type({overlayType: keyof({ categorical: null, continuous: null })}), union([ type({ @@ -794,22 +791,20 @@ export const OverlayConfig = intersection([ ]); export type BubbleOverlayConfig = TypeOf; -export const BubbleOverlayConfig = intersection([ - CommonOverlayConfig, - type({ - aggregationConfig: union([ - type({ - overlayType: literal('categorical'), - numeratorValues: array(string), - denominatorValues: array(string), - }), - type({ - overlayType: literal('continuous'), - aggregator: keyof({ mean: null, median: null }), - }), - ]), - }), -]); +export const BubbleOverlayConfig = type({ + overlayVariable: VariableDescriptor, + aggregationConfig: union([ + type({ + overlayType: literal('categorical'), + numeratorValues: array(string), + denominatorValues: array(string), + }), + type({ + overlayType: literal('continuous'), + aggregator: keyof({ mean: null, median: null }), + }), + ]), +}); export interface StandaloneMapMarkersRequestParams { studyId: string; @@ -889,6 +884,31 @@ export const StandaloneMapBubblesResponse = type({ ), }); +export interface StandaloneMapBubblesLegendRequestParams { + studyId: string; + filters: Filter[]; + config: { + outputEntityId: string; + colorLegendConfig: { + geoAggregateVariable: VariableDescriptor; + quantitativeOverlayConfig: BubbleOverlayConfig; + }; + sizeConfig: { + geoAggregateVariable: VariableDescriptor; + }; + }; +} + +export type StandaloneMapBubblesLegendResponse = TypeOf< + typeof StandaloneMapBubblesLegendResponse +>; +export const StandaloneMapBubblesLegendResponse = type({ + minColorValue: number, + maxColorValue: number, + minSizeValue: number, + maxSizeValue: number, +}); + export interface ContinousVariableMetadataRequestParams { studyId: string; filters: Filter[]; diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index 5facf2e299..7234c29fba 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -527,13 +527,15 @@ function MapAnalysisImpl(props: ImplProps) { } }, [previewMarkerData]); + console.log('here2'); + const markers = useMemo( () => markersData?.map((markerProps) => markerType === 'pie' ? ( ) : markerType === 'bubble' ? ( - + ) : ( ) diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index e71960bc9d..5ae29232aa 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -5,6 +5,7 @@ import { GeoConfig } from '../../../core/types/geoConfig'; import DataClient, { BubbleOverlayConfig, OverlayConfig, + StandaloneMapBubblesLegendRequestParams, StandaloneMapBubblesRequestParams, StandaloneMapBubblesResponse, StandaloneMapMarkersRequestParams, @@ -173,6 +174,12 @@ export function useStandaloneMapMarkers( | StandaloneMapMarkersResponse | StandaloneMapBubblesResponse; vocabulary: string[] | undefined; + bubbleLegendData?: { + minColorValue: number; + maxColorValue: number; + minSizeValue: number; + maxSizeValue: number; + }; } | undefined // const overlayType = overlayConfig?.overlayType; @@ -253,27 +260,26 @@ export function useStandaloneMapMarkers( // ? overlayConfig?.overlayValues.map((ov) => ov.binLabel) // : undefined, // }; + console.log({ markerType }); + if (markerType === 'bubble') { const bubbleOverlayConfig = overlayConfig as | BubbleOverlayConfig | undefined; if ( - bubbleOverlayConfig && - bubbleOverlayConfig.aggregationConfig.overlayType === 'categorical' && - (bubbleOverlayConfig.aggregationConfig.numeratorValues.length === 0 || - bubbleOverlayConfig.aggregationConfig.denominatorValues.length === - 0) + !bubbleOverlayConfig || + (bubbleOverlayConfig.aggregationConfig.overlayType === + 'categorical' && + (bubbleOverlayConfig.aggregationConfig.numeratorValues.length === + 0 || + bubbleOverlayConfig.aggregationConfig.denominatorValues.length === + 0)) ) { - return { - rawMarkersData: { - mapElements: [], - }, - vocabulary: undefined, - }; + return undefined; } - const requestParams: StandaloneMapBubblesRequestParams = { + const markerRequestParams: StandaloneMapBubblesRequestParams = { studyId, filters: filters || [], config: { @@ -282,18 +288,49 @@ export function useStandaloneMapMarkers( longitudeVariable, overlayConfig: bubbleOverlayConfig, outputEntityId, - // need to get the actual valueSpec instead of just 'count' + // is valueSpec always count? valueSpec: 'count', viewport, }, }; - // now get and return the data - return { - rawMarkersData: await dataClient.getStandaloneBubbles( + const legendRequestParams: StandaloneMapBubblesLegendRequestParams = { + studyId, + filters: filters || [], + config: { + outputEntityId, + colorLegendConfig: { + geoAggregateVariable: { + entityId: geoConfig.entity.id, + variableId: geoConfig.aggregationVariableIds.at(-1) as string, + }, + quantitativeOverlayConfig: bubbleOverlayConfig, + }, + sizeConfig: { + geoAggregateVariable: { + entityId: geoConfig.entity.id, + variableId: geoConfig.aggregationVariableIds[0], + }, + }, + }, + }; + + const [rawMarkersData, bubbleLegendData] = await Promise.all([ + dataClient.getStandaloneBubbles( 'standalone-map', - requestParams + markerRequestParams ), + dataClient.getStandaloneBubblesLegend( + 'standalone-map', + legendRequestParams + ), + ]); + + console.log({ rawMarkersData, bubbleLegendData }); + + return { + rawMarkersData, + bubbleLegendData, // vocabulary: // overlayType === 'categorical' // switch statement style guide time!! // ? overlayConfig?.overlayValues @@ -408,22 +445,25 @@ export function useStandaloneMapMarkers( * and create markers. */ const finalMarkersData = useMemo(() => { - const maxOverlayCount = - markerType === 'bubble' - ? rawPromise.value?.rawMarkersData - ? Math.max( - ...rawPromise.value.rawMarkersData.mapElements.map( - (mapElement) => mapElement.entityCount - ) - ) - : 0 - : undefined; + // const maxOverlayCount = + // markerType === 'bubble' + // ? rawPromise.value?.rawMarkersData + // ? Math.max( + // ...rawPromise.value.rawMarkersData.mapElements.map( + // (mapElement) => mapElement.entityCount + // ) + // ) + // : 0 + // : undefined; const bubbleValueToDiameterMapper = - markerType === 'bubble' && maxOverlayCount + markerType === 'bubble' && rawPromise.value?.bubbleLegendData ? (value: number) => { + const bubbleLegendData = rawPromise.value!.bubbleLegendData!; + // const largestCircleArea = 9000; const largestCircleDiameter = 90; + const smallestCircleDiameter = 10; // Area scales directly with value // const constant = largestCircleArea / maxOverlayCount; @@ -435,8 +475,14 @@ export function useStandaloneMapMarkers( // const radius = Math.log10(value) * constant; // Radius scales directly with value - const scalingFactor = largestCircleDiameter / maxOverlayCount; - const diameter = value * scalingFactor; + // y = mx + b, m = (y2 - y1) / (x2 - x1), b = y1 - m * x1 + const m = + (largestCircleDiameter - smallestCircleDiameter) / + (bubbleLegendData.maxSizeValue - bubbleLegendData.minSizeValue); + const b = + smallestCircleDiameter - m * bubbleLegendData.minSizeValue; + // const scalingFactor = largestCircleDiameter / maxOverlayCount; + const diameter = m * value + b; // return 2 * radius; return diameter; From 5d328f31c7cdcc22aaba611651949ca1d33a6229 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Wed, 26 Jul 2023 18:20:20 -0400 Subject: [PATCH 19/56] Color bubbles according to overlayValue and legend endpoint data --- .../libs/components/src/map/BubbleMarker.tsx | 1 + .../libs/components/src/types/plots/addOns.ts | 46 +++++++++++++++++ .../ScatterplotVisualization.tsx | 49 ++----------------- .../BubbleMarkerConfigurationMenu.tsx | 5 +- .../analysis/hooks/standaloneMapMarkers.tsx | 12 +++++ 5 files changed, 66 insertions(+), 47 deletions(-) diff --git a/packages/libs/components/src/map/BubbleMarker.tsx b/packages/libs/components/src/map/BubbleMarker.tsx index 17993485a8..7917964c0a 100755 --- a/packages/libs/components/src/map/BubbleMarker.tsx +++ b/packages/libs/components/src/map/BubbleMarker.tsx @@ -11,6 +11,7 @@ import { NumberRange } from '../types/general'; // Don't need some of these props, but have to have them because of the general marker API/type definitions export interface BubbleMarkerProps extends BoundsDriftMarkerProps { + // There should only be one element in this array data: { value: number; label: string; diff --git a/packages/libs/components/src/types/plots/addOns.ts b/packages/libs/components/src/types/plots/addOns.ts index 4c40d47ac0..8303c525e9 100644 --- a/packages/libs/components/src/types/plots/addOns.ts +++ b/packages/libs/components/src/types/plots/addOns.ts @@ -242,6 +242,52 @@ const Berlin = [ 'rgb(229, 149, 144)', 'rgb(255, 173, 173)', ]; + +export const getValueToGradientColorMapper = ( + minValue: number, + maxValue: number +): ((value: number) => string) | undefined => { + const gradientColorscaleType = + minValue != null && maxValue != null + ? minValue >= 0 && maxValue >= 0 + ? 'sequential' + : minValue <= 0 && maxValue <= 0 + ? 'sequential reversed' + : 'divergent' + : undefined; + + if (gradientColorscaleType === null) { + return undefined; + } + + // Initialize normalization function. + const normalize = scaleLinear(); + + if (gradientColorscaleType === 'divergent') { + // Diverging colorscale, assume 0 is midpoint. Colorscale must be symmetric around the midpoint + const maxAbsOverlay = + Math.abs(minValue) > maxValue ? Math.abs(minValue) : maxValue; + // For each point, normalize the data to [-1, 1], then retrieve the + // corresponding color + normalize.domain([-maxAbsOverlay, maxAbsOverlay]).range([-1, 1]); + } else { + normalize.domain([minValue, maxValue]); + + if (gradientColorscaleType === 'sequential reversed') { + // Normalize data to [1, 0], so that the colorscale goes in reverse. + // NOTE: can remove once we add the ability for users to set colorscale range. + normalize.range([1, 0]); + } else { + // Then we use the sequential (from 0 to inf) colorscale. + // For each point, normalize the data to [0, 1], then retrieve the + // corresponding color + normalize.range([0, 1]); + } + } + + return (value) => gradientDivergingColorscaleMap(normalize(value)); +}; + // Lighten in LAB space, then convert to RGB for plotting. export const ConvergingGradientColorscale = Berlin.map((color) => rgb(lab(color).darker(-1)).toString() diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx index 2f55d1cb65..5d720a7c8a 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx @@ -87,6 +87,7 @@ import { gradientSequentialColorscaleMap, gradientDivergingColorscaleMap, SequentialGradientColorscale, + getValueToGradientColorMapper, } from '@veupathdb/components/lib/types/plots/addOns'; import { VariablesByInputName } from '../../../utils/data-element-constraints'; import { useRouteMatch } from 'react-router'; @@ -572,22 +573,6 @@ function ScatterplotViz(props: VisualizationProps) { ? overlayVariable?.distributionDefaults?.rangeMax : 0; - // Diverging colorscale, assume 0 is midpoint. Colorscale must be symmetric around the midpoint - const maxAbsOverlay = - Math.abs(overlayMin) > overlayMax ? Math.abs(overlayMin) : overlayMax; - const gradientColorscaleType: - | 'sequential' - | 'sequential reversed' - | 'divergent' - | undefined = - overlayMin != null && overlayMax != null - ? overlayMin >= 0 && overlayMax >= 0 - ? 'sequential' - : overlayMin <= 0 && overlayMax <= 0 - ? 'sequential reversed' - : 'divergent' - : undefined; - const inputsForValidation = useMemo( (): InputSpec[] => [ { @@ -720,37 +705,14 @@ function ScatterplotViz(props: VisualizationProps) { response.completeCasesTable ); - let overlayValueToColorMapper: ((a: number) => string) | undefined; - - if ( + const overlayValueToColorMapper: ((a: number) => string) | undefined = response.scatterplot.data.every( (series) => 'seriesGradientColorscale' in series ) && (overlayVariable?.type === 'integer' || overlayVariable?.type === 'number') - ) { - // create the value to color mapper (continuous overlay) - // Initialize normalization function. - const normalize = scaleLinear(); - - if (gradientColorscaleType === 'divergent') { - // For each point, normalize the data to [-1, 1], then retrieve the corresponding color - normalize.domain([-maxAbsOverlay, maxAbsOverlay]).range([-1, 1]); - overlayValueToColorMapper = (a) => - gradientDivergingColorscaleMap(normalize(a)); - } else if (gradientColorscaleType === 'sequential reversed') { - // Normalize data to [1, 0], so that the colorscale goes in reverse. NOTE: can remove once we add the ability for users to set colorscale range. - normalize.domain([overlayMin, overlayMax]).range([1, 0]); - overlayValueToColorMapper = (a) => - gradientSequentialColorscaleMap(normalize(a)); - } else { - // Then we use the sequential (from 0 to inf) colorscale. - // For each point, normalize the data to [0, 1], then retrieve the corresponding color - normalize.domain([overlayMin, overlayMax]).range([0, 1]); - overlayValueToColorMapper = (a) => - gradientSequentialColorscaleMap(normalize(a)); - } - } + ? getValueToGradientColorMapper(overlayMin, overlayMax) + : undefined; const overlayVocabulary = computedOverlayVariableDescriptor ? response.scatterplot.config.variables.find( @@ -822,8 +784,6 @@ function ScatterplotViz(props: VisualizationProps) { facetEntity, computedOverlayVariableDescriptor, neutralPaletteProps.colorPalette, - gradientColorscaleType, - maxAbsOverlay, overlayMin, overlayMax, ]) @@ -2451,6 +2411,7 @@ function processInputData( Number.isNaN(element) ) ) { + // here markerColorsGradient = seriesGradientColorscale.map((a: number) => overlayValueToColorMapper(a) ); diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx index 9e7c2048f1..f469387603 100644 --- a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx @@ -292,11 +292,10 @@ export function BubbleMarkerConfigurationMenu({ title: ( <> - Y-axis aggregation{' '} {selectedVariable ? categoricalMode - ? '(categorical Y)' - : '(continuous Y)' + ? 'Aggregation (categorical variable)' + : 'Proportion (continuous variable)' : ''} diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 5ae29232aa..19c0b09b90 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -18,6 +18,7 @@ import { useDefaultAxisRange } from '../../../core/hooks/computeDefaultAxisRange import { isEqual, max, some } from 'lodash'; import { ColorPaletteDefault, + getValueToGradientColorMapper, gradientSequentialColorscaleMap, } from '@veupathdb/components/lib/types/plots'; import { @@ -489,6 +490,14 @@ export function useStandaloneMapMarkers( } : undefined; + const bubbleValueToColorMapper = + markerType === 'bubble' && rawPromise.value?.bubbleLegendData + ? getValueToGradientColorMapper( + rawPromise.value.bubbleLegendData.minColorValue, + rawPromise.value.bubbleLegendData.maxColorValue + ) + : undefined; + return rawPromise.value?.rawMarkersData.mapElements.map( ({ geoAggregateValue, @@ -578,6 +587,9 @@ export function useStandaloneMapMarkers( { label: '', value: entityCount, + color: + 'overlayValue' in otherProps && + bubbleValueToColorMapper?.(otherProps.overlayValue), }, ]; From a08bfa11a2a50f426715b05856a8fc3eb903c894 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Thu, 27 Jul 2023 21:01:31 -0400 Subject: [PATCH 20/56] Add gradient legend to bubble map mode --- .gitignore | 3 + .../plotControls/PlotBubbleLegend.tsx | 2 + .../plotControls/PlotLegend.stories.tsx | 1 + .../eda/src/lib/map/analysis/MapAnalysis.tsx | 132 ++++++++++-------- .../eda/src/lib/map/analysis/MapLegend.tsx | 16 ++- .../BubbleMarkerConfigurationMenu.tsx | 60 ++++---- .../analysis/hooks/standaloneMapMarkers.tsx | 66 +++++---- .../analysis/utils/defaultOverlayConfig.ts | 11 +- 8 files changed, 171 insertions(+), 120 deletions(-) diff --git a/.gitignore b/.gitignore index 71456e7b66..529cfe1c88 100644 --- a/.gitignore +++ b/.gitignore @@ -107,6 +107,9 @@ dist # TernJS port file .tern-port +# VSCode config files +.vscode + .editorconfig .pnp.* diff --git a/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx index b9110d725d..f1910ad5d6 100644 --- a/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx +++ b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx @@ -4,6 +4,7 @@ import _ from 'lodash'; // set props for custom legend function export interface PlotLegendBubbleProps { + legendMin: number; legendMax: number; // legendMin: number; valueToDiameterMapper: ((value: number) => number) | undefined; @@ -20,6 +21,7 @@ export interface PlotLegendBubbleProps { // make gradient colorscale legend into a component so it can be more easily incorporated into DK's custom legend if we need export default function PlotBubbleLegend({ + legendMin, legendMax, // legendMin, valueToDiameterMapper, diff --git a/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx b/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx index f6911bb270..d108713139 100755 --- a/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx +++ b/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx @@ -556,6 +556,7 @@ export const BubbleMarkerLegend = () => {
); } - }, [previewMarkerData]); + }, [activeMarkerConfiguration, markerType, previewMarkerData]); console.log('here2'); @@ -792,6 +796,12 @@ function MapAnalysisImpl(props: ImplProps) { entities={studyEntities} onChange={updateMarkerConfigurations} configuration={activeMarkerConfiguration} + overlayConfiguration={ + activeOverlayConfig.value && + 'aggregationConfig' in activeOverlayConfig.value + ? activeOverlayConfig.value + : undefined + } starredVariables={ analysisState.analysis?.descriptor.starredVariables ?? [] } @@ -1241,71 +1251,63 @@ function MapAnalysisImpl(props: ImplProps) {
{(markerType === 'count' || markerType === 'proportion') && ( -
-
+ )} {/* Maybe should reintroduce loading placeholder */} - {markerType === 'bubble' && markersData !== undefined && ( - -
- markerData.data[0].value ?? 0 - ) - ) - : 0 - } - valueToDiameterMapper={ - markersData.length > 0 - ? (markersData as BubbleMarkerProps[])[0] - .valueToDiameterMapper - : undefined - } - containerStyles={{ - border: 'none', - boxShadow: 'none', - padding: 0, - width: 'auto', - maxWidth: 400, - }} - /> -
-
+ {markerType === 'bubble' && ( + <> + +
+ 0 + ? (markersData as BubbleMarkerProps[])[0] + .valueToDiameterMapper + : undefined, + }} + /> +
+
+ +
+ 'white'), + }} + /> +
+
+ )} {/* ); } + +const DraggableLegendPanel = (props: { + zIndex: number; + panelTitle?: string; + defaultPosition?: DraggablePanelCoordinatePair; + children: React.ReactNode; +}) => ( + + {props.children} + +); diff --git a/packages/libs/eda/src/lib/map/analysis/MapLegend.tsx b/packages/libs/eda/src/lib/map/analysis/MapLegend.tsx index 3edcbda737..c0e3959547 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapLegend.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapLegend.tsx @@ -1,15 +1,17 @@ import Spinner from '@veupathdb/components/lib/components/Spinner'; -import PlotLegend from '@veupathdb/components/lib/components/plotControls/PlotLegend'; +import PlotLegend, { + PlotLegendProps, +} from '@veupathdb/components/lib/components/plotControls/PlotLegend'; import { LegendItemsProps } from '@veupathdb/components/lib/components/plotControls/PlotListLegend'; interface Props { - legendItems: LegendItemsProps[]; + plotLegendProps: PlotLegendProps; isLoading: boolean; showCheckbox?: boolean; } export function MapLegend(props: Props) { - const { legendItems, isLoading, showCheckbox } = props; + const { plotLegendProps, isLoading, showCheckbox } = props; return isLoading ? (
@@ -17,9 +19,9 @@ export function MapLegend(props: Props) {
) : ( ); } diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx index f469387603..d3440a5295 100644 --- a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx @@ -51,14 +51,14 @@ interface Props > { onChange: (configuration: BubbleMarkerConfiguration) => void; configuration: BubbleMarkerConfiguration; - // overlayConfiguration: BubbleOverlayConfig | undefined; + overlayConfiguration: BubbleOverlayConfig | undefined; } // Currently identical to pie marker configuration menu export function BubbleMarkerConfigurationMenu({ entities, configuration, - // overlayConfiguration, + overlayConfiguration, onChange, starredVariables, toggleStarredVariable, @@ -118,21 +118,35 @@ export function BubbleMarkerConfigurationMenu({ // }, []); const categoricalMode = isSuitableCategoricalVariable(selectedVariable); + + const aggregationConfig = overlayConfiguration?.aggregationConfig; + const numeratorValues = + aggregationConfig && 'numeratorValues' in aggregationConfig + ? aggregationConfig.numeratorValues + : undefined; + const denominatorValues = + aggregationConfig && 'denominatorValues' in aggregationConfig + ? aggregationConfig.denominatorValues + : undefined; + const aggregator = + aggregationConfig && 'aggregator' in aggregationConfig + ? aggregationConfig.aggregator + : undefined; + const vocabulary = + selectedVariable && 'vocabulary' in selectedVariable + ? selectedVariable.vocabulary + : undefined; + const classes = useInputStyles(); if ( - categoricalMode && - configuration.numeratorValues !== undefined && - configuration.denominatorValues !== undefined + numeratorValues !== undefined && + denominatorValues !== undefined && + !numeratorValues.every((value) => denominatorValues.includes(value)) ) { - if ( - !configuration.numeratorValues.every((value) => - configuration.denominatorValues?.includes(value) - ) - ) - throw new Error( - 'To calculate a proportion, all selected numerator values must also be present in the denominator' - ); + throw new Error( + 'To calculate a proportion, all selected numerator values must also be present in the denominator' + ); } const aggregationInputs = ( @@ -156,8 +170,8 @@ export function BubbleMarkerConfigurationMenu({ aggregator: value, }) } - value={configuration.aggregator} - buttonDisplayContent={configuration.aggregator} + value={aggregator} + buttonDisplayContent={aggregator} items={aggregatorOptions.map((option) => ({ value: option, display: option, @@ -198,12 +212,8 @@ export function BubbleMarkerConfigurationMenu({ }} > onChange({ ...configuration, @@ -220,12 +230,8 @@ export function BubbleMarkerConfigurationMenu({ style={{ gridColumn: 2, gridRow: 3, justifyContent: 'center' }} > onChange({ ...configuration, diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 19c0b09b90..beb67ccacf 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -6,6 +6,7 @@ import DataClient, { BubbleOverlayConfig, OverlayConfig, StandaloneMapBubblesLegendRequestParams, + StandaloneMapBubblesLegendResponse, StandaloneMapBubblesRequestParams, StandaloneMapBubblesResponse, StandaloneMapMarkersRequestParams, @@ -95,6 +96,8 @@ interface MapMarkers { // vocabulary: string[] | undefined; /** data for creating a legend */ legendItems: LegendItemsProps[]; + bubbleLegendData?: StandaloneMapBubblesLegendResponse; + bubbleValueToColorMapper?: (value: number) => string; /** is the request pending? */ pending: boolean; /** any error returned from the data request */ @@ -440,28 +443,12 @@ export function useStandaloneMapMarkers( ) as NumberRange; const vocabulary = rawPromise.value?.vocabulary; + const bubbleLegendData = rawPromise.value?.bubbleLegendData; - /** - * Merge the overlay data into the basicMarkerData, if available, - * and create markers. - */ - const finalMarkersData = useMemo(() => { - // const maxOverlayCount = - // markerType === 'bubble' - // ? rawPromise.value?.rawMarkersData - // ? Math.max( - // ...rawPromise.value.rawMarkersData.mapElements.map( - // (mapElement) => mapElement.entityCount - // ) - // ) - // : 0 - // : undefined; - - const bubbleValueToDiameterMapper = - markerType === 'bubble' && rawPromise.value?.bubbleLegendData + const bubbleValueToDiameterMapper = useMemo( + () => + markerType === 'bubble' && bubbleLegendData ? (value: number) => { - const bubbleLegendData = rawPromise.value!.bubbleLegendData!; - // const largestCircleArea = 9000; const largestCircleDiameter = 90; const smallestCircleDiameter = 10; @@ -488,15 +475,36 @@ export function useStandaloneMapMarkers( // return 2 * radius; return diameter; } - : undefined; + : undefined, + [bubbleLegendData, markerType] + ); - const bubbleValueToColorMapper = - markerType === 'bubble' && rawPromise.value?.bubbleLegendData + const bubbleValueToColorMapper = useMemo( + () => + markerType === 'bubble' && bubbleLegendData ? getValueToGradientColorMapper( - rawPromise.value.bubbleLegendData.minColorValue, - rawPromise.value.bubbleLegendData.maxColorValue + bubbleLegendData.minColorValue, + bubbleLegendData.maxColorValue ) - : undefined; + : undefined, + [bubbleLegendData, markerType] + ); + + /** + * Merge the overlay data into the basicMarkerData, if available, + * and create markers. + */ + const finalMarkersData = useMemo(() => { + // const maxOverlayCount = + // markerType === 'bubble' + // ? rawPromise.value?.rawMarkersData + // ? Math.max( + // ...rawPromise.value.rawMarkersData.mapElements.map( + // (mapElement) => mapElement.entityCount + // ) + // ) + // : 0 + // : undefined; return rawPromise.value?.rawMarkersData.mapElements.map( ({ @@ -614,10 +622,12 @@ export function useStandaloneMapMarkers( } ); }, [ - rawPromise, + rawPromise.value?.rawMarkersData.mapElements, vocabulary, markerType, overlayType, + bubbleValueToColorMapper, + bubbleValueToDiameterMapper, defaultDependentAxisRange, dependentAxisLogScale, ]); @@ -664,6 +674,8 @@ export function useStandaloneMapMarkers( totalVisibleWithOverlayEntityCount: countSum, totalVisibleEntityCount, legendItems, + bubbleLegendData, + bubbleValueToColorMapper, pending: rawPromise.pending, error: rawPromise.error, }; diff --git a/packages/libs/eda/src/lib/map/analysis/utils/defaultOverlayConfig.ts b/packages/libs/eda/src/lib/map/analysis/utils/defaultOverlayConfig.ts index 7e0f7685c9..904d2722a8 100644 --- a/packages/libs/eda/src/lib/map/analysis/utils/defaultOverlayConfig.ts +++ b/packages/libs/eda/src/lib/map/analysis/utils/defaultOverlayConfig.ts @@ -48,8 +48,8 @@ export async function getDefaultOverlayConfig( markerType, binningMethod = 'equalInterval', aggregator = 'mean', - numeratorValues = [], - denominatorValues = [], + numeratorValues, + denominatorValues, } = props; if (overlayVariable != null && overlayEntity != null) { @@ -58,6 +58,8 @@ export async function getDefaultOverlayConfig( entityId: overlayEntity.id, }; + console.log({ denominatorValues, vocab: overlayVariable.vocabulary }); + if (CategoricalVariableDataShape.is(overlayVariable.dataShape)) { // categorical if (markerType === 'bubble') { @@ -65,8 +67,9 @@ export async function getDefaultOverlayConfig( overlayVariable: overlayVariableDescriptor, aggregationConfig: { overlayType: 'categorical', - numeratorValues, - denominatorValues, + numeratorValues: numeratorValues ?? [], + denominatorValues: + denominatorValues ?? overlayVariable.vocabulary ?? [], }, }; } else { From 0c934140fb843d827699b1722a96042e6a5e2b37 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Fri, 28 Jul 2023 13:45:58 -0400 Subject: [PATCH 21/56] Fix bubble menu icon sizing issue --- .../lib/map/analysis/MarkerConfiguration/icons/BubbleMarker.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarker.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarker.tsx index 3cddd4e6e1..144ddb8dd6 100644 --- a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarker.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/icons/BubbleMarker.tsx @@ -14,6 +14,7 @@ export function BubbleMarker(props: SVGProps) { viewBox="0 0 32.443 32.443" // style={{enableBackground: 'new 0 0 32.443 32.443'}} xmlSpace="preserve" + {...props} > - - - + + + + + ); } From 91c3306eb977b22c201945f7c45bdd621400d503 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Wed, 2 Aug 2023 16:03:00 -0400 Subject: [PATCH 36/56] Change verbiage of bubble popup --- packages/libs/components/src/map/BubbleMarker.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/libs/components/src/map/BubbleMarker.tsx b/packages/libs/components/src/map/BubbleMarker.tsx index e772055f2c..0da278cf94 100755 --- a/packages/libs/components/src/map/BubbleMarker.tsx +++ b/packages/libs/components/src/map/BubbleMarker.tsx @@ -37,11 +37,12 @@ export default function BubbleMarker(props: BubbleMarkerProps) { const popupContent = (
-
- Count (size) {props.data.value} +
+ Count {props.data.value}
- Aggregation value (color) {props.data.colorValue} + Color value{' '} + {props.data.colorValue}
); @@ -57,7 +58,7 @@ export default function BubbleMarker(props: BubbleMarkerProps) { popupContent={{ content: popupContent, size: { - width: 200, + width: 170, height: 100, }, }} From 928553525392320bb3c53cba927b1ca21291a2f4 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Wed, 2 Aug 2023 16:34:49 -0400 Subject: [PATCH 37/56] Add comments --- .../src/components/plotControls/PlotBubbleLegend.tsx | 3 +++ packages/libs/components/src/map/BubbleMarker.tsx | 3 +++ packages/libs/components/src/map/Types.ts | 1 + .../libs/components/src/stories/BubbleMarkers.stories.tsx | 1 - packages/libs/eda/src/lib/core/api/DataClient/types.ts | 2 +- .../MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx | 4 +++- packages/libs/eda/src/lib/map/analysis/appState.ts | 1 + .../eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx | 1 - 8 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx index 42802a43fb..4f06baa798 100644 --- a/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx +++ b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx @@ -29,6 +29,9 @@ export default function PlotBubbleLegend({ const padding = 5; const numCircles = 3; + // The largest circle's value will be the first number that's larger than + // legendMax and has only one significant digit. Each smaller circle will + // be half the size of the last (rounded and >= 1) const legendMaxLog10 = Math.floor(Math.log10(legendMax)); const largestCircleValue = legendMax <= 10 diff --git a/packages/libs/components/src/map/BubbleMarker.tsx b/packages/libs/components/src/map/BubbleMarker.tsx index 0da278cf94..6bc9e13478 100755 --- a/packages/libs/components/src/map/BubbleMarker.tsx +++ b/packages/libs/components/src/map/BubbleMarker.tsx @@ -54,6 +54,9 @@ export default function BubbleMarker(props: BubbleMarkerProps) { bounds={props.bounds} icon={SVGBubbleIcon as L.Icon} duration={duration} + // This makes sure smaller markers are on top of larger ones. + // The factor of 1000 ensures that the offset dominates over + // the default zIndex, which itself varies. zIndexOffset={-props.data.value * 1000} popupContent={{ content: popupContent, diff --git a/packages/libs/components/src/map/Types.ts b/packages/libs/components/src/map/Types.ts index 9f69e7273f..146f0912c2 100644 --- a/packages/libs/components/src/map/Types.ts +++ b/packages/libs/components/src/map/Types.ts @@ -35,6 +35,7 @@ export interface MarkerProps { height: number; }; }; + /* This offset gets added to the default zIndex */ zIndexOffset?: number; } diff --git a/packages/libs/components/src/stories/BubbleMarkers.stories.tsx b/packages/libs/components/src/stories/BubbleMarkers.stories.tsx index 8877eee4b0..c4359164b6 100644 --- a/packages/libs/components/src/stories/BubbleMarkers.stories.tsx +++ b/packages/libs/components/src/stories/BubbleMarkers.stories.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { Story, Meta } from '@storybook/react/types-6-0'; -// import { action } from '@storybook/addon-actions'; import { MapVEuMapProps } from '../map/MapVEuMap'; diff --git a/packages/libs/eda/src/lib/core/api/DataClient/types.ts b/packages/libs/eda/src/lib/core/api/DataClient/types.ts index 5c74515d3f..4333667f77 100755 --- a/packages/libs/eda/src/lib/core/api/DataClient/types.ts +++ b/packages/libs/eda/src/lib/core/api/DataClient/types.ts @@ -148,7 +148,7 @@ const plotConfig = intersection([ }), ]); -// to be distinguised from geo-viewports +// to be distinguished from geo-viewports export type NumericViewport = TypeOf; const numericViewport = type({ xMin: string, diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx index ab38c776d4..0d9e3c47d1 100644 --- a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx @@ -199,7 +199,9 @@ function isSuitableCategoricalVariable(variable?: VariableTreeNode): boolean { ); } -// We currently call this function twice per value change. If the number of values becomes vary large, we may want to optimize this? +// We currently call this function twice per value change. +// If the number of values becomes vary large, we may want to optimize this? +// Maybe O(n^2) isn't that bad though. export const validateProportionValues = ( numeratorValues: string[] | undefined, denominatorValues: string[] | undefined diff --git a/packages/libs/eda/src/lib/map/analysis/appState.ts b/packages/libs/eda/src/lib/map/analysis/appState.ts index 24ba461563..f27e62a375 100644 --- a/packages/libs/eda/src/lib/map/analysis/appState.ts +++ b/packages/libs/eda/src/lib/map/analysis/appState.ts @@ -167,6 +167,7 @@ export function useAppState(uiStateKey: string, analysisState: AnalysisState) { [uiStateKey]: defaultAppState, })); } else { + // Ensures forward compatibility of analyses with new marker types const missingMarkerConfigs = defaultAppState.markerConfigurations.filter( (defaultConfig) => diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 98d53b34ea..cdb5df6193 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -315,7 +315,6 @@ export function useStandaloneMapMarkers( }, }; - // now get and return the data return { rawMarkersData: await dataClient.getStandaloneMapMarkers( 'standalone-map', From 7db7cd3aef4aee771aec20540e067671eb5314e5 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Wed, 2 Aug 2023 16:56:25 -0400 Subject: [PATCH 38/56] Remove clear button from bubble menu variable input --- .../libs/eda/src/lib/map/analysis/MapAnalysis.tsx | 1 - .../BubbleMarkerConfigurationMenu.tsx | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index 52e3dddfc4..a570eff654 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -798,7 +798,6 @@ function MapAnalysisImpl(props: ImplProps) { configurationMenu: activeMarkerConfiguration?.type === 'bubble' ? ( { onChange: (configuration: BubbleMarkerConfiguration) => void; configuration: BubbleMarkerConfiguration; @@ -148,7 +153,12 @@ export function BubbleMarkerConfigurationMenu({

Date: Wed, 2 Aug 2023 17:36:00 -0400 Subject: [PATCH 39/56] Use _.get() where possible to shorten expressions --- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 27 ++++++------------- .../analysis/hooks/standaloneVizPlugins.ts | 8 +++--- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index a570eff654..5ab4517168 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -108,6 +108,7 @@ import { sharedStandaloneMarkerProperties } from './MarkerConfiguration/Categori import { mFormatter, kFormatter } from '../../core/utils/big-number-formatters'; import { getCategoricalValues } from './utils/categoricalValues'; import { DraggablePanelCoordinatePair } from '@veupathdb/coreui/lib/components/containers/DraggablePanel'; +import _ from 'lodash'; enum MapSideNavItemLabels { Download = 'Download', @@ -403,25 +404,13 @@ function MapAnalysisImpl(props: ImplProps) { dataClient, subsettingClient, markerType: activeMarkerConfiguration?.type, - binningMethod: - activeMarkerConfiguration && - 'binningMethod' in activeMarkerConfiguration - ? activeMarkerConfiguration.binningMethod - : undefined, - aggregator: - activeMarkerConfiguration && 'aggregator' in activeMarkerConfiguration - ? activeMarkerConfiguration.aggregator - : undefined, - numeratorValues: - activeMarkerConfiguration && - 'numeratorValues' in activeMarkerConfiguration - ? activeMarkerConfiguration.numeratorValues - : undefined, - denominatorValues: - activeMarkerConfiguration && - 'denominatorValues' in activeMarkerConfiguration - ? activeMarkerConfiguration.denominatorValues - : undefined, + binningMethod: _.get(activeMarkerConfiguration, 'binningMethod'), + aggregator: _.get(activeMarkerConfiguration, 'aggregator'), + numeratorValues: _.get(activeMarkerConfiguration, 'numeratorValues'), + denominatorValues: _.get( + activeMarkerConfiguration, + 'denominatorValues' + ), }); }, [ activeMarkerConfiguration, diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneVizPlugins.ts b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneVizPlugins.ts index 3a135165ee..78dcad9c78 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneVizPlugins.ts +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneVizPlugins.ts @@ -29,6 +29,7 @@ import { histogramRequest } from './plugins/histogram'; import { scatterplotRequest } from './plugins/scatterplot'; //TO DO import timeline SVGIcon import LineSVG from '../../../core/components/visualizations/implementations/selectorIcons/LineSVG'; +import _ from 'lodash'; interface Props { selectedOverlayConfig?: OverlayConfig | BubbleOverlayConfig; @@ -52,11 +53,8 @@ export function useStandaloneVizPlugins({ // part of this interface. getOverlayVariable: (_) => selectedOverlayConfig?.overlayVariable, getOverlayType: () => - selectedOverlayConfig - ? 'overlayType' in selectedOverlayConfig - ? selectedOverlayConfig.overlayType - : selectedOverlayConfig.aggregationConfig.overlayType - : undefined, + _.get(selectedOverlayConfig, 'overlayType') ?? + _.get(selectedOverlayConfig, 'aggregationConfig.overlayType'), getOverlayVocabulary: () => { const overlayValues = selectedOverlayConfig && 'overlayValues' in selectedOverlayConfig From 7d01edec36c9b1abd37e2c28b3403b481d5f8729 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Thu, 3 Aug 2023 15:12:20 -0400 Subject: [PATCH 40/56] Change === null to == null to prevent error --- packages/libs/components/src/types/plots/addOns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/libs/components/src/types/plots/addOns.ts b/packages/libs/components/src/types/plots/addOns.ts index 5c08c7f60c..57d506185a 100644 --- a/packages/libs/components/src/types/plots/addOns.ts +++ b/packages/libs/components/src/types/plots/addOns.ts @@ -256,7 +256,7 @@ export const getValueToGradientColorMapper = ( : 'divergent' : undefined; - if (gradientColorscaleType === null) { + if (gradientColorscaleType == null) { return undefined; } From 1af6c2fdcc61306463ea5c60b37366551f5fae6f Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Fri, 4 Aug 2023 14:13:30 -0400 Subject: [PATCH 41/56] Wire up plot controls in VolcanoPlotViz component --- .../ScatterplotVisualization.tsx | 36 ++-- .../VolcanoPlotVisualization.tsx | 154 ++++++++++++++++-- 2 files changed, 154 insertions(+), 36 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx index 2f55d1cb65..cab427ae73 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx @@ -166,6 +166,24 @@ const modalPlotContainerStyles = { margin: 'auto', }; +// implement gradient color for slider opacity +export const colorSpecProps: SliderWidgetProps['colorSpec'] = { + type: 'gradient', + tooltip: '#aaa', + knobColor: '#aaa', + // normal slider color: e.g., from 0 to 1 + trackGradientStart: '#fff', + trackGradientEnd: '#000', +}; + +// slider settings +const markerBodyOpacityContainerStyles = { + height: '4em', + width: '20em', + marginLeft: '1em', + marginBottom: '0.5em', +}; + // define ScatterPlotDataWithCoverage and export export interface ScatterPlotDataWithCoverage extends CoverageStatistics { dataSetProcess: ScatterPlotData | FacetedData; @@ -1300,24 +1318,6 @@ function ScatterplotViz(props: VisualizationProps) { setTruncatedDependentAxisWarning, ]); - // slider settings - const markerBodyOpacityContainerStyles = { - height: '4em', - width: '20em', - marginLeft: '1em', - marginBottom: '0.5em', - }; - - // implement gradient color for slider opacity - const colorSpecProps: SliderWidgetProps['colorSpec'] = { - type: 'gradient', - tooltip: '#aaa', - knobColor: '#aaa', - // normal slider color: e.g., from 0 to 1 - trackGradientStart: '#fff', - trackGradientEnd: '#000', - }; - const scatterplotProps: ScatterPlotProps = { interactive: !isFaceted(data.value?.dataSetProcess) ? true : false, showSpinner: filteredCounts.pending || data.pending, diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx index 8200b9a446..68013ad70a 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx @@ -6,7 +6,7 @@ import VolcanoPlot, { } from '@veupathdb/components/lib/plots/VolcanoPlot'; import * as t from 'io-ts'; -import { useCallback, useState, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { usePromise } from '../../../hooks/promise'; import { useUpdateThumbnailEffect } from '../../../hooks/thumbnails'; @@ -39,8 +39,14 @@ import { DifferentialAbundanceConfig } from '../../computations/plugins/differen import { yellow } from '@material-ui/core/colors'; import PlotLegend from '@veupathdb/components/lib/components/plotControls/PlotLegend'; import { significanceColors } from '@veupathdb/components/lib/types/plots'; -import { NumberRange } from '../../../types/general'; +import { NumberOrDateRange, NumberRange } from '../../../types/general'; import { max, min } from 'lodash'; + +// plot controls +import SliderWidget from '@veupathdb/components/lib/components/widgets/Slider'; +import { colorSpecProps } from './ScatterplotVisualization'; +import { ResetButtonCoreUI } from '../../ResetButton'; +import AxisRangeControl from '@veupathdb/components/lib/components/plotControls/AxisRangeControl'; // end imports const DEFAULT_SIG_THRESHOLD = 0.05; @@ -75,15 +81,19 @@ function createDefaultConfig(): VolcanoPlotConfig { log2FoldChangeThreshold: DEFAULT_FC_THRESHOLD, significanceThreshold: DEFAULT_SIG_THRESHOLD, markerBodyOpacity: 0.5, + independentAxisRange: undefined, + dependentAxisRange: undefined, }; } export type VolcanoPlotConfig = t.TypeOf; - +// eslint-disable-next-line @typescript-eslint/no-redeclare export const VolcanoPlotConfig = t.partial({ log2FoldChangeThreshold: t.number, significanceThreshold: t.number, markerBodyOpacity: t.number, + independentAxisRange: NumberRange, + dependentAxisRange: NumberRange, }); interface Options @@ -185,13 +195,10 @@ function VolcanoPlotViz(props: VisualizationProps) { // Determine mins, maxes of axes in the plot. These are different than the data mins/maxes because // of the log transform and the little bit of padding, or because axis ranges are supplied. - // NOTE: this state may be unnecessary depending on how we implement user-controlled axis ranges - const [xAxisRange, setXAxisRange] = - useState(undefined); const independentAxisRange = useMemo(() => { if (!data.value) return undefined; - if (xAxisRange) { - return xAxisRange; + if (vizConfig.independentAxisRange) { + return vizConfig.independentAxisRange; } else { const { x: { min: dataXMin, max: dataXMax }, @@ -203,15 +210,12 @@ function VolcanoPlotViz(props: VisualizationProps) { max: dataXMax + (dataXMax - dataXMin) * AXIS_PADDING_FACTOR, }; } - }, [data.value, xAxisRange, rawDataMinMaxValues]); + }, [data.value, vizConfig.independentAxisRange, rawDataMinMaxValues]); - // NOTE: this state may be unnecessary depending on how we implement user-controlled axis ranges - const [yAxisRange, setYAxisRange] = - useState(undefined); const dependentAxisRange = useMemo(() => { if (!data.value) return undefined; - if (yAxisRange) { - return yAxisRange; + if (vizConfig.dependentAxisRange) { + return vizConfig.dependentAxisRange; } else { const { y: { min: dataYMin, max: dataYMax }, @@ -225,7 +229,7 @@ function VolcanoPlotViz(props: VisualizationProps) { max: yAxisMax + (yAxisMax - yAxisMin) * AXIS_PADDING_FACTOR, }; } - }, [data.value, yAxisRange, rawDataMinMaxValues]); + }, [data.value, vizConfig.dependentAxisRange, rawDataMinMaxValues]); const significanceThreshold = vizConfig.significanceThreshold ?? DEFAULT_SIG_THRESHOLD; @@ -290,8 +294,8 @@ function VolcanoPlotViz(props: VisualizationProps) { [ data, // vizConfig.checkedLegendItems, TODO - // vizConfig.independentAxisRange, TODO - // vizConfig.dependentAxisRange, TODO + vizConfig.independentAxisRange, + vizConfig.dependentAxisRange, vizConfig.markerBodyOpacity, ] ); @@ -343,7 +347,121 @@ function VolcanoPlotViz(props: VisualizationProps) { const plotNode = ; // TODO - const controlsNode = <> ; + const controlsNode = ( +
+ { + updateVizConfig({ markerBodyOpacity: newValue }); + }} + containerStyles={{ width: '20em' }} + showLimits={true} + label={'Marker opacity'} + colorSpec={colorSpecProps} + /> +
+
+
+ } + containerStyles={{ + marginRight: 0, + paddingLeft: 0, + }} + /> + + updateVizConfig({ independentAxisRange: undefined }) + } + /> +
+ { + const typeCheckedNewRange = + typeof newRange?.min === 'number' && + typeof newRange?.max === 'number' + ? { + min: newRange.min, + max: newRange.max, + } + : undefined; + updateVizConfig({ + independentAxisRange: typeCheckedNewRange, + }); + }} + /> +
+ {/** vertical line to separate x from y range controls*/} +
+
+
+ } + containerStyles={{ + marginRight: 0, + paddingLeft: 0, + }} + /> + updateVizConfig({ dependentAxisRange: undefined })} + /> +
+ { + const typeCheckedNewRange = + typeof newRange?.min === 'number' && + typeof newRange?.max === 'number' + ? { + min: newRange.min, + max: newRange.max, + } + : undefined; + updateVizConfig({ + dependentAxisRange: typeCheckedNewRange, + }); + }} + /> +
+
+
+ ); const legendNode = finalData && countsData && ( Date: Mon, 7 Aug 2023 09:56:50 -0400 Subject: [PATCH 42/56] Fix incorrect bubble config input labels --- .../MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx index 93114defa7..99bacf8cad 100644 --- a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx @@ -167,8 +167,8 @@ export function BubbleMarkerConfigurationMenu({ {selectedVariable ? categoricalMode - ? 'Aggregation (categorical variable)' - : 'Proportion (continuous variable)' + ? 'Proportion (categorical variable)' + : 'Aggregation (continuous variable)' : ''} From 562ef76d7b5fbe83fb727107a714143e19350a12 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Mon, 7 Aug 2023 11:40:52 -0400 Subject: [PATCH 43/56] Wire up step prop in AxisRangeControl for number inputs --- .../src/components/plotControls/AxisRangeControl.tsx | 4 ++++ .../src/components/widgets/NumberAndDateRangeInputs.tsx | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/libs/components/src/components/plotControls/AxisRangeControl.tsx b/packages/libs/components/src/components/plotControls/AxisRangeControl.tsx index 0f52658666..ccffa18c62 100755 --- a/packages/libs/components/src/components/plotControls/AxisRangeControl.tsx +++ b/packages/libs/components/src/components/plotControls/AxisRangeControl.tsx @@ -22,6 +22,8 @@ export interface AxisRangeControlProps disabled?: boolean; /** is this for a log scale axis? If so, we'll validate the min value to be > 0 */ logScale?: boolean; + /** specify step for increment/decrement buttons in MUI number inputs; MUI's default is 1 */ + step?: number; } export default function AxisRangeControl({ @@ -33,6 +35,7 @@ export default function AxisRangeControl({ // add disabled prop to disable input fields: default is false disabled = false, logScale = false, + step = undefined, }: AxisRangeControlProps) { const validator = useCallback( ( @@ -87,6 +90,7 @@ export default function AxisRangeControl({ validator={validator} // add disabled prop to disable input fields disabled={disabled} + step={step} /> ) ) : null; diff --git a/packages/libs/components/src/components/widgets/NumberAndDateRangeInputs.tsx b/packages/libs/components/src/components/widgets/NumberAndDateRangeInputs.tsx index 50f1558b52..cdc625cb07 100755 --- a/packages/libs/components/src/components/widgets/NumberAndDateRangeInputs.tsx +++ b/packages/libs/components/src/components/widgets/NumberAndDateRangeInputs.tsx @@ -6,6 +6,7 @@ import { NumberInput, DateInput } from './NumberAndDateInputs'; import Button from './Button'; import Notification from './Notification'; import { NumberRange, DateRange, NumberOrDateRange } from '../../types/general'; +import { propTypes } from 'react-bootstrap/esm/Image'; export type BaseProps = { /** Externally controlled range. */ @@ -44,7 +45,7 @@ export type BaseProps = { disabled?: boolean; }; -export type NumberRangeInputProps = BaseProps; +export type NumberRangeInputProps = BaseProps & { step?: number }; export function NumberRangeInput(props: NumberRangeInputProps) { return ; @@ -85,6 +86,7 @@ function BaseInput({ clearButtonLabel = 'Clear', // add disabled prop to disable input fields disabled = false, + ...props }: BaseInputProps) { if (validator && required) console.log( @@ -161,6 +163,7 @@ function BaseInput({ ]); const { min, max } = localRange ?? {}; + const step = 'step' in props ? props.step : undefined; return (
@@ -188,6 +191,7 @@ function BaseInput({ }} // add disabled prop to disable input fields disabled={disabled} + step={step} /> ) : ( ) : ( Date: Mon, 7 Aug 2023 11:42:28 -0400 Subject: [PATCH 44/56] Format default axis ranges and pass step prop to the inputs --- .../implementations/VolcanoPlotVisualization.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx index 68013ad70a..029a1924df 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx @@ -206,8 +206,8 @@ function VolcanoPlotViz(props: VisualizationProps) { // We can use the dataMin and dataMax here because we don't have a further transform // Add a little padding to prevent clipping the glyph representing the extreme points return { - min: dataXMin - (dataXMax - dataXMin) * AXIS_PADDING_FACTOR, - max: dataXMax + (dataXMax - dataXMin) * AXIS_PADDING_FACTOR, + min: Math.floor(dataXMin - (dataXMax - dataXMin) * AXIS_PADDING_FACTOR), + max: Math.ceil(dataXMax + (dataXMax - dataXMin) * AXIS_PADDING_FACTOR), }; } }, [data.value, vizConfig.independentAxisRange, rawDataMinMaxValues]); @@ -225,8 +225,8 @@ function VolcanoPlotViz(props: VisualizationProps) { const yAxisMax = -Math.log10(dataYMin); // Add a little padding to prevent clipping the glyph representing the extreme points return { - min: yAxisMin - (yAxisMax - yAxisMin) * AXIS_PADDING_FACTOR, - max: yAxisMax + (yAxisMax - yAxisMin) * AXIS_PADDING_FACTOR, + min: Math.floor(yAxisMin - (yAxisMax - yAxisMin) * AXIS_PADDING_FACTOR), + max: Math.ceil(yAxisMax + (yAxisMax - yAxisMin) * AXIS_PADDING_FACTOR), }; } }, [data.value, vizConfig.dependentAxisRange, rawDataMinMaxValues]); @@ -412,6 +412,7 @@ function VolcanoPlotViz(props: VisualizationProps) { independentAxisRange: typeCheckedNewRange, }); }} + step={0.01} />
{/** vertical line to separate x from y range controls*/} @@ -457,6 +458,7 @@ function VolcanoPlotViz(props: VisualizationProps) { dependentAxisRange: typeCheckedNewRange, }); }} + step={0.01} />
From cb44ae26f0f0b46452276c0528c7315d24123c66 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Mon, 7 Aug 2023 12:09:45 -0400 Subject: [PATCH 45/56] Use sequential colormap when data is sequential --- packages/libs/components/src/types/plots/addOns.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/libs/components/src/types/plots/addOns.ts b/packages/libs/components/src/types/plots/addOns.ts index 57d506185a..2f1db3b82e 100644 --- a/packages/libs/components/src/types/plots/addOns.ts +++ b/packages/libs/components/src/types/plots/addOns.ts @@ -269,6 +269,8 @@ export const getValueToGradientColorMapper = ( Math.abs(minValue) > maxValue ? Math.abs(minValue) : maxValue; // For each point, normalize the data to [-1, 1] normalize.domain([-maxAbsOverlay, maxAbsOverlay]).range([-1, 1]); + + return (value) => gradientDivergingColorscaleMap(normalize(value)); } else { normalize.domain([minValue, maxValue]); @@ -281,9 +283,9 @@ export const getValueToGradientColorMapper = ( // For each point, normalize the data to [0, 1] normalize.range([0, 1]); } - } - return (value) => gradientDivergingColorscaleMap(normalize(value)); + return (value) => gradientSequentialColorscaleMap(normalize(value)); + } }; // Lighten in LAB space, then convert to RGB for plotting. From a2c1cede5942561bfadcdb3591162043af848bde Mon Sep 17 00:00:00 2001 From: "Dae Kun (DK) Kwon" Date: Mon, 7 Aug 2023 13:52:59 -0400 Subject: [PATCH 46/56] address feedbacks --- .../lib/core/api/SubsettingClient/types.ts | 2 +- .../HistogramVisualization.tsx | 93 +++++++++---------- 2 files changed, 47 insertions(+), 48 deletions(-) diff --git a/packages/libs/eda/src/lib/core/api/SubsettingClient/types.ts b/packages/libs/eda/src/lib/core/api/SubsettingClient/types.ts index c7ce822e3d..9b45c730e5 100644 --- a/packages/libs/eda/src/lib/core/api/SubsettingClient/types.ts +++ b/packages/libs/eda/src/lib/core/api/SubsettingClient/types.ts @@ -21,7 +21,7 @@ export const StudyResponse = type({ }); export interface DistributionRequestParams { - filters: Filter[]; + filters?: Filter[]; binSpec?: { displayRangeMin: number | string; displayRangeMax: number | string; diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/HistogramVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/HistogramVisualization.tsx index 0e02ceedf7..079adf7c18 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/HistogramVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/HistogramVisualization.tsx @@ -454,7 +454,7 @@ function HistogramViz(props: VisualizationProps) { // get distribution data const subsettingClient = useSubsettingClient(); - const getData = useCallback(async () => { + const getDistributionData = useCallback(async () => { if (vizConfig.xAxisVariable != null && xAxisVariable != null) { const [displayRangeMin, displayRangeMax, binWidth, binUnits] = NumberVariable.is(xAxisVariable) @@ -477,34 +477,26 @@ function HistogramViz(props: VisualizationProps) { (xAxisVariable as DateVariable).distributionDefaults.binUnits, ]; - const distribution = await getDistribution( + // try to call once + const distribution = await subsettingClient.getDistribution( + studyMetadata.id, + vizConfig.xAxisVariable?.entityId ?? '', + vizConfig.xAxisVariable?.variableId ?? '', { - entityId: vizConfig.xAxisVariable?.entityId ?? '', - variableId: vizConfig.xAxisVariable?.variableId ?? '', - filters: filters, - }, - (filters) => { - return subsettingClient.getDistribution( - studyMetadata.id, - vizConfig.xAxisVariable?.entityId ?? '', - vizConfig.xAxisVariable?.variableId ?? '', - { - valueSpec: 'count', - filters, - binSpec: { - // Note: technically any arbitrary values can be used here for displayRangeMin/Max - // but used more accurate value anyway - displayRangeMin: DateVariable.is(xAxisVariable) - ? displayRangeMin + 'T00:00:00Z' - : displayRangeMin, - displayRangeMax: DateVariable.is(xAxisVariable) - ? displayRangeMax + 'T00:00:00Z' - : displayRangeMax, - binWidth: binWidth ?? 1, - binUnits: binUnits, - }, - } - ); + valueSpec: 'count', + filters, + binSpec: { + // Note: technically any arbitrary values can be used here for displayRangeMin/Max + // but used more accurate value anyway + displayRangeMin: DateVariable.is(xAxisVariable) + ? displayRangeMin + 'T00:00:00Z' + : displayRangeMin, + displayRangeMax: DateVariable.is(xAxisVariable) + ? displayRangeMax + 'T00:00:00Z' + : displayRangeMax, + binWidth: binWidth ?? 1, + binUnits: binUnits, + }, } ); @@ -513,7 +505,7 @@ function HistogramViz(props: VisualizationProps) { series: [ distributionResponseToDataSeries( 'Subset', - distribution.foreground, + distribution, red, NumberVariable.is(xAxisVariable) ? 'number' : 'date' ), @@ -524,10 +516,11 @@ function HistogramViz(props: VisualizationProps) { } return undefined; - }, [filters, xAxisVariable, vizConfig.xAxisVariable]); + }, [filters, xAxisVariable, vizConfig.xAxisVariable, subsettingClient]); - const getDistributionData = usePromise( - useCallback(() => getData(), [getData]) + // need useCallback to avoid infinite loop + const distributionDataPromise = usePromise( + useCallback(() => getDistributionData(), [getDistributionData]) ); const dataRequestConfig: DataRequestConfig = useDeepValue( @@ -558,8 +551,11 @@ function HistogramViz(props: VisualizationProps) { ) return undefined; - // wait till getDistributionData is ready - if (getDistributionData.pending || getDistributionData.value == null) + // wait till distributionDataPromise is ready + if ( + distributionDataPromise.pending || + distributionDataPromise.value == null + ) return undefined; if ( @@ -657,8 +653,8 @@ function HistogramViz(props: VisualizationProps) { computation.descriptor.type, overlayEntity, facetEntity, - getDistributionData.pending, - getDistributionData.value, + distributionDataPromise.pending, + distributionDataPromise.value, ]) ); @@ -670,26 +666,29 @@ function HistogramViz(props: VisualizationProps) { // which will result in correct min/max value for multiple filters // More specifically, data-based min and summary-based max are correct values const dataBasedIndependentAxisMinMax = useMemo(() => { - return histogramDefaultIndependentAxisMinMax(getDistributionData); - }, [getDistributionData]); + return histogramDefaultIndependentAxisMinMax(distributionDataPromise); + }, [distributionDataPromise]); const summaryBasedIndependentAxisMinMax = useMemo(() => { - if (getDistributionData.value != null) + if ( + !distributionDataPromise.pending && + distributionDataPromise.value != null + ) return { min: DateVariable.is(xAxisVariable) ? ( - (getDistributionData?.value?.series[0]?.summary?.min as string) ?? - '' + (distributionDataPromise?.value?.series[0]?.summary + ?.min as string) ?? '' ).split('T')[0] - : getDistributionData?.value?.series[0]?.summary?.min, + : distributionDataPromise?.value?.series[0]?.summary?.min, max: DateVariable.is(xAxisVariable) ? ( - (getDistributionData?.value?.series[0]?.summary?.max as string) ?? - '' + (distributionDataPromise?.value?.series[0]?.summary + ?.max as string) ?? '' ).split('T')[0] - : getDistributionData?.value?.series[0]?.summary?.max, + : distributionDataPromise?.value?.series[0]?.summary?.max, }; - }, [getDistributionData]); + }, [distributionDataPromise]); const independentAxisMinMax = useMemo(() => { return { @@ -702,7 +701,7 @@ function HistogramViz(props: VisualizationProps) { summaryBasedIndependentAxisMinMax?.max, ]), }; - }, [getDistributionData]); + }, [distributionDataPromise]); // Note: defaultIndependentRange in the Histogram Viz should keep its initial range // regardless of the change of the data to ensure the truncation behavior From ac8883b45c4dba82da485a0983ed037636192fb7 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Mon, 7 Aug 2023 16:55:16 -0400 Subject: [PATCH 47/56] Require markerBodyOpacity and clean up comments --- packages/libs/components/src/plots/VolcanoPlot.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 3873ae07ff..752952a79c 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -65,7 +65,7 @@ export interface VolcanoPlotProps { /** Title of the plot */ plotTitle?: string; /** marker fill opacity: range from 0 to 1 */ - markerBodyOpacity?: number; + markerBodyOpacity: number; /** Truncation bar fill color. If no color provided, truncation bars will be filled with a black and white pattern */ truncationBarFill?: string; /** container name */ @@ -120,8 +120,8 @@ function TruncationRectangle(props: TruncationRectangleProps) { function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { const { data = EmptyVolcanoPlotData, - independentAxisRange, // not yet implemented - expect this to be set by user - dependentAxisRange, // not yet implemented - expect this to be set by user + independentAxisRange, + dependentAxisRange, significanceThreshold, log2FoldChangeThreshold, markerBodyOpacity, @@ -197,7 +197,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { style={{ ...containerStyles, position: 'relative' }} >
{/* The XYChart takes care of laying out the chart elements (children) appropriately. @@ -303,7 +303,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { {/* Wrapping in a group in order to change the opacity of points. The GlyphSeries is somehow a bunch of glyphs which are so there should be a way to pass opacity down to those elements, but I haven't found it yet */} - + Date: Mon, 7 Aug 2023 16:57:07 -0400 Subject: [PATCH 48/56] Move color definition for opacity slider to Slider component --- .../components/src/components/widgets/Slider.tsx | 11 +++++++++++ .../implementations/ScatterplotVisualization.tsx | 14 ++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/libs/components/src/components/widgets/Slider.tsx b/packages/libs/components/src/components/widgets/Slider.tsx index 1af6d64a99..f61b8d01e2 100644 --- a/packages/libs/components/src/components/widgets/Slider.tsx +++ b/packages/libs/components/src/components/widgets/Slider.tsx @@ -8,6 +8,17 @@ import { DARK_GRAY, LIGHT_GRAY, MEDIUM_GRAY } from '../../constants/colors'; import { debounce } from 'lodash'; import { NumberOrDate } from '../../types/general'; +// a color spec shared among plot components that implements the track gradient +export const plotsSliderOpacityGradientColorSpec: SliderWidgetProps['colorSpec'] = + { + type: 'gradient', + tooltip: '#aaa', + knobColor: '#aaa', + // normal slider color: e.g., from 0 to 1 + trackGradientStart: '#fff', + trackGradientEnd: '#000', + }; + export type SliderWidgetProps = { /** The minimum value of the slider. */ minimum?: number; diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx index cab427ae73..3ef6ec0cd8 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx @@ -138,7 +138,7 @@ import { ResetButtonCoreUI } from '../../ResetButton'; // add Slider and SliderWidgetProps import SliderWidget, { - SliderWidgetProps, + plotsSliderOpacityGradientColorSpec, } from '@veupathdb/components/lib/components/widgets/Slider'; import { FloatingScatterplotExtraProps } from '../../../../map/analysis/hooks/plugins/scatterplot'; @@ -166,16 +166,6 @@ const modalPlotContainerStyles = { margin: 'auto', }; -// implement gradient color for slider opacity -export const colorSpecProps: SliderWidgetProps['colorSpec'] = { - type: 'gradient', - tooltip: '#aaa', - knobColor: '#aaa', - // normal slider color: e.g., from 0 to 1 - trackGradientStart: '#fff', - trackGradientEnd: '#000', -}; - // slider settings const markerBodyOpacityContainerStyles = { height: '4em', @@ -1626,7 +1616,7 @@ function ScatterplotViz(props: VisualizationProps) { containerStyles={markerBodyOpacityContainerStyles} showLimits={true} label={'Marker opacity'} - colorSpec={colorSpecProps} + colorSpec={plotsSliderOpacityGradientColorSpec} /> {/* axis range control UIs */} From b18884adcfca52b5d54d4522a99168a7488451d1 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Mon, 7 Aug 2023 16:58:10 -0400 Subject: [PATCH 49/56] Tweak plot control layout and do some clean up --- .../VolcanoPlotVisualization.tsx | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx index 029a1924df..0b946fcb3a 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx @@ -43,14 +43,16 @@ import { NumberOrDateRange, NumberRange } from '../../../types/general'; import { max, min } from 'lodash'; // plot controls -import SliderWidget from '@veupathdb/components/lib/components/widgets/Slider'; -import { colorSpecProps } from './ScatterplotVisualization'; +import SliderWidget, { + plotsSliderOpacityGradientColorSpec, +} from '@veupathdb/components/lib/components/widgets/Slider'; import { ResetButtonCoreUI } from '../../ResetButton'; import AxisRangeControl from '@veupathdb/components/lib/components/plotControls/AxisRangeControl'; // end imports const DEFAULT_SIG_THRESHOLD = 0.05; const DEFAULT_FC_THRESHOLD = 2; +const DEFAULT_MARKER_OPACITY = 0.7; /** * The padding ensures we don't clip off part of the glyphs that represent the most extreme points. * We could have also used d3.scale.nice but then we dont have precise control of where the extremes @@ -80,7 +82,7 @@ function createDefaultConfig(): VolcanoPlotConfig { return { log2FoldChangeThreshold: DEFAULT_FC_THRESHOLD, significanceThreshold: DEFAULT_SIG_THRESHOLD, - markerBodyOpacity: 0.5, + markerBodyOpacity: DEFAULT_MARKER_OPACITY, independentAxisRange: undefined, dependentAxisRange: undefined, }; @@ -103,7 +105,7 @@ interface Options // Volcano Plot Visualization // The volcano plot visualization takes no input variables. The received data populates all parts of the plot. // The user can control the threshold lines, which affect the marker colors. Additional controls -// will include axis ranges. +// include axis ranges and marker opacity slider. function VolcanoPlotViz(props: VisualizationProps) { const { options, @@ -325,7 +327,9 @@ function VolcanoPlotViz(props: VisualizationProps) { * Since we are rendering a single point in order to display an empty viz, let's hide the data point * by setting the marker opacity to 0 when data.value doesn't exist */ - markerBodyOpacity: data.value ? vizConfig.markerBodyOpacity ?? 0.5 : 0, + markerBodyOpacity: data.value + ? vizConfig.markerBodyOpacity ?? DEFAULT_MARKER_OPACITY + : 0, containerStyles: plotContainerStyles, /** * Let's not display comparisonLabels before we have data for the viz. This prevents what may be @@ -346,23 +350,8 @@ function VolcanoPlotViz(props: VisualizationProps) { // @ts-ignore const plotNode = ; - // TODO const controlsNode = ( -
- { - updateVizConfig({ markerBodyOpacity: newValue }); - }} - containerStyles={{ width: '20em' }} - showLimits={true} - label={'Marker opacity'} - colorSpec={colorSpecProps} - /> +
) { step={0.01} />
- {/** vertical line to separate x from y range controls*/} + {/** vertical line to separate x from y range controls */}
) { />
+ { + updateVizConfig({ markerBodyOpacity: newValue }); + }} + containerStyles={{ width: '20em', marginTop: '1.5em' }} + showLimits={true} + label={'Marker opacity'} + colorSpec={plotsSliderOpacityGradientColorSpec} + />
); From aacff447ccd201c04b5446558c091c7a6437293b Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Tue, 8 Aug 2023 00:24:41 -0400 Subject: [PATCH 50/56] Refactor useStandaloneMapMarkers --- .../analysis/hooks/standaloneMapMarkers.tsx | 320 +++++++++++------- 1 file changed, 200 insertions(+), 120 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index cdb5df6193..64044c746d 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -445,131 +445,36 @@ export function useStandaloneMapMarkers( * and create markers. */ const finalMarkersData = useMemo(() => { - return rawPromise.value?.rawMarkersData.mapElements.map( - ({ - geoAggregateValue, - entityCount, - avgLat, - avgLon, - minLat, - minLon, - maxLat, - maxLon, - ...otherProps - }) => { - const bounds = { - southWest: { lat: minLat, lng: minLon }, - northEast: { lat: maxLat, lng: maxLon }, - }; - const position = { lat: avgLat, lng: avgLon }; - const overlayValues = - 'overlayValues' in otherProps ? otherProps.overlayValues : undefined; - - const donutData = - vocabulary && overlayValues && overlayValues.length - ? overlayValues.map(({ binLabel, value }) => ({ - label: binLabel, - value: value, - color: - overlayType === 'categorical' - ? ColorPaletteDefault[vocabulary.indexOf(binLabel)] - : gradientSequentialColorscaleMap( - vocabulary.length > 1 - ? vocabulary.indexOf(binLabel) / - (vocabulary.length - 1) - : 0.5 - ), - })) - : []; - - // TO DO: address diverging colorscale (especially if there are use-cases) - - // now reorder the data, adding zeroes if necessary. - const reorderedData = - vocabulary != null - ? vocabulary.map( - ( - overlayLabel // overlay label can be 'female' or a bin label '(0,100]' - ) => - donutData.find(({ label }) => label === overlayLabel) ?? { - label: fixLabelForOtherValues(overlayLabel), - value: 0, - } - ) - : // however, if there is no overlay data - // provide a simple entity count marker in the palette's first colour - [ - { - label: 'unknown', - value: entityCount, - color: '#333', - }, - ]; - - const count = - vocabulary != null && overlayValues // if there's an overlay (all expected use cases) - ? overlayValues - .filter(({ binLabel }) => vocabulary.includes(binLabel)) - .reduce((sum, { count }) => (sum = sum + count), 0) - : entityCount; // fallback if not - - const commonMarkerProps = { - id: geoAggregateValue, - key: geoAggregateValue, - bounds: bounds, - position: position, - duration: defaultAnimationDuration, - }; - - switch (markerType) { - case 'pie': { - return { - ...commonMarkerProps, - data: reorderedData, - markerLabel: kFormatter(count), - } as DonutMarkerProps; - } - case 'bubble': { - const bubbleData = { - value: entityCount, - diameter: bubbleValueToDiameterMapper?.(entityCount) ?? 0, - colorValue: - 'overlayValue' in otherProps - ? otherProps.overlayValue - : undefined, - color: - ('overlayValue' in otherProps && - bubbleValueToColorMapper?.(otherProps.overlayValue)) || - undefined, - }; - - return { - ...commonMarkerProps, - data: bubbleData, - markerLabel: String(entityCount), - } as BubbleMarkerProps; - } - default: { - return { - ...commonMarkerProps, - data: reorderedData, - markerLabel: mFormatter(count), - dependentAxisRange: defaultDependentAxisRange, - dependentAxisLogScale, - } as ChartMarkerProps; - } - } - } - ); + if (rawPromise.value == null) return undefined; + + return markerType === 'bubble' + ? processRawBubblesData( + (rawPromise.value.rawMarkersData as StandaloneMapBubblesResponse) + .mapElements, + (props.overlayConfig as BubbleOverlayConfig | undefined) + ?.aggregationConfig, + bubbleValueToDiameterMapper, + bubbleValueToColorMapper + ) + : processRawMarkersData( + (rawPromise.value.rawMarkersData as StandaloneMapMarkersResponse) + .mapElements, + markerType, + defaultDependentAxisRange, + dependentAxisLogScale, + vocabulary, + overlayType + ); }, [ - rawPromise.value?.rawMarkersData.mapElements, - vocabulary, - markerType, - overlayType, bubbleValueToColorMapper, bubbleValueToDiameterMapper, defaultDependentAxisRange, dependentAxisLogScale, + markerType, + overlayType, + props.overlayConfig, + rawPromise.value, + vocabulary, ]); /** @@ -622,6 +527,181 @@ export function useStandaloneMapMarkers( }; } +const processRawMarkersData = ( + mapElements: StandaloneMapMarkersResponse['mapElements'], + markerType: 'count' | 'proportion' | 'pie', + defaultDependentAxisRange: NumberRange, + dependentAxisLogScale: boolean, + vocabulary?: string[], + overlayType?: 'categorical' | 'continuous' +) => { + return mapElements.map( + ({ + geoAggregateValue, + entityCount, + avgLat, + avgLon, + minLat, + minLon, + maxLat, + maxLon, + overlayValues, + }) => { + const { bounds, position } = getBoundsAndPosition( + minLat, + minLon, + maxLat, + maxLon, + avgLat, + avgLon + ); + + const donutData = + vocabulary && overlayValues && overlayValues.length + ? overlayValues.map(({ binLabel, value }) => ({ + label: binLabel, + value: value, + color: + overlayType === 'categorical' + ? ColorPaletteDefault[vocabulary.indexOf(binLabel)] + : gradientSequentialColorscaleMap( + vocabulary.length > 1 + ? vocabulary.indexOf(binLabel) / (vocabulary.length - 1) + : 0.5 + ), + })) + : []; + + // TO DO: address diverging colorscale (especially if there are use-cases) + + // now reorder the data, adding zeroes if necessary. + const reorderedData = + vocabulary != null + ? vocabulary.map( + ( + overlayLabel // overlay label can be 'female' or a bin label '(0,100]' + ) => + donutData.find(({ label }) => label === overlayLabel) ?? { + label: fixLabelForOtherValues(overlayLabel), + value: 0, + } + ) + : // however, if there is no overlay data + // provide a simple entity count marker in the palette's first colour + [ + { + label: 'unknown', + value: entityCount, + color: '#333', + }, + ]; + + const count = + vocabulary != null && overlayValues // if there's an overlay (all expected use cases) + ? overlayValues + .filter(({ binLabel }) => vocabulary.includes(binLabel)) + .reduce((sum, { count }) => (sum = sum + count), 0) + : entityCount; // fallback if not + + const commonMarkerProps = { + data: reorderedData, + id: geoAggregateValue, + key: geoAggregateValue, + bounds, + position, + duration: defaultAnimationDuration, + }; + + switch (markerType) { + case 'pie': { + return { + ...commonMarkerProps, + markerLabel: kFormatter(count), + } as DonutMarkerProps; + } + default: { + return { + ...commonMarkerProps, + markerLabel: mFormatter(count), + dependentAxisRange: defaultDependentAxisRange, + dependentAxisLogScale, + } as ChartMarkerProps; + } + } + } + ); +}; + +const processRawBubblesData = ( + mapElements: StandaloneMapBubblesResponse['mapElements'], + aggregationConfig?: BubbleOverlayConfig['aggregationConfig'], + bubbleValueToDiameterMapper?: (value: number) => number, + bubbleValueToColorMapper?: (value: number) => string +) => { + return mapElements.map( + ({ + geoAggregateValue, + entityCount, + avgLat, + avgLon, + minLat, + minLon, + maxLat, + maxLon, + overlayValue, + }) => { + const { bounds, position } = getBoundsAndPosition( + minLat, + minLon, + maxLat, + maxLon, + avgLat, + avgLon + ); + + // TO DO: address diverging colorscale (especially if there are use-cases) + + const bubbleData = { + value: entityCount, + diameter: bubbleValueToDiameterMapper?.(entityCount) ?? 0, + colorValue: overlayValue, + colorLabel: aggregationConfig + ? aggregationConfig.overlayType === 'continuous' + ? aggregationConfig.aggregator + : 'Proportion' + : undefined, + color: bubbleValueToColorMapper?.(overlayValue) || undefined, + }; + + return { + id: geoAggregateValue, + key: geoAggregateValue, + bounds, + position, + duration: defaultAnimationDuration, + data: bubbleData, + markerLabel: String(entityCount), + } as BubbleMarkerProps; + } + ); +}; + +const getBoundsAndPosition = ( + minLat: number, + minLon: number, + maxLat: number, + maxLon: number, + avgLat: number, + avgLon: number +) => { + const bounds = { + southWest: { lat: minLat, lng: minLon }, + northEast: { lat: maxLat, lng: maxLon }, + }; + const position = { lat: avgLat, lng: avgLon }; + return { bounds, position }; +}; + function fixLabelForOtherValues(input: string): string { return input === UNSELECTED_TOKEN ? UNSELECTED_DISPLAY_TEXT : input; } From 835d820f4ea5ff5638d3ad656244a185d07cdafd Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Tue, 8 Aug 2023 00:41:08 -0400 Subject: [PATCH 51/56] Fix issues when min and max bubble values are equal --- .../analysis/hooks/standaloneMapMarkers.tsx | 66 ++++++++++++++++--- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 64044c746d..68d3e7792a 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -396,14 +396,60 @@ export function useStandaloneMapMarkers( const vocabulary = rawPromise.value?.vocabulary; const bubbleLegendData = rawPromise.value?.bubbleLegendData; + const adjustedSizeData = useMemo( + () => + bubbleLegendData && + bubbleLegendData.minSizeValue === bubbleLegendData.maxSizeValue + ? { + minSizeValue: 0, + maxSizeValue: bubbleLegendData.maxSizeValue || 1, + } + : undefined, + [bubbleLegendData] + ); + const adjustedColorData = useMemo( + () => + bubbleLegendData && + bubbleLegendData.minColorValue === bubbleLegendData.maxColorValue + ? bubbleLegendData.maxColorValue >= 0 + ? { + minColorValue: 0, + maxColorValue: bubbleLegendData.maxColorValue || 1, + } + : { + minColorValue: bubbleLegendData.minColorValue, + maxColorValue: 0, + } + : undefined, + [bubbleLegendData] + ); + const adjustedBubbleLegendData = useMemo( + () => + bubbleLegendData + ? { + ...bubbleLegendData, + ...adjustedSizeData, + ...adjustedColorData, + } + : undefined, + [adjustedColorData, adjustedSizeData, bubbleLegendData] + ); + const bubbleValueToDiameterMapper = useMemo( () => - markerType === 'bubble' && bubbleLegendData + markerType === 'bubble' && adjustedBubbleLegendData ? (value: number) => { // const largestCircleArea = 9000; const largestCircleDiameter = 90; const smallestCircleDiameter = 10; + if ( + adjustedBubbleLegendData.minSizeValue === + adjustedBubbleLegendData.maxSizeValue + ) { + return (largestCircleDiameter + smallestCircleDiameter) / 2; + } + // Area scales directly with value // const constant = largestCircleArea / maxOverlayCount; // const area = value * constant; @@ -417,27 +463,29 @@ export function useStandaloneMapMarkers( // y = mx + b, m = (y2 - y1) / (x2 - x1), b = y1 - m * x1 const m = (largestCircleDiameter - smallestCircleDiameter) / - (bubbleLegendData.maxSizeValue - bubbleLegendData.minSizeValue); + (adjustedBubbleLegendData.maxSizeValue - + adjustedBubbleLegendData.minSizeValue); const b = - smallestCircleDiameter - m * bubbleLegendData.minSizeValue; + smallestCircleDiameter - + m * adjustedBubbleLegendData.minSizeValue; const diameter = m * value + b; // return 2 * radius; return diameter; } : undefined, - [bubbleLegendData, markerType] + [adjustedBubbleLegendData, markerType] ); const bubbleValueToColorMapper = useMemo( () => - markerType === 'bubble' && bubbleLegendData + markerType === 'bubble' && adjustedBubbleLegendData ? getValueToGradientColorMapper( - bubbleLegendData.minColorValue, - bubbleLegendData.maxColorValue + adjustedBubbleLegendData.minColorValue, + adjustedBubbleLegendData.maxColorValue ) : undefined, - [bubbleLegendData, markerType] + [adjustedBubbleLegendData, markerType] ); /** @@ -519,7 +567,7 @@ export function useStandaloneMapMarkers( totalVisibleWithOverlayEntityCount: countSum, totalVisibleEntityCount, legendItems, - bubbleLegendData, + bubbleLegendData: adjustedBubbleLegendData, bubbleValueToDiameterMapper, bubbleValueToColorMapper, pending: rawPromise.pending, From ef1580f6d0c78989115916b578afb90e4e8fa93f Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Tue, 8 Aug 2023 00:55:17 -0400 Subject: [PATCH 52/56] Add rest of bubble popup overlay label logic --- .../libs/components/src/map/BubbleMarker.tsx | 18 +++++++++++------- .../analysis/hooks/standaloneMapMarkers.tsx | 3 ++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/libs/components/src/map/BubbleMarker.tsx b/packages/libs/components/src/map/BubbleMarker.tsx index 6bc9e13478..15776da395 100755 --- a/packages/libs/components/src/map/BubbleMarker.tsx +++ b/packages/libs/components/src/map/BubbleMarker.tsx @@ -6,11 +6,13 @@ import { ContainerStylesAddon } from '../types/plots'; export interface BubbleMarkerProps extends BoundsDriftMarkerProps { data: { - // The size value + /* The size value */ value: number; diameter: number; - // The color value + /* The color value (shown in the popup) */ colorValue?: number; + /* Label shown next to the color value in the popup */ + colorLabel?: string; color?: string; }; // isAtomic: add a special thumbtack icon if this is true @@ -37,13 +39,15 @@ export default function BubbleMarker(props: BubbleMarkerProps) { const popupContent = (
-
- Count {props.data.value} -
- Color value{' '} - {props.data.colorValue} + Count {props.data.value}
+ {props.data.colorValue && ( +
+ {props.data.colorLabel}{' '} + {props.data.colorValue} +
+ )}
); diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 68d3e7792a..0744459532 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -35,6 +35,7 @@ import { DonutMarkerProps } from '@veupathdb/components/lib/map/DonutMarker'; import { ChartMarkerProps } from '@veupathdb/components/lib/map/ChartMarker'; import { BubbleMarkerProps } from '@veupathdb/components/lib/map/BubbleMarker'; import { validateProportionValues } from '../MarkerConfiguration/BubbleMarkerConfigurationMenu'; +import _ from 'lodash'; /** * We can use this viewport to request all available data @@ -715,7 +716,7 @@ const processRawBubblesData = ( colorValue: overlayValue, colorLabel: aggregationConfig ? aggregationConfig.overlayType === 'continuous' - ? aggregationConfig.aggregator + ? _.capitalize(aggregationConfig.aggregator) : 'Proportion' : undefined, color: bubbleValueToColorMapper?.(overlayValue) || undefined, From 74cfc94f4c35def503ae505629fa979191f0ac77 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Tue, 8 Aug 2023 00:56:41 -0400 Subject: [PATCH 53/56] Simplify some bits of code --- .../components/plotControls/PlotBubbleLegend.tsx | 2 -- .../src/stories/plotControls/PlotLegend.stories.tsx | 1 - .../libs/eda/src/lib/map/analysis/MapAnalysis.tsx | 1 - .../lib/map/analysis/hooks/standaloneMapMarkers.tsx | 13 ++++++------- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx index 4f06baa798..d52ba6414d 100644 --- a/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx +++ b/packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx @@ -4,7 +4,6 @@ import _ from 'lodash'; // set props for custom legend function export interface PlotLegendBubbleProps { - legendMin: number; legendMax: number; valueToDiameterMapper: ((value: number) => number) | undefined; } @@ -17,7 +16,6 @@ export interface PlotLegendBubbleProps { // }; export default function PlotBubbleLegend({ - legendMin, legendMax, valueToDiameterMapper, }: PlotLegendBubbleProps) { diff --git a/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx b/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx index e315a49ff7..fa0f372f08 100755 --- a/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx +++ b/packages/libs/components/src/stories/plotControls/PlotLegend.stories.tsx @@ -556,7 +556,6 @@ export const BubbleMarkerLegend = () => {
diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index 5ab4517168..c750a27a1a 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -1269,7 +1269,6 @@ function MapAnalysisImpl(props: ImplProps) { isLoading={pending} plotLegendProps={{ type: 'bubble', - legendMin: bubbleLegendData?.minSizeValue ?? 0, legendMax: bubbleLegendData?.maxSizeValue ?? 0, valueToDiameterMapper: bubbleValueToDiameterMapper, }} diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 0744459532..146be4ec61 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -719,7 +719,7 @@ const processRawBubblesData = ( ? _.capitalize(aggregationConfig.aggregator) : 'Proportion' : undefined, - color: bubbleValueToColorMapper?.(overlayValue) || undefined, + color: bubbleValueToColorMapper?.(overlayValue), }; return { @@ -742,14 +742,13 @@ const getBoundsAndPosition = ( maxLon: number, avgLat: number, avgLon: number -) => { - const bounds = { +) => ({ + bounds: { southWest: { lat: minLat, lng: minLon }, northEast: { lat: maxLat, lng: maxLon }, - }; - const position = { lat: avgLat, lng: avgLon }; - return { bounds, position }; -}; + }, + position: { lat: avgLat, lng: avgLon }, +}); function fixLabelForOtherValues(input: string): string { return input === UNSELECTED_TOKEN ? UNSELECTED_DISPLAY_TEXT : input; From 551eb20d27b941f8facca6f02a97d02356621b68 Mon Sep 17 00:00:00 2001 From: Dave Falke Date: Tue, 8 Aug 2023 10:14:35 -0400 Subject: [PATCH 54/56] Add non-success response logging to `FetchClient` (#406) * Add callback option for non-success responses * Remove unused module * Add static method to set onNonSuccessResponse handler --- packages/libs/http-utils/src/FetchClient.ts | 38 +++- packages/libs/web-common/src/bootstrap.js | 6 + packages/libs/web-common/src/util/api.ts | 196 -------------------- 3 files changed, 42 insertions(+), 198 deletions(-) delete mode 100644 packages/libs/web-common/src/util/api.ts diff --git a/packages/libs/http-utils/src/FetchClient.ts b/packages/libs/http-utils/src/FetchClient.ts index 779a41a77e..c305aebef0 100644 --- a/packages/libs/http-utils/src/FetchClient.ts +++ b/packages/libs/http-utils/src/FetchClient.ts @@ -37,12 +37,25 @@ export interface FetchApiOptions { init?: RequestInit; /** Implementation of `fetch` function. Defaults to `window.fetch`. */ fetchApi?: Window['fetch']; + /** + * Callback that can be used for reporting errors. A Promise rejection will + * still occur. + */ + onNonSuccessResponse?: (error: Error) => void; +} + +class FetchClientError extends Error { + name = 'FetchClientError'; } export abstract class FetchClient { + /** Default callback used, if none is specified to constructor. */ + private static onNonSuccessResponse: FetchApiOptions['onNonSuccessResponse']; + protected readonly baseUrl: string; protected readonly init: RequestInit; protected readonly fetchApi: Window['fetch']; + protected readonly onNonSuccessResponse: FetchApiOptions['onNonSuccessResponse']; // Subclasses can set this to false to disable including a traceparent header with all requests. protected readonly includeTraceidHeader: boolean = true; @@ -50,6 +63,23 @@ export abstract class FetchClient { this.baseUrl = options.baseUrl; this.init = options.init ?? {}; this.fetchApi = options.fetchApi ?? window.fetch; + this.onNonSuccessResponse = + options.onNonSuccessResponse ?? FetchClient.onNonSuccessResponse; + } + + /** + * Set a default callback for all instances. Should only be called once. + */ + public static setOnNonSuccessResponse( + callback: FetchApiOptions['onNonSuccessResponse'] + ) { + if (this.onNonSuccessResponse) { + console.warn( + 'FetchClient.setOnNonSuccessResponse() should only be called once.' + ); + return; + } + this.onNonSuccessResponse = callback; } protected async fetch(apiRequest: ApiRequest): Promise { @@ -74,9 +104,13 @@ export abstract class FetchClient { return await transformResponse(responseBody); } - throw new Error( - `${response.status} ${response.statusText}${'\n'}${await response.text()}` + const fetchError = new FetchClientError( + `${request.method.toUpperCase()} ${request.url}: ${response.status} ${ + response.statusText + }${'\n'}${await response.text()}` ); + this.onNonSuccessResponse?.(fetchError); + throw fetchError; } } diff --git a/packages/libs/web-common/src/bootstrap.js b/packages/libs/web-common/src/bootstrap.js index 1a038707d6..5ebdeb64ab 100644 --- a/packages/libs/web-common/src/bootstrap.js +++ b/packages/libs/web-common/src/bootstrap.js @@ -16,6 +16,7 @@ import { debounce, identity, uniq, flow } from 'lodash'; // TODO Remove auth_tkt from url before proceeding +import { FetchClient } from '@veupathdb/http-utils'; import { initialize as initializeWdk_ } from '@veupathdb/wdk-client/lib/Core/main'; import * as WdkComponents from '@veupathdb/wdk-client/lib/Components'; import * as WdkControllers from '@veupathdb/wdk-client/lib/Controllers'; @@ -94,6 +95,11 @@ export function initialize(options = {}) { context.store.dispatch(loadSiteConfig(siteConfig)); + // Add non-success response handler for FetchClient instances + FetchClient.setOnNonSuccessResponse((error) => { + context.wdkService.submitError(error); + }); + return context; } diff --git a/packages/libs/web-common/src/util/api.ts b/packages/libs/web-common/src/util/api.ts deleted file mode 100644 index ea054617d2..0000000000 --- a/packages/libs/web-common/src/util/api.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { mapValues, compose } from 'lodash/fp'; -import { - Decoder, - standardErrorReport, -} from '@veupathdb/wdk-client/lib/Utils/Json'; - -/* - * An "Api" is an abstraction for interacting with resources. - * - * There are two primary interfaces: `ApiRequest` and `ApiRequestHandler`. - * - * An `ApiRequest` represents a HTTP-like request for a resource. - * - * An `ApiRequestHandler` represents an implentation that can handle a request. - * Typically this will be based on the `fetch` API. - */ - -/** - * Represents an HTTP-like request for a resource. - */ -export interface ApiRequest { - /** Path to resource, relative to a fixed base url. */ - path: string; - /** Request method for resource. */ - method: string; - /** Body of request */ - body?: any; - /** Headers to add to the request. */ - headers?: Record; - /** Transform response body. This is a good place to do validation. */ - transformResponse: (body: unknown) => Promise; -} - -export interface ApiRequestCreator { - (...args: U): ApiRequest; -} - -export interface ApiRequestsObject { - [Key: string]: ApiRequestCreator; -} - -type ApiRequestToBound> = - R extends ApiRequestCreator - ? (...args: U) => Promise - : never; - -export type BoundApiRequestsObject = { - [P in keyof T]: T[P] extends ApiRequestCreator - ? (...args: B) => Promise - : never; -}; - -export function bindApiRequestCreators( - requestCreators: T, - handler: ApiRequestHandler -): BoundApiRequestsObject { - return mapValues( - (requestCreator) => compose(handler, requestCreator), - requestCreators - ) as BoundApiRequestsObject; -} - -// XXX Not sure if these belong here, since they are specific to an ApiRequestHandler - -/** Helper to create a request with a JSON body. */ -export function createJsonRequest(init: ApiRequest): ApiRequest { - return { - ...init, - body: JSON.stringify(init.body), - headers: { - ...init.headers, - 'Content-Type': 'application/json', - }, - }; -} - -/** Helper to create a request with a plain text body. */ -export function createPlainTextRequest(init: ApiRequest): ApiRequest { - return { - ...init, - headers: { - ...init.headers, - 'Content-Type': 'text/plain', - }, - }; -} - -/** Standard transformer that uses a `Json.ts` `decoder` type. */ -export function standardTransformer(decoder: Decoder) { - return async function transform(body: unknown): Promise { - const result = decoder(body); - if (result.status === 'ok') return result.value; - const report = `Expected ${result.expected}${ - result.context ? 'at _' + result.context : '' - }, but got ${JSON.stringify(result.value)}.`; - throw new Error('Could not decode response.\n' + report); - }; -} - -/** - * A function that takes an `ApiRequest` and returns a `Promise`. - */ -export interface ApiRequestHandler { - (request: ApiRequest): Promise; -} - -/** - * Options for a `fetch`-based request handler. - */ -export interface FetchApiOptions { - /** Base url for service endpoint. */ - baseUrl: string; - /** Global optoins for all requests. */ - init?: RequestInit; - /** Implementation of `fetch` function. Defaults to `window.fetch`. */ - fetchApi?: Window['fetch']; -} - -/** - * A `fetch`-based implentation of an `ApiRequestHandler`. - */ -export function createFetchApiRequestHandler( - options: FetchApiOptions -): ApiRequestHandler { - const { baseUrl, init = {}, fetchApi = window.fetch } = options; - return async function fetchApiRequestHandler( - apiRequest: ApiRequest - ): Promise { - const { transformResponse, path, body, ...restReq } = apiRequest; - const request = new Request(baseUrl + path, { - ...init, - ...restReq, - body: body, - headers: { - ...restReq.headers, - ...init.headers, - }, - }); - const response = await fetchApi(request); - // TODO Make this behavior configurable - if (response.ok) { - const responseBody = await fetchResponseBody(response); - - return await transformResponse(responseBody); - } - throw new Error( - `${response.status} ${response.statusText}${'\n'}${await response.text()}` - ); - }; -} - -export abstract class FetchClient { - protected readonly baseUrl: string; - protected readonly init: RequestInit; - protected readonly fetchApi: Window['fetch']; - - constructor(options: FetchApiOptions) { - this.baseUrl = options.baseUrl; - this.init = options.init ?? {}; - this.fetchApi = options.fetchApi ?? window.fetch; - } - - protected async fetch(apiRequest: ApiRequest): Promise { - const { baseUrl, init, fetchApi } = this; - const { transformResponse, path, body, ...restReq } = apiRequest; - const request = new Request(baseUrl + path, { - ...init, - ...restReq, - body: body, - headers: { - ...restReq.headers, - ...init.headers, - }, - }); - const response = await fetchApi(request); - // TODO Make this behavior configurable - if (response.ok) { - const responseBody = await fetchResponseBody(response); - - return await transformResponse(responseBody); - } - throw new Error( - `${response.status} ${response.statusText}${'\n'}${await response.text()}` - ); - } -} - -async function fetchResponseBody(response: Response) { - const contentType = response.headers.get('Content-Type'); - - return contentType == null - ? undefined - : contentType.startsWith('application/json') - ? response.json() - : response.text(); -} From 0c8b7f9a4382ae764bb78444c015b8e12e3a7a70 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Tue, 8 Aug 2023 11:09:57 -0400 Subject: [PATCH 55/56] Position opacity slider beneath 'Plot controls' subheader and above axis controls --- .../VolcanoPlotVisualization.tsx | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx index 0b946fcb3a..c5b98ce08c 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx @@ -351,7 +351,28 @@ function VolcanoPlotViz(props: VisualizationProps) { const plotNode = ; const controlsNode = ( -
+
+ + { + updateVizConfig({ markerBodyOpacity: newValue }); + }} + containerStyles={{ width: '20em', marginTop: '0.5em' }} + showLimits={true} + label={'Marker opacity'} + colorSpec={plotsSliderOpacityGradientColorSpec} + /> +
) { />
- { - updateVizConfig({ markerBodyOpacity: newValue }); - }} - containerStyles={{ width: '20em', marginTop: '1.5em' }} - showLimits={true} - label={'Marker opacity'} - colorSpec={plotsSliderOpacityGradientColorSpec} - />
); From ff12879f35312a27aaa890ca0b87ad5d142f2b43 Mon Sep 17 00:00:00 2001 From: Connor Howington Date: Tue, 8 Aug 2023 17:17:11 -0400 Subject: [PATCH 56/56] Remove unnecessary bubble size short circuit --- .../src/lib/map/analysis/hooks/standaloneMapMarkers.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 146be4ec61..935b6160a1 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -444,13 +444,6 @@ export function useStandaloneMapMarkers( const largestCircleDiameter = 90; const smallestCircleDiameter = 10; - if ( - adjustedBubbleLegendData.minSizeValue === - adjustedBubbleLegendData.maxSizeValue - ) { - return (largestCircleDiameter + smallestCircleDiameter) / 2; - } - // Area scales directly with value // const constant = largestCircleArea / maxOverlayCount; // const area = value * constant;