diff --git a/src/lib/components/accordion/index.tsx b/src/lib/components/accordion/index.tsx index c41d8fd83e4d14d3e2fad8c948807cc442bb97a3..ee5fbd542e2941b34c93c74ab924e7668697e014 100644 --- a/src/lib/components/accordion/index.tsx +++ b/src/lib/components/accordion/index.tsx @@ -97,7 +97,7 @@ type AccordionBodyProps = { export function AccordionBody({ isOpen = false, children, className = '' }: AccordionBodyProps) { return ( <div - className={`overflow-hidden transition-max-height duration-300 ease-in-out w-full box-border ml-2 ${isOpen ? 'max-h-screen' : 'max-h-0'} ${className}`} + className={`overflow-hidden transition-max-height duration-300 ease-in-out box-border ml-2 ${isOpen ? 'max-h-screen' : 'max-h-0'} ${className}`} > {isOpen && <div>{children}</div>} </div> diff --git a/src/lib/components/colorComponents/colorPicker/index.tsx b/src/lib/components/colorComponents/colorPicker/index.tsx index b8a98fe826d381def971e7cb1bd2f6e6c84aa9e8..f74bff71290380ccb2fbdddbdc699fee6e733d39 100644 --- a/src/lib/components/colorComponents/colorPicker/index.tsx +++ b/src/lib/components/colorComponents/colorPicker/index.tsx @@ -1,17 +1,9 @@ -import React from 'react'; import { visualizationColors } from '@/config'; import { Popover, PopoverTrigger, PopoverContent } from '@/lib/components/layout/Popover'; -const hexToRgb = (hex: string): [number, number, number] => { - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - return [r, g, b]; -}; - type Props = { value: [number, number, number]; - onChange: (val: [number, number, number]) => void; + onChange: (val: number) => void; }; export function ColorPicker({ value, onChange }: Props) { @@ -33,8 +25,7 @@ export function ColorPicker({ value, onChange }: Props) { e.stopPropagation(); }} > - {visualizationColors.GPCat.colors[14].map(hexColor => { - const [r, g, b] = hexToRgb(hexColor); + {visualizationColors.GPCat.colors[14].map((hexColor, i) => { return ( <div key={hexColor} @@ -42,7 +33,7 @@ export function ColorPicker({ value, onChange }: Props) { style={{ backgroundColor: hexColor }} onClick={e => { e.stopPropagation(); - onChange([r, g, b]); + onChange(i); }} /> ); diff --git a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx index 8afe33fbbaad16efcf7d698cbe1c821c1516f256..86b3f1e39f6a1d480ac6be98e467dfb3136be37f 100644 --- a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx +++ b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx @@ -1,10 +1,11 @@ -import React from 'react'; import { CompositeLayer, Layer } from 'deck.gl'; import { LineLayer, ScatterplotLayer, TextLayer } from '@deck.gl/layers'; import { CompositeLayerType, Coordinate, LayerProps } from '../../mapvis.types'; import { BrushingExtension, CollisionFilterExtension } from '@deck.gl/extensions'; import { scaleLinear, ScaleLinear, color, interpolateRgb } from 'd3'; import { Node } from '@/lib/data-access'; +import { nodeColorRGB } from '../../utils'; +import { CategoricalStats } from 'ts-common'; interface ColorScales { [label: string]: ScaleLinear<string, string>; @@ -43,8 +44,12 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> { const colorAttribute = nodeSettings.colorAttribute; const attributeData = nodeDistribution[colorAttribute]; - if (nodeSettings.colorAttributeType === 'numerical' && attributeData) { - colorScales[label] = this.setNumericalColor(attributeData.min, attributeData.max, nodeSettings.colorScale); + if (nodeSettings.colorAttributeType === 'number' && attributeData) { + colorScales[label] = this.setNumericalColor( + attributeData.statistics.min, + attributeData.statistics.max, + nodeSettings.colorScale, + ); } } }); @@ -134,10 +139,14 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> { pickable: true, getFillColor: d => { if (layerSettings?.nodes[label].colorByAttribute) { - let attributeValue = d.attributes[layerSettings?.nodes[label].colorAttribute]; - if (layerSettings?.nodes[label].colorAttributeType === 'categorical') { - return layerSettings?.nodes[label]?.colorMapping[attributeValue]; - } else if (layerSettings?.nodes[label].colorAttributeType === 'numerical') { + const attribute = layerSettings?.nodes[label].colorAttribute; + let attributeValue = d.attributes[attribute]; + if (['string', 'categorical'].includes(layerSettings?.nodes[label].colorAttributeType)) { + return nodeColorRGB( + layerSettings?.nodes[label]?.colorMapping[attributeValue] ?? + (graphMetadata.nodes.types[label].attributes[attribute].statistics as CategoricalStats).values.indexOf(attributeValue), + ); + } else if (layerSettings?.nodes[label].colorAttributeType === 'number') { if (typeof attributeValue === 'string') { const numericValue = parseFloat(attributeValue.replace(/[^0-9.]/g, '')); if (!isNaN(numericValue)) { @@ -148,7 +157,7 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> { return this.rgbStringToArray(colorScale(attributeValue)); } } - return layerSettings?.nodes[label].color; + return nodeColorRGB(layerSettings?.nodes[label].color); }, getPosition: (d: Node) => getNodeLocation(d._id), getRadius: () => layerSettings?.nodes[label]?.size, @@ -168,7 +177,7 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> { data: nodes, getPosition: (d: Node) => getNodeLocation(d._id), getText: (d: Node) => d.label, - getSize: (d: Node) => (layerSettings?.nodes[label]?.size * 2) / d.label.length, + getSize: (d: Node) => (layerSettings?.nodes[label]?.size * 2.5) / d.label.length, getAlignmentBaseline: 'center', getRadius: 10, radiusScale: 20, @@ -176,7 +185,7 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> { sizeUnits: 'meters', sizeMaxPixels: 64, characterSet: 'auto', - fontFamily: 'monospace', + fontFamily: 'InterVariable', billboard: false, getAngle: () => 0, collisionGroup: 'textLabels', diff --git a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx index e7c811694bf25e50bd40f69df55701323c187a47..273e14f79ff8c3ca66a3eced0e390385566325b8 100644 --- a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx +++ b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx @@ -9,7 +9,7 @@ import { isEqual } from 'lodash-es'; import { CategoricalStats } from '@/lib/statistics'; const defaultNodeSettings = (index: number) => ({ - color: nodeColorRGB(index + 1), + color: index, colorMapping: {}, colorScale: undefined, colorByAttribute: false, @@ -121,11 +121,11 @@ export function NodeLinkOptions({ <AccordionItem> <AccordionHead> - <div className="flex justify-between items-center"> + <div className="flex w-full justify-between items-center"> <span className="font-semibold">Color</span> {!nodeSettings?.colorByAttribute && ( <ColorPicker - value={nodeSettings?.color} + value={nodeColorRGB(nodeSettings?.color)} onChange={val => { updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, color: val } }, @@ -170,7 +170,7 @@ export function NodeLinkOptions({ }) } /> - {nodeSettings.colorAttributeType === 'numerical' ? ( + {nodeSettings.colorAttributeType === 'number' ? ( <div> <p>Select color scale:</p> <DropdownColorLegend @@ -192,11 +192,11 @@ export function NodeLinkOptions({ {( graphMetadata.nodes.types[nodeType]?.attributes?.[nodeSettings.colorAttribute] ?.statistics as CategoricalStats - ).values.map((attr: string) => ( + ).values.map((attr: string, i: number) => ( <div key={attr} className="flex items-center justify-between"> <p className="truncate w-18">{attr.length > 0 ? attr : 'Empty val'}</p> <ColorPicker - value={(nodeSettings?.colorMapping ?? {})[attr] ?? [0, 0, 0]} + value={nodeColorRGB((nodeSettings?.colorMapping ?? {})[attr] ?? i)} onChange={val => { updateLayerSettings({ nodes: { diff --git a/src/lib/vis/visualizations/mapvis/mapvis.tsx b/src/lib/vis/visualizations/mapvis/mapvis.tsx index 1404cc1bd04cd6ec8deb2f80593043db7eda86e3..33c0a4eb61d953defde15b50dc90675ad5499189 100644 --- a/src/lib/vis/visualizations/mapvis/mapvis.tsx +++ b/src/lib/vis/visualizations/mapvis/mapvis.tsx @@ -10,7 +10,7 @@ import { Attribution, ActionBar, MapTooltip, MapSettings } from './components'; import { useSelectionLayer, useCoordinateLookup } from './hooks'; import { VisualizationTooltip } from '@/lib/components/VisualizationTooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/lib/components/tooltip'; -import { isGeoJsonType, rgbToHex } from './utils'; +import { isGeoJsonType, nodeColorHex, rgbToHex } from './utils'; import { NodeType } from '../nodelinkvis/types'; import { ChoroplethLayer } from './layers/choropleth-layer/ChoroplethLayer'; @@ -207,10 +207,10 @@ export const MapVis = forwardRef((props: VisualizationPropTypes<MapProps>, refEx // Handle clicking on a node (when it's not a feature in the choropleth layer) if ( Object.prototype.hasOwnProperty.call(object, 'attributes') && - Object.prototype.hasOwnProperty.call(object, 'id') && + Object.prototype.hasOwnProperty.call(object, '_id') && Object.prototype.hasOwnProperty.call(object, 'label') ) { - const objectLocation: Coordinate = coordinateLookup[object.id]; + const objectLocation: Coordinate = coordinateLookup[object._id]; props.handleSelect({ nodes: [object] }); if (objectLocation) { @@ -253,7 +253,7 @@ export const MapVis = forwardRef((props: VisualizationPropTypes<MapProps>, refEx // If the feature contains node IDs, handle selecting the corresponding nodes const ids = object.properties.nodes; if (ids && ids.length > 0) { - const nodes = props.data.nodes.filter(node => ids.includes((node as unknown as { id: string }).id)); + const nodes = props.data.nodes.filter(node => ids.includes(node._id)); props.handleSelect({ nodes: [...nodes] }); } } @@ -308,11 +308,7 @@ export const MapVis = forwardRef((props: VisualizationPropTypes<MapProps>, refEx } colorHeader={ node.selectedType === 'node' - ? rgbToHex( - props?.settings?.nodelink?.nodes?.[node.label]?.color?.[0] ?? 0, - props?.settings?.nodelink?.nodes?.[node.label]?.color?.[1] ?? 0, - props?.settings?.nodelink?.nodes?.[node.label]?.color?.[2] ?? 0, - ) + ? nodeColorHex(props?.settings?.nodelink?.nodes?.[node.label]?.color) : node.selectedType === 'area' ? rgbToHex(node.color[0], node.color[1], node.color[2]) : 'hsl(var(--clr-node))' diff --git a/src/lib/vis/visualizations/mapvis/mapvis.types.ts b/src/lib/vis/visualizations/mapvis/mapvis.types.ts index 51a2939f02b12934084033c5348c1b9ef33514cb..745270eca1a145032c94e8139bb2863ad4569ff2 100644 --- a/src/lib/vis/visualizations/mapvis/mapvis.types.ts +++ b/src/lib/vis/visualizations/mapvis/mapvis.types.ts @@ -9,7 +9,7 @@ export type Coordinate = [number, number]; export type LocationInfo = { lat: string; lon: string }; export type MapNodeData = { - color: [number, number, number]; + color: number; hidden: boolean; fixed: boolean; min: number; @@ -25,7 +25,7 @@ export type MapNodeData = { colorAttribute?: string | undefined; colorAttributeType?: string | undefined; colorScale: string; - colorMapping?: { [label: string]: [number, number, number] }; + colorMapping?: { [label: string]: number }; }; export type MapEdgeData = { diff --git a/src/lib/vis/visualizations/mapvis/utils.ts b/src/lib/vis/visualizations/mapvis/utils.ts index 4cb0f9305d9129c2e68fb3eb84215f847ee54e12..b5fe310f0d2669919a65040ba936e0a729f070fd 100644 --- a/src/lib/vis/visualizations/mapvis/utils.ts +++ b/src/lib/vis/visualizations/mapvis/utils.ts @@ -3,8 +3,8 @@ import { NodeType } from '../nodelinkvis/types'; import { GeoJsonType } from './mapvis.types'; import { SearchResultType } from './mapvis.types'; -export function nodeColorRGB(num: number) { - const colorVal = visualizationColors.GPCat.colors[14][num % visualizationColors.GPCat.colors[14].length]; +export function nodeColorRGB(num?: number): [number, number, number] { + const colorVal = nodeColorHex(num); const hex = colorVal.replace(/^#/, ''); const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); @@ -12,6 +12,12 @@ export function nodeColorRGB(num: number) { return [r, g, b]; } +export function nodeColorHex(num?: number): string { + let colorVal = visualizationColors.GPCat.colors[14][(num ?? 0) % visualizationColors.GPCat.colors[14].length]; + if (colorVal == null) colorVal = visualizationColors.GPCat.colors[14][0]; + return colorVal; +} + export const isGeoJsonType = (data: NodeType | GeoJsonType | SearchResultType): data is GeoJsonType => { return (data as GeoJsonType).properties !== undefined; }; diff --git a/src/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/src/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx index a380eb3346725458b997bbb2fc463f32599d0164..446962cbe2e2f0f19664c96ac31e8b28c626c7fa 100644 --- a/src/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx +++ b/src/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx @@ -598,7 +598,7 @@ export const NLPixi = forwardRef((props: Props, refExternal) => { fill: 0xffffff, wordWrap: true, breakWords: true, - wordWrapWidth: config.NODE_RADIUS, + wordWrapWidth: config.NODE_RADIUS + 5, align: 'center', }, });