Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into timeslider-honour-big…
Browse files Browse the repository at this point in the history
…-filters
  • Loading branch information
bobular committed Oct 6, 2023
2 parents 952d591 + 2799816 commit 981f21f
Show file tree
Hide file tree
Showing 40 changed files with 1,521 additions and 403 deletions.
27 changes: 22 additions & 5 deletions packages/libs/components/src/map/ChartMarker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@ import {
MarkerScaleDefault,
} from '../types/plots';

export type BaseMarkerData = {
value: number;
label: string;
color?: string;
};

export interface ChartMarkerProps
extends BoundsDriftMarkerProps,
MarkerScaleAddon,
DependentAxisLogScaleAddon {
borderColor?: string;
borderWidth?: number;
data: {
value: number;
label: string;
color?: string;
}[];
data: BaseMarkerData[];
isAtomic?: boolean; // add a special thumbtack icon if this is true (it's a marker that won't disaggregate if zoomed in further)
// changed to dependentAxisRange
dependentAxisRange?: NumberRange | null; // y-axis range for setting global max
Expand Down Expand Up @@ -315,3 +317,18 @@ function chartMarkerSVGIcon(props: ChartMarkerStandaloneProps): {
sumValuesString,
};
}

export function getChartMarkerDependentAxisRange(
data: ChartMarkerProps['data'],
isLogScale: boolean
) {
return {
min: isLogScale
? Math.min(
0.1,
...data.filter(({ value }) => value > 0).map(({ value }) => value)
)
: 0,
max: Math.max(...data.map((d) => d.value)),
};
}
7 changes: 2 additions & 5 deletions packages/libs/components/src/map/DonutMarker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,13 @@ import {
} from '../types/plots';

import { last } from 'lodash';
import { BaseMarkerData } from './ChartMarker';

