-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #257 from VEuPathDB/95-bubble-marker-component
Bubble markers map type
- Loading branch information
Showing
29 changed files
with
1,902 additions
and
581 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -107,6 +107,9 @@ dist | |
# TernJS port file | ||
.tern-port | ||
|
||
# VSCode config files | ||
.vscode | ||
|
||
.editorconfig | ||
|
||
.pnp.* | ||
|
118 changes: 118 additions & 0 deletions
118
packages/libs/components/src/components/plotControls/PlotBubbleLegend.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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({ | ||
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 = '🖈'; | ||
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 }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.