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 (
+
+ );
+}
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;