Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bubble markers map type #257

Merged
merged 58 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
3ff0400
preliminary bubble marker component
moontrip May 25, 2023
2991c81
Merge branch 'main' into 95-bubble-marker-component
chowington Jun 8, 2023
e739bb3
Update bubble spec and design
chowington Jun 8, 2023
fc03c3e
Merge branch 'main' into 95-bubble-marker-component
chowington Jun 9, 2023
c207973
Merge branch 'main' into 95-bubble-marker-component
chowington Jun 13, 2023
9306842
Get working bubble markers on map
chowington Jun 14, 2023
3a324ad
Merge branch 'main' into 95-bubble-marker-component
chowington Jun 14, 2023
96ad6e9
Add dedicated bubble marker menu option
chowington Jun 14, 2023
0688eec
Make Bubble Marker Legend
chowington Jun 20, 2023
fb5e693
Merge branch 'main' into 95-bubble-marker-component
chowington Jun 20, 2023
d0a5f21
Add missing stuff related to bubble markers
chowington Jun 20, 2023
910286a
Add bubble marker legend to SAM
chowington Jun 20, 2023
764efb2
Set max bubble marker size
chowington Jun 20, 2023
e50b2e6
Merge branch 'main' into 95-bubble-marker-component
chowington Jun 26, 2023
3d0902a
Add outline to bubble markers and update legend tick calculation
chowington Jun 28, 2023
29b79a5
Fix bubble outline shape and draw smaller bubbles over bigger
chowington Jun 29, 2023
2a6aced
Clean up
chowington Jun 29, 2023
0cccd34
Merge branch 'main' into 95-bubble-marker-component
chowington Jul 5, 2023
e9891fc
Allow analysis backwards compatibility when adding new map types
chowington Jul 5, 2023
dba1a40
Merge branch 'main' into 95-bubble-marker-component
chowington Jul 24, 2023
709d508
Fix bugs from merge
chowington Jul 24, 2023
70f8236
Add aggregation input and use bubble endpoint for data
chowington Jul 25, 2023
8a01b1e
Use correct value for bubble size
chowington Jul 25, 2023
4942c67
Reintroduce appState backwards compatibility logic
chowington Jul 25, 2023
c49a557
Use bubble legend endpoint for bubble sizes
chowington Jul 26, 2023
5d328f3
Color bubbles according to overlayValue and legend endpoint data
chowington Jul 26, 2023
a08bfa1
Add gradient legend to bubble map mode
chowington Jul 28, 2023
fa1703d
Merge branch 'main' into 95-bubble-marker-component
chowington Jul 28, 2023
0c93414
Fix bubble menu icon sizing issue
chowington Jul 28, 2023
a54d45b
Align bubble menu inputs veertically
chowington Jul 28, 2023
de8d421
Improve proportion validation UX
chowington Jul 28, 2023
33b4a04
Improve gradient legend default position
chowington Jul 28, 2023
d5bb5dc
Factor out aggregation inputs in lineplot viz
chowington Jul 31, 2023
94f489b
Use refactored aggregation inputs in bubble menu
chowington Jul 31, 2023
520d88e
Merge branch 'main' into 95-bubble-marker-component
chowington Jul 31, 2023
50bdb5a
Add proportion error message back
chowington Jul 31, 2023
1a528ca
#BringBackThePieLegend >:(
chowington Jul 31, 2023
027de87
Reduce extra whitespace in gradient legend
chowington Jul 31, 2023
799f120
Clean up bubble marker datatype
chowington Aug 1, 2023
fa4d806
Refactor types and clean up PR
chowington Aug 1, 2023
cf437c4
Merge branch 'main' into 95-bubble-marker-component
chowington Aug 1, 2023
0279985
Add bubble popup and remove numbers in bubbles
chowington Aug 2, 2023
790c162
Increase width of size legend slightly
chowington Aug 2, 2023
3a8a3de
updated bubble marker
asizemore Aug 2, 2023
0f84a4b
Merge pull request #400 from VEuPathDB/add-bubble-icon
chowington Aug 2, 2023
91c3306
Change verbiage of bubble popup
chowington Aug 2, 2023
9285535
Add comments
chowington Aug 2, 2023
7db7cd3
Remove clear button from bubble menu variable input
chowington Aug 2, 2023
4e41f15
Use _.get() where possible to shorten expressions
chowington Aug 2, 2023
7d01ede
Change === null to == null to prevent error
chowington Aug 3, 2023
5618004
Fix incorrect bubble config input labels
chowington Aug 7, 2023
cb44ae2
Use sequential colormap when data is sequential
chowington Aug 7, 2023
aacff44
Refactor useStandaloneMapMarkers
chowington Aug 8, 2023
835d820
Fix issues when min and max bubble values are equal
chowington Aug 8, 2023
ef1580f
Add rest of bubble popup overlay label logic
chowington Aug 8, 2023
74cfc94
Simplify some bits of code
chowington Aug 8, 2023
f57339b
Merge branch 'main' into 95-bubble-marker-component
chowington Aug 8, 2023
ff12879
Remove unnecessary bubble size short circuit
chowington Aug 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ dist
# TernJS port file
.tern-port