// ts definition for HistogramMarkerSVGProps: need some adjustment but for now, just use Donut marker one
export interface DonutMarkerProps
extends BoundsDriftMarkerProps,
MarkerScaleAddon {
data: {
value: number;
label: string;
color?: string;
}[];
data: BaseMarkerData[];
// isAtomic: add a special thumbtack icon if this is true
isAtomic?: boolean;
onClick?: (event: L.LeafletMouseEvent) => void | undefined;
Expand Down
187 changes: 187 additions & 0 deletions packages/libs/components/src/plots/BipartiteNetwork.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { BipartiteNetworkData, NodeData } from '../types/plots/network';
import { partition } from 'lodash';
import { LabelPosition, Link, NodeWithLabel } from './Network';
import { Graph } from '@visx/network';
import { Text } from '@visx/text';
import {
CSSProperties,
Ref,
forwardRef,
useImperativeHandle,
useRef,
} from 'react';
import { DEFAULT_CONTAINER_HEIGHT } from './PlotlyPlot';
import Spinner from '../components/Spinner';
import { ToImgopts } from 'plotly.js';
import domToImage from 'dom-to-image';

export interface BipartiteNetworkProps {
/** Bipartite network data */
data: BipartiteNetworkData;
/** Name of column 1 */
column1Name?: string;
/** Name of column 2 */
column2Name?: string;
/** styling for the plot's container */
containerStyles?: CSSProperties;
/** container name */
containerClass?: string;
/** shall we show the loading spinner? */
showSpinner?: boolean;
/** plot width */
width?: number;
}

// The BipartiteNetwork function draws a two-column network using visx. This component handles
// the positioning of each column, and consequently the positioning of nodes and links.
function BipartiteNetwork(
props: BipartiteNetworkProps,
ref: Ref<HTMLDivElement>
) {
const {
data,
column1Name,
column2Name,
containerStyles = { width: '100%', height: DEFAULT_CONTAINER_HEIGHT },
containerClass = 'web-components-plot',
showSpinner = false,
width,
} = props;

// Defaults
// Many of the below can get optional props in the future as we figure out optimal layouts
const DEFAULT_WIDTH = 400;
const DEFAULT_NODE_VERTICAL_SPACE = 30;
const DEFAULT_TOP_PADDING = 40;
const DEFAULT_COLUMN1_X = 100;
const DEFAULT_COLUMN2_X = (width ?? DEFAULT_WIDTH) - DEFAULT_COLUMN1_X;

// Use ref forwarding to enable screenshotting of the plot for thumbnail versions.
const plotRef = useRef<HTMLDivElement>(null);
useImperativeHandle<HTMLDivElement, any>(
ref,
() => ({
// The thumbnail generator makePlotThumbnailUrl expects to call a toImage function
toImage: async (imageOpts: ToImgopts) => {
if (!plotRef.current) throw new Error('Plot not ready');
return domToImage.toPng(plotRef.current, imageOpts);
},
}),
[]
);

// In order to assign coordinates to each node, we'll separate the
// nodes based on their column, then will use their order in the column
// (given by columnXNodeIDs) to finally assign the coordinates.
const nodesByColumn: NodeData[][] = partition(data.nodes, (node) => {
return data.column1NodeIDs.includes(node.id);
});

const nodesByColumnWithCoordinates = nodesByColumn.map(
(column, columnIndex) => {
const columnWithCoordinates = column.map((node) => {
// Find the index of the node in the column
type ColumnName = keyof typeof data;
const columnName = ('column' +
(columnIndex + 1) +
'NodeIDs') as ColumnName;
const indexInColumn = data[columnName].findIndex(
(id) => id === node.id
);

return {
// columnIndex of 0 refers to the left-column nodes whereas 1 refers to right-column nodes
x: columnIndex === 0 ? DEFAULT_COLUMN1_X : DEFAULT_COLUMN2_X,
y: DEFAULT_TOP_PADDING + DEFAULT_NODE_VERTICAL_SPACE * indexInColumn,
labelPosition:
columnIndex === 0 ? 'left' : ('right' as LabelPosition),
...node,
};
});
return columnWithCoordinates;
}
);

// Assign coordinates to links based on the newly created node coordinates
const linksWithCoordinates = data.links.map((link) => {
const sourceNode = nodesByColumnWithCoordinates[0].find(
(node) => node.id === link.source.id
);
const targetNode = nodesByColumnWithCoordinates[1].find(
(node) => node.id === link.target.id
);
return {
...link,
source: {
x: sourceNode?.x,
y: sourceNode?.y,
...link.source,
},
target: {
x: targetNode?.x,
y: targetNode?.y,
...link.target,
},
};
});

return (
<div
className={containerClass}
style={{ ...containerStyles, position: 'relative' }}
>
<div ref={plotRef} style={{ width: '100%', height: '100%' }}>
<svg
width={width ?? DEFAULT_WIDTH}
height={
Math.max(data.column1NodeIDs.length, data.column2NodeIDs.length) *
DEFAULT_NODE_VERTICAL_SPACE +
DEFAULT_TOP_PADDING
}
>
{/* Draw names of node colums if they exist */}
{column1Name && (
<Text
x={DEFAULT_COLUMN1_X}
y={DEFAULT_TOP_PADDING / 2}
textAnchor="end"
>
{column1Name}
</Text>
)}
{column2Name && (
<Text
x={DEFAULT_COLUMN2_X}
y={DEFAULT_TOP_PADDING / 2}
textAnchor="start"
>
{column2Name}
</Text>
)}

<Graph
graph={{
nodes: nodesByColumnWithCoordinates[0].concat(
nodesByColumnWithCoordinates[1]
),
links: linksWithCoordinates,
}}
// Using our Link component so that it uses our nice defaults and
// can better expand to handle more complex events (hover and such).
linkComponent={({ link }) => <Link link={link} />}
nodeComponent={({ node }) => {
const nodeWithLabelProps = {
node: node,
labelPosition: node.labelPosition,
};
return <NodeWithLabel {...nodeWithLabelProps} />;
}}
/>
</svg>
{showSpinner && <Spinner />}
</div>
</div>
);
}

export default forwardRef(BipartiteNetwork);
11 changes: 7 additions & 4 deletions packages/libs/components/src/plots/Network.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { DefaultNode } from '@visx/network';
import { Text } from '@visx/text';
import { LinkData, NodeData } from '../types/plots/network';

export type LabelPosition = 'right' | 'left';

interface NodeWithLabelProps {
/** Network node */
node: NodeData;
/** Function to run when a user clicks either the node or label */
onClick?: () => void;
/** Should the label be drawn to the left or right of the node? */
labelPosition?: 'right' | 'left';
labelPosition?: LabelPosition;
/** Font size for the label. Ex. "1em" */
fontSize?: string;
/** Font weight for the label */
Expand All @@ -20,9 +22,10 @@ interface NodeWithLabelProps {
// NodeWithLabel draws one node and an optional label for the node. Both the node and
// label can be styled.
export function NodeWithLabel(props: NodeWithLabelProps) {
const DEFAULT_NODE_RADIUS = 4;
const DEFAULT_NODE_COLOR = '#aaa';
const DEFAULT_NODE_RADIUS = 6;
const DEFAULT_NODE_COLOR = '#fff';
const DEFAULT_STROKE_WIDTH = 1;
const DEFAULT_STROKE = '#111';

const {
node,
Expand Down Expand Up @@ -58,7 +61,7 @@ export function NodeWithLabel(props: NodeWithLabelProps) {
r={nodeRadius}
fill={color ?? DEFAULT_NODE_COLOR}
onClick={onClick}
stroke={stroke}
stroke={stroke ?? DEFAULT_STROKE}
strokeWidth={strokeWidth ?? DEFAULT_STROKE_WIDTH}
/>
{/* Note that Text becomes a tspan */}
Expand Down
Loading

0 comments on commit 981f21f

Please sign in to comment.