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
import React, { useEffect, useState } from 'react';
import { ReportingForm } from './reporting/ReportingForm';
import { AlertingForm } from './alerting/AlertingForm';
import { Button, Icon } from '../components';
import { AddItem } from './components/AddItem';
import { StartScreen } from './components/StartScreen';
import { MonitorType } from './components/Sidebar';
type Props = {
active: string;
activeCategory: MonitorType | undefined;
adding: boolean;
setAdding: (val: boolean) => void;
setActiveCategory: (val: MonitorType | undefined) => void;
};
export function SettingsPanel(props: Props) {
const [contacts, setContacts] = useState<string[]>([]);
const getContacts = (): string[] => ['Jan', 'Piet'];
useEffect(() => {
if (!contacts) {
const userContacts = getContacts();
setContacts(userContacts);
}
}, []);
return props.activeCategory ? (
<div className="w-3/4 p-4">
{props.adding ? (
<AddItem category={props.activeCategory} />
) : (
props.active &&
props.activeCategory && (
<div>
{props.activeCategory === 'report' ? (
<ReportingForm activeTemplate={props.active} />
) : (
<AlertingForm activeTemplate={props.active} />
)}
</div>
)
)}
<div className="flex justify-end mt-2">
<Button label="Delete" variantType="secondary" variant="outline" />
<Button label="Save" variantType="primary" className="ml-2" />
</div>
</div>
) : (
<StartScreen setAdding={props.setAdding} setActiveCategory={props.setActiveCategory} />
);
}
import React, { useState } from 'react';
import { Accordion, AccordionItem, AccordionHead, AccordionBody } from '../../components/accordion';
import { TextEditor } from '../../components/textEditor';
import { EditorState } from 'lexical';
type Props = {
activeTemplate: string;
};
export function AlertingForm(props: Props) {
const [editorState, setEditorState] = useState<EditorState | undefined>(undefined);
return (
<div>
<span className="text-lg text-secondary-600 font-bold mb-4">Alert ID: {props.activeTemplate}</span>
<Accordion defaultOpenAll={true} className="border-t divide-y">
<AccordionItem className="pt-2 pb-4">
<AccordionHead showArrow={false}>
<span className="font-semibold">Recipient(s)</span>
</AccordionHead>
<AccordionBody>
<div>
<input className="border" />
</div>
</AccordionBody>
</AccordionItem>
<AccordionItem className="pt-2 pb-4">
<AccordionHead showArrow={false}>
<span className="font-semibold">Alerting text</span>
</AccordionHead>
<AccordionBody>
<TextEditor
editorState={editorState}
setEditorState={setEditorState}
showToolbar={true}
placeholder="Start typing your alert template..."
/>
</AccordionBody>
</AccordionItem>
</Accordion>
</div>
);
}
import React, { useState } from 'react';
import { Input } from '../../components';
import { MonitorType } from './Sidebar';
import { Input, Button } from '../../components';
import { MonitorType as InsightType, MonitorType } from './Sidebar';
import { useAppDispatch, useSessionCache } from '../../data-access';
import { addInsight } from '../../data-access/store/insightSharingSlice';
import { wsCreateInsight } from '../../data-access/broker/wsInsightSharing';
import { addError, addSuccess, } from '@graphpolaris/shared/lib/data-access/store/configSlice';
type Props = {
category: MonitorType;
setAdding: (val: false | MonitorType) => void;
setActive: (val: string) => void;
type: InsightType;
};
export function AddItem(props: Props) {
const [value, setValue] = useState<string>('');
const [name, setName] = useState<string>('');
const [description, setDescription] = useState<string>('');
const dispatch = useAppDispatch();
const session = useSessionCache();
const handleSave = async () => {
if (!name.trim()) {
dispatch(addError('Name is required'));
return;
}
const newInsight = {
name,
description,
recipients: [],
template: '',
frequency: props.type === 'report' ? 'Daily' : '',
saveStateId: session.currentSaveState || '',
type: props.type,
};
wsCreateInsight(newInsight, (data: any, status: string) => {
if (status === 'success') {
dispatch(addInsight(data));
props.setActive(data.id);
props.setAdding(false);
dispatch(addSuccess('Succesfully created ' + props.type));
} else {
console.error('Failed to create insight:', data);
dispatch(addError('Failed to create new ' + props.type))
}
});
};
return (
<div>
<span className="text-lg text-secondary-600 font-bold mb-4">Add a new {props.category}ing service</span>
<Input type="text" label="Name" value={value} onChange={setValue} />
<span className="text-lg text-secondary-600 font-bold mb-4">Add a new {props.type}ing service</span>
<Input type="text" label="Name" value={name} onChange={setName} className="mb-2" />
<Input type="text" label="Description" value={description} onChange={setDescription} className="mb-2" />
<Button label="Save" onClick={handleSave} disabled={!name || !description} className="mt-2" />
</div>
);
}
import React from 'react';
import React, { useEffect } from 'react';
import { Button } from '../../components';
import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../../components/accordion';
import { useAppDispatch, useInsights } from '../../data-access';
import { setInsights } from '../../data-access/store/insightSharingSlice';
import { useSessionCache } from '../../data-access';
import { wsGetInsights } from '../../data-access/broker/wsInsightSharing';
export type MonitorType = 'report' | 'alert';
type SidebarProps = {
reports: string[];
alerts: string[];
changeActive: (category: MonitorType, val: string) => void;
setAdding: (val: boolean) => void;
setActiveCategory: (val: MonitorType | undefined) => void;
setAdding: (val: false | MonitorType) => void;
setActive: (val: string) => void;
};
export function Sidebar(props: SidebarProps) {
const dispatch = useAppDispatch();
const session = useSessionCache();
const insights = useInsights();
useEffect(() => {
if (session.currentSaveState && session.currentSaveState !== '') {
wsGetInsights(session.currentSaveState, (data: any, status: string) => {
dispatch(setInsights(data));
});
}
}, [session.currentSaveState]);
return (
<div className="w-1/4 border-r overflow-auto flex flex-col h-full">
<>
<span className="text-lg text-secondary-700 font-semibold px-2 py-4">Insight Sharing</span>
<Accordion defaultOpenIndex={0}>
<Accordion defaultOpenAll={true}>
<AccordionItem className="">
<AccordionHead className="border-b bg-secondary-50 hover:bg-secondary-100 p-1">
<div className="w-full flex justify-between">
<span className="font-semibold">Reports</span>
<div
onClick={(e) => {
props.setAdding(true);
props.setActiveCategory('report');
props.setAdding('report');
e.stopPropagation();
}}
>
......@@ -41,18 +53,20 @@ export function Sidebar(props: SidebarProps) {
</AccordionHead>
<AccordionBody className="ml-0">
<ul className="space-y-2">
{props.reports.map((name, index) => (
<li
key={index}
className="cursor-pointer p-2 hover:bg-secondary-50"
onClick={() => {
props.changeActive('report', name);
props.setAdding(false);
}}
>
{name}
</li>
))}
{insights
.filter((insight) => insight.type === 'report')
.map((report) => (
<li
key={report.id}
className="cursor-pointer p-2 hover:bg-secondary-50"
onClick={() => {
props.setAdding(false);
props.setActive(report.id);
}}
>
{report.name}
</li>
))}
</ul>
</AccordionBody>
</AccordionItem>
......@@ -62,8 +76,7 @@ export function Sidebar(props: SidebarProps) {
<span className="font-semibold">Alerts</span>
<div
onClick={(e) => {
props.setAdding(true);
props.setActiveCategory('alert');
props.setAdding('alert');
e.stopPropagation();
}}
>
......@@ -80,22 +93,24 @@ export function Sidebar(props: SidebarProps) {
</AccordionHead>
<AccordionBody className="ml-0">
<ul className="space-y-2">
{props.alerts.map((name, index) => (
<li
key={index}
className="cursor-pointer p-2 hover:bg-secondary-50"
onClick={() => {
props.changeActive('alert', name);
props.setAdding(false);
}}
>
{name}
</li>
))}
{insights
.filter((insight) => insight.type === 'alert')
.map((alert) => (
<li
key={alert.id}
className="cursor-pointer p-2 hover:bg-secondary-50"
onClick={() => {
props.setAdding(false);
props.setActive(alert.id);
}}
>
{alert.name}
</li>
))}
</ul>
</AccordionBody>
</AccordionItem>
</Accordion>
</div>
</>
);
}
......@@ -3,13 +3,12 @@ import { Button, Icon } from '../../components';
import { MonitorType } from './Sidebar';
type Props = {
setAdding: (val: boolean) => void;
setActiveCategory: (val: MonitorType | undefined) => void;
setAdding: (val: false | MonitorType) => void;
};
export function StartScreen(props: Props) {
return (
<div className="w-full flex justify-center items-center">
<div className="w-full h-full flex justify-center items-center">
<div className="">
<span className="text-lg text-secondary-700 font-bold mb-4">Start</span>
<div>
......@@ -19,8 +18,7 @@ export function StartScreen(props: Props) {
variant="outline"
className="mb-2"
onClick={() => {
props.setAdding(true);
props.setActiveCategory('report');
props.setAdding('report');
}}
/>
<Button
......@@ -28,8 +26,7 @@ export function StartScreen(props: Props) {
label="New alert"
variant="outline"
onClick={() => {
props.setAdding(true);
props.setActiveCategory('alert');
props.setAdding('alert');
}}
/>
</div>
......
import React, { useState } from 'react';
import { Input, LoadingSpinner } from '../../components';
import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../../components/accordion';
import { TextEditor } from '../../components/textEditor';
import { EditorState } from 'lexical';
type Props = {
activeTemplate: string;
};
export function ReportingForm(props: Props) {
const [loading, setLoading] = useState(false);
const [editorState, setEditorState] = useState<EditorState | undefined>(undefined);
return loading ? (
<LoadingSpinner />
) : (
props.activeTemplate && (
<div>
<span className="text-lg text-secondary-600 font-bold mb-4">Report ID: {props.activeTemplate}</span>
<Accordion defaultOpenAll={true} className="border-t divide-y">
<AccordionItem className="pt-2 pb-4">
<AccordionHead showArrow={false}>
<span className="font-semibold">Recipient(s)</span>
</AccordionHead>
<AccordionBody>
<div>
<input className="border" />
</div>
</AccordionBody>
</AccordionItem>
<AccordionItem className="pt-2 pb-4">
<AccordionHead showArrow={false}>
<span className="font-semibold">Repeat</span>
</AccordionHead>
<AccordionBody>
<Input label="Frequency" type="dropdown" value={'Daily'} onChange={() => {}} options={['Daily', 'Weekly']} className="mb-1" />
</AccordionBody>
</AccordionItem>
<AccordionItem className="pt-2 pb-4">
<AccordionHead showArrow={false}>
<span className="font-semibold">Email template</span>
</AccordionHead>
<AccordionBody>
<TextEditor
editorState={editorState}
setEditorState={setEditorState}
showToolbar={true}
placeholder="Start typing your report template..."
/>
</AccordionBody>
</AccordionItem>
</Accordion>
</div>
)
);
}
......@@ -17,7 +17,7 @@ import { updateVisualization, addVisualization } from '../../data-access/store/v
import { VisualizationPropTypes, VISComponentType } from '../common';
import { ErrorBoundary } from '../../components/errorBoundary';
import { addError } from '../../data-access/store/configSlice';
import { canViewFeature } from '@graphpolaris/shared/lib/components/featureFlags/featureFlags';
import { canViewFeature } from '../../components/featureFlags';
type PromiseFunc = () => Promise<{ default: VISComponentType<any> }>;
export const Visualizations: Record<string, PromiseFunc> = {
......
......@@ -6,6 +6,7 @@ import { CustomChartPlotly, plotTypeOptions } from './components/CustomChartPlot
import { Input } from '@graphpolaris/shared/lib/components/inputs';
import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill';
import { Button } from '@graphpolaris/shared/lib/components/buttons';
import { GraphQueryResult } from '@graphpolaris/shared/lib/data-access';
export interface Vis1DProps {
plotType: (typeof plotTypeOptions)[number]; // plotly plot type
......@@ -13,7 +14,10 @@ export interface Vis1DProps {
selectedEntity: string; // node label to plot
xAxisLabel?: string;
yAxisLabel?: string;
zAxisLabel?: string;
showAxis: boolean;
groupData?: string;
stack: boolean;
}
const defaultSettings: Vis1DProps = {
......@@ -22,13 +26,34 @@ const defaultSettings: Vis1DProps = {
selectedEntity: '',
xAxisLabel: '',
yAxisLabel: '',
zAxisLabel: '',
showAxis: true,
groupData: undefined,
stack: false,
};
export interface Vis1DVisHandle {
exportImageInternal: () => void;
}
export const getAttributeValues = (query: GraphQueryResult, selectedEntity: string, attributeKey: string | number | undefined): any[] => {
if (!selectedEntity || !attributeKey) {
return [];
}
if (attributeKey == ' ') {
return [];
}
return query.nodes
.filter((item) => item.label === selectedEntity)
.map((item) => {
// Check if the attribute exists, return its value if it does, or an empty string otherwise
return item.attributes && attributeKey in item.attributes && item.attributes[attributeKey] != ''
? item.attributes[attributeKey]
: 'NoData';
});
};
const Vis1D = forwardRef<Vis1DVisHandle, VisualizationPropTypes<Vis1DProps>>(({ data, settings }, refExternal) => {
const internalRef = useRef<HTMLDivElement>(null);
......@@ -67,29 +92,33 @@ const Vis1D = forwardRef<Vis1DVisHandle, VisualizationPropTypes<Vis1DProps>>(({
},
}));
const getAttributeValues = (attributeKey: string | number | undefined) => {
if (!settings.selectedEntity || !attributeKey) {
return [];
}
return data.nodes
.filter((item) => item.label === settings.selectedEntity && item.attributes && attributeKey in item.attributes)
.map((item) => item.attributes[attributeKey]);
};
const xAxisData = useMemo(() => getAttributeValues(settings.xAxisLabel), [data, settings.selectedEntity, settings.xAxisLabel]);
const yAxisData = useMemo(() => getAttributeValues(settings.yAxisLabel), [data, settings.selectedEntity, settings.yAxisLabel]);
const xAxisData = useMemo(
() => getAttributeValues(data, settings.selectedEntity, settings.xAxisLabel),
[data, settings.selectedEntity, settings.xAxisLabel],
);
const yAxisData = useMemo(
() => getAttributeValues(data, settings.selectedEntity, settings.yAxisLabel),
[data, settings.selectedEntity, settings.yAxisLabel],
);
const zAxisData = useMemo(
() => getAttributeValues(data, settings.selectedEntity, settings.zAxisLabel),
[data, settings.selectedEntity, settings.zAxisLabel],
);
return (
<div className="h-full w-full flex items-center justify-center overflow-hidden" ref={internalRef}>
<CustomChartPlotly
xAxisData={xAxisData as string[] | number[]}
yAxisData={yAxisData as string[] | number[]}
zAxisData={zAxisData as string[] | number[]}
plotType={settings.plotType}
title={settings.title}
showAxis={settings.showAxis}
xAxisLabel={settings.xAxisLabel}
yAxisLabel={settings.yAxisLabel}
zAxisLabel={settings.zAxisLabel}
groupBy={settings.groupData}
stack={settings.stack}
/>
</div>
);
......@@ -120,7 +149,15 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
const newAttributeOptions = Object.keys(graphMetadata.nodes.types[settings.selectedEntity].attributes);
if (settings.xAxisLabel === '') {
updateSettings({ xAxisLabel: newAttributeOptions[0] });
// !TODO: instead of contain "datum" chekc type: if it is date
if (newAttributeOptions[0].includes('Datum')) {
updateSettings({ groupData: 'yearly' });
} else {
updateSettings({ groupData: undefined });
}
}
newAttributeOptions.unshift(' ');
setAttributeOptions(newAttributeOptions);
} else {
}
......@@ -158,6 +195,9 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
options={mutablePlotTypes}
onChange={(value: string | number) => {
updateSettings({ plotType: value as (typeof plotTypeOptions)[number] });
if (value === 'bar' || value === 'histogram' || value === 'pie') {
updateSettings({ yAxisLabel: '' });
}
}}
/>
</div>
......@@ -168,7 +208,14 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
value={settings.xAxisLabel}
options={attributeOptions}
onChange={(value) => {
updateSettings({ xAxisLabel: value as string });
const valueString = value as string;
updateSettings({ xAxisLabel: valueString });
if (!valueString.includes('Datum')) {
updateSettings({ groupData: undefined });
} else {
updateSettings({ groupData: 'monthly' });
}
}}
/>
</div>
......@@ -185,6 +232,37 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
/>
</div>
)}
{(settings.plotType === 'bar' || settings.plotType === 'scatter' || settings.plotType === 'histogram') && (
<div className="mb-2">
<Input
type="dropdown"
label="Color:"
value={settings.zAxisLabel}
options={attributeOptions}
onChange={(value) => {
updateSettings({ zAxisLabel: value as string });
}}
/>
</div>
)}
{settings.plotType === 'histogram' && (
<div className="mb-2">
<Input type="boolean" label="Normalize: " value={settings.stack} onChange={(val) => updateSettings({ stack: val })} />
</div>
)}
{settings.xAxisLabel?.includes('Datum') && (
<div className="mb-2">
<Input
type="dropdown"
label="Group Time:"
value={settings.groupData}
options={['', 'monthly', 'quarterly', 'yearly']}
onChange={(value) => {
updateSettings({ groupData: value as string });
}}
/>
</div>
)}
<div className="mb-2">
<Input type="boolean" label="Show axis" value={settings.showAxis} onChange={(val) => updateSettings({ showAxis: val })} />
</div>
......
......@@ -2,6 +2,8 @@ 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';
import { PlotType } from 'plotly.js';
import { scaleOrdinal, scaleLinear, scaleQuantize } from 'd3';
const getCSSVariableHSL = (varName: string) => {
const rootStyles = getComputedStyle(document.documentElement);
......@@ -9,121 +11,541 @@ const getCSSVariableHSL = (varName: string) => {
return `hsl(${hslValue})`;
};
export const plotTypeOptions = ['bar', 'scatter', 'line', 'histogram', 'pie'] as const;
export type SupportedPlotType = (typeof plotTypeOptions)[number];
export interface CustomChartPlotlyProps {
xAxisData: string[] | number[];
plotType: (typeof plotTypeOptions)[number];
yAxisData: string[] | number[];
zAxisData?: string[] | number[];
plotType: SupportedPlotType;
title: string;
showAxis: boolean;
yAxisData: string[] | number[];
xAxisLabel?: string;
yAxisLabel?: string;
zAxisLabel?: string;
groupBy?: string;
stack: boolean;
}
export const getPlotData = (
const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableData?: (string | number)[]) => {
// Function to parse the date-time string into a JavaScript Date object
const parseDate = (dateStr: string) => {
// Remove nanoseconds part and use just the standard "YYYY-MM-DD HH:MM:SS" part
const cleanedDateStr = dateStr.split('.')[0];
return new Date(cleanedDateStr);
};
// Grouping logic
const groupedData = xAxisData.reduce(
(acc, dateStr, index) => {
const date = parseDate(dateStr);
let groupKey: string;
if (groupBy === 'yearly') {
groupKey = date.getFullYear().toString(); // Group by year (e.g., "2012")
} else if (groupBy === 'quarterly') {
const month = date.getMonth() + 1; // Adjust month for zero-indexed months
const quarter = Math.floor((month - 1) / 3) + 1; // Calculate quarter (Q1-Q4)
groupKey = `${date.getFullYear()}-Q${quarter}`;
} else if (groupBy === 'monthly') {
// Group by month, e.g., "2012-07"
groupKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
} else {
// Default case: group by year (or some other grouping logic)
groupKey = date.getFullYear().toString();
}
// Initialize the group if it doesn't exist
if (!acc[groupKey]) {
acc[groupKey] = additionalVariableData
? typeof additionalVariableData[0] === 'number'
? 0 // Initialize sum for numbers
: [] // Initialize array for strings
: 0; // Initialize count for no additional data
}
// Aggregate additional variable if provided
if (additionalVariableData) {
if (typeof additionalVariableData[index] === 'number') {
acc[groupKey] = (acc[groupKey] as number) + (additionalVariableData[index] as number);
} else if (typeof additionalVariableData[index] === 'string') {
acc[groupKey] = [...(acc[groupKey] as string[]), additionalVariableData[index] as string];
}
} else {
// Increment the count if no additionalVariableData
acc[groupKey] = (acc[groupKey] as number) + 1;
}
return acc;
},
{} as Record<string, number | string[]>,
);
// Extract grouped data into arrays for Plotly
const xValuesGrouped = Object.keys(groupedData);
const yValuesGrouped = Object.values(groupedData);
return { xValuesGrouped, yValuesGrouped };
};
const computeStringTickValues = (xValues: any[], maxTicks: number, maxLabelLength: number): any[] => {
const truncatedValues = xValues.map((label) => (label.length > maxLabelLength ? `${label.slice(0, maxLabelLength)}…` : label));
return truncatedValues;
};
export const preparePlotData = (
xAxisData: (string | number)[],
plotType: (typeof plotTypeOptions)[number],
plotType: SupportedPlotType,
yAxisData?: (string | number)[],
): Partial<Plotly.PlotData>[] => {
zAxisData?: (string | number)[],
xAxisLabel?: string,
yAxisLabel?: string,
zAxisLabel?: string,
showAxis = true,
groupBy?: string,
stack?: boolean,
): { plotData: Partial<Plotly.PlotData>[]; layout: Partial<Plotly.Layout> } => {
const primaryColor = getCSSVariableHSL('--clr-sec--400');
const lengthLabelsX = 7; // !TODO computed number of elements based
const lengthLabelsY = 8; // !TODO computed number of elements based
const mainColors = visualizationColors.GPCat.colors[14];
const primaryColor = getCSSVariableHSL('--clr-sec--400');
const sharedTickFont = {
family: 'monospace',
size: 12,
color: '#374151', // !TODO get GP value
};
let xValues: (string | number)[] = [];
let yValues: (string | number)[] = [];
if (plotType === 'scatter' || plotType === 'line') {
if (xAxisData.length != 0 && yAxisData && yAxisData.length != 0) {
let colorScale: any;
let colorDataZ: string[] = [];
let colorbar: any = {};
if (zAxisData && zAxisData.length > 0 && typeof zAxisData[0] === 'number') {
const mainColorsSeq = visualizationColors.GPSeq.colors[9];
const numericZAxisData = zAxisData.filter((item): item is number => typeof item === 'number');
const zMin = numericZAxisData.reduce((min, val) => (val < min ? val : min), zAxisData[0]);
const zMax = numericZAxisData.reduce((max, val) => (val > max ? val : max), zAxisData[0]);
// !TODO: option to have a linear or quantize scale
colorScale = scaleQuantize<string>().domain([zMin, zMax]).range(mainColorsSeq);
colorDataZ = zAxisData?.map((item) => colorScale(item) || primaryColor);
colorbar = {
title: 'Color Legend',
tickvals: [zMin, zMax],
ticktext: [`${zMin}`, `${zMax}`],
};
} else {
const uniqueZAxisData = Array.from(new Set(zAxisData));
if (zAxisData && uniqueZAxisData) {
colorScale = scaleOrdinal<string>().domain(uniqueZAxisData.map(String)).range(mainColors);
colorDataZ = zAxisData?.map((item) => colorScale(String(item)) || primaryColor);
const sortedDomain = uniqueZAxisData.sort();
colorbar = {
title: 'Color Legend',
tickvals: sortedDomain,
ticktext: sortedDomain.map((val) => String(val)),
tickmode: 'array',
};
}
}
if (!groupBy) {
if (xAxisData.length !== 0 && yAxisData && yAxisData.length !== 0) {
xValues = xAxisData;
yValues = yAxisData;
} else if (xAxisData.length != 0 && yAxisData && yAxisData.length == 0) {
} 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) {
} 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);
if (groupBy) {
if (yAxisData && yAxisData.length !== 0) {
const { xValuesGrouped, yValuesGrouped } = groupByTime(xAxisData as string[], groupBy, yAxisData);
xValues = xValuesGrouped;
yValues = yValuesGrouped.flat();
} else {
const { xValuesGrouped, yValuesGrouped } = groupByTime(xAxisData as string[], groupBy);
xValues = xValuesGrouped;
yValues = yValuesGrouped.flat();
}
} else {
xValues = xAxisData;
yValues = xAxisData.map((_, index) => index + 1);
}
}
switch (plotType) {
case 'bar':
return [
{
type: 'bar',
x: xValues,
y: yValues,
marker: { color: primaryColor },
},
];
case 'scatter':
return [
{
type: 'scatter',
x: xValues,
y: yValues,
mode: 'markers',
marker: { color: primaryColor, size: 12 },
},
];
case 'line':
return [
{
type: 'scatter',
x: xValues,
y: yValues,
mode: 'lines',
line: { color: primaryColor },
},
];
case 'histogram':
// !TODO: Apply for other data types?
if (typeof xAxisData[0] === 'string') {
const frequencyMap = xAxisData.reduce(
(acc, item) => {
acc[item] = (acc[item] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
let sortedLabels: string[] = [];
let sortedFrequencies = [];
let truncatedXLabels: string[] = [];
let truncatedYLabels: string[] = [];
let yAxisRange: number[] = [];
if (typeof xValues[0] === 'string') {
truncatedXLabels = computeStringTickValues(xValues, 2, lengthLabelsX);
}
if (typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line')) {
truncatedYLabels = computeStringTickValues(yValues, 2, lengthLabelsY);
}
const plotData = (() => {
switch (plotType) {
case 'bar':
if (typeof xAxisData[0] === 'string' && groupBy == undefined) {
const frequencyMap = xAxisData.reduce(
(acc, item) => {
acc[item] = (acc[item] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
const sortedEntries = Object.entries(frequencyMap).sort((a, b) => b[1] - a[1]);
const sortedLabels = sortedEntries.map(([label]) => label);
const sortedFrequencies = sortedEntries.map(([, frequency]) => frequency);
const sortedEntries = Object.entries(frequencyMap).sort((a, b) => b[1] - a[1]);
sortedLabels = sortedEntries.map(([label]) => String(label));
sortedFrequencies = sortedEntries.map(([, frequency]) => frequency);
// !TODO: y ranges: max value showed is rounded, eg 54 -> 50
// need to specify tickvales and ticktext
const maxYValue = Math.max(...sortedFrequencies);
yAxisRange = [0, maxYValue];
return [
{
type: 'bar' as PlotType,
x: xValues,
y: yValues,
marker: {
color: colorDataZ?.length != 0 ? colorDataZ : primaryColor,
},
customdata: sortedLabels,
hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
},
];
} else {
return [
{
type: 'bar' as PlotType,
x: xValues,
y: yValues,
marker: { color: primaryColor },
customdata: xValues,
hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
},
];
}
case 'scatter':
return [
{
type: 'scatter' as PlotType,
x: xValues,
y: yValues,
mode: 'markers' as 'markers',
marker: {
color: zAxisData && zAxisData.length > 0 ? colorDataZ : primaryColor,
size: 7,
stroke: 1,
},
customdata:
xValues.length === 0
? yValues.map((y) => `Y: ${y}`)
: yValues.length === 0
? xValues.map((x) => `X: ${x}`)
: xValues.map((x, index) => {
const zValue = zAxisData && zAxisData.length > 0 ? zAxisData[index] : null;
return zValue ? `X: ${x} | Y: ${yValues[index]} | Color: ${zValue}` : `X: ${x} | Y: ${yValues[index]}`;
}),
hovertemplate: '<b>%{customdata}</b><extra></extra>',
},
];
case 'line':
return [
{
type: 'bar',
x: sortedLabels,
y: sortedFrequencies,
marker: { color: primaryColor },
type: 'scatter' as PlotType,
x: xValues,
y: yValues,
mode: 'lines' as 'lines',
line: { color: primaryColor },
customdata: xValues.map((label) => (label === 'undefined' || label === 'null' || label === '' ? 'nonData' : '')),
hovertemplate: '<b>%{customdata}</b><extra></extra>',
},
];
} else {
case 'histogram':
if (typeof xAxisData[0] === 'string') {
if (zAxisData && zAxisData?.length > 0) {
const frequencyMap = xAxisData.reduce(
(acc, item, index) => {
const color = zAxisData ? colorScale(zAxisData[index]) : primaryColor;
if (!acc[item]) {
acc[item] = {
count: 0,
colors: [],
zValues: [],
zValueCounts: {},
};
}
acc[item].count++;
acc[item].colors.push(color);
acc[item].zValues.push(zAxisData[index].toString());
// Group and count zValues
const zValue = zAxisData[index] || '(Empty)';
acc[item].zValueCounts[zValue] = (acc[item].zValueCounts[zValue] || 0) + 1;
return acc;
},
{} as Record<
string,
{
count: number;
colors: string[];
zValues: string[];
zValueCounts: Record<string, number>; // To store grouped counts
}
>,
);
const colorToLegendName = new Map();
const sortedCategories = Object.entries(frequencyMap).sort((a, b) => b[1].count - a[1].count);
const tracesByColor: Record<string, { x: string[]; y: number[] }> = {};
sortedCategories.forEach(([label, { colors, zValues }]) => {
colors.forEach((color, idx) => {
const zValue = zValues[idx];
if (!colorToLegendName.has(color)) {
colorToLegendName.set(color, zValue);
}
if (!tracesByColor[color]) {
tracesByColor[color] = { x: [], y: [] };
}
tracesByColor[color].x.push(label);
tracesByColor[color].y.push(1);
});
});
sortedLabels = sortedCategories.map((element) => element[0]);
const traces = Array.from(colorToLegendName.entries()).map(([color, legendName]) => {
const colorData = tracesByColor[color];
const categoryCountMap: Record<string, number> = {};
sortedLabels.forEach((label) => {
categoryCountMap[label] = frequencyMap[label].count;
});
const yValues = colorData.x.map((label, idx) => {
const totalCount = categoryCountMap[label];
const countForColor = colorData.y[idx];
return stack ? (countForColor / totalCount) * 100 : countForColor;
});
const customdata = colorData.x.map((label, idx) => {
const colorTranslation = colorToLegendName.get(color) === ' ' ? '(Empty)' : colorToLegendName.get(color);
const percentage = ((100 * frequencyMap[label].zValueCounts[colorTranslation]) / frequencyMap[label].count).toFixed(1);
return [label, !stack ? frequencyMap[label]?.zValueCounts[colorTranslation] || 0 : percentage, colorTranslation || ' '];
});
return {
x: colorData.x,
y: yValues,
type: 'bar' as PlotType,
name: legendName,
marker: { color: color },
customdata: customdata,
hovertemplate:
'<b>X: %{customdata[0]}</b><br>' + '<b>Y: %{customdata[1]}</b><br>' + '<b>Color: %{customdata[2]}</b><extra></extra>',
...(stack ? { stackgroup: 'one' } : {}),
};
});
return traces;
} else {
const frequencyMap = xAxisData.reduce(
(acc, item) => {
acc[item] = (acc[item] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
const sortedEntries = Object.entries(frequencyMap).sort((a, b) => b[1] - a[1]);
sortedLabels = sortedEntries.map(([label]) => String(label));
sortedFrequencies = sortedEntries.map(([, frequency]) => frequency);
return [
{
type: 'bar' as PlotType,
x: sortedLabels,
y: sortedFrequencies,
marker: { color: primaryColor },
customdata: sortedLabels,
hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
},
];
}
} else {
if (zAxisData && zAxisData?.length > 0) {
const binCount = 20; // Number of bins (you can make this configurable)
const numericXAxisData = xAxisData.map((val) => Number(val)).filter((val) => !isNaN(val));
const xMin = numericXAxisData.reduce((min, val) => Math.min(min, val), Infinity);
const xMax = numericXAxisData.reduce((max, val) => Math.max(max, val), -Infinity);
const binSize = (xMax - xMin) / binCount;
// Create bins
const bins = Array.from({ length: binCount }, (_, i) => ({
range: [xMin + i * binSize, xMin + (i + 1) * binSize],
count: 0,
zValueCounts: {} as Record<string, number>, // To track zAxisData counts per bin
}));
// Assign data points to bins
numericXAxisData.forEach((xValue, index) => {
const zValue = zAxisData ? zAxisData[index] || '(Empty)' : '(Empty)';
const binIndex = Math.floor((xValue - xMin) / binSize);
const bin = bins[Math.min(binIndex, bins.length - 1)]; // Ensure the last value falls into the final bin
bin.count++;
bin.zValueCounts[zValue] = (bin.zValueCounts[zValue] || 0) + 1;
});
const colorToLegendName = new Map();
const tracesByColor: Record<string, { x: string[]; y: number[] }> = {};
bins.forEach((bin, binIndex) => {
const binLabel = `[${bin.range[0].toFixed(1)}, ${bin.range[1].toFixed(1)})`;
Object.entries(bin.zValueCounts).forEach(([zValue, count]) => {
const color = zAxisData ? colorScale(zValue) : primaryColor;
if (!colorToLegendName.has(color)) {
colorToLegendName.set(color, zValue);
}
if (!tracesByColor[color]) {
tracesByColor[color] = { x: [], y: [] };
}
tracesByColor[color].x.push(binLabel);
tracesByColor[color].y.push(stack ? (count / bin.count) * 100 : count);
});
});
const traces = Array.from(colorToLegendName.entries()).map(([color, legendName]) => {
const colorData = tracesByColor[color];
const customdata = colorData.x.map((binLabel, idx) => {
const countForColor = colorData.y[idx];
const percentage = stack ? countForColor.toFixed(1) + '%' : countForColor.toFixed(0);
return [binLabel, countForColor, percentage, legendName];
});
return {
x: colorData.x,
y: colorData.y,
type: 'bar' as PlotType,
name: legendName,
marker: { color },
customdata,
autobinx: true,
hovertemplate:
'<b>Bin: %{customdata[0]}</b><br>' +
'<b>Count/Percentage: %{customdata[2]}</b><br>' +
'<b>Group: %{customdata[3]}</b><extra></extra>',
...(stack ? { stackgroup: 'one' } : {}),
};
});
return traces;
} else {
// No zAxisData, simple histogram logic
return [
{
type: 'histogram' as PlotType,
x: xAxisData,
marker: { color: primaryColor },
customdata: xAxisData,
},
];
}
}
case 'pie':
return [
{
type: 'histogram',
x: xAxisData,
marker: { color: primaryColor },
type: 'pie' as PlotType,
labels: xValues.map(String),
values: xAxisData,
marker: { colors: mainColors },
},
];
}
case 'pie':
return [
{
type: 'pie',
labels: xValues.map(String),
values: xAxisData,
marker: { colors: mainColors },
},
];
default:
return [];
}
default:
return [];
}
})();
const layout: Partial<Plotly.Layout> = {
barmode: 'stack',
xaxis: {
title: {
text: showAxis ? (xAxisLabel ? xAxisLabel : '') : '',
standoff: 30,
},
tickfont: sharedTickFont,
showgrid: false,
visible: showAxis,
...(typeof xAxisData[0] === 'string' || (plotType === 'histogram' && sortedLabels.length > 0)
? { type: 'category', categoryarray: sortedLabels, categoryorder: 'array' }
: {}),
showline: true,
zeroline: false,
tickvals: typeof xValues[0] == 'string' ? xValues : undefined,
ticktext: typeof xValues[0] == 'string' ? truncatedXLabels : undefined,
},
yaxis: {
showgrid: false,
visible: showAxis,
showline: true,
zeroline: false,
tickfont: sharedTickFont,
title: {
text: showAxis ? (yAxisLabel ? yAxisLabel : '') : '',
standoff: 30,
},
tickvals: typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line') ? yValues : undefined,
ticktext: typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line') ? truncatedYLabels : undefined,
},
font: {
family: 'Inter',
size: 12,
color: '#374151',
},
hoverlabel: {
bgcolor: 'rgba(255, 255, 255, 0.8)',
bordercolor: 'rgba(0, 0, 0, 0.2)',
font: {
family: 'monospace',
size: 14,
color: '#374151',
},
},
};
return { plotData, layout };
};
export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({
......@@ -134,6 +556,10 @@ export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({
yAxisData,
xAxisLabel,
yAxisLabel,
groupBy,
zAxisData,
zAxisLabel,
stack,
}) => {
const internalRef = useRef<HTMLDivElement>(null);
const [divSize, setDivSize] = useState({ width: 0, height: 0 });
......@@ -182,10 +608,72 @@ export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({
setHoveredPoint(null);
};
const { plotData, layout } = preparePlotData(
xAxisData,
plotType,
yAxisData,
zAxisData,
xAxisLabel,
yAxisLabel,
zAxisLabel,
showAxis,
groupBy,
stack,
);
// !TODO: implement pattern fill for nonData
/*
useEffect(() => {
const svg = document.querySelector('svg');
if (svg) {
// Create or find the `defs` section
let defs = svg.querySelector('defs');
if (!defs) {
defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
svg.insertBefore(defs, svg.firstChild);
}
// Check if the pattern already exists
let pattern = defs.querySelector('#diagonalHatch');
if (!pattern) {
// Create the diagonal hatch pattern
pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
pattern.setAttribute('id', 'diagonalHatch');
pattern.setAttribute('width', '6');
pattern.setAttribute('height', '6');
pattern.setAttribute('patternTransform', 'rotate(45)');
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('width', '2');
rect.setAttribute('height', '6');
rect.setAttribute('fill', '#cccccc');
pattern.appendChild(rect);
defs.appendChild(pattern);
}
//const bars = select('.points').selectAll('path').nodes();
const bars = document.querySelectorAll('.points path');
//console.log(bars);
if (plotType === 'histogram') {
bars.forEach((bar, index) => {
const customData = (plotData[0] as any).customdata[index];
//console.log(select(bar), customData, customData == 'nonData');
select(bar).style('fill', 'rgb(250, 0, 0)');
if (customData == 'nonData') {
//select(bar).style('fill', 'url(#diagonalHatch)');
}
//console.log(bar);
});
}
}
}, [plotData]);
*/
return (
<div className="h-full w-full flex items-center justify-center overflow-hidden relative" ref={internalRef}>
<Plot
data={getPlotData(xAxisData, plotType, yAxisData)}
data={plotData}
config={{
responsive: true,
scrollZoom: false,
......@@ -193,38 +681,11 @@ export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({
displaylogo: false,
}}
layout={{
...layout,
width: divSize.width,
height: divSize.height,
title: title,
dragmode: false,
font: {
family: 'Inter, sans-serif',
size: 12,
color: '#374151',
},
xaxis: {
title: showAxis ? (xAxisLabel ? xAxisLabel : '') : '',
showgrid: false,
visible: showAxis,
showline: true,
zeroline: false,
},
yaxis: {
title: showAxis ? (yAxisLabel ? yAxisLabel : '') : '',
showgrid: false,
visible: showAxis,
showline: true,
zeroline: false,
},
hoverlabel: {
bgcolor: 'rgba(255, 255, 255, 0.8)',
bordercolor: 'rgba(0, 0, 0, 0.2)',
font: {
family: 'Inter, sans-serif',
size: 14,
color: '#374151',
},
},
}}
onHover={handleHover}
onUnhover={handleUnhover}
......