# VSCode config files
.vscode

.editorconfig

.pnp.*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React from 'react';
import { range } from 'd3';
import _ from 'lodash';

// set props for custom legend function
export interface PlotLegendBubbleProps {
legendMax: number;
valueToDiameterMapper: ((value: number) => number) | undefined;
}

// 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;
// };

export default function PlotBubbleLegend({
legendMax,
valueToDiameterMapper,
}: PlotLegendBubbleProps) {
if (valueToDiameterMapper) {
// Declare constants
const tickFontSize = '0.8em';
// const legendTextSize = '1.0em';
const circleStrokeWidth = 3;
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
? 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 tickLength = largestCircleRadius + 5;

return (
<svg
width={largestCircleDiameter + (tickLength - largestCircleRadius) + 50}
height={largestCircleDiameter + circleStrokeWidth * 2 + padding * 2}
>
{circleValues.map((value, i) => {
const circleDiameter = valueToDiameterMapper(value);
const circleRadius = circleDiameter / 2;
const tickY =
padding +
largestCircleDiameter +
circleStrokeWidth -
circleDiameter;

return (
<>
<circle
cx={padding + largestCircleRadius + circleStrokeWidth}
cy={
padding +
largestCircleDiameter +
circleStrokeWidth -
circleRadius
}
r={circleRadius}
stroke="black"
strokeWidth={circleStrokeWidth}
fill="white"
/>
<g
className="axisTick"
overflow="visible"
key={'gradientTick' + i}
>
<line
x1={padding + largestCircleRadius + circleStrokeWidth + 1}
x2={
padding +
largestCircleRadius +
circleStrokeWidth +
tickLength +
1
}
y1={tickY}
y2={tickY}
stroke="black"
strokeDasharray="2 2"
strokeWidth={2}
/>
<text
x={padding + largestCircleRadius + tickLength + 5}
y={tickY}
dominantBaseline="middle"
fontSize={tickFontSize}
>
{value}
</text>
</g>
</>
);
})}
</svg>
);
} 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ export default function PlotGradientLegend({

return (
<div>
<svg id="gradientLegend" height={gradientBoxHeight + 20} width={150}>
<svg
id="gradientLegend"
height={gradientBoxHeight + 20}
width={gradientBoxWidth + 60}
>
<defs>
<linearGradient id="linearGradient" x1="0" x2="0" y1="1" y2="0">
{stopPoints}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,6 +14,7 @@ export type PlotLegendProps = PlotLegendBaseProps &
(
| ({ type: 'list' } & PlotListLegendProps)
| ({ type: 'colorscale' } & PlotLegendGradientProps)
| ({ type: 'bubble' } & PlotLegendBubbleProps)
);

export default function PlotLegend({
Expand All @@ -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') && (
<div
style={{
display: 'inline-block', // for general usage (e.g., story)
Expand Down Expand Up @@ -63,6 +66,9 @@ export default function PlotLegend({
{type === 'colorscale' && (
<PlotGradientLegend {...(otherProps as PlotLegendGradientProps)} />
)}
{type === 'bubble' && (
<PlotBubbleLegend {...(otherProps as PlotLegendBubbleProps)} />
)}
</div>
)}
</>
Expand Down
2 changes: 2 additions & 0 deletions packages/libs/components/src/map/BoundsDriftMarker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default function BoundsDriftMarker({
showPopup,
popupContent,
popupClass,
zIndexOffset,
}: BoundsDriftMarkerProps) {
const map = useMap();
const boundingBox = new LatLngBounds([
Expand Down Expand Up @@ -300,6 +301,7 @@ export default function BoundsDriftMarker({
mouseout: (e: LeafletMouseEvent) => handleMouseOut(e),
dblclick: handleDoubleClick,
}}
zIndexOffset={zIndexOffset}
{...optionalIconProp}
>
{showPopup && popup}
Expand Down
173 changes: 173 additions & 0 deletions packages/libs/components/src/map/BubbleMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// import React from 'react';
import L from 'leaflet';
import BoundsDriftMarker, { BoundsDriftMarkerProps } from './BoundsDriftMarker';

import { ContainerStylesAddon } from '../types/plots';

export interface BubbleMarkerProps extends BoundsDriftMarkerProps {
data: {
/* The size value */
value: number;
diameter: number;
/* 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
isAtomic?: boolean;
onClick?: (event: L.LeafletMouseEvent) => void | undefined;
}

/**
* this is a SVG bubble marker icon
*/
export default function BubbleMarker(props: BubbleMarkerProps) {
const { html: svgHTML, diameter: size } = bubbleMarkerSVGIcon(props);

// set icon as divIcon
const SVGBubbleIcon = L.divIcon({
chowington marked this conversation as resolved.
Show resolved Hide resolved
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;

const popupContent = (
<div style={{ fontSize: 16, lineHeight: '150%' }}>
<div>
<b style={{ marginRight: '0.15rem' }}>Count</b> {props.data.value}
</div>
{props.data.colorValue && (
<div style={{ marginTop: '0.5rem' }}>
<b style={{ marginRight: '0.15rem' }}>{props.data.colorLabel}</b>{' '}
{props.data.colorValue}
</div>
)}
</div>
);

return (
<BoundsDriftMarker
id={props.id}
position={props.position}
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,
size: {
width: 170,
height: 100,
},
}}
showPopup={props.showPopup}
/>
);
}

type BubbleMarkerStandaloneProps = Omit<
BubbleMarkerProps,
| 'id'
| 'position'
| 'bounds'
| 'onClick'
| 'duration'
| 'showPopup'
| 'popupClass'
| 'popupContent'
> &
ContainerStylesAddon;

export function BubbleMarkerStandalone(props: BubbleMarkerStandaloneProps) {
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 (
<div
style={{
fontSize: '12px',
lineHeight: 1.5,
width: diameter,
height: diameter,
...props.containerStyles,
}}
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}

function bubbleMarkerSVGIcon(props: BubbleMarkerStandaloneProps): {
html: string;
diameter: number;
} {
// const scale = props.markerScale ?? MarkerScaleDefault;
const diameter = props.data.diameter;
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 +=
'<svg width="' +
outlineRadius * 2 +
'" height="' +
outlineRadius * 2 +
'">'; // 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 +=
'<circle cx="' +
outlineRadius +
'" cy="' +
outlineRadius +
'" r="' +
outlineRadius +
'" stroke="green" stroke-width="0" fill="white" />';

// create bubble
svgHTML +=
'<circle cx="' +
outlineRadius +
'" cy="' +
outlineRadius +
'" r="' +
radius +
'" stroke="white" 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 +=
// '<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" opacity="1" fill="white" font-family="Arial, Helvetica, sans-serif" font-weight="bold" font-size="1em">' +
// props.data.value +
// '</text>';

// check isAtomic: draw pushpin if true
if (props.isAtomic) {
let pushPinCode = '&#128392;';
svgHTML +=
'<text x="86%" y="14%" dominant-baseline="middle" text-anchor="middle" opacity="0.75" font-weight="bold" font-size="1.2em">' +
pushPinCode +
'</text>';
}

svgHTML += '</svg>';

return { html: svgHTML, diameter: diameter };
}
2 changes: 2 additions & 0 deletions packages/libs/components/src/map/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface MarkerProps {
height: number;
};
};
/* This offset gets added to the default zIndex */
zIndexOffset?: number;
}

export type AnimationFunction = ({
Expand Down
Loading