diff --git a/libs/shared/lib/components/accordion/accordion.stories.tsx b/libs/shared/lib/components/accordion/accordion.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ffab9d0e130979e571867e68ec08bf79b186f80e --- /dev/null +++ b/libs/shared/lib/components/accordion/accordion.stories.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { StoryObj, Meta } from '@storybook/react'; +import { Accordion, AccordionItem, AccordionHead, AccordionBody } from '.'; +import { EntityPill, RelationPill } from '../pills'; + +export default { + title: 'Components/Accordion', + component: Accordion, + decorators: [(Story) => <div className="flex m-5">{Story()}</div>], +} as Meta<typeof Accordion>; + +type Story = StoryObj<typeof Accordion>; + +export const Default: Story = { + render: () => ( + <div className="max-w-md mx-auto my-10"> + <Accordion defaultOpenIndex={0} className="w-64"> + <AccordionItem> + <AccordionHead> + <EntityPill title="PERSON" /> + </AccordionHead> + <AccordionBody>This is the content of Section 1.</AccordionBody> + </AccordionItem> + <AccordionItem> + <AccordionHead> + <EntityPill title="INDICENT" /> + </AccordionHead> + <AccordionBody> + <Accordion> + <AccordionItem> + <AccordionHead showArrow={false}>Location info</AccordionHead> + <AccordionBody>Location info settings</AccordionBody> + </AccordionItem> + <AccordionItem> + <AccordionHead showArrow={false}>Color and shape</AccordionHead> + <AccordionBody>Color and shape settings</AccordionBody> + </AccordionItem> + </Accordion> + </AccordionBody> + </AccordionItem> + <AccordionItem> + <AccordionHead> + <RelationPill title="PERSON_OF" /> + </AccordionHead> + <AccordionBody>This is the content of Section 3.</AccordionBody> + </AccordionItem> + </Accordion> + </div> + ), +}; diff --git a/libs/shared/lib/components/accordion/index.tsx b/libs/shared/lib/components/accordion/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ffd343e62485704d56ab9761a312237668ed087a --- /dev/null +++ b/libs/shared/lib/components/accordion/index.tsx @@ -0,0 +1,105 @@ +import React, { useState, ReactElement } from 'react'; +import { Button } from '../buttons'; + +type AccordionProps = { + children: React.ReactNode; + defaultOpenIndex?: number; + defaultOpenAll?: boolean; + className?: string; +}; + +export function Accordion({ children, defaultOpenIndex, defaultOpenAll = false, className = '' }: AccordionProps) { + const childrenArray = React.Children.toArray(children); + const [openIndexes, setOpenIndexes] = useState<number[]>(() => { + if (defaultOpenAll) { + return childrenArray.map((_, index) => index); + } else if (defaultOpenIndex !== undefined) { + return [defaultOpenIndex]; + } else { + return []; + } + }); + + const toggleIndex = (index: number) => { + setOpenIndexes((currentIndexes) => + currentIndexes.includes(index) ? currentIndexes.filter((i) => i !== index) : [...currentIndexes, index], + ); + }; + + return ( + <div className={`w-full ${className}`}> + {React.Children.map(children, (child, index) => { + if (React.isValidElement(child)) { + return React.cloneElement(child as ReactElement<AccordionItemProps>, { + isOpen: openIndexes.includes(index), + onToggle: () => toggleIndex(index), + }); + } + return child; + })} + </div> + ); +} + +type AccordionItemProps = { + isOpen?: boolean; + onToggle?: () => void; + children: React.ReactNode; + className?: string; +}; + +export function AccordionItem({ isOpen = false, onToggle, children, className = '' }: AccordionItemProps) { + return ( + <div className={`w-full ${className}`}> + {React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child as ReactElement<AccordionHeadProps | AccordionBodyProps>, { + isOpen, + onToggle, + }); + } + return child; + })} + </div> + ); +} + +type AccordionHeadProps = { + isOpen?: boolean; + onToggle?: () => void; + children: React.ReactNode; + showArrow?: boolean; + className?: string; +}; + +export function AccordionHead({ isOpen = false, onToggle, children, showArrow = true, className = '' }: AccordionHeadProps) { + return ( + <div className={`cursor-pointer flex items-center w-full box-border ${className}`} onClick={onToggle}> + {showArrow && ( + <Button + size="2xs" + iconComponent={!isOpen ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'} + variant="ghost" + className="mr-1" + /> + )} + {children} + </div> + ); +} + +type AccordionBodyProps = { + isOpen?: boolean; + children: React.ReactNode; + className?: string; +}; + +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}`} + > + {isOpen && <div>{children}</div>} + </div> + ); +} diff --git a/libs/shared/lib/components/colorComponents/colorPicker/index.tsx b/libs/shared/lib/components/colorComponents/colorPicker/index.tsx index ef0e49e5e4876fb3ee00ec417779ead60f2dd34a..f323841e79b5ee02b707a354904cd8c4532ca0a1 100644 --- a/libs/shared/lib/components/colorComponents/colorPicker/index.tsx +++ b/libs/shared/lib/components/colorComponents/colorPicker/index.tsx @@ -26,7 +26,10 @@ export default function ColorPicker({ value, updateValue }: Props) { className="p-1 inline-block cursor-pointer" ref={refs.setReference} {...getReferenceProps({ - onClick: () => setOpen(!open), + onClick: (e) => { + setOpen(!open); + e.stopPropagation(); + }, })} > <div diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/choropleth-layer/ChoroplethOptions.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/choropleth-layer/ChoroplethOptions.tsx index 6f19671e936b46a569e4e865af62a7c1f90d38b0..b077728f8136973be96eef4c9465a8f72b31cd13 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/layers/choropleth-layer/ChoroplethOptions.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/layers/choropleth-layer/ChoroplethOptions.tsx @@ -1,8 +1,9 @@ import React, { useEffect } from 'react'; import ColorPicker from '@graphpolaris/shared/lib/components/colorComponents/colorPicker'; import { MapProps } from '../../mapvis'; -import { Button, DropdownColorLegend, EntityPill, Input, RelationPill } from '@graphpolaris/shared/lib/components'; +import { DropdownColorLegend, EntityPill, Input, RelationPill } from '@graphpolaris/shared/lib/components'; import { LayerSettingsComponentType } from '../../mapvis.types'; +import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '@graphpolaris/shared/lib/components/accordion'; const areaColoringStrategies = ['Node count', 'Edge count', 'Incoming edges', 'Outgoing edges', 'Connected edges', 'Attribute']; @@ -50,131 +51,108 @@ export function ChoroplethOptions({ } }, [graphMetadata, layerType, settings, updateLayerSettings]); - const handleCollapseToggle = (type: string, itemType: 'nodes' | 'edges') => { - if (layerSettings) { - updateLayerSettings({ - [itemType]: { - ...layerSettings[itemType], - [type]: { - ...layerSettings[itemType][type], - collapsed: !layerSettings[itemType][type]?.collapsed ?? true, - }, - }, - }); - } - }; - return ( layerSettings && ( <div> - <div className="mt-2"> - <p className="text-bold">Area color</p> - <Input - inline - label="Based on" - type="dropdown" - value={layerSettings?.coloringStrategy} - options={areaColoringStrategies} - onChange={(val) => updateLayerSettings({ coloringStrategy: val as coloringStrategiesType })} - /> - <DropdownColorLegend value={settings?.colorScale} onChange={(val) => updateLayerSettings({ colorScale: val })} /> - <Input - label="Opacity" - type="slider" - min={0} - max={1} - step={0.05} - unit="%" - value={layerSettings?.opacity ?? 0.8} - onChange={(val) => updateLayerSettings({ opacity: val as number })} - /> - </div> - - {graphMetadata.nodes.labels.map((nodeType) => { - const nodeSettings = layerSettings?.nodes?.[nodeType] || {}; - - return ( - <div className="mt-2" key={nodeType}> - <div className="flex items-center"> - <Button - size="2xs" - iconComponent={nodeSettings.collapsed ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'} - variant="ghost" - onClick={() => handleCollapseToggle(nodeType, 'nodes')} - /> - <div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(nodeType, 'nodes')}> + <Accordion defaultOpenAll={true}> + <AccordionItem className="mt-2"> + <AccordionHead className="flex items-center"> + <span className="font-semibold">General</span> + </AccordionHead> + <AccordionBody> + <p className="text-bold">Area color</p> + <Input + inline + label="Based on" + type="dropdown" + value={layerSettings?.coloringStrategy} + options={areaColoringStrategies} + onChange={(val) => updateLayerSettings({ coloringStrategy: val as coloringStrategiesType })} + /> + <DropdownColorLegend value={settings?.colorScale} onChange={(val) => updateLayerSettings({ colorScale: val })} /> + <Input + label="Opacity" + type="slider" + min={0} + max={1} + step={0.05} + unit="%" + value={layerSettings?.opacity ?? 0.8} + onChange={(val) => updateLayerSettings({ opacity: val as number })} + /> + </AccordionBody> + </AccordionItem> + + {graphMetadata.nodes.labels.map((nodeType) => { + const nodeSettings = layerSettings?.nodes?.[nodeType] || {}; + + return ( + <AccordionItem className="mt-2" key={nodeType}> + <AccordionHead className="flex items-center"> <EntityPill title={nodeType} /> - </div> - </div> - - {!nodeSettings.collapsed && ( - <div> - <Input - inline - label="Latitude" - type="dropdown" - value={settings?.location[nodeType].lat} - options={[...spatialAttributes[nodeType]]} - disabled={spatialAttributes[nodeType].length < 1} - onChange={(val) => updateSpatialAttribute(nodeType, 'lat', val as string)} - /> - <Input - inline - label="Longitude" - type="dropdown" - value={settings?.location[nodeType].lon} - options={[...spatialAttributes[nodeType]]} - disabled={spatialAttributes[nodeType].length < 1} - onChange={(val) => updateSpatialAttribute(nodeType, 'lon', val as string)} - /> - </div> - )} - </div> - ); - })} + </AccordionHead> + + <AccordionBody> + <div> + <Input + inline + label="Latitude" + type="dropdown" + value={settings?.location[nodeType].lat} + options={[...spatialAttributes[nodeType]]} + disabled={spatialAttributes[nodeType].length < 1} + onChange={(val) => updateSpatialAttribute(nodeType, 'lat', val as string)} + /> + <Input + inline + label="Longitude" + type="dropdown" + value={settings?.location[nodeType].lon} + options={[...spatialAttributes[nodeType]]} + disabled={spatialAttributes[nodeType].length < 1} + onChange={(val) => updateSpatialAttribute(nodeType, 'lon', val as string)} + /> + </div> + </AccordionBody> + </AccordionItem> + ); + })} - {graphMetadata.edges.labels.map((edgeType) => { - const edgeSettings = layerSettings?.edges?.[edgeType] || {}; + {graphMetadata.edges.labels.map((edgeType) => { + const edgeSettings = layerSettings?.edges?.[edgeType] || {}; - return ( - <div key={edgeType} className="mt-2"> - <div className="flex items-center"> - <Button - size="2xs" - iconComponent={edgeSettings?.collapsed ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'} - variant="ghost" - onClick={() => handleCollapseToggle(edgeType, 'edges')} - /> - <div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(edgeType, 'edges')}> + return ( + <AccordionItem key={edgeType} className="mt-2"> + <AccordionHead className="flex items-center"> <RelationPill title={edgeType} /> - </div> - </div> - - {!edgeSettings.collapsed && ( - <div> - <div className="flex justify-between"> - <span className="font-bold">Color</span> - <ColorPicker - value={edgeSettings.color} - updateValue={(val) => - updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, color: val } } }) - } + </AccordionHead> + + <AccordionBody> + <div> + <div className="flex justify-between"> + <span className="font-bold">Color</span> + <ColorPicker + value={edgeSettings.color} + updateValue={(val) => + updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, color: val } } }) + } + /> + </div> + + <Input + label="Edges on hover" + type="boolean" + value={layerSettings?.enableBrushing} + onChange={(val) => { + updateLayerSettings({ enableBrushing: val as boolean }); + }} /> </div> - - <Input - label="Edges on hover" - type="boolean" - value={layerSettings?.enableBrushing} - onChange={(val) => { - updateLayerSettings({ enableBrushing: val as boolean }); - }} - /> - </div> - )} - </div> - ); - })} + </AccordionBody> + </AccordionItem> + ); + })} + </Accordion> </div> ) ); diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/heatmap-layer/HeatLayerOptions.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/heatmap-layer/HeatLayerOptions.tsx index fcd791172d41b3320150ac5aacfe09942b52d3e6..f8f32efe8530d73f61a020fe658b03dd22f8f63e 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/layers/heatmap-layer/HeatLayerOptions.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/layers/heatmap-layer/HeatLayerOptions.tsx @@ -1,7 +1,8 @@ import React, { useEffect } from 'react'; import { MapProps } from '../../mapvis'; -import { Button, EntityPill, Input } from '@graphpolaris/shared/lib/components'; +import { EntityPill, Input } from '@graphpolaris/shared/lib/components'; import { LayerSettingsComponentType } from '../../mapvis.types'; +import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '@graphpolaris/shared/lib/components/accordion'; export function HeatLayerOptions({ settings, @@ -38,84 +39,64 @@ export function HeatLayerOptions({ } }, [graphMetadata, layerType, settings, updateLayerSettings]); - const handleCollapseToggle = (type: string, itemType: 'nodes' | 'edges') => { - if (layerSettings) { - updateLayerSettings({ - [itemType]: { - ...layerSettings[itemType], - [type]: { - ...layerSettings[itemType][type], - collapsed: !layerSettings[itemType][type]?.collapsed ?? true, - }, - }, - }); - } - }; - return ( layerSettings && ( <div> - {graphMetadata.nodes.labels.map((nodeType) => { - const nodeSettings = layerSettings?.nodes?.[nodeType] || {}; + <Accordion defaultOpenAll={true}> + {graphMetadata.nodes.labels.map((nodeType) => { + const nodeSettings = layerSettings?.nodes?.[nodeType] || {}; - return ( - <div className="mt-2" key={nodeType}> - <div className="flex items-center"> - <Button - size="2xs" - iconComponent={nodeSettings.collapsed ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'} - variant="ghost" - onClick={() => handleCollapseToggle(nodeType, 'nodes')} - /> - <div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(nodeType, 'nodes')}> + return ( + <AccordionItem className="mt-2" key={nodeType}> + <AccordionHead className="flex items-center"> <EntityPill title={nodeType} /> - </div> - </div> + </AccordionHead> - {!nodeSettings.collapsed && ( - <div> - <Input - label="Hidden" - type="boolean" - value={nodeSettings.hidden ?? false} - onChange={(val) => { - updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, hidden: val } } }); - }} - /> - <Input - inline - label="Latitude" - type="dropdown" - value={settings?.location[nodeType].lat} - options={[...spatialAttributes[nodeType]]} - disabled={spatialAttributes[nodeType].length < 1} - onChange={(val) => updateSpatialAttribute(nodeType, 'lat', val as string)} - /> - <Input - inline - label="Longitude" - type="dropdown" - value={settings?.location[nodeType].lon} - options={[...spatialAttributes[nodeType]]} - disabled={spatialAttributes[nodeType].length < 1} - onChange={(val) => updateSpatialAttribute(nodeType, 'lon', val as string)} - /> - <Input - label="Size" - type="slider" - min={0} - max={40} - step={1} - value={nodeSettings.size} - onChange={(val) => { - updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, size: Number(val) } } }); - }} - /> - </div> - )} - </div> - ); - })} + <AccordionBody> + <div> + <Input + label="Hidden" + type="boolean" + value={nodeSettings.hidden ?? false} + onChange={(val) => { + updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, hidden: val } } }); + }} + /> + <Input + inline + label="Latitude" + type="dropdown" + value={settings?.location[nodeType].lat} + options={[...spatialAttributes[nodeType]]} + disabled={spatialAttributes[nodeType].length < 1} + onChange={(val) => updateSpatialAttribute(nodeType, 'lat', val as string)} + /> + <Input + inline + label="Longitude" + type="dropdown" + value={settings?.location[nodeType].lon} + options={[...spatialAttributes[nodeType]]} + disabled={spatialAttributes[nodeType].length < 1} + onChange={(val) => updateSpatialAttribute(nodeType, 'lon', val as string)} + /> + <Input + label="Size" + type="slider" + min={0} + max={40} + step={1} + value={nodeSettings.size} + onChange={(val) => { + updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, size: Number(val) } } }); + }} + /> + </div> + </AccordionBody> + </AccordionItem> + ); + })} + </Accordion> </div> ) ); diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx index 70b08dd096885b67ea95f2299ac23f46bebe83a1..fee3d3670707f3689670eeeea847301818a02d0d 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx @@ -1,9 +1,10 @@ import React, { useEffect } from 'react'; import ColorPicker from '@graphpolaris/shared/lib/components/colorComponents/colorPicker'; -import { Button, DropdownColorLegend, EntityPill, Icon, Input, RelationPill } from '@graphpolaris/shared/lib/components'; +import { DropdownColorLegend, EntityPill, Icon, Input, RelationPill } from '@graphpolaris/shared/lib/components'; import { MapProps } from '../../mapvis'; import { LayerSettingsComponentType } from '../../mapvis.types'; import { nodeColorRGB } from '../../utils'; +import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '@graphpolaris/shared/lib/components/accordion'; import { isEqual } from 'lodash-es'; const defaultNodeSettings = (index: number) => ({ @@ -65,42 +66,20 @@ export function NodeLinkOptions({ } }, [graphMetadata]); - const handleCollapseToggle = (type: string, itemType: 'nodes' | 'edges') => { - if (layerSettings) { - updateLayerSettings({ - [itemType]: { - ...layerSettings[itemType], - [type]: { - ...layerSettings[itemType][type], - collapsed: !layerSettings[itemType][type]?.collapsed ?? true, - }, - }, - }); - } - }; - return ( layerSettings && ( <div> - {graphMetadata.nodes.labels.map((nodeType) => { - const nodeSettings = layerSettings?.nodes?.[nodeType] || {}; + <Accordion defaultOpenAll={true}> + {graphMetadata.nodes.labels.map((nodeType) => { + const nodeSettings = layerSettings?.nodes?.[nodeType] || {}; - return ( - layerSettings?.nodes?.[nodeType] && ( - <div className="mt-2" key={nodeType}> - <div className="flex items-center"> - <Button - size="2xs" - iconComponent={nodeSettings.collapsed ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'} - variant="ghost" - onClick={() => handleCollapseToggle(nodeType, 'nodes')} - /> - <div className="flex flex-grow mr-2 cursor-pointer" onClick={() => handleCollapseToggle(nodeType, 'nodes')}> - <EntityPill title={nodeType} /> - </div> - </div> + return ( + <AccordionItem className="mt-2" key={nodeType}> + <AccordionHead className="flex items-center"> + <EntityPill title={nodeType} /> + </AccordionHead> - {!nodeSettings.collapsed && ( + <AccordionBody> <div> <Input label="Hidden" @@ -110,277 +89,281 @@ export function NodeLinkOptions({ updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, hidden: val } } }) } /> - <div className="border-t-2 my-2"> - <span className="font-bold mt-2">Location attributes</span> - <Input - inline - label="Latitude" - type="dropdown" - value={settings?.location[nodeType]?.lat} - options={[...spatialAttributes[nodeType]]} - disabled={spatialAttributes[nodeType].length < 1} - onChange={(val) => updateSpatialAttribute(nodeType, 'lat', val as string)} - /> - <Input - inline - label="Longitude" - type="dropdown" - value={settings?.location[nodeType]?.lon} - options={[...spatialAttributes[nodeType]]} - disabled={spatialAttributes[nodeType].length < 1} - onChange={(val) => updateSpatialAttribute(nodeType, 'lon', val as string)} - /> - </div> - - <div className="border-t-2 my-2"> - <div className="flex justify-between"> - <span className="font-bold">Color</span> - {!nodeSettings.colorByAttribute && ( - <ColorPicker - value={nodeSettings.color} - updateValue={(val) => { - updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, color: val } } }); - }} + <Accordion> + <AccordionItem> + <AccordionHead> + <span className="font-semibold">Location attributes</span> + </AccordionHead> + <AccordionBody> + <Input + inline + label="Latitude" + type="dropdown" + value={settings?.location[nodeType]?.lat} + options={[...spatialAttributes[nodeType]]} + disabled={spatialAttributes[nodeType].length < 1} + onChange={(val) => updateSpatialAttribute(nodeType, 'lat', val as string)} /> - )} - </div> + <Input + inline + label="Longitude" + type="dropdown" + value={settings?.location[nodeType]?.lon} + options={[...spatialAttributes[nodeType]]} + disabled={spatialAttributes[nodeType].length < 1} + onChange={(val) => updateSpatialAttribute(nodeType, 'lon', val as string)} + /> + </AccordionBody> + </AccordionItem> - <div> - <Input - label="By attribute" - type="boolean" - value={nodeSettings.colorByAttribute ?? false} - onChange={(val) => - updateLayerSettings({ - nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorByAttribute: val } }, - }) - } - /> - {nodeSettings.colorByAttribute && ( - <div> - <Input - inline - label="Color based on" - type="dropdown" - value={nodeSettings.colorAttribute} - options={Object.keys(graphMetadata.nodes.types[nodeType]?.attributes)} - onChange={(val) => - updateLayerSettings({ - nodes: { - ...layerSettings.nodes, - [nodeType]: { - ...nodeSettings, - colorAttribute: String(val), - colorAttributeType: graphMetadata.nodes.types[nodeType].attributes[val].dimension, + <AccordionItem> + <AccordionHead> + <div className="flex justify-between items-center"> + <span className="font-semibold">Color</span> + {!nodeSettings.colorByAttribute && ( + <ColorPicker + value={nodeSettings.color} + updateValue={(val) => { + updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, color: val } } }); + }} + /> + )} + </div> + </AccordionHead> + <AccordionBody> + <Input + label="By attribute" + type="boolean" + value={nodeSettings.colorByAttribute ?? false} + onChange={(val) => + updateLayerSettings({ + nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorByAttribute: val } }, + }) + } + /> + {nodeSettings.colorByAttribute && ( + <div> + <Input + inline + label="Color based on" + type="dropdown" + value={nodeSettings.colorAttribute} + options={Object.keys(graphMetadata.nodes.types[nodeType]?.attributes)} + onChange={(val) => + updateLayerSettings({ + nodes: { + ...layerSettings.nodes, + [nodeType]: { + ...nodeSettings, + colorAttribute: String(val), + colorAttributeType: graphMetadata.nodes.types[nodeType].attributes[val].dimension, + }, }, - }, - }) - } - /> - {nodeSettings.colorAttributeType === 'numerical' ? ( - <div> - <p>Select color scale:</p> - <DropdownColorLegend - value={settings?.colorScale} - onChange={(val) => - updateLayerSettings({ - nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorScale: val } }, - }) - } - /> - </div> - ) : ( - nodeSettings.colorAttributeType === 'categorical' && - nodeSettings.colorAttribute && ( + }) + } + /> + {nodeSettings.colorAttributeType === 'numerical' ? ( <div> - {( - graphMetadata.nodes.types[nodeType] as { attributes: { [key: string]: { values: string[] } } } - )?.attributes?.[nodeSettings.colorAttribute]?.values.map((attr: string) => ( - <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]} - updateValue={(val) => { - updateLayerSettings({ - nodes: { - ...layerSettings.nodes, - [nodeType]: { - ...nodeSettings, - colorMapping: { ...nodeSettings.colorMapping, [attr]: val }, - }, - }, - }); - }} - /> - </div> - ))} + <p>Select color scale:</p> + <DropdownColorLegend + value={settings?.colorScale} + onChange={(val) => + updateLayerSettings({ + nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorScale: val } }, + }) + } + /> </div> - ) - )} - </div> - )} - </div> - </div> - - <div className="border-t-2 my-2"> - <span className="font-bold mt-2">Shape & Size</span> - <Input - inline - label="Shape" - type="dropdown" - value={nodeSettings.shape} - options={['circle', 'square', 'triangle', 'diamond', 'location', 'star']} - disabled={true} - onChange={(val) => - updateLayerSettings({ - nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, shape: String(val) } }, - }) - } - /> - <Input - label="Size" - type="slider" - min={0} - max={80} - step={5} - value={nodeSettings.size} - onChange={(val) => - updateLayerSettings({ - nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, size: Number(val) } }, - }) - } - /> - </div> - </div> - )} - </div> - ) - ); - })} - {graphMetadata.edges.labels.map((edgeType) => { - const edgeSettings = layerSettings?.edges?.[edgeType] || {}; - - return ( - layerSettings?.edges?.[edgeType] && ( - <div className="mt-2" key={edgeType}> - <div className="flex items-center"> - <Button - size="2xs" - iconComponent={edgeSettings.collapsed ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'} - variant="ghost" - onClick={() => handleCollapseToggle(edgeType, 'edges')} - /> - <div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(edgeType, 'edges')}> - <RelationPill title={edgeType} /> - </div> - </div> - - {!edgeSettings.collapsed && ( - <div> - <Input - label="Hidden" - type="boolean" - value={edgeSettings.hidden ?? false} - onChange={(val) => { - updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, hidden: val } } }); - }} - /> - - <div className="flex justify-between"> - <span className="font-bold">Color</span> - <ColorPicker - value={edgeSettings.color} - updateValue={(val) => - updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, color: val } } }) - } - /> - </div> - - <Input - label="Enable brushing" - type="boolean" - value={settings.enableBrushing} - onChange={(val) => { - updateLayerSettings({ enableBrushing: val as boolean }); - }} - /> - - <div> - <div className="flex items-center gap-1"> - <Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" /> - <span>Width</span> - </div> - <Input - label="Fixed" - type="boolean" - value={edgeSettings.fixed} - onChange={(val) => - updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, fixed: val } } }) - } - /> - {!edgeSettings.fixed ? ( - <div> + ) : ( + nodeSettings.colorAttributeType === 'categorical' && + nodeSettings.colorAttribute && ( + <div> + {( + graphMetadata.nodes.types[nodeType] as { attributes: { [key: string]: { values: string[] } } } + )?.attributes?.[nodeSettings.colorAttribute]?.values.map((attr: string) => ( + <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]} + updateValue={(val) => { + updateLayerSettings({ + nodes: { + ...layerSettings.nodes, + [nodeType]: { + ...nodeSettings, + colorMapping: { ...nodeSettings.colorMapping, [attr]: val }, + }, + }, + }); + }} + /> + </div> + ))} + </div> + ) + )} + </div> + )} + </AccordionBody> + </AccordionItem> + <AccordionItem> + <AccordionHead> + <span className="font-semibold">Shape & Size</span> + </AccordionHead> + <AccordionBody> <Input - label="Based on" + inline + label="Shape" type="dropdown" - size="xs" - options={ - graphMetadata.edges.types[edgeType]?.attributes - ? Object.keys(graphMetadata.edges.types[edgeType].attributes).filter( - (key) => graphMetadata.edges.types[edgeType].attributes[key].dimension === 'numerical', - ) - : [] + value={nodeSettings.shape} + options={['circle', 'square', 'triangle', 'diamond', 'location', 'star']} + disabled={true} + onChange={(val) => + updateLayerSettings({ + nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, shape: String(val) } }, + }) } - value={edgeSettings.sizeAttribute} + /> + <Input + label="Size" + type="slider" + min={0} + max={80} + step={5} + value={nodeSettings.size} onChange={(val) => updateLayerSettings({ - edges: { ...settings.edges, [edgeType]: { ...edgeSettings, sizeAttribute: String(val) } }, + nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, size: Number(val) } }, }) } /> - <div className="flex"> + </AccordionBody> + </AccordionItem> + </Accordion> + </div> + </AccordionBody> + </AccordionItem> + ); + })} + {graphMetadata.edges.labels.map((edgeType) => { + const edgeSettings = layerSettings?.edges?.[edgeType] || {}; + + return ( + <AccordionItem className="mt-2" key={edgeType}> + <AccordionHead className="flex items-center"> + <RelationPill title={edgeType} /> + </AccordionHead> + + <AccordionBody> + <Input + label="Hidden" + type="boolean" + value={edgeSettings.hidden ?? false} + onChange={(val) => { + updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, hidden: val } } }); + }} + /> + + <Input + label="Enable brushing" + type="boolean" + value={settings.enableBrushing} + onChange={(val) => { + updateLayerSettings({ enableBrushing: val as boolean }); + }} + /> + + <Accordion> + <AccordionItem> + <AccordionHead> + <span className="font-semibold">Color</span> + </AccordionHead> + <AccordionBody> + <ColorPicker + value={edgeSettings.color} + updateValue={(val) => + updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, color: val } } }) + } + /> + </AccordionBody> + </AccordionItem> + <AccordionItem> + <AccordionHead> + <span className="font-semibold">Width</span> + </AccordionHead> + <AccordionBody> + <Input + label="Fixed" + type="boolean" + value={edgeSettings.fixed} + onChange={(val) => + updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, fixed: val } } }) + } + /> + {!edgeSettings.fixed ? ( + <div> <Input - type="number" - label="min" + label="Based on" + type="dropdown" size="xs" - value={edgeSettings.min} + options={ + graphMetadata.edges.types[edgeType]?.attributes + ? Object.keys(graphMetadata.edges.types[edgeType].attributes).filter( + (key) => graphMetadata.edges.types[edgeType].attributes[key].dimension === 'numerical', + ) + : [] + } + value={edgeSettings.sizeAttribute} onChange={(val) => - updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, min: val } } }) + updateLayerSettings({ + edges: { ...settings.edges, [edgeType]: { ...edgeSettings, sizeAttribute: String(val) } }, + }) } /> + <div className="flex"> + <Input + type="number" + label="min" + size="xs" + value={edgeSettings.min} + onChange={(val) => + updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, min: val } } }) + } + /> + <Input + type="number" + label="max" + size="xs" + value={edgeSettings.max} + onChange={(val) => + updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, max: val } } }) + } + /> + </div> + </div> + ) : ( + <div> <Input - type="number" - label="max" - size="xs" - value={edgeSettings.max} + type="slider" + label="Width" + min={0} + max={10} + step={0.2} + value={edgeSettings.width} onChange={(val) => - updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, max: val } } }) + updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, width: Number(val) } } }) } /> </div> - </div> - ) : ( - <div> - <Input - type="slider" - label="Width" - min={0} - max={10} - step={0.2} - value={edgeSettings.width} - onChange={(val) => - updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, width: Number(val) } } }) - } - /> - </div> - )} - </div> - </div> - )} - </div> - ) - ); - })} + )} + </AccordionBody> + </AccordionItem> + </Accordion> + </AccordionBody> + </AccordionItem> + ); + })} + </Accordion> </div> ) ); diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx index c7e6c50baff8a21a553f572b58eaf860d084cc8b..cd5809a7db70fbca6b0db98531915e264b189591 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx @@ -281,7 +281,9 @@ export const MapVis = forwardRef((props: VisualizationPropTypes<MapProps>, refEx <Tooltip open={true} interactive={false} boundaryElement={ref} showArrow={true}> <TooltipTrigger x={searchResult.x} y={searchResult.y} /> <TooltipContent> - <MapTooltip type="location" data={{ ...searchResult }} key={searchResult.name} /> + <VisualizationTooltip name={searchResult.name} colorHeader="#FB9637"> + <MapTooltip type="location" data={{ ...searchResult }} key={searchResult.name} /> + </VisualizationTooltip> </TooltipContent> </Tooltip> )} diff --git a/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx b/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx index 011beea2504b09fc208f7abff19b9864bf14f247..3fa1b9a54bc47240e0cdbf534abae1bb092ec4b5 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx @@ -15,6 +15,7 @@ import { Button } from '@graphpolaris/shared/lib/components/buttons'; import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill'; import { cloneDeep } from 'lodash-es'; import { useImmer } from 'use-immer'; +import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '@graphpolaris/shared/lib/components/accordion'; import html2canvas from 'html2canvas'; export interface PaohVisHandle { @@ -1030,29 +1031,25 @@ const PaohSettings = ({ settings, graphMetadata, updateSettings }: Visualization } ></Input> </div> - <Button - variantType="secondary" - variant="ghost" - size="sm" - className="-pt-3 mt-1" - onClick={toggleCollapseAttrRows} - iconComponent={areCollapsedAttrRows ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'} - > - attributes:{' '} - </Button> - {!areCollapsedAttrRows && ( - <div className=""> - <Input - type="checkbox" - value={settings.attributeRowShow} - options={rowNodeInformation.attributes} - onChange={(val: string[] | string) => { - const updatedVal = Array.isArray(val) ? val : [val]; - updateSettings({ attributeRowShow: updatedVal }); - }} - /> - </div> - )} + + <Accordion> + <AccordionItem> + <AccordionHead> + <span className="font-semibold">attributes: </span> + </AccordionHead> + <AccordionBody> + <Input + type="checkbox" + value={settings.attributeRowShow} + options={rowNodeInformation.attributes} + onChange={(val: string[] | string) => { + const updatedVal = Array.isArray(val) ? val : [val]; + updateSettings({ attributeRowShow: updatedVal }); + }} + /> + </AccordionBody> + </AccordionItem> + </Accordion> <div> <span className="text-xs font-semibold">Node used in Column</span> @@ -1075,30 +1072,24 @@ const PaohSettings = ({ settings, graphMetadata, updateSettings }: Visualization /> </div> - <Button - variantType="secondary" - variant="ghost" - size="sm" - className="-pt-3 mt-1" - onClick={toggleCollapseAttrColumns} - iconComponent={areCollapsedAttrColumns ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'} - > - attributes:{' '} - </Button> - - {!areCollapsedAttrColumns && ( - <div className=""> - <Input - type="checkbox" - value={settings.attributeColumnShow} - options={columnsNodeInformation.attributes} - onChange={(val: string[] | string) => { - const updatedVal = Array.isArray(val) ? val : [val]; - updateSettings({ attributeColumnShow: updatedVal }); - }} - /> - </div> - )} + <Accordion> + <AccordionItem> + <AccordionHead> + <span className="font-semibold">attributes: </span> + </AccordionHead> + <AccordionBody> + <Input + type="checkbox" + value={settings.attributeColumnShow} + options={columnsNodeInformation.attributes} + onChange={(val: string[] | string) => { + const updatedVal = Array.isArray(val) ? val : [val]; + updateSettings({ attributeColumnShow: updatedVal }); + }} + /> + </AccordionBody> + </AccordionItem> + </Accordion> <Input type="slider" diff --git a/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx b/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx index 873f15a8fc9eeb7ce9d558a836ab4b55485f6b84..816d1bd9070e0e6e0896f580a620698726323f74 100644 --- a/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx +++ b/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx @@ -6,6 +6,7 @@ import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/confi import { Button } from '@graphpolaris/shared/lib/components/buttons'; import { useSearchResultData } from '@graphpolaris/shared/lib/data-access'; import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill'; +import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '@graphpolaris/shared/lib/components/accordion'; import html2canvas from 'html2canvas'; export interface TableVisHandle { @@ -148,11 +149,6 @@ const TableSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio } }, [graphMetadata]); - const [areCollapsedAttr, setAreCollapsedAttr] = useState<boolean>(true); - const toggleCollapseAttr = () => { - setAreCollapsedAttr(!areCollapsedAttr); - }; - const selectedNodeAttributes = useMemo(() => { if (settings.displayEntity) { const nodeType = graphMetadata.nodes.types[settings.displayEntity]; @@ -214,20 +210,12 @@ const TableSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio onChange={(val) => updateSettings({ maxBarsCount: val })} /> </div> - <div className="flex flex-col items-start space-y-2"> - <span className="text-sm">Attributes to display:</span> - <Button - className="w-full text-justify justify-start" - variantType="secondary" - variant="ghost" - size="sm" - onClick={toggleCollapseAttr} - iconComponent={areCollapsedAttr ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'} - > - attributes:{' '} - </Button> - <div className=""> - {!areCollapsedAttr && ( + <Accordion> + <AccordionItem> + <AccordionHead> + <span className="text-sm">Attributes to display:</span> + </AccordionHead> + <AccordionBody> <Input type="checkbox" value={settings.displayAttributes} @@ -237,9 +225,10 @@ const TableSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio updateSettings({ displayAttributes: updatedVal }); }} /> - )} - </div> - </div> + test + </AccordionBody> + </AccordionItem> + </Accordion> </div> </SettingsContainer> );