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
Showing
with 1531 additions and 10 deletions
import { GraphQueryResult } from '@/lib/data-access';
import { plotTypeOptions } from './components/MakePlot';
import { Vis1DInputData } from './subPlots/types';
type VariableSelection = {
name: string;
type: string;
};
export interface Vis1DProps {
plotType: (typeof plotTypeOptions)[number]; // plotly plot type
title: string; // title of the plot
selectedEntity: string; // node label to plot
xAxisLabel: string;
yAxisLabel: string;
zAxisLabel?: string;
plotType: (typeof plotTypeOptions)[number];
title: string;
selectedEntity: string;
xAxisLabel: VariableSelection;
yAxisLabel: VariableSelection;
zAxisLabel: VariableSelection;
showAxis: boolean;
groupData?: string;
//groupData?: 'quarterly' | 'hourly' | 'minutely' | 'yearly' | 'monthly' | undefined;
groupDataByTime?: string;
groupAggregation?: string;
stack: boolean;
}
......@@ -23,6 +30,26 @@ export interface Vis1DVisHandle {
exportImageInternal: () => void;
}
export const getAttributeValuesSimple = (query: GraphQueryResult, selectedEntity: string, data: VariableSelection): Vis1DInputData => {
if (!selectedEntity || !data.name || data.name === ' ') {
return { data: [], type: data.type, label: data.name as string };
}
const attValues = query.graph.nodes
.filter(item => item.label === selectedEntity)
.map(item => {
return item.attributes && data.name in item.attributes && item.attributes[data.name] !== ''
? (item.attributes[data.name] as string | number)
: 'NoData';
});
return {
data: attValues,
type: data.type,
label: data.name as string,
};
};
export const getAttributeValues = (
query: GraphQueryResult,
selectedEntity: string,
......@@ -43,17 +70,21 @@ export const getAttributeValues = (
// Check if the attribute exists, return its value if it does, or an empty string otherwise
return [
item._id,
item.attributes && attributeKey in item.attributes && item.attributes[attributeKey] != ''
item.attributes && attributeKey in item.attributes && item.attributes[attributeKey] !== ''
? (item.attributes[attributeKey] as string | number)
: 'NoData',
];
});
const uniqueValues = Array.from(new Set(attValues.map(item => item[1])));
//console.log('attValues ', attValues);
const attValuesFilterd = attValues.map(e => e[1]);
//const uniqueValues = Array.from(new Set(attValues.map(item => item[1])));
return attValuesFilterd;
/*
if (!groupBy) {
return uniqueValues;
}
if (groupBy === 'count') {
const agg = Object.entries(
attValues
......@@ -93,4 +124,5 @@ export const getAttributeValues = (
console.error('Invalid groupBy value', groupBy);
throw new Error('Invalid groupBy value');
}
*/
};
import { Vis1DInputData } from '../subPlots/types';
import { getCommonLayout, getCSSVariableHSL, sharedTickFont } from '../prepareLayout/LayoutCommon';
import { PlotType } from 'plotly.js';
import { scaleOrdinal, scaleQuantize } from 'd3';
import { visualizationColors } from 'ts-common';
import { groupByTime } from './utilsTime';
export const preparePlotDataAndLayoutBar = (
xData: Vis1DInputData,
yData: Vis1DInputData,
zData: Vis1DInputData,
showAxis: boolean,
groupDataByTime?: string,
) => {
const primaryColor = getCSSVariableHSL('--clr-sec--400');
const mainColors = visualizationColors.GPCat.colors[14];
// data preparation
let xValues: (string | number)[] = [];
let yValues: (string | number)[] = [];
if (groupDataByTime !== undefined) {
if (yData.data && yData.data.length !== 0) {
const { xValuesGrouped, yValuesGrouped } = groupByTime(xData.data as string[], groupDataByTime, yData.data);
xValues = xValuesGrouped;
yValues = yValuesGrouped.flat();
} else {
const { xValuesGrouped, yValuesGrouped } = groupByTime(xData.data as string[], groupDataByTime);
xValues = xValuesGrouped;
yValues = yValuesGrouped.flat();
}
} else {
if (xData.data.length !== 0 && yData && yData.data.length !== 0) {
xValues = xData.data;
yValues = yData.data;
} else if (xData.data.length !== 0 && (!yData || yData.data.length === 0)) {
xValues = xData.data;
yValues = xData.data.map((_, index) => index + 1);
} else if (xData.data.length === 0 && yData && yData.data.length !== 0) {
xValues = yData.data.map((_, index) => index + 1);
yValues = yData.data;
}
}
let colorScale: any;
let colorDataZ: string[] = [];
let colorbar: any = {};
if (zData.data && zData.data.length > 0 && zData.type === 'number') {
const mainColorsSeq = visualizationColors.GPSeq.colors[9];
//const numericZAxisData = zData.data.filter((item): item is number => typeof item === 'number');
const zMin = zData.data.reduce((min, val) => (val < min ? val : min), zData.data[0]);
const zMax = zData.data.reduce((max, val) => (val > max ? val : max), zData.data[0]);
// !TODO: option to have a linear or quantize scale
colorScale = scaleQuantize<string>()
.domain([zMin as number, zMax as number])
.range(mainColorsSeq);
colorDataZ = zData.data?.map(item => colorScale(item) || primaryColor);
colorbar = {
title: 'Color Legend',
tickvals: [zMin, zMax],
ticktext: [`${zMin}`, `${zMax}`],
};
} else {
const uniqueZAxisData = Array.from(new Set(zData.data));
if (zData.data && uniqueZAxisData) {
colorScale = scaleOrdinal<string>().domain(uniqueZAxisData.map(String)).range(mainColors);
colorDataZ = zData.data?.map(item => colorScale(String(item)) || primaryColor);
const sortedDomain = uniqueZAxisData.sort();
colorbar = {
title: 'Color Legend',
tickvals: sortedDomain,
ticktext: sortedDomain.map(val => String(val)),
tickmode: 'array',
};
}
}
//
const data: Plotly.Data[] = (() => {
if (xData.type === 'string' && groupDataByTime === undefined) {
const frequencyMap = xData.data.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]) => String(label));
const sortedFrequencies = sortedEntries.map(([, frequency]) => frequency);
// Fix Y-axis range rounding issue
const maxYValue = Math.ceil(Math.max(...sortedFrequencies) / 10) * 10;
const yAxisRange = [0, maxYValue];
return [
{
type: 'bar' as PlotType,
x: sortedLabels,
y: sortedFrequencies,
marker: {
color: colorDataZ?.length ? colorDataZ : primaryColor,
},
customdata: sortedLabels,
hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
},
];
} else if (xData.type === 'date') {
return [
{
type: 'bar' as PlotType,
x: xValues,
y: yValues,
marker: {
color: colorDataZ?.length ? colorDataZ : primaryColor,
},
customdata: xValues,
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>',
},
];
}
})();
const layout: Partial<Plotly.Layout> = getCommonLayout({
showAxis,
xAxisLabel: xData.label,
yAxisLabel: yData.label,
xValues,
yValues,
plotType: 'line',
xAxisData: xValues,
sortedLabels: [],
sharedTickFont,
});
return { data, layout };
};
import { Vis1DInputData } from '../subPlots/types';
import { getCommonLayout, getCSSVariableHSL, sharedTickFont } from '../prepareLayout/LayoutCommon';
import { PlotType } from 'plotly.js';
import { scaleOrdinal } from 'd3';
import { visualizationColors } from 'ts-common';
export const preparePlotDataAndLayoutHistogramCount = (
xData: Vis1DInputData,
yData: Vis1DInputData,
zData: Vis1DInputData,
showAxis: boolean,
stackVariable: boolean,
) => {
const primaryColor = getCSSVariableHSL('--clr-sec--400');
const mainColors = visualizationColors.GPCat.colors[14];
const isCategorical = typeof xData.data[0] === 'string';
const frequencyMap = xData.data.reduce(
(acc, item, index) => {
if (!acc[item]) acc[item] = { count: 0, zValueCounts: {} };
acc[item].count += (yData.data?.[index] as number) ?? 1;
if (zData.data) {
const zValue = zData.data[index] || '(Empty)';
acc[item].zValueCounts[zValue] = (acc[item].zValueCounts[zValue] || 0) + 1;
}
return acc;
},
{} as Record<string, { count: number; zValueCounts: Record<string, number> }>,
);
const sortedCategories = Object.entries(frequencyMap).sort((a, b) => b[1].count - a[1].count);
const sortedLabels = sortedCategories.map(([label]) => label);
let traces;
if (isCategorical) {
if (zData.data && zData.data.length > 0) {
const uniqueZValues = Array.from(new Set(zData.data.map(String)));
const colorScale = scaleOrdinal<string>().domain(uniqueZValues).range(mainColors);
const groupedTraces: Record<string, { x: string[]; y: number[] }> = {};
sortedCategories.forEach(([label, data]) => {
Object.entries(data.zValueCounts).forEach(([zValue, count]) => {
const color = colorScale(zValue);
if (!groupedTraces[color]) {
groupedTraces[color] = { x: [], y: [] };
}
groupedTraces[color].x.push(label);
groupedTraces[color].y.push(stackVariable ? (count / data.count) * 100 : count);
});
});
traces = Object.entries(groupedTraces).map(([color, traceData]) => ({
type: 'bar' as PlotType,
x: traceData.x,
y: traceData.y,
name: uniqueZValues.find(z => colorScale(z) === color) || '(Unknown)',
marker: { color },
hovertemplate: '<b>X: %{x}</b><br><b>Y: %{y}</b><extra></extra>',
...(stackVariable ? { stackgroup: 'one' } : {}),
}));
} else {
traces = [
{
type: 'bar' as PlotType,
x: sortedLabels,
y: sortedCategories.map(([_, data]) => data.count),
marker: { color: primaryColor },
},
];
}
} else {
traces = [
{
type: 'histogram' as PlotType,
x: xData.data,
marker: { color: primaryColor },
},
];
}
const layout: Partial<Plotly.Layout> = getCommonLayout({
showAxis,
xAxisLabel: xData.label,
yAxisLabel: yData.label,
xValues: xData.data,
yValues: yData.data,
plotType: 'bar',
xAxisData: xData.data,
sortedLabels,
sharedTickFont,
});
return { data: traces, layout };
};
import { Vis1DInputData } from '../subPlots/types';
import { getCommonLayout, getCSSVariableHSL, sharedTickFont } from '../prepareLayout/LayoutCommon';
import { PlotType } from 'plotly.js';
export const preparePlotDataAndLayoutHistogramDegree = (xData: Vis1DInputData, yData: Vis1DInputData, showAxis: boolean) => {
const primaryColor = getCSSVariableHSL('--clr-sec--400');
const combinedData = xData.data.map((xValue, index) => ({
x: xValue,
y: yData.data[index],
}));
const sortedData = combinedData.sort((a, b) => Number(b.y) - Number(a.y));
const sortedXValues = sortedData.map(item => item.x);
const sortedYValues = sortedData.map(item => item.y);
const traces = [
{
type: 'bar' as PlotType,
x: sortedXValues,
y: sortedYValues,
marker: { color: primaryColor },
},
];
const layout: Partial<Plotly.Layout> = getCommonLayout({
showAxis,
xAxisLabel: 'Nodes',
yAxisLabel: 'Count',
xValues: xData.data,
yValues: xData.data,
plotType: 'bar',
xAxisData: xData.data,
sortedLabels: sortedXValues,
sharedTickFont,
});
return { data: traces, layout };
};
import { Vis1DInputData } from '../subPlots/types';
import { getCommonLayout, getCSSVariableHSL, sharedTickFont } from '../prepareLayout/LayoutCommon';
import { PlotType } from 'plotly.js';
import { groupByTime } from './utilsTime';
export const preparePlotDataAndLayoutLine = (
xData: Vis1DInputData,
yData: Vis1DInputData,
showAxis: boolean,
stack: boolean,
groupDataByTime?: string,
) => {
const primaryColor = getCSSVariableHSL('--clr-sec--400');
// data preparation
let xValues: (string | number)[] = [];
let yValues: (string | number)[] = [];
if (groupDataByTime !== undefined) {
if (yData.data && yData.data.length !== 0) {
const { xValuesGrouped, yValuesGrouped } = groupByTime(xData.data as string[], groupDataByTime, yData.data);
xValues = xValuesGrouped;
yValues = yValuesGrouped.flat();
} else {
const { xValuesGrouped, yValuesGrouped } = groupByTime(xData.data as string[], groupDataByTime);
xValues = xValuesGrouped;
yValues = yValuesGrouped.flat();
}
} else {
if (xData.data.length !== 0 && yData && yData.data.length !== 0) {
xValues = xData.data;
yValues = yData.data;
} else if (xData.data.length !== 0 && (!yData || yData.data.length === 0)) {
xValues = xData.data;
yValues = xData.data.map((_, index) => index + 1);
} else if (xData.data.length === 0 && yData && yData.data.length !== 0) {
xValues = yData.data.map((_, index) => index + 1);
yValues = yData.data;
}
}
const data: Plotly.Data[] = [
{
type: 'scatter' as PlotType,
x: xValues,
y: yValues,
mode: 'lines' as const,
line: { color: primaryColor },
customdata: xValues.map(label => (label === 'undefined' || label === 'null' || label === '' ? 'nonData' : '')),
hovertemplate: '<b>%{customdata}</b><extra></extra>',
},
];
const layout: Partial<Plotly.Layout> = getCommonLayout({
showAxis,
xAxisLabel: xData.label,
yAxisLabel: yData.label,
xValues,
yValues,
plotType: 'line',
xAxisData: xValues,
sortedLabels: [],
sharedTickFont,
});
return { data, layout };
};
import { Vis1DInputData } from '../subPlots/types';
import { getCommonLayout, getCSSVariableHSL, sharedTickFont } from '../prepareLayout/LayoutCommon';
import { PlotType } from 'plotly.js';
import { visualizationColors } from 'ts-common';
export const preparePlotDataAndLayoutPie = (xData: Vis1DInputData, showAxis: boolean) => {
const mainColors = visualizationColors.GPCat.colors[14];
const counts: Record<string, number> = {};
xData.data.forEach(val => {
const key = String(val);
counts[key] = (counts[key] || 0) + 1;
});
const labels = Object.keys(counts);
const values = Object.values(counts);
const data: Plotly.Data[] = [
{
type: 'pie' as PlotType,
labels,
values,
marker: { colors: mainColors },
},
];
const layout: Partial<Plotly.Layout> = getCommonLayout({
showAxis,
xAxisLabel: xData.label,
yAxisLabel: '',
xValues: xData.data,
yValues: [],
plotType: 'line',
xAxisData: xData.data,
sortedLabels: [],
sharedTickFont,
});
return { data, layout };
};
import { Vis1DInputData } from '../subPlots/types';
import { getCommonLayout, getCSSVariableHSL, sharedTickFont } from '../prepareLayout/LayoutCommon';
import { PlotType } from 'plotly.js';
import { scaleOrdinal, scaleQuantize } from 'd3';
import { visualizationColors } from 'ts-common';
import { computeStringTickValues } from '../prepareData/utils';
export const preparePlotDataAndLayoutScatter = (xData: Vis1DInputData, yData: Vis1DInputData, zData: Vis1DInputData, showAxis: boolean) => {
const primaryColor = getCSSVariableHSL('--clr-sec--400');
const mainColors = visualizationColors.GPCat.colors[14];
const lengthLabelsX = 7; // !TODO computed number of elements based
const lengthLabelsY = 8; // !TODO computed number of elements based
// data preparation
let xValues: (string | number)[] = [];
let yValues: (string | number)[] = [];
if (xData.data.length !== 0 && yData && yData.data.length !== 0) {
xValues = xData.data;
yValues = yData.data;
} else if (xData.data.length !== 0 && (!yData || yData.data.length === 0)) {
xValues = xData.data;
yValues = xData.data.map((_, index) => index + 1);
} else if (xData.data.length === 0 && yData && yData.data.length !== 0) {
xValues = yData.data.map((_, index) => index + 1);
yValues = yData.data;
}
let colorScale: any;
let colorDataZ: string[] = [];
let colorbar: any = {};
if (zData.data && zData.data.length > 0 && zData.type === 'number') {
const mainColorsSeq = visualizationColors.GPSeq.colors[9];
//const numericZAxisData = zData.data.filter((item): item is number => typeof item === 'number');
const zMin = zData.data.reduce((min, val) => (val < min ? val : min), zData.data[0]);
const zMax = zData.data.reduce((max, val) => (val > max ? val : max), zData.data[0]);
// !TODO: option to have a linear or quantize scale
colorScale = scaleQuantize<string>()
.domain([zMin as number, zMax as number])
.range(mainColorsSeq);
colorDataZ = zData.data?.map(item => colorScale(item) || primaryColor);
colorbar = {
title: 'Color Legend',
tickvals: [zMin, zMax],
ticktext: [`${zMin}`, `${zMax}`],
};
} else {
const uniqueZAxisData = Array.from(new Set(zData.data));
if (zData.data && uniqueZAxisData) {
colorScale = scaleOrdinal<string>().domain(uniqueZAxisData.map(String)).range(mainColors);
colorDataZ = zData.data?.map(item => colorScale(String(item)) || primaryColor);
const sortedDomain = uniqueZAxisData.sort();
colorbar = {
title: 'Color Legend',
tickvals: sortedDomain,
ticktext: sortedDomain.map(val => String(val)),
tickmode: 'array',
};
}
}
const data: Plotly.Data[] = [
{
type: 'scatter' as PlotType,
x: xValues,
y: yValues,
mode: 'markers' as const,
marker: {
color: zData.data && zData.data.length > 0 ? colorDataZ : primaryColor,
size: 7,
},
customdata:
xValues.length === 0
? yValues.map(y => `Y: ${y}`)
: yValues.length === 0
? xValues.map(x => `X: ${x}`)
: xValues.map((x, index) => {
const zValue = zData.data && zData.data.length > 0 ? zData.data[index] : null;
return zValue ? `X: ${x} | Y: ${yValues[index]} | Color: ${zValue}` : `X: ${x} | Y: ${yValues[index]}`;
}),
hovertemplate: '<b>%{customdata}</b><extra></extra>',
},
];
let truncatedXLabels: string[] = [];
let truncatedYLabels: string[] = [];
if (typeof xValues[0] === 'string') {
truncatedXLabels = computeStringTickValues(xValues, 2, lengthLabelsX);
}
if (typeof yValues[0] === 'string') {
truncatedYLabels = computeStringTickValues(yValues, 2, lengthLabelsY);
}
const layout: Partial<Plotly.Layout> = getCommonLayout({
showAxis,
xAxisLabel: xData.label,
yAxisLabel: yData.label,
xValues,
yValues,
plotType: 'line',
xAxisData: xValues,
sortedLabels: [],
sharedTickFont,
});
return { data, layout };
};
import { GraphQueryResult } from '@/lib/data-access';
import { Vis1DInputData, VariableSelection } from './../subPlots/types';
import { Vis1DProps } from '../subPlots/types';
export 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 getAttributeValuesSimple = (query: GraphQueryResult, selectedEntity: string, data: VariableSelection): Vis1DInputData => {
if (!selectedEntity || !data.name || data.name === ' ') {
return { data: [], type: data.type, label: data.name as string };
}
const attValues = query.graph.nodes
.filter(item => item.label === selectedEntity)
.map(item => {
return item.attributes && data.name in item.attributes && item.attributes[data.name] !== ''
? (item.attributes[data.name] as string | number)
: 'NoData';
});
console.log('count: ', {
data: attValues,
type: data.type,
label: data.name as string,
});
return {
data: attValues,
type: data.type,
label: data.name as string,
};
};
export const getAttributeValuesDegree = (
query: GraphQueryResult,
selectedEntity: string,
data: VariableSelection,
): { xAxisData: Vis1DInputData; yAxisData: Vis1DInputData } => {
if (!selectedEntity || !data.name || data.name === ' ') {
return {
xAxisData: { data: [], type: data.type, label: data.name as string },
yAxisData: { data: [], type: data.type, label: data.name as string },
};
}
const attValues = query.graph.nodes
.filter(item => item.label === selectedEntity)
.map(item => {
return [
item._id,
item.attributes && data.name in item.attributes && item.attributes[data.name] != ''
? (item.attributes[data.name] as string | number)
: 'NoData',
];
});
const uniqueValues = Array.from(new Set(attValues.map(item => item[1])));
const attValuesMap = Object.fromEntries(attValues);
const ids = attValues.map(item => item[0]);
const degree = query.graph.edges.reduce(
(acc, item) => {
if (ids.includes(item.from)) {
acc[attValuesMap[item.from]] = (acc[attValuesMap[item.from]] || 0) + 1;
} else if (ids.includes(item.to)) {
acc[attValuesMap[item.to]] = (acc[attValuesMap[item.to]] || 0) + 1;
}
return acc;
},
{} as Record<string, number>,
);
const dataReturn = attValues
.map(item => ({ id: item[0], value: degree[item[1]] || 0, position: uniqueValues.indexOf(item[0]) }))
.sort((a, b) => a.position - b.position)
.map(item => item.value);
console.log('ids ', ids);
console.log('degree: ', {
data: dataReturn,
type: data.type,
label: data.name as string,
});
const yAxisData = {
data: dataReturn,
type: data.type,
label: data.name as string,
};
const xAxisData = {
data: ids,
type: 'string',
label: 'Ids',
};
return { xAxisData, yAxisData };
};
export const getAllAxisData = (query: GraphQueryResult, settings: Vis1DProps) => {
let xAxisData: Vis1DInputData = {
data: [],
label: '',
type: '',
};
let yAxisData: Vis1DInputData = {
data: [],
label: '',
type: '',
};
let zAxisData: Vis1DInputData = {
data: [],
label: '',
type: '',
};
if (settings.groupAggregation === 'count') {
xAxisData = getAttributeValuesSimple(query, settings.selectedEntity, settings.xAxisLabel);
yAxisData = getAttributeValuesSimple(query, settings.selectedEntity, settings.yAxisLabel);
zAxisData = getAttributeValuesSimple(query, settings.selectedEntity, settings.zAxisLabel);
} else if (settings.groupAggregation === 'degree') {
const degreeData = getAttributeValuesDegree(query, settings.selectedEntity, settings.xAxisLabel);
xAxisData = degreeData.xAxisData;
yAxisData = degreeData.yAxisData;
}
return { xAxisData, yAxisData, zAxisData };
};
const getWeekNumber = (date: Date): string => {
const startDate = new Date(date.getFullYear(), 0, 1); // Start of the year
const days = Math.floor((date.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000));
const weekNumber = Math.ceil((days + 1) / 7); // Week number is 1-based
return `${date.getFullYear()}-W${weekNumber.toString().padStart(2, '0')}`; // Format as "YYYY-WXX"
};
export const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableData?: (string | number)[]) => {
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 if (groupBy === 'weekly') {
// Group by week, e.g., "2012-W15"
groupKey = getWeekNumber(date);
} else if (groupBy === 'daily') {
// Group by day, e.g., "2012-07-15"
groupKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
} else if (groupBy === 'hourly') {
// Group by hour, e.g., "2012-07-15 14"
groupKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}`;
} else if (groupBy === 'minutely') {
// Group by minute, e.g., "2012-07-15 14:30"
groupKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
} else {
// Default case: group by year (or some other grouping logic)
groupKey = date.getFullYear().toString();
}
//console.log('additionalVariableData ', additionalVariableData);
// 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[]>,
);
// Sorting logic outside the reduce block
const sortedArray = Object.entries(groupedData)
.map(([key, value]) => ({
x: key,
y: value,
dateObj: (() => {
if (key.includes('Q')) {
return new Date(parseInt(key.split('-')[0], 10), (parseInt(key.split('-')[1].replace('Q', ''), 10) - 1) * 3);
} else if (key.includes('-W')) {
const [year, week] = key.split('-W').map(Number);
const jan1 = new Date(year, 0, 1);
return new Date(year, 0, jan1.getDate() + (week - 1) * 7);
} else {
return new Date(key);
}
})(),
}))
.sort((a, b) => a.dateObj.getTime() - b.dateObj.getTime());
const xValuesGrouped = sortedArray.map(({ x }) => x);
const yValuesGrouped = sortedArray.map(({ y }) => y);
return { xValuesGrouped, yValuesGrouped };
};
import { computeStringTickValues } from '../prepareData/utils';
interface LayoutProps {
showAxis: boolean;
xAxisLabel: string;
yAxisLabel: string;
xValues: any[];
yValues: any[];
plotType: 'scatter' | 'line' | 'histogram' | 'bar';
xAxisData: any[];
sortedLabels: any[];
sharedTickFont: { family: string; size: number; color: string };
}
export const getCommonLayout = ({
showAxis,
xAxisLabel,
yAxisLabel,
xValues,
yValues,
plotType,
xAxisData,
sortedLabels,
sharedTickFont,
}: LayoutProps): Partial<Plotly.Layout> => {
const lengthLabelsX = 7; // !TODO computed number of elements based
const lengthLabelsY = 8; // !TODO computed number of elements based
let truncatedXLabels: string[] = [];
let truncatedYLabels: string[] = [];
if (typeof xValues[0] === 'string') {
truncatedXLabels = computeStringTickValues(xValues, 2, lengthLabelsX);
}
if (typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line')) {
truncatedYLabels = computeStringTickValues(yValues, 2, lengthLabelsY);
}
return {
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: 'Inter',
size: 13,
color: '#374151',
},
},
};
};
export const getCSSVariableHSL = (varName: string) => {
const rootStyles = getComputedStyle(document.documentElement);
const hslValue = rootStyles.getPropertyValue(varName).trim().replace('deg', '');
return `hsl(${hslValue})`;
};
export const sharedTickFont = {
family: 'Inter',
size: 11,
color: '#374151',
};
import React, { useEffect, useRef, useState } from 'react';
import Plot from 'react-plotly.js';
import { DropdownSelector } from '@/lib/components/DropdownSelector/DropdownSelector';
import { Vis1DInputData } from './types';
import { preparePlotDataAndLayoutBar } from '../prepareData/preparePlotBar';
import { Input } from '@/lib/components/inputs';
type Vis1DBarProps = {
xData: Vis1DInputData;
yData: Vis1DInputData;
zData: Vis1DInputData;
groupDataByTime?: string;
showAxis: boolean;
};
export const Vis1DBar: React.FC<Vis1DBarProps> = ({ xData, yData, zData, showAxis, groupDataByTime }) => {
const internalRef = useRef<HTMLDivElement>(null);
const [divSize, setDivSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const handleResize = () => {
if (internalRef.current) {
const { width, height } = internalRef.current.getBoundingClientRect();
setDivSize({ width, height });
}
};
handleResize();
window.addEventListener('resize', handleResize);
if (internalRef.current) {
new ResizeObserver(handleResize).observe(internalRef.current);
}
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
const { data, layout } = preparePlotDataAndLayoutBar(xData, yData, zData, showAxis, groupDataByTime);
return (
<div className="h-full w-full flex items-center justify-center overflow-hidden relative" ref={internalRef}>
<Plot
data={data}
config={{
responsive: true,
scrollZoom: false,
displayModeBar: false,
displaylogo: false,
}}
layout={{
...layout,
width: divSize.width,
height: divSize.height,
dragmode: false,
}}
/>
</div>
);
};
interface Vis1DSettingsBarProps {
settings: any;
updateSettings: (updatedSettings: any) => void;
attributeOptions: { name: string; type: string }[];
filterYLabel: string;
setFilterYLabel: React.Dispatch<React.SetStateAction<string>>;
filterZLabel: string;
setFilterZLabel: React.Dispatch<React.SetStateAction<string>>;
}
export const Vis1DSettingsBar = ({
settings,
updateSettings,
attributeOptions,
filterYLabel,
setFilterYLabel,
filterZLabel,
setFilterZLabel,
}: Vis1DSettingsBarProps) => {
return (
<div>
<div className="mb-2">
<DropdownSelector
label="Y Axis"
selectedValue={settings.yAxisLabel.name}
options={attributeOptions}
filter={filterYLabel}
setFilter={setFilterYLabel}
onChange={selected => {
updateSettings({ yAxisLabel: { name: selected.name, type: selected.type } });
}}
/>
<DropdownSelector
label="Z Axis"
selectedValue={settings.zAxisLabel.name}
options={attributeOptions}
filter={filterZLabel}
setFilter={setFilterZLabel}
onChange={selected => {
updateSettings({ zAxisLabel: { name: selected.name, type: selected.type } });
}}
/>
{(settings.xAxisLabel.name?.includes('Datum') ||
settings.xAxisLabel.type === 'date' ||
settings.xAxisLabel.type === 'datetime') && (
<div className="mb-2">
<Input
type="dropdown"
label="Group Time:"
value={settings.groupDataByTime}
options={['', 'hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly']}
onChange={value => {
updateSettings({ groupDataByTime: value as string });
}}
/>
</div>
)}
</div>
</div>
);
};
import React, { useEffect, useRef, useState } from 'react';
import Plot from 'react-plotly.js';
import { DropdownSelector } from '@/lib/components/DropdownSelector/DropdownSelector';
import { Vis1DInputData } from './types';
import { preparePlotDataAndLayoutHistogramCount } from '../prepareData/preparePlotHistogramCount';
import { Input } from '@/lib/components/inputs';
type Vis1DHistogramCountProps = {
xData: Vis1DInputData;
yData: Vis1DInputData;
zData: Vis1DInputData;
showAxis: boolean;
stackVariable: boolean;
};
export const Vis1DHistogramCount: React.FC<Vis1DHistogramCountProps> = ({ xData, yData, zData, showAxis, stackVariable }) => {
const internalRef = useRef<HTMLDivElement>(null);
const [divSize, setDivSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const handleResize = () => {
if (internalRef.current) {
const { width, height } = internalRef.current.getBoundingClientRect();
setDivSize({ width, height });
}
};
handleResize();
window.addEventListener('resize', handleResize);
if (internalRef.current) {
new ResizeObserver(handleResize).observe(internalRef.current);
}
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
const { data, layout } = preparePlotDataAndLayoutHistogramCount(xData, yData, zData, showAxis, stackVariable);
return (
<div className="h-full w-full flex items-center justify-center overflow-hidden relative" ref={internalRef}>
<Plot
data={data}
config={{
responsive: true,
scrollZoom: false,
displayModeBar: false,
displaylogo: false,
}}
layout={{
...layout,
width: divSize.width,
height: divSize.height,
dragmode: false,
}}
/>
</div>
);
};
interface Vis1DSettingsHistogramCountProps {
settings: any;
updateSettings: (updatedSettings: any) => void;
attributeOptions: { name: string; type: string }[];
filterYLabel: string;
setFilterYLabel: React.Dispatch<React.SetStateAction<string>>;
filterZLabel: string;
setFilterZLabel: React.Dispatch<React.SetStateAction<string>>;
}
export const Vis1DSettingsHistogramCount = ({
settings,
updateSettings,
attributeOptions,
filterYLabel,
setFilterYLabel,
filterZLabel,
setFilterZLabel,
}: Vis1DSettingsHistogramCountProps) => {
return (
<div>
<div className="mb-2">
{/*
<DropdownSelector
label="Y Axis"
selectedValue={settings.yAxisLabel.name}
options={attributeOptions}
filter={filterYLabel}
setFilter={setFilterYLabel}
onChange={selected => {
updateSettings({ yAxisLabel: { name: selected.name, type: selected.type } });
}}
/>
*/}
<DropdownSelector
label="Z Axis"
selectedValue={settings.zAxisLabel.name}
options={attributeOptions}
filter={filterZLabel}
setFilter={setFilterZLabel}
onChange={selected => {
updateSettings({ zAxisLabel: { name: selected.name, type: selected.type } });
}}
/>
<div className="mb-2">
<Input type="boolean" label="Stack: " value={settings.stack} onChange={val => updateSettings({ stack: val })} />
</div>
</div>
</div>
);
};
import React, { useEffect, useRef, useState } from 'react';
import Plot from 'react-plotly.js';
import { Vis1DInputData } from './types';
import { preparePlotDataAndLayoutHistogramDegree } from '../prepareData/preparePlotHistogramDegree';
type Vis1DHistogramDegreeProps = {
xData: Vis1DInputData;
yData: Vis1DInputData;
showAxis: boolean;
};
export const Vis1DHistogramDegree: React.FC<Vis1DHistogramDegreeProps> = ({ xData, yData, showAxis }) => {
const internalRef = useRef<HTMLDivElement>(null);
const [divSize, setDivSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const handleResize = () => {
if (internalRef.current) {
const { width, height } = internalRef.current.getBoundingClientRect();
setDivSize({ width, height });
}
};
handleResize();
window.addEventListener('resize', handleResize);
if (internalRef.current) {
new ResizeObserver(handleResize).observe(internalRef.current);
}
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
const { data, layout } = preparePlotDataAndLayoutHistogramDegree(xData, yData, showAxis);
return (
<div className="h-full w-full flex items-center justify-center overflow-hidden relative" ref={internalRef}>
<Plot
data={data}
config={{
responsive: true,
scrollZoom: false,
displayModeBar: false,
displaylogo: false,
}}
layout={{
...layout,
width: divSize.width,
height: divSize.height,
dragmode: false,
}}
/>
</div>
);
};
interface Vis1DSettingsHistogramDegreeProps {
settings: any;
updateSettings: (updatedSettings: any) => void;
attributeOptions: { name: string; type: string }[];
}
export const Vis1DSettingsHistogramDegree = ({ settings, updateSettings, attributeOptions }: Vis1DSettingsHistogramDegreeProps) => {
return <div></div>;
};
import React, { useEffect, useRef, useState } from 'react';
import Plot from 'react-plotly.js';
import { DropdownSelector } from '@/lib/components/DropdownSelector/DropdownSelector';
import { Vis1DInputData } from './types';
import { preparePlotDataAndLayoutLine } from '../prepareData/preparePlotLine';
import { Input } from '@/lib/components/inputs';
type Vis1DLineProps = {
xData: Vis1DInputData;
yData: Vis1DInputData;
groupDataByTime?: string;
showAxis: boolean;
};
export const Vis1DLine: React.FC<Vis1DLineProps> = ({ xData, yData, groupDataByTime, showAxis }) => {
// common to all vis
const internalRef = useRef<HTMLDivElement>(null);
const [divSize, setDivSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const handleResize = () => {
if (internalRef.current) {
const { width, height } = internalRef.current.getBoundingClientRect();
setDivSize({ width, height });
}
};
handleResize();
window.addEventListener('resize', handleResize);
if (internalRef.current) {
new ResizeObserver(handleResize).observe(internalRef.current);
}
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
// specific to this vis
const { data, layout } = preparePlotDataAndLayoutLine(xData, yData, showAxis, false, groupDataByTime);
return (
<div className="h-full w-full flex items-center justify-center overflow-hidden relative" ref={internalRef}>
<Plot
data={data}
config={{
responsive: true,
scrollZoom: false,
displayModeBar: false,
displaylogo: false,
}}
layout={{
...layout,
width: divSize.width,
height: divSize.height,
dragmode: false,
}}
/>
</div>
);
};
interface Vis1DSettingsLineProps {
settings: any;
updateSettings: (updatedSettings: any) => void;
attributeOptions: { name: string; type: string }[];
filterYLabel: string;
setFilterYLabel: React.Dispatch<React.SetStateAction<string>>;
}
export const Vis1DSettingsLine = ({
settings,
updateSettings,
attributeOptions,
filterYLabel,
setFilterYLabel,
}: Vis1DSettingsLineProps) => {
return (
<div>
<div className="mb-2">
<DropdownSelector
label="Y Axis"
selectedValue={settings.yAxisLabel.name}
options={attributeOptions}
filter={filterYLabel}
setFilter={setFilterYLabel}
onChange={selected => {
updateSettings({ yAxisLabel: { name: selected.name, type: selected.type } });
}}
/>
<div className="mb-2">
<Input type="boolean" label="Stack: " value={settings.stack} onChange={val => updateSettings({ stack: val })} />
</div>
{(settings.xAxisLabel.name?.includes('Datum') ||
settings.xAxisLabel.type === 'date' ||
settings.xAxisLabel.type === 'datetime') && (
<div className="mb-2">
<Input
type="dropdown"
label="Group Time:"
value={settings.groupDataByTime}
options={['', 'hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly']}
onChange={value => {
updateSettings({ groupDataByTime: value as string });
}}
/>
</div>
)}
</div>
</div>
);
};
import React, { useEffect, useRef, useState } from 'react';
import Plot from 'react-plotly.js';
import { Vis1DInputData } from './types';
import { preparePlotDataAndLayoutPie } from '../prepareData/preparePlotPie';
type Vis1DPieProps = {
xData: Vis1DInputData;
showAxis: boolean;
};
export const Vis1DPie: React.FC<Vis1DPieProps> = ({ xData, showAxis }) => {
// common to all vis
const internalRef = useRef<HTMLDivElement>(null);
const [divSize, setDivSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const handleResize = () => {
if (internalRef.current) {
const { width, height } = internalRef.current.getBoundingClientRect();
setDivSize({ width, height });
}
};
handleResize();
window.addEventListener('resize', handleResize);
if (internalRef.current) {
new ResizeObserver(handleResize).observe(internalRef.current);
}
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
// specific to this vis
const { data, layout } = preparePlotDataAndLayoutPie(xData, showAxis);
return (
<div className="h-full w-full flex items-center justify-center overflow-hidden relative" ref={internalRef}>
<Plot
data={data}
config={{
responsive: true,
scrollZoom: false,
displayModeBar: false,
displaylogo: false,
}}
layout={{
...layout,
width: divSize.width,
height: divSize.height,
dragmode: false,
}}
/>
</div>
);
};
import React, { useEffect, useRef, useState } from 'react';
import Plot from 'react-plotly.js';
import { DropdownSelector } from '@/lib/components/DropdownSelector/DropdownSelector';
import { Vis1DInputData } from './types';
import { preparePlotDataAndLayoutScatter } from '../prepareData/preparePlotScatter';
type Vis1DScatterProps = {
xData: Vis1DInputData;
yData: Vis1DInputData;
zData: Vis1DInputData;
showAxis: boolean;
};
export const Vis1DScatter: React.FC<Vis1DScatterProps> = ({ xData, yData, zData, showAxis }) => {
const internalRef = useRef<HTMLDivElement>(null);
const [divSize, setDivSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const handleResize = () => {
if (internalRef.current) {
const { width, height } = internalRef.current.getBoundingClientRect();
setDivSize({ width, height });
}
};
handleResize();
window.addEventListener('resize', handleResize);
if (internalRef.current) {
new ResizeObserver(handleResize).observe(internalRef.current);
}
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
const { data, layout } = preparePlotDataAndLayoutScatter(xData, yData, zData, showAxis);
return (
<div className="h-full w-full flex items-center justify-center overflow-hidden relative" ref={internalRef}>
<Plot
data={data}
config={{
responsive: true,
scrollZoom: false,
displayModeBar: false,
displaylogo: false,
}}
layout={{
...layout,
width: divSize.width,
height: divSize.height,
dragmode: false,
}}
/>
</div>
);
};
interface Vis1DSettingsScatterProps {
settings: any;
updateSettings: (updatedSettings: any) => void;
attributeOptions: { name: string; type: string }[];
filterYLabel: string;
setFilterYLabel: React.Dispatch<React.SetStateAction<string>>;
filterZLabel: string;
setFilterZLabel: React.Dispatch<React.SetStateAction<string>>;
}
export const Vis1DSettingsScatter = ({
settings,
updateSettings,
attributeOptions,
filterYLabel,
setFilterYLabel,
filterZLabel,
setFilterZLabel,
}: Vis1DSettingsScatterProps) => {
return (
<div>
<div className="mb-2">
<DropdownSelector
label="Y Axis"
selectedValue={settings.yAxisLabel.name}
options={attributeOptions}
filter={filterYLabel}
setFilter={setFilterYLabel}
onChange={selected => {
updateSettings({ yAxisLabel: { name: selected.name, type: selected.type } });
}}
/>
<DropdownSelector
label="Z Axis"
selectedValue={settings.zAxisLabel.name}
options={attributeOptions}
filter={filterZLabel}
setFilter={setFilterZLabel}
onChange={selected => {
updateSettings({ zAxisLabel: { name: selected.name, type: selected.type } });
}}
/>
</div>
</div>
);
};
export type Vis1DInputData = {
data: (string | number)[];
label: string;
type: string;
};
export interface Vis1DProps {
plotType: (typeof plotTypeOptions)[number];
title: string;
selectedEntity: string;
xAxisLabel: VariableSelection;
yAxisLabel: VariableSelection;
zAxisLabel: VariableSelection;
showAxis: boolean;
//groupData?: 'quarterly' | 'hourly' | 'minutely' | 'yearly' | 'monthly' | undefined;
groupDataByTime?: string;
groupAggregation?: string;
stack: boolean;
}
export const plotTypeOptions = ['bar', 'scatter', 'line', 'histogram', 'pie'] as const;
export type VariableSelection = {
name: string;
type: string;
};
export enum Vis1DHistogramAggregation {
COUNT = 'count',
DEGREE = 'degree',
}
export const vis1DHistogramAggregationOptions = Object.values(Vis1DHistogramAggregation);
export interface Vis1DVisHandle {
exportImageInternal: () => void;
}
import html2canvas from 'html2canvas';
export const exportImageInternal = (internalRef: React.RefObject<HTMLDivElement>) => {
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 = 'Vis1D.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();
};