Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • graphpolaris/frontend-v2
  • rijkheere/frontend-v-2-reordering-paoh
2 results
Show changes
Commits on Source (4)
Showing
with 514 additions and 175 deletions
......@@ -61,6 +61,7 @@ export const schemaSlice = createSlice({
setSchema: (state, action: PayloadAction<SchemaGraph>) => {
if (action.payload === undefined) throw new Error('Schema is undefined');
state.graph = action.payload;
state.loading = false;
},
clearSchema: (state) => {
state.graph = new SchemaGraphology().export();
......
......@@ -31,6 +31,9 @@ export class ListLayoutFactory implements ILayoutFactory<ListLayoutAlgorithms> {
const Y_OFFSET = 50;
const X_RELATION_OFFSET = 50;
const X_RELATION_OVERLAP_OFFSET = X_RELATION_OFFSET * 0.05;
const RELATION_IDENTIFIER = 'relation';
export abstract class ListLayout extends Layout<ListLayoutProvider> {
protected defaultLayoutSettings = {
......@@ -80,7 +83,7 @@ export class ListNodesFirstLayout extends ListLayout {
boundingBox = { x1: 0, x2: 1000, y1: 0, y2: 1000 };
}
const relationNodes = graph.nodes().filter((node) => node.startsWith('Relation'));
const relationNodes = graph.nodes().filter((node) => node.toLowerCase().startsWith(RELATION_IDENTIFIER));
const entityNodes = graph.nodes().filter((node) => !relationNodes.includes(node));
let y = 0;
......@@ -90,10 +93,12 @@ export class ListNodesFirstLayout extends ListLayout {
graph.updateNodeAttribute(node, 'y', () => y);
});
let nodeXIncrement = 0;
relationNodes.map((node, index) => {
const relationsY = y + Y_OFFSET + index * Y_OFFSET;
graph.updateNodeAttribute(node, 'x', () => boundingBox.x1);
graph.updateNodeAttribute(node, 'x', () => boundingBox.x1 + nodeXIncrement);
graph.updateNodeAttribute(node, 'y', () => relationsY);
nodeXIncrement += X_RELATION_OVERLAP_OFFSET;
});
if (this.verbose) {
......@@ -124,14 +129,16 @@ export class ListEdgesFirstLayout extends ListLayout {
boundingBox = { x1: 0, x2: 1000, y1: 0, y2: 1000 };
}
const relationNodes = graph.nodes().filter((node) => node.startsWith('Relation'));
const relationNodes = graph.nodes().filter((node) => node.toLowerCase().startsWith(RELATION_IDENTIFIER));
const entityNodes = graph.nodes().filter((node) => !relationNodes.includes(node));
let y = 0;
let nodeXIncrement = 0;
relationNodes.map((node, index) => {
y = index * Y_OFFSET;
graph.updateNodeAttribute(node, 'x', () => boundingBox.x1);
y = index * Y_OFFSET + nodeXIncrement;
graph.updateNodeAttribute(node, 'x', () => +X_RELATION_OFFSET + nodeXIncrement);
graph.updateNodeAttribute(node, 'y', () => y);
nodeXIncrement += X_RELATION_OVERLAP_OFFSET;
});
entityNodes.map((node, index) => {
......@@ -168,7 +175,7 @@ export class ListIntersectedLayout extends ListLayout {
boundingBox = { x1: 0, x2: 1000, y1: 0, y2: 1000 };
}
const relationNodes = graph.nodes().filter((node) => node.startsWith('Relation'));
const relationNodes = graph.nodes().filter((node) => node.toLowerCase().startsWith(RELATION_IDENTIFIER));
const entityNodes = graph.nodes().filter((node) => !relationNodes.includes(node));
const graphAllNodes = graph.nodes();
......@@ -187,13 +194,16 @@ export class ListIntersectedLayout extends ListLayout {
});
let y = 0;
let nodeXIncrement = 0;
intersectedList.map((node, index) => {
y = index * Y_OFFSET;
graph.updateNodeAttribute(node, 'x', () => {
if (node.startsWith('Relation')) {
return boundingBox.x1 + X_RELATION_OFFSET;
nodeXIncrement += X_RELATION_OVERLAP_OFFSET;
if (node.toLowerCase().startsWith(RELATION_IDENTIFIER)) {
return boundingBox.x1 + X_RELATION_OFFSET + nodeXIncrement;
} else {
return boundingBox.x1;
return boundingBox.x1 + nodeXIncrement;
}
});
graph.updateNodeAttribute(node, 'y', () => y);
......
......@@ -56,7 +56,6 @@ export function InspectorPanel(props: { children?: React.ReactNode }) {
{inspector}
<div className="flex flex-col w-full">
{buildInfo === 'dev' && (
<div className="mt-auto p-2 bg-light">
<Button
variantType="primary"
......@@ -72,7 +71,6 @@ export function InspectorPanel(props: { children?: React.ReactNode }) {
className="block w-full"
/>
</div>
)}
</div>
</Panel>
);
......
......@@ -21,7 +21,8 @@ export type MockDataI = typeof mockDataArray[number];
export const loadMockData = async (fileName: MockDataI) => {
const json = await import(`./${fileName.replace('_', '/')}.json` /* @vite-ignore */);
const filename = `./${fileName.replace('_', '/')}.json`;
const json = await import(filename /* @vite-ignore */);
const { nodes, edges, metaData } = graphQueryBackend2graphQuery(json.default);
return {
data: {
......
import React from 'react';
import { Meta } from '@storybook/react';
import { SchemaUtils } from '@graphpolaris/shared/lib/schema/schema-utils';
import { schemaSlice, setSchema } from '@graphpolaris/shared/lib/data-access/store';
import { movieSchemaRaw, northwindSchemaRaw, twitterSchemaRaw } from '@graphpolaris/shared/lib/mock-data';
import { SchemaUtils } from '@graphpolaris/shared/lib/schema/schema-utils';
import { configureStore } from '@reduxjs/toolkit';
import { Meta } from '@storybook/react';
import { Provider } from 'react-redux';
import { Schema } from './Schema';
import { movieSchemaRaw } from '@graphpolaris/shared/lib/mock-data';
const Component: Meta<typeof Schema> = {
/* 👇 The title prop is optional.
......@@ -100,11 +99,14 @@ export const TestTooltip = {
},
};
export const TestMovieSchema = {
play: async () => {
console.log('TestMovieSchema');
const dispatch = Mockstore.dispatch;
const schema = SchemaUtils.schemaBackend2Graphology(movieSchemaRaw);
const data = await movieSchemaRaw;
console.log('data', data);
const schema = SchemaUtils.schemaBackend2Graphology(data);
dispatch(setSchema(schema.export()));
},
......@@ -113,17 +115,16 @@ export const TestMovieSchema = {
export const TestNorthWindSchema = {
play: async () => {
const dispatch = Mockstore.dispatch;
const schema = SchemaUtils.schemaBackend2Graphology(northwindSchemaRaw);
const schema = await SchemaUtils.schemaBackend2Graphology(northwindSchemaRaw);
dispatch(setSchema(schema.export()));
},
};
export const TestTwitterSchema = {
play: async () => {
const dispatch = Mockstore.dispatch;
const schema = SchemaUtils.schemaBackend2Graphology(twitterSchemaRaw);
const schema = await SchemaUtils.schemaBackend2Graphology(twitterSchemaRaw);
dispatch(setSchema(schema.export()));
},
......
......@@ -30,7 +30,7 @@ export const Visualizations: Record<string, PromiseFunc> = {
SemanticSubstratesVis: () => import('../visualizations/semanticsubstratesvis/semanticsubstratesvis'),
}),
...(isVisualizationReleased('MapVis') && { MapVis: () => import('../visualizations/mapvis/mapvis') }),
...(isVisualizationReleased('Vis0D') && { Vis0D: () => import('../visualizations/Vis0D/Vis0D') }),
...(isVisualizationReleased('Vis0D') && { Vis0D: () => import('../visualizations/vis0D/Vis0D') }),
...(isVisualizationReleased('Vis1D') && { Vis1D: () => import('../visualizations/vis1D/Vis1D') }),
};
......
import React, { useRef, useImperativeHandle, forwardRef } from 'react';
import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common';
import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config';
import html2canvas from 'html2canvas';
import { Input } from '@graphpolaris/shared/lib/components/inputs';
export interface Vis0DProps {
title: string;
}
const settings: Vis0DProps = {
title: '',
};
export interface Vis0DVisHandle {
exportImageInternal: () => void;
}
const formatNumber = (number: number) => {
return number.toLocaleString('de-DE');
};
const Vis0D = forwardRef<Vis0DVisHandle, VisualizationPropTypes<Vis0DProps>>(({ data, settings }, refExternal) => {
const internalRef = useRef<HTMLDivElement>(null);
useImperativeHandle(refExternal, () => ({
exportImageInternal() {
const captureImage = () => {
const element = internalRef.current;
if (element) {
html2canvas(element, {
backgroundColor: '#FFFFFF',
})
.then((canvas) => {
const finalImage = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = finalImage;
link.download = 'Vis0D.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch((error) => {
console.error('Error capturing image:', error);
});
} else {
console.error('Container element not found');
}
};
const renderCanvas = () => {
requestAnimationFrame(() => {
captureImage();
});
};
renderCanvas();
},
}));
// !FIXME: When stats pills are ready, substitue results accordingly
return (
<div className="h-full w-full flex flex-col items-center justify-center overflow-hidden" ref={internalRef}>
{settings.title && <span className="text-3xl text-center mb-4">{settings.title}</span>}
{data?.nodes?.length > 0 ? (
<span className="text-4xl text-center">Select 0D data</span>
) : (
<span className="text-8xl text-center">{formatNumber(1231312)}</span>
)}{' '}
</div>
);
});
const Vis0DSettings = ({ settings, updateSettings }: VisualizationSettingsPropTypes<Vis0DProps>) => {
return (
<SettingsContainer>
<Input type="text" label="Title" value={settings.title} onChange={(value) => updateSettings({ title: value as string })} />
</SettingsContainer>
);
};
const Vis0DRef = React.createRef<Vis0DVisHandle>();
export const Vis0DComponent: VISComponentType<Vis0DProps> = {
component: React.forwardRef((props: VisualizationPropTypes<Vis0DProps>, ref) => <Vis0D {...props} ref={Vis0DRef} />),
settingsComponent: Vis0DSettings,
settings: settings,
exportImage: () => {
if (Vis0DRef.current) {
Vis0DRef.current.exportImageInternal();
} else {
console.error('0Dvis reference is not set.');
}
},
};
export default Vis0DComponent;
......@@ -5,4 +5,4 @@ export * from './tablevis/tablevis';
export * from './matrixvis/matrixvis';
export * from './semanticsubstratesvis/semanticsubstratesvis';
export * from './vis1D/Vis1D';
export * from './Vis0D/Vis0D';
export * from './vis0D/Vis0D';
import React, { useEffect, useMemo, useRef, forwardRef, useImperativeHandle } from 'react';
import { Table, AugmentedNodeAttributes } from './components/Table';
import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common';
import { Input } from '@graphpolaris/shared/lib/components/inputs';
import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config';
import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '@graphpolaris/shared/lib/components/accordion';
import { Button } from '@graphpolaris/shared/lib/components/buttons';
import { useSearchResultData } from '@graphpolaris/shared/lib/data-access';
import { Input } from '@graphpolaris/shared/lib/components/inputs';
import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill';
import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '@graphpolaris/shared/lib/components/accordion';
import { useSearchResultData } from '@graphpolaris/shared/lib/data-access';
import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config';
import html2canvas from 'html2canvas';
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
import { VISComponentType, VisualizationPropTypes, VisualizationSettingsPropTypes } from '../../common';
import { AugmentedNodeAttributes, Table } from './components/Table';
export interface TableVisHandle {
exportImageInternal: () => void;
......
import React, { useRef, useImperativeHandle, forwardRef, useEffect, useState, Fragment } from 'react';
import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common';
import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config';
import html2canvas from 'html2canvas';
import { Input } from '@graphpolaris/shared/lib/components/inputs';
import { EntityPill, RelationPill } from '@graphpolaris/shared/lib/components/pills/Pill';
import { Button } from '@graphpolaris/shared/lib/components/buttons';
export interface Vis0DProps {
title: string;
selectedEntity: string;
selectedAttribute: string;
selectedStat: string;
}
const settings: Vis0DProps = {
title: '',
selectedEntity: '',
selectedAttribute: '',
selectedStat: '',
};
export interface Vis0DVisHandle {
exportImageInternal: () => void;
}
const formatNumber = (number: number) => {
return number.toLocaleString('de-DE');
};
const Vis0D = forwardRef<Vis0DVisHandle, VisualizationPropTypes<Vis0DProps>>(({ settings, graphMetadata }, refExternal) => {
const [statRender, setStatRender] = useState<number | undefined>(undefined);
const internalRef = useRef<HTMLDivElement>(null);
useImperativeHandle(refExternal, () => ({
exportImageInternal() {
const captureImage = () => {
const element = internalRef.current;
if (element) {
html2canvas(element, {
backgroundColor: '#FFFFFF',
})
.then((canvas) => {
const finalImage = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = finalImage;
link.download = 'Vis0D.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch((error) => {
console.error('Error capturing image:', error);
});
} else {
console.error('Container element not found');
}
};
const renderCanvas = () => {
requestAnimationFrame(() => {
captureImage();
});
};
renderCanvas();
},
}));
useEffect(() => {
if (settings.selectedEntity != '' && graphMetadata.nodes.types && settings.selectedAttribute != '' && settings.selectedStat != '') {
const nodesLabels = graphMetadata.nodes.labels;
let attributes = [];
if (nodesLabels.includes(settings.selectedEntity)) {
attributes = Object.keys(graphMetadata.nodes.types[settings.selectedEntity].attributes);
} else {
attributes = Object.keys(graphMetadata.edges.types[settings.selectedEntity].attributes);
}
if (attributes.includes(settings.selectedAttribute)) {
let statsAvailable = [];
if (nodesLabels.includes(settings.selectedEntity)) {
statsAvailable = Object.keys(
graphMetadata.nodes.types[settings.selectedEntity].attributes[settings.selectedAttribute].statistics,
);
} else {
statsAvailable = Object.keys(
graphMetadata.edges.types[settings.selectedEntity].attributes[settings.selectedAttribute].statistics,
);
}
if (statsAvailable.includes(settings.selectedStat)) {
let statValue = 0;
if (nodesLabels.includes(settings.selectedEntity)) {
statValue = (
graphMetadata.nodes.types[settings.selectedEntity].attributes[settings.selectedAttribute].statistics as Record<string, number>
)[settings.selectedStat];
} else {
statValue = (
graphMetadata.edges.types[settings.selectedEntity].attributes[settings.selectedAttribute].statistics as Record<string, number>
)[settings.selectedStat];
}
setStatRender(statValue);
} else {
setStatRender(undefined);
}
} else {
setStatRender(undefined);
}
}
}, [settings.selectedEntity, settings.selectedAttribute, settings.selectedStat]);
return (
<div className="h-full w-full flex flex-col items-center justify-center overflow-hidden" ref={internalRef}>
{settings.title && <span className="text-3xl text-center mb-4">{settings.title}</span>}
{statRender === undefined ? (
<span className="text-4xl text-center">Select 0D data</span>
) : (
<span className="text-8xl text-center">{formatNumber(statRender)}</span>
)}
</div>
);
});
const Vis0DSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<Vis0DProps>) => {
const [attributeOptions, setAttributeOptions] = useState<string[]>([]);
const [statsOptions, setStatsOptions] = useState<string[]>([]);
useEffect(() => {
if (settings.selectedEntity === '' && graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0) {
const firstEntity = graphMetadata.nodes.labels[0];
const attributesFirstEntity = Object.keys(graphMetadata.nodes.types[firstEntity].attributes);
setAttributeOptions(attributesFirstEntity);
const selectedAttribute = attributesFirstEntity[0];
const attributeSelectedStatistics = graphMetadata.nodes.types[firstEntity].attributes[selectedAttribute].statistics;
const notNaNStats = Object.keys(attributeSelectedStatistics).filter((key) => {
const value = attributeSelectedStatistics[key as keyof typeof attributeSelectedStatistics];
return typeof value === 'number' && !isNaN(value);
});
setStatsOptions(notNaNStats as string[]);
updateSettings({ selectedEntity: firstEntity, selectedAttribute: selectedAttribute, selectedStat: notNaNStats[0] });
}
}, [graphMetadata]);
useEffect(() => {
if (
settings.selectedEntity != '' &&
settings.selectedAttribute != '' &&
graphMetadata &&
graphMetadata.nodes &&
graphMetadata.nodes.labels.length > 0
) {
const nodesLabels = graphMetadata.nodes.labels;
// attribute management
let attributesFirstEntity = [];
if (nodesLabels.includes(settings.selectedEntity)) {
attributesFirstEntity = Object.keys(graphMetadata.nodes.types[settings.selectedEntity].attributes);
} else {
attributesFirstEntity = Object.keys(graphMetadata.edges.types[settings.selectedEntity].attributes);
}
setAttributeOptions(attributesFirstEntity);
let selectedAttribute = '';
if (settings.selectedAttribute === '' || !attributesFirstEntity.includes(settings.selectedAttribute)) {
selectedAttribute = attributesFirstEntity[0];
updateSettings({ selectedAttribute: selectedAttribute });
} else {
selectedAttribute = settings.selectedAttribute;
}
// stat management
let attributeSelectedStatistics: Record<string, number> = {};
if (nodesLabels.includes(settings.selectedEntity)) {
attributeSelectedStatistics = graphMetadata.nodes.types[settings.selectedEntity].attributes[selectedAttribute].statistics as Record<
string,
number
>;
} else {
attributeSelectedStatistics = graphMetadata.edges.types[settings.selectedEntity].attributes[selectedAttribute].statistics as Record<
string,
number
>;
}
const notNaNStats = Object.keys(attributeSelectedStatistics).filter((key) => {
const value = attributeSelectedStatistics[key as keyof typeof attributeSelectedStatistics];
// !TODO: include string stats
return !isNaN(value);
});
setStatsOptions(notNaNStats as string[]);
if (settings.selectedStat == '' || !notNaNStats.includes(settings.selectedStat)) {
updateSettings({ selectedStat: notNaNStats[0] });
} else {
updateSettings({ selectedStat: settings.selectedStat });
}
}
}, [settings.selectedEntity, settings.selectedAttribute]);
return (
<SettingsContainer>
<div className="p-1">
<Input
className="mb-2"
type="text"
label="Title"
value={settings.title}
onChange={(value) => updateSettings({ title: value as string })}
/>
<Input
className="w-full text-justify justify-start mb-2"
type="dropdown"
value={settings.selectedEntity}
options={[...graphMetadata.nodes.labels, ...graphMetadata.edges.labels]}
onChange={(val) => updateSettings({ selectedEntity: val as string })}
overrideRender={
graphMetadata.nodes.labels.includes(settings.selectedEntity) ? (
<EntityPill
title={
<div className="flex flex-row justify-between items-center cursor-pointer">
<span>{settings.selectedEntity || ''}</span>
<Button variantType="secondary" variant="ghost" size="2xs" iconComponent="icon-[ic--baseline-arrow-drop-down]" />
</div>
}
/>
) : (
<RelationPill
title={
<div className="flex flex-row justify-between items-center cursor-pointer">
<span>{settings.selectedEntity || ''}</span>
<Button variantType="secondary" variant="ghost" size="2xs" iconComponent="icon-[ic--baseline-arrow-drop-down]" />
</div>
}
/>
)
}
></Input>
<div className="mb-2 w-full">
<Input
className="w-full text-justify justify-start"
type="dropdown"
value={settings.selectedAttribute}
options={attributeOptions}
onChange={(val) => updateSettings({ selectedAttribute: val as string })}
></Input>
</div>
<div className="mb-2">
<Input
className="w-full text-justify justify-start"
type="dropdown"
value={settings.selectedStat}
options={statsOptions}
onChange={(val) => updateSettings({ selectedStat: val as string })}
></Input>
</div>
</div>
</SettingsContainer>
);
};
const Vis0DRef = React.createRef<Vis0DVisHandle>();
export const Vis0DComponent: VISComponentType<Vis0DProps> = {
component: React.forwardRef((props: VisualizationPropTypes<Vis0DProps>, ref) => <Vis0D {...props} ref={Vis0DRef} />),
settingsComponent: Vis0DSettings,
settings: settings,
exportImage: () => {
if (Vis0DRef.current) {
Vis0DRef.current.exportImageInternal();
} else {
console.error('0Dvis reference is not set.');
}
},
};
export default Vis0DComponent;
......@@ -4,27 +4,31 @@ import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/confi
import html2canvas from 'html2canvas';
import { CustomChartPlotly, plotTypeOptions } from './components/CustomChartPlotly';
import { Input } from '@graphpolaris/shared/lib/components/inputs';
import { DropdownTextAndIcon } from '@graphpolaris/shared/lib/components/selectors/textAndIcon';
import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill';
import { Button } from '@graphpolaris/shared/lib/components/buttons';
export interface Vis1DProps {
plotType: (typeof plotTypeOptions)[number];
title: string;
attribute?: string;
nodeLabel: string;
plotType: (typeof plotTypeOptions)[number]; // plotly plot type
title: string; // title of the plot
nodeLabel: string; // node label to plot
xAxisLabel?: string;
yAxisLabel?: string;
showAxis: boolean;
}
const defaultSettings: Vis1DProps = {
plotType: 'bar',
title: '',
attribute: '',
nodeLabel: '',
xAxisLabel: '',
yAxisLabel: '',
showAxis: true,
};
export interface Vis1DVisHandle {
exportImageInternal: () => void;
}
const Vis1D = forwardRef<Vis1DVisHandle, VisualizationPropTypes<Vis1DProps>>(({ data, settings }, refExternal) => {
const internalRef = useRef<HTMLDivElement>(null);
......@@ -63,48 +67,52 @@ const Vis1D = forwardRef<Vis1DVisHandle, VisualizationPropTypes<Vis1DProps>>(({
},
}));
const attributeValues = useMemo(() => {
if (!settings.nodeLabel || !settings.attribute) {
const getAttributeValues = (attributeKey: string | number | undefined) => {
if (!settings.nodeLabel || !attributeKey) {
return [];
}
return data.nodes
.filter((item) => item.label === settings.nodeLabel && item.attributes && settings.attribute! in item.attributes)
.map((item) => item.attributes[settings.attribute!] as string | number);
}, [data, settings.nodeLabel, settings.attribute]);
.filter((item) => item.label === settings.nodeLabel && item.attributes && attributeKey in item.attributes)
.map((item) => item.attributes[attributeKey]);
};
const xAxisData = useMemo(() => getAttributeValues(settings.xAxisLabel), [data, settings.nodeLabel, settings.xAxisLabel]);
const yAxisData = useMemo(() => getAttributeValues(settings.yAxisLabel), [data, settings.nodeLabel, settings.yAxisLabel]);
return (
<div className="h-full w-full flex items-center justify-center overflow-hidden" ref={internalRef}>
<CustomChartPlotly data={attributeValues as string[] | number[]} plotType={settings.plotType} title={settings.title} />
<CustomChartPlotly
xAxisData={xAxisData as string[] | number[]}
yAxisData={yAxisData as string[] | number[]}
plotType={settings.plotType}
title={settings.title}
showAxis={settings.showAxis}
xAxisLabel={settings.xAxisLabel}
yAxisLabel={settings.yAxisLabel}
/>
</div>
);
});
const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<Vis1DProps>) => {
const mutablePlotTypes = [...plotTypeOptions];
const [attributeOptions, setAttributeOptions] = useState<{ name: string; type: string }[]>([]);
const [selectedOption, setSelectedOption] = useState<{ name: string; type: string } | null>(null);
const [attributeOptions, setAttributeOptions] = useState<string[]>([]);
const handleChange = (option: { name: string; type: string }) => {
setSelectedOption(option);
updateSettings({ attribute: option.name });
};
useEffect(() => {
if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0) {
if (settings.nodeLabel === '' && graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0) {
const nodeLabelTemp = graphMetadata.nodes.labels[0];
updateSettings({ nodeLabel: nodeLabelTemp });
}
}, [graphMetadata]);
}, [settings.nodeLabel, graphMetadata]);
useEffect(() => {
if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0 && settings.nodeLabel != '') {
const newAttributeOptions = Object.entries(graphMetadata.nodes.types[settings.nodeLabel].attributes).map(([key, value]) => ({
name: key,
type: value.attributeType,
}));
updateSettings({ attribute: newAttributeOptions[0].name });
const newAttributeOptions = Object.keys(graphMetadata.nodes.types[settings.nodeLabel].attributes);
if (settings.xAxisLabel === '') {
updateSettings({ xAxisLabel: newAttributeOptions[0] });
}
// initialize the selected option for creating the dropdown and plots
setSelectedOption(newAttributeOptions[0]);
setAttributeOptions(newAttributeOptions);
}
}, [graphMetadata, settings.nodeLabel]);
......@@ -129,14 +137,13 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
/>
}
/>
<div className="mb-2">
<Input type="text" label="Title" value={settings.title} onChange={(value) => updateSettings({ title: value as string })} />
</div>
<div className="mb-2">
<Input
type="dropdown"
label="Type Chart"
label="Chart"
value={settings.plotType}
options={mutablePlotTypes}
onChange={(value: string | number) => {
......@@ -145,7 +152,31 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
/>
</div>
<div className="mb-2">
<DropdownTextAndIcon value={selectedOption} onChange={handleChange} options={attributeOptions} />
<Input
type="dropdown"
label="X-axis:"
value={settings.xAxisLabel}
options={attributeOptions}
onChange={(value) => {
updateSettings({ xAxisLabel: value as string });
}}
/>
</div>
{(settings.plotType === 'scatter' || settings.plotType === 'line') && (
<div className="mb-2">
<Input
type="dropdown"
label="Y-axis:"
value={settings.yAxisLabel}
options={attributeOptions}
onChange={(value) => {
updateSettings({ yAxisLabel: value as string });
}}
/>
</div>
)}
<div className="mb-2">
<Input type="boolean" label="Show axis" value={settings.showAxis} onChange={(val) => updateSettings({ showAxis: val })} />
</div>
</div>
</SettingsContainer>
......
import { visualizationColors } from 'config';
import React, { useRef, useEffect, useState } from 'react';
import Plot from 'react-plotly.js';
import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip';
const getCSSVariableHSL = (varName: string) => {
const rootStyles = getComputedStyle(document.documentElement);
const hslValue = rootStyles.getPropertyValue(varName).trim().replace('deg', '');
return `hsl(${hslValue})`;
};
export const plotTypeOptions = ['bar', 'scatter', 'line', 'histogram', 'pie'] as const;
export interface CustomChartPlotlyProps {
data: string[] | number[];
xAxisData: string[] | number[];
plotType: (typeof plotTypeOptions)[number];
title: string;
showAxis: boolean;
yAxisData: string[] | number[];
xAxisLabel?: string;
yAxisLabel?: string;
}
export const getPlotData = (data: (string | number)[], plotType: (typeof plotTypeOptions)[number]): Partial<Plotly.PlotData>[] => {
export const getPlotData = (
xAxisData: (string | number)[],
plotType: (typeof plotTypeOptions)[number],
yAxisData?: (string | number)[],
): Partial<Plotly.PlotData>[] => {
const mainColors = visualizationColors.GPCat.colors[14];
const xValues = data.map((_, index) => index + 1);
const primaryColor = getCSSVariableHSL('--clr-sec--400');
let xValues: (string | number)[] = [];
let yValues: (string | number)[] = [];
if (plotType === 'scatter' || plotType === 'line') {
if (xAxisData.length != 0 && yAxisData && yAxisData.length != 0) {
xValues = xAxisData;
yValues = yAxisData;
} else if (xAxisData.length != 0 && yAxisData && yAxisData.length == 0) {
xValues = xAxisData;
yValues = xAxisData.map((_, index) => index + 1);
} else if (xAxisData.length == 0 && yAxisData && yAxisData.length != 0) {
xValues = yAxisData.map((_, index) => index + 1);
yValues = yAxisData;
} else if (xAxisData.length == 0 && yAxisData && yAxisData.length == 0) {
} else {
}
} else {
xValues = xAxisData;
yValues = xAxisData.map((_, index) => index + 1);
}
switch (plotType) {
case 'bar':
......@@ -20,8 +55,9 @@ export const getPlotData = (data: (string | number)[], plotType: (typeof plotTyp
{
type: 'bar',
x: xValues,
y: data,
marker: { color: mainColors },
y: yValues,
marker: { color: primaryColor },
hoverinfo: 'none',
},
];
case 'scatter':
......@@ -29,9 +65,10 @@ export const getPlotData = (data: (string | number)[], plotType: (typeof plotTyp
{
type: 'scatter',
x: xValues,
y: data,
y: yValues,
mode: 'markers',
marker: { color: mainColors, size: 12 },
marker: { color: primaryColor, size: 12 },
hoverinfo: 'none',
},
];
case 'line':
......@@ -39,17 +76,19 @@ export const getPlotData = (data: (string | number)[], plotType: (typeof plotTyp
{
type: 'scatter',
x: xValues,
y: data,
y: yValues,
mode: 'lines',
line: { color: mainColors },
line: { color: primaryColor },
hoverinfo: 'none',
},
];
case 'histogram':
return [
{
type: 'histogram',
x: data,
marker: { color: mainColors },
x: xAxisData,
marker: { color: primaryColor },
hoverinfo: 'none',
},
];
case 'pie':
......@@ -57,18 +96,29 @@ export const getPlotData = (data: (string | number)[], plotType: (typeof plotTyp
{
type: 'pie',
labels: xValues.map(String),
values: data,
values: xAxisData,
marker: { colors: mainColors },
hoverinfo: 'none',
},
];
default:
return [];
}
};
export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({ data, plotType, title }) => {
export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({
xAxisData,
plotType,
title,
showAxis,
yAxisData,
xAxisLabel,
yAxisLabel,
}) => {
const internalRef = useRef<HTMLDivElement>(null);
const [divSize, setDivSize] = useState({ width: 0, height: 0 });
const [hoveredPoint, setHoveredPoint] = useState<{ left: number; top: number; value: number } | null>(null);
useEffect(() => {
const handleResize = () => {
......@@ -78,7 +128,7 @@ export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({ data, plot
}
};
handleResize(); // Set initial size
handleResize();
window.addEventListener('resize', handleResize);
if (internalRef.current) {
new ResizeObserver(handleResize).observe(internalRef.current);
......@@ -89,28 +139,86 @@ export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({ data, plot
};
}, []);
const handleHover = (event: any) => {
const { points } = event;
if (points.length) {
const point = points[0];
const plotRect = internalRef.current?.getBoundingClientRect(); // Get the plot's bounding box
if (plotRect) {
// Calculate the position of the tooltip
const xIndex = point.xaxis.d2p(point.x); // Convert x value to pixel position
const yIndex = point.yaxis.d2p(point.y); // Convert y value to pixel position
setHoveredPoint({
left: xIndex, // Center tooltip above the point
top: plotRect.top + yIndex, // Position below the point
value: point.y, // Value to display
});
}
}
};
return (
<div className="h-full w-full flex items-center justify-center overflow-hidden" ref={internalRef}>
<div className="h-full w-full flex items-center justify-center overflow-hidden relative" ref={internalRef}>
<Plot
data={getPlotData(data, plotType)}
config={{ responsive: true, displayModeBar: false }}
data={getPlotData(xAxisData, plotType, yAxisData)}
config={{
responsive: true,
scrollZoom: false,
displayModeBar: false,
staticPlot: true,
displaylogo: false,
}}
layout={{
width: divSize.width,
height: divSize.height,
title: title,
dragmode: false,
font: {
family: 'Inter, sans-serif',
size: 16,
color: '#374151',
size: 12,
color: '#374151', // change to gp default color
},
xaxis: {
title: 'Category',
title: showAxis ? (xAxisLabel ? xAxisLabel : '') : '',
showgrid: false,
visible: showAxis,
showline: true,
zeroline: false,
},
yaxis: {
title: 'Value',
title: showAxis ? (yAxisLabel ? yAxisLabel : '') : '',
showgrid: false,
visible: showAxis,
showline: true,
zeroline: false,
},
}}
onHover={handleHover}
onUnhover={() => setHoveredPoint(null)}
/>
{hoveredPoint && (
<div>
<Tooltip open={true} showArrow={true}>
<TooltipTrigger />
<TooltipContent
style={{
position: 'absolute',
left: hoveredPoint.left,
top: hoveredPoint.top,
transform: 'translate(-50%, -100%)',
}}
>
<div>
<strong>Value:</strong> {hoveredPoint.value} <br />
</div>
</TooltipContent>
</Tooltip>
</div>
)}
</div>
);
};