updated real time vis

This commit is contained in:
Nalvazhuthi 2025-03-20 16:30:43 +05:30
parent 4549a5cae4
commit 7950b58ba8
25 changed files with 1310 additions and 506 deletions

11
app/package-lock.json generated
View File

@ -11,6 +11,7 @@
"chart.js": "^4.4.8",
"path": "^0.12.7",
"react": "^19.0.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.3.0",
"zustand": "^5.0.3"
@ -3247,6 +3248,16 @@
"node": ">=0.10.0"
}
},
"node_modules/react-chartjs-2": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
"license": "MIT",
"peerDependencies": {
"chart.js": "^4.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-dom": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",

View File

@ -13,6 +13,7 @@
"chart.js": "^4.4.8",
"path": "^0.12.7",
"react": "^19.0.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.3.0",
"zustand": "^5.0.3"

View File

@ -6,7 +6,7 @@ const Templates = () => {
const { templates, removeTemplate } = useTemplateStore();
const { setSelectedZone } = useSelectedZoneStore();
console.log('templates: ', templates);
console.log("templates: ", templates);
const handleDeleteTemplate = (id: string) => {
removeTemplate(id);
};
@ -15,40 +15,49 @@ const Templates = () => {
setSelectedZone((prev) => ({
...prev,
panelOrder: template.panelOrder,
activeSides: Array.from(new Set([...prev.activeSides, ...template.panelOrder])),
activeSides: Array.from(
new Set([...prev.activeSides, ...template.panelOrder])
),
widgets: template.widgets,
}));
};
return (
<div className="template-list" style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
gap: '1rem',
padding: '1rem'
}}>
<div
className="template-list"
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))",
gap: "1rem",
padding: "1rem",
}}
>
{templates.map((template) => (
<div key={template.id} className="template-item" style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
padding: '1rem',
transition: 'box-shadow 0.3s ease',
}}>
<div
key={template.id}
className="template-item"
style={{
border: "1px solid #e0e0e0",
borderRadius: "8px",
padding: "1rem",
transition: "box-shadow 0.3s ease",
}}
>
{template.snapshot && (
<div style={{ position: 'relative', paddingBottom: '56.25%' }}> {/* 16:9 aspect ratio */}
<div style={{ position: "relative", paddingBottom: "56.25%" }}>
{" "}
{/* 16:9 aspect ratio */}
<img
src={template.snapshot} // Corrected from template.image to template.snapshot
alt={`${template.name} preview`}
style={{
position: 'absolute',
width: '100%',
height: '100%',
objectFit: 'contain',
borderRadius: '4px',
cursor: 'pointer',
transition: 'transform 0.3s ease',
position: "absolute",
width: "100%",
height: "100%",
objectFit: "contain",
borderRadius: "4px",
cursor: "pointer",
transition: "transform 0.3s ease",
// ':hover': {
// transform: 'scale(1.05)'
// }
@ -57,17 +66,19 @@ const Templates = () => {
/>
</div>
)}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: '0.5rem'
}}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: "0.5rem",
}}
>
<div
onClick={() => handleLoadTemplate(template)}
style={{
cursor: 'pointer',
fontWeight: '500',
cursor: "pointer",
fontWeight: "500",
// ':hover': {
// textDecoration: 'underline'
// }
@ -78,13 +89,13 @@ const Templates = () => {
<button
onClick={() => handleDeleteTemplate(template.id)}
style={{
padding: '0.25rem 0.5rem',
background: '#ff4444',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
transition: 'opacity 0.3s ease',
padding: "0.25rem 0.5rem",
background: "#ff4444",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
transition: "opacity 0.3s ease",
// ':hover': {
// opacity: 0.8
// }
@ -97,12 +108,14 @@ const Templates = () => {
</div>
))}
{templates.length === 0 && (
<div style={{
textAlign: 'center',
color: '#666',
padding: '2rem',
gridColumn: '1 / -1'
}}>
<div
style={{
textAlign: "center",
color: "#666",
padding: "2rem",
gridColumn: "1 / -1",
}}
>
No saved templates yet. Create one in the visualization view!
</div>
)}
@ -111,4 +124,3 @@ const Templates = () => {
};
export default Templates;

View File

@ -2,10 +2,10 @@ import { useState } from "react";
import ToggleHeader from "../../../../ui/inputs/ToggleHeader";
import Widgets2D from "./Widgets2D";
import Widgets3D from "./Widgets3D";
import WidgetsTemplate from "./WidgetsTemplate";
import WidgetsFloating from "./WidgetsFloating";
const Widgets = () => {
const [activeOption, setActiveOption] = useState("2D");
const [activeOption, setActiveOption] = useState("Floating");
const handleToggleClick = (option: string) => {
setActiveOption(option);
@ -14,13 +14,13 @@ const Widgets = () => {
return (
<div className="widget-left-sideBar">
<ToggleHeader
options={["2D", "3D", "Templates"]}
options={["2D", "3D", "Floating"]}
activeOption={activeOption}
handleClick={handleToggleClick}
/>
{activeOption === "2D" && <Widgets2D />}
{activeOption === "3D" && <Widgets3D />}
{activeOption === "Templates" && <WidgetsTemplate />}
{activeOption === "Floating" && <WidgetsFloating />}
</div>
);
};

View File

@ -0,0 +1,191 @@
import React, {
useState,
DragEvent,
MouseEvent,
useRef,
useEffect,
} from "react";
const WidgetsFloating: React.FC = () => {
const stateWorking = [
{ "Oil Tank": "24/341" },
{ "Oil Refin": "36.023" },
{ Transmission: "36.023" },
{ Fuel: "36732" },
{ Power: "1300" },
{ Time: "13-9-2023" },
];
// State for storing the dragged widget and its position
const [draggedFloating, setDraggedFloating] = useState<string | null>(null);
const [position, setPosition] = useState<{ x: number; y: number }>({
x: 0,
y: 0,
});
// State to store all placed widgets with their positions
const [placedWidgets, setPlacedWidgets] = useState<
{ name: string; x: number; y: number }[]
>([]);
const canvasRef = useRef<HTMLDivElement>(null);
// Handle the drag start event
const handleDragStart = (
event: DragEvent<HTMLDivElement>,
widget: string
) => {
setDraggedFloating(widget);
// Initialize position to the current position when drag starts
setPosition({ x: event.clientX, y: event.clientY });
};
// Handle the drag move event
const handleDragMove = (event: MouseEvent) => {
if (!draggedFloating) return;
// Calculate new position and update it
const canvas = canvasRef.current;
if (canvas) {
const canvasRect = canvas.getBoundingClientRect();
const newX = event.clientX - canvasRect.left;
const newY = event.clientY - canvasRect.top;
setPosition({ x: newX, y: newY });
}
};
// Handle the drag end event
const handleDragEnd = () => {
if (draggedFloating) {
// Store the final position of the dragged widget
setPlacedWidgets((prevWidgets) => [
...prevWidgets,
{ name: draggedFloating, x: position.x, y: position.y },
]);
// Reset the dragged floating widget after dragging is completed
setDraggedFloating(null);
}
};
useEffect(() => {
console.log("position: ", position);
console.log("draggedFloating: ", draggedFloating);
}, [draggedFloating, position]);
return (
<div
id="real-time-vis-canvas"
ref={canvasRef}
className="canvas"
style={{
position: "relative",
width: "100%",
height: "400px",
backgroundColor: "#f0f0f0",
border: "1px solid #ccc",
}}
onMouseMove={handleDragMove}
onMouseUp={handleDragEnd}
>
{/* The floating widget that's being dragged */}
{draggedFloating && (
<div
className="floating"
style={{
position: "absolute",
left: `${position.x}px`,
top: `${position.y}px`,
cursor: "move",
backgroundColor: "lightblue",
padding: "10px",
borderRadius: "5px",
}}
>
{draggedFloating}
</div>
)}
{/* Render all placed widgets */}
{placedWidgets.map((widget, index) => (
<div
key={index}
className="floating"
style={{
position: "absolute",
left: `${widget.x}px`,
top: `${widget.y}px`,
backgroundColor: "lightgreen",
padding: "10px",
borderRadius: "5px",
}}
>
{widget.name}
</div>
))}
{/* The rest of your floating widgets */}
<div
className="floating working-state"
draggable
onDragStart={(e) => handleDragStart(e, "working-state")}
style={{ position: "absolute", top: "50px", left: "50px" }}
>
<div className="state-working-top">
<div className="state-working-main">
<div className="state">State</div>
<div className="working-status">
<span className="working">Working</span>
<span className="dot"></span>
</div>
</div>
<div className="img">
<img
src="https://source.unsplash.com/random/150x100/?factory"
alt="Factory"
/>
</div>
</div>
<div className="state-working-data">
{stateWorking.map((state, index) => {
const key = Object.keys(state)[0];
const value = state[key];
return (
<div className="data-row" key={index}>
<span className="data-key">{key}:</span>
<span className="data-value">{value}</span>
</div>
);
})}
</div>
</div>
{/* Other floating widgets */}
<div
className="floating"
draggable
onDragStart={(e) => handleDragStart(e, "floating-2")}
style={{ position: "absolute", top: "120px", left: "150px" }}
>
floating-2
</div>
<div
className="floating"
draggable
onDragStart={(e) => handleDragStart(e, "floating-3")}
style={{ position: "absolute", top: "200px", left: "250px" }}
>
floating-3
</div>
<div
className="floating"
draggable
onDragStart={(e) => handleDragStart(e, "floating-4")}
style={{ position: "absolute", top: "300px", left: "350px" }}
>
floating-4
</div>
</div>
);
};
export default WidgetsFloating;

View File

@ -1,12 +0,0 @@
import React from 'react'
const WidgetsTemplate = () => {
return (
<div>
WidgetsTemplate
</div>
)
}
export default WidgetsTemplate

View File

@ -65,7 +65,7 @@ const SideBarRight: React.FC = () => {
)}
{/* realtime visualization */}
{activeModule === "visualization" && <Visualization />}
{toggleUI && activeModule === "visualization" && <Visualization />}
</div>
);
};

View File

@ -1,13 +1,9 @@
import React, { useEffect, useState } from "react";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import { RemoveIcon } from "../../../../icons/ExportCommonIcons";
import RegularDropDown from "../../../../ui/inputs/RegularDropDown";
import { AddIcon, RemoveIcon } from "../../../../icons/ExportCommonIcons";
import MultiLevelDropDown from "../../../../ui/inputs/MultiLevelDropDown";
interface Child {
id: number;
easing: string;
}
// Define the data structure for demonstration purposes
const DATA_STRUCTURE = {
furnace: {
coolingRate: "coolingRate",
@ -35,13 +31,22 @@ const DATA_STRUCTURE = {
data3: "Data 3",
},
timestamp: {
data1: "Data 1",
data1: {
Data01: "Data 01",
Data02: "Data 02",
Data03: "Data 03",
},
data2: "Data 2",
data3: "Data 3",
},
},
};
interface Child {
id: number;
easing: string;
}
interface Group {
id: number;
easing: string;
@ -65,11 +70,7 @@ const Data = () => {
{
id: Date.now(),
easing: "Connecter 1",
children: [
{ id: Date.now(), easing: "Linear" },
{ id: Date.now() + 1, easing: "Ease Out" },
{ id: Date.now() + 2, easing: "Linear" },
],
children: [{ id: Date.now(), easing: "Linear" }],
},
],
}));
@ -121,7 +122,43 @@ const Data = () => {
{selectedChartId?.title && (
<div className="sideBarHeader">{selectedChartId?.title}</div>
)}
{/* <MultiLevelDropDown data={DATA_STRUCTURE} /> */}
{/* Render groups dynamically */}
{chartDataGroups[selectedChartId?.id]?.map((group) => (
<div key={group.id}>
{group.children.map((child, index) => (
<div key={child.id} className="datas">
<div className="datas__label">Input {index + 1}</div>
<div className="datas__class">
<MultiLevelDropDown data={DATA_STRUCTURE} />
{/* Add Icon */}
{group.children.length < 7 && (
<div
className="icon"
onClick={() => handleAddClick(group.id)} // Pass groupId to handleAddClick
>
<AddIcon />
</div>
)}
{/* Remove Icon */}
<span
className={`datas__separator ${
group.children.length > 1 ? "" : "disable"
}`}
onClick={(e) => {
e.stopPropagation(); // Prevent event bubbling
removeChild(group.id, child.id); // Pass groupId and childId to removeChild
}}
>
<RemoveIcon />
</span>
</div>
</div>
))}
</div>
))}
{/* Info Box */}
<div className="infoBox">
<span className="infoIcon">i</span>
<p>

View File

@ -19,6 +19,7 @@ import useTemplateStore from "../../store/useTemplateStore";
import { useSelectedZoneStore } from "../../store/useZoneStore";
const Tools: React.FC = () => {
const { templates } = useTemplateStore();
const [activeTool, setActiveTool] = useState("cursor");
const [activeSubTool, setActiveSubTool] = useState("cursor");
const [toggleThreeD, setToggleThreeD] = useState(true);
@ -196,7 +197,13 @@ const Tools: React.FC = () => {
<div className="draw-tools">
<div
className={`tool-button`}
onClick={() => handleSaveTemplate({ addTemplate, selectedZone })}
onClick={() =>
handleSaveTemplate({
addTemplate,
selectedZone,
templates,
})
}
>
<SaveTemplateIcon isActive={false} />
</div>

View File

@ -0,0 +1,94 @@
import { useRef, useMemo } from "react";
import { Bar, Line } from "react-chartjs-2";
interface ChartComponentProps {
type: any;
title: string;
fontFamily?: string;
fontSize?: string;
fontWeight?: "Light" | "Regular" | "Bold";
data: any;
}
const LineGraphComponent = ({
title,
fontFamily,
fontSize,
fontWeight = "Regular",
}: ChartComponentProps) => {
// Memoize Font Weight Mapping
const chartFontWeightMap = useMemo(
() => ({
Light: "lighter" as const,
Regular: "normal" as const,
Bold: "bold" as const,
}),
[]
);
// Parse and Memoize Font Size
const fontSizeValue = useMemo(
() => (fontSize ? parseInt(fontSize) : 12),
[fontSize]
);
// Determine and Memoize Font Weight
const fontWeightValue = useMemo(
() => chartFontWeightMap[fontWeight],
[fontWeight, chartFontWeightMap]
);
// Memoize Chart Font Style
const chartFontStyle = useMemo(
() => ({
family: fontFamily || "Arial",
size: fontSizeValue,
weight: fontWeightValue,
}),
[fontFamily, fontSizeValue, fontWeightValue]
);
const options = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: title,
font: chartFontStyle,
},
legend: {
display: false,
},
},
scales: {
x: {
ticks: {
display: false, // This hides the x-axis labels
},
},
},
}),
[title, chartFontStyle]
);
const chartData = {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [
{
label: "My First Dataset",
data: [65, 59, 80, 81, 56, 55, 40],
backgroundColor: "#6f42c1",
borderColor: "#ffffff",
borderWidth: 2,
fill: false,
},
],
};
return <Bar data={chartData} options={options} />;
};
export default LineGraphComponent;

View File

@ -0,0 +1,93 @@
import { useRef, useMemo } from "react";
import { Line } from "react-chartjs-2";
interface ChartComponentProps {
type: any;
title: string;
fontFamily?: string;
fontSize?: string;
fontWeight?: "Light" | "Regular" | "Bold";
data: any;
}
const LineGraphComponent = ({
title,
fontFamily,
fontSize,
fontWeight = "Regular",
}: ChartComponentProps) => {
// Memoize Font Weight Mapping
const chartFontWeightMap = useMemo(
() => ({
Light: "lighter" as const,
Regular: "normal" as const,
Bold: "bold" as const,
}),
[]
);
// Parse and Memoize Font Size
const fontSizeValue = useMemo(
() => (fontSize ? parseInt(fontSize) : 12),
[fontSize]
);
// Determine and Memoize Font Weight
const fontWeightValue = useMemo(
() => chartFontWeightMap[fontWeight],
[fontWeight, chartFontWeightMap]
);
// Memoize Chart Font Style
const chartFontStyle = useMemo(
() => ({
family: fontFamily || "Arial",
size: fontSizeValue,
weight: fontWeightValue,
}),
[fontFamily, fontSizeValue, fontWeightValue]
);
const options = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: title,
font: chartFontStyle,
},
legend: {
display: false,
},
},
scales: {
x: {
ticks: {
display: false, // This hides the x-axis labels
},
},
},
}),
[title, chartFontStyle]
);
const chartData = {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [
{
label: "My First Dataset",
data: [65, 59, 80, 81, 56, 55, 40],
backgroundColor: "#6f42c1", // Updated to #6f42c1 (Purple)
borderColor: "#ffffff", // Keeping border color white
borderWidth: 2,
fill: false,
},
],
};
return <Line data={chartData} options={options} />;
};
export default LineGraphComponent;

View File

@ -0,0 +1,90 @@
import { useRef, useMemo } from "react";
import { Pie } from "react-chartjs-2";
interface ChartComponentProps {
type: any;
title: string;
fontFamily?: string;
fontSize?: string;
fontWeight?: "Light" | "Regular" | "Bold";
data: any;
}
const PieChartComponent = ({
title,
fontFamily,
fontSize,
fontWeight = "Regular",
}: ChartComponentProps) => {
// Memoize Font Weight Mapping
const chartFontWeightMap = useMemo(
() => ({
Light: "lighter" as const,
Regular: "normal" as const,
Bold: "bold" as const,
}),
[]
);
// Parse and Memoize Font Size
const fontSizeValue = useMemo(
() => (fontSize ? parseInt(fontSize) : 12),
[fontSize]
);
// Determine and Memoize Font Weight
const fontWeightValue = useMemo(
() => chartFontWeightMap[fontWeight],
[fontWeight, chartFontWeightMap]
);
// Memoize Chart Font Style
const chartFontStyle = useMemo(
() => ({
family: fontFamily || "Arial",
size: fontSizeValue,
weight: fontWeightValue,
}),
[fontFamily, fontSizeValue, fontWeightValue]
);
// Access the CSS variable for the primary accent color
const accentColor = getComputedStyle(document.documentElement)
.getPropertyValue("--accent-color")
.trim();
const options = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: title,
font: chartFontStyle,
},
legend: {
display: false,
},
},
}),
[title, chartFontStyle]
);
const chartData = {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [
{
label: "Dataset",
data: [12, 19, 3, 5, 2, 3],
backgroundColor: ["#6f42c1"],
borderColor: "#ffffff",
borderWidth: 2,
},
],
};
return <Pie data={chartData} options={options} />;
};
export default PieChartComponent;

View File

@ -11,7 +11,7 @@ type Side = "top" | "bottom" | "left" | "right";
// Define the type for the props passed to the Buttons component
interface ButtonsProps {
selectedZone: {
zoneName: string; // Add zoneName property
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
@ -25,7 +25,7 @@ interface ButtonsProps {
};
setSelectedZone: React.Dispatch<
React.SetStateAction<{
zoneName: string; // Ensure zoneName is included in the state type
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
@ -38,12 +38,18 @@ interface ButtonsProps {
}[];
}>
>;
hiddenPanels: Side[]; // Add this prop for hidden panels
setHiddenPanels: React.Dispatch<React.SetStateAction<Side[]>>; // Add this prop for updating hidden panels
}
const AddButtons: React.FC<ButtonsProps> = ({
selectedZone,
setSelectedZone,
setHiddenPanels,
hiddenPanels,
}) => {
// Local state to track hidden panels
// Function to toggle lock/unlock a panel
const toggleLockPanel = (side: Side) => {
const newLockedPanels = selectedZone.lockedPanels.includes(side)
@ -61,18 +67,14 @@ const AddButtons: React.FC<ButtonsProps> = ({
// Function to toggle visibility of a panel
const toggleVisibility = (side: Side) => {
const newActiveSides = selectedZone.activeSides.includes(side)
? selectedZone.activeSides.filter((s) => s !== side)
: [...selectedZone.activeSides, side];
const updatedZone = {
...selectedZone,
activeSides: newActiveSides,
panelOrder: newActiveSides,
};
// Update the selectedZone state
setSelectedZone(updatedZone);
const isHidden = hiddenPanels.includes(side);
if (isHidden) {
// If the panel is already hidden, remove it from the hiddenPanels array
setHiddenPanels(hiddenPanels.filter((panel) => panel !== side));
} else {
// If the panel is visible, add it to the hiddenPanels array
setHiddenPanels([...hiddenPanels, side]);
}
};
// Function to clean all widgets from a panel
@ -145,8 +147,12 @@ const AddButtons: React.FC<ButtonsProps> = ({
<div className="extra-Bs">
{/* Hide Panel */}
<div
className="icon"
title="Hide Panel"
className={`icon ${
hiddenPanels.includes(side) ? "active" : ""
}`}
title={
hiddenPanels.includes(side) ? "Show Panel" : "Hide Panel"
}
onClick={() => toggleVisibility(side)}
>
<EyeIcon />

View File

@ -0,0 +1,178 @@
import React, { useEffect, useRef } from "react";
import { useSelectedZoneStore } from "../../../store/useZoneStore";
import { Widget } from "../../../store/useWidgetStore";
// Define the type for `Side`
type Side = "top" | "bottom" | "left" | "right";
interface DisplayZoneProps {
zonesData: {
[key: string]: {
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
widgets: Widget[];
};
};
selectedZone: {
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
widgets: {
id: string;
type: string;
title: string;
panel: Side;
data: any;
}[];
};
setSelectedZone: React.Dispatch<
React.SetStateAction<{
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
widgets: {
id: string;
type: string;
title: string;
panel: Side;
data: any;
}[];
}>
>;
}
const DisplayZone: React.FC<DisplayZoneProps> = ({
zonesData,
selectedZone,
setSelectedZone,
}) => {
// Ref for the container element
const containerRef = useRef<HTMLDivElement | null>(null);
// Example state for selectedOption and options (adjust based on your actual use case)
const [selectedOption, setSelectedOption] = React.useState<string | null>(
null
);
const [options, setOptions] = React.useState<string[]>([]);
// Scroll to the selected option when it changes
useEffect(() => {
const container = containerRef.current;
if (container && selectedOption) {
// Handle scrolling to the selected option
const index = options.findIndex((option) => {
const formattedOption = formatOptionName(option);
const selectedFormattedOption =
selectedOption?.split("_")[1] || selectedOption;
return formattedOption === selectedFormattedOption;
});
if (index !== -1) {
const optionElement = container.children[index] as HTMLElement;
if (optionElement) {
optionElement.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
}
}
}
}, [selectedOption, options]);
useEffect(() => {
const container = containerRef.current;
const handleWheel = (event: WheelEvent) => {
event.preventDefault();
if (container) {
container.scrollBy({
left: event.deltaY * 2, // Adjust the multiplier for faster scrolling
behavior: "smooth",
});
}
};
let isDragging = false;
let startX: number;
let scrollLeft: number;
const handleMouseDown = (event: MouseEvent) => {
isDragging = true;
startX = event.pageX - (container?.offsetLeft || 0);
scrollLeft = container?.scrollLeft || 0;
};
const handleMouseMove = (event: MouseEvent) => {
if (!isDragging || !container) return;
event.preventDefault();
const x = event.pageX - (container.offsetLeft || 0);
const walk = (x - startX) * 2; // Adjust the multiplier for faster dragging
container.scrollLeft = scrollLeft - walk;
};
const handleMouseUp = () => {
isDragging = false;
};
const handleMouseLeave = () => {
isDragging = false;
};
if (container) {
container.addEventListener("wheel", handleWheel, { passive: false });
container.addEventListener("mousedown", handleMouseDown);
container.addEventListener("mousemove", handleMouseMove);
container.addEventListener("mouseup", handleMouseUp);
container.addEventListener("mouseleave", handleMouseLeave);
}
return () => {
if (container) {
container.removeEventListener("wheel", handleWheel);
container.removeEventListener("mousedown", handleMouseDown);
container.removeEventListener("mousemove", handleMouseMove);
container.removeEventListener("mouseup", handleMouseUp);
container.removeEventListener("mouseleave", handleMouseLeave);
}
};
}, []);
// Helper function to format option names (customize as needed)
const formatOptionName = (option: string): string => {
// Replace underscores with spaces and capitalize the first letter
return option.replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase());
};
return (
<div
ref={containerRef}
className={`zoon-wrapper ${
selectedZone.activeSides.includes("bottom") && "bottom"
}`}
>
{Object.keys(zonesData).map((zoneName, index) => (
<div
key={index}
className={`zone ${
selectedZone.zoneName === zoneName ? "active" : ""
}`}
onClick={() => {
setSelectedZone({
zoneName,
...zonesData[zoneName],
});
}}
>
{zoneName}
</div>
))}
</div>
);
};
export default DisplayZone;

View File

@ -1,217 +1,84 @@
import { useMemo, useState } from "react";
import ChartComponent from "../../layout/sidebarLeft/visualization/widgets/ChartComponent";
import { useWidgetStore } from "../../../store/useWidgetStore";
import PieGraphComponent from "../charts/PieGraphComponent";
import BarGraphComponent from "../charts/BarGraphComponent";
import LineGraphComponent from "../charts/LineGraphComponent";
export const DraggableWidget = ({ widget }: { widget: any }) => {
const { selectedChartId, setSelectedChartId } = useWidgetStore();
// State for managing the popup visibility and customization options
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [customizationOptions, setCustomizationOptions] = useState({
templateBackground: "#ffffff",
cardBackground: "#ffffff",
cardOpacity: 1,
cardBlur: 0,
font: "Arial",
margin: 0,
radius: 5,
shadow: "Low",
});
// Handle pointer down to select the chart
const handlePointerDown = () => {
if (selectedChartId?.id !== widget.id) {
setSelectedChartId(widget);
}
};
// Handle double-click to open the popup
const handleDoubleClick = () => {
setIsPopupOpen(true);
};
// Close the popup
const handleClosePopup = () => {
setIsPopupOpen(false);
};
// Save the changes made in the popup
const handleSaveChanges = () => {
// Here you can save the customizationOptions to your store or API
console.log("Saved Customization Options:", customizationOptions);
setIsPopupOpen(false);
};
// Compute dynamic card styles based on customizationOptions
const cardStyle = useMemo(() => {
const shadowLevels = {
Low: "0px 2px 4px rgba(0, 0, 0, 0.1)",
Medium: "0px 4px 8px rgba(0, 0, 0, 0.2)",
High: "0px 8px 16px rgba(0, 0, 0, 0.3)",
};
return {
backgroundColor: customizationOptions.cardBackground,
opacity: customizationOptions.cardOpacity,
filter: `blur(${customizationOptions.cardBlur}px)`,
fontFamily: customizationOptions.font,
margin: `${customizationOptions.margin}px`,
borderRadius: `${customizationOptions.radius}px`,
// boxShadow: shadowLevels[customizationOptions.shadow],
};
}, [customizationOptions]);
return (
<>
<div
key={widget.id}
className={`chart-container ${selectedChartId?.id === widget.id && "activeChart"}`}
style={cardStyle} // Apply dynamic card styles here
className={`chart-container ${
selectedChartId?.id === widget.id && "activeChart"
}`}
onPointerDown={handlePointerDown}
onDoubleClick={handleDoubleClick} // Add double-click event
>
{widget.type === "progress" ? (
// <ProgressCard title={widget.title} data={widget.data} />
<></>
) : (
<ChartComponent
<>
{widget.type === "line" && (
<LineGraphComponent
type={widget.type}
title={widget.title}
fontFamily={customizationOptions.font} // Pass font customization to ChartComponent
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
data={widget.data}
data={{
measurements: [
{ name: "testDevice", fields: "powerConsumption" },
{ name: "furnace", fields: "powerConsumption" },
],
interval: 1000,
duration: "1h",
}}
/>
)}
</div>
{/* Popup for Customizing Template Theme */}
{isPopupOpen && (
<div className="popup-overlay">
<div className="popup-content">
<h2>Customize Template Theme</h2>
<div className="form-group">
<label>Template Background</label>
<input
type="color"
value={customizationOptions.templateBackground}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
templateBackground: e.target.value,
}))
}
{widget.type === "bar" && (
<BarGraphComponent
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
data={{
measurements: [
{ name: "testDevice", fields: "powerConsumption" },
{ name: "furnace", fields: "powerConsumption" },
],
interval: 1000,
duration: "1h",
}}
/>
</div>
<div className="form-group">
<label>Card Background</label>
<input
type="color"
value={customizationOptions.cardBackground}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
cardBackground: e.target.value,
}))
}
/>
</div>
<div className="form-group">
<label>Card Opacity</label>
<input
type="range"
min="0"
max="1"
step="0.1"
value={customizationOptions.cardOpacity}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
cardOpacity: parseFloat(e.target.value),
}))
}
/>
</div>
<div className="form-group">
<label>Card Blur</label>
<input
type="range"
min="0"
max="10"
value={customizationOptions.cardBlur}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
cardBlur: parseInt(e.target.value),
}))
}
/>
</div>
<div className="form-group">
<label>Font</label>
<select
value={customizationOptions.font}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
font: e.target.value,
}))
}
>
<option value="Arial">Arial</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Courier New">Courier New</option>
</select>
</div>
<div className="form-group">
<label>Margin</label>
<input
type="number"
value={customizationOptions.margin}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
margin: parseInt(e.target.value),
}))
}
/>
</div>
<div className="form-group">
<label>Radius</label>
<input
type="number"
value={customizationOptions.radius}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
radius: parseInt(e.target.value),
}))
}
/>
</div>
<div className="form-group">
<label>Shadow</label>
<select
value={customizationOptions.shadow}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
shadow: e.target.value,
}))
}
>
<option value="Low">Low</option>
<option value="Medium">Medium</option>
<option value="High">High</option>
</select>
</div>
<div className="popup-actions">
<button onClick={handleClosePopup}>Cancel</button>
<button onClick={handleSaveChanges}>Save Changes</button>
</div>
</div>
</div>
)}
{widget.type === "pie" && (
<PieGraphComponent
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
data={{
measurements: [
{ name: "testDevice", fields: "powerConsumption" },
{ name: "furnace", fields: "powerConsumption" },
],
interval: 1000,
duration: "1h",
}}
/>
)}
</>
)}
</div>
</>
);
};

View File

@ -84,46 +84,53 @@ const Panel: React.FC<PanelProps> = ({ selectedZone, setSelectedZone }) => {
e.preventDefault();
const { draggedAsset } = useWidgetStore.getState();
if (draggedAsset) {
if (selectedZone.lockedPanels.includes(panel)) return;
if (!draggedAsset) return;
if (isPanelLocked(panel)) return;
const currentWidgetsInPanel = selectedZone.widgets.filter(
(w) => w.panel === panel
).length;
const currentWidgetsCount = getCurrentWidgetCount(panel);
const maxCapacity = calculatePanelCapacity(panel);
const dimensions = panelDimensions[panel];
const CHART_WIDTH = 200;
const CHART_HEIGHT = 200;
let maxCharts = 0;
if (currentWidgetsCount >= maxCapacity) return;
if (dimensions) {
if (panel === "top" || panel === "bottom") {
maxCharts = Math.floor(dimensions.width / CHART_WIDTH);
} else {
maxCharts = Math.floor(dimensions.height / CHART_HEIGHT);
}
} else {
maxCharts = panel === "top" || panel === "bottom" ? 5 : 3;
}
if (currentWidgetsInPanel >= maxCharts) {
return;
}
const updatedZone = {
...selectedZone,
widgets: [
...selectedZone.widgets,
{
...draggedAsset,
id: generateUniqueId(),
panel,
},
],
addWidgetToPanel(draggedAsset, panel);
};
setSelectedZone(updatedZone);
// Helper functions
const isPanelLocked = (panel: Side) =>
selectedZone.lockedPanels.includes(panel);
const getCurrentWidgetCount = (panel: Side) =>
selectedZone.widgets.filter(w => w.panel === panel).length;
const calculatePanelCapacity = (panel: Side) => {
const CHART_WIDTH = 200;
const CHART_HEIGHT = 200;
const FALLBACK_HORIZONTAL_CAPACITY = 5;
const FALLBACK_VERTICAL_CAPACITY = 3;
const dimensions = panelDimensions[panel];
if (!dimensions) {
return panel === "top" || panel === "bottom"
? FALLBACK_HORIZONTAL_CAPACITY
: FALLBACK_VERTICAL_CAPACITY;
}
return panel === "top" || panel === "bottom"
? Math.floor(dimensions.width / CHART_WIDTH)
: Math.floor(dimensions.height / CHART_HEIGHT);
};
const addWidgetToPanel = (asset: any, panel: Side) => {
const newWidget = {
...asset,
id: generateUniqueId(),
panel,
};
setSelectedZone(prev => ({
...prev,
widgets: [...prev.widgets, newWidget]
}));
};
useEffect(() => {
@ -172,7 +179,7 @@ const Panel: React.FC<PanelProps> = ({ selectedZone, setSelectedZone }) => {
}}
>
<div
className="panel-content"
className={`panel-content ${isPlaying && "fullScreen"}`}
style={{
pointerEvents: selectedZone.lockedPanels.includes(side)
? "none"
@ -180,6 +187,7 @@ const Panel: React.FC<PanelProps> = ({ selectedZone, setSelectedZone }) => {
opacity: selectedZone.lockedPanels.includes(side) ? "0.8" : "1",
}}
>
<>{}</>
{selectedZone.widgets
.filter((w) => w.panel === side)
.map((widget) => (
@ -193,5 +201,3 @@ const Panel: React.FC<PanelProps> = ({ selectedZone, setSelectedZone }) => {
};
export default Panel;
// only load selected template

View File

@ -3,6 +3,7 @@ import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import Panel from "./Panel";
import AddButtons from "./AddButtons";
import { useSelectedZoneStore } from "../../../store/useZoneStore";
import DisplayZone from "./DisplayZone";
type Side = "top" | "bottom" | "left" | "right";
@ -15,6 +16,7 @@ interface Widget {
}
const RealTimeVisulization: React.FC = () => {
const [hiddenPanels, setHiddenPanels] = React.useState<Side[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
const [zonesData, setZonesData] = useState<{
[key: string]: {
@ -74,34 +76,19 @@ const RealTimeVisulization: React.FC = () => {
style={{
height: isPlaying ? "100vh" : "",
width: isPlaying ? "100%" : "",
left: isPlaying ? "50%" : "",
left: isPlaying ? "0%" : "",
}}
>
<div
className={`zoon-wrapper ${
selectedZone.activeSides.includes("bottom") && "bottom"
}`}
>
{Object.keys(zonesData).map((zoneName, index) => (
<div
key={index}
className={`zone ${
selectedZone.zoneName === zoneName ? "active" : ""
}`}
onClick={() => {
setSelectedZone({
zoneName,
...zonesData[zoneName],
});
}}
>
{zoneName}
</div>
))}
</div>
<DisplayZone
zonesData={zonesData}
selectedZone={selectedZone}
setSelectedZone={setSelectedZone}
/>
{!isPlaying && (
<AddButtons
hiddenPanels={hiddenPanels}
setHiddenPanels={setHiddenPanels}
selectedZone={selectedZone}
setSelectedZone={setSelectedZone}
/>

View File

@ -1,5 +1,4 @@
import React, { useState } from "react";
import React, { useState, useRef, useEffect } from "react";
// Dropdown Item Component
const DropdownItem = ({
@ -27,9 +26,11 @@ const DropdownItem = ({
const NestedDropdown = ({
label,
children,
onSelect,
}: {
label: string;
children: React.ReactNode;
onSelect: (selectedLabel: string) => void;
}) => {
const [open, setOpen] = useState(false);
@ -37,36 +38,47 @@ const NestedDropdown = ({
<div className="nested-dropdown">
{/* Dropdown Trigger */}
<div
className="dropdown-trigger"
className={`dropdown-trigger ${open ? "open" : ""}`}
onClick={() => setOpen(!open)} // Toggle submenu on click
>
{label} <span className="icon">{open ? "▼" : "▶"}</span>
</div>
{/* Submenu */}
{open && <div className="submenu">{children}</div>}
{open && (
<div className="submenu">
{React.Children.map(children, (child) =>
React.cloneElement(child as React.ReactElement, { onSelect })
)}
</div>
)}
</div>
);
};
// Recursive Function to Render Nested Data
const renderNestedData = (data: Record<string, any>) => {
const renderNestedData = (
data: Record<string, any>,
onSelect: (selectedLabel: string) => void
) => {
return Object.entries(data).map(([key, value]) => {
if (typeof value === "object" && !Array.isArray(value)) {
// If the value is an object, render it as a nested dropdown
return (
<NestedDropdown key={key} label={key}>
{renderNestedData(value)}
<NestedDropdown key={key} label={key} onSelect={onSelect}>
{renderNestedData(value, onSelect)}
</NestedDropdown>
);
} else if (Array.isArray(value)) {
// If the value is an array, render each item as a dropdown item
return value.map((item, index) => (
<DropdownItem key={index} label={item} />
<DropdownItem key={index} label={item} onClick={() => onSelect(item)} />
));
} else {
// If the value is a simple string, render it as a dropdown item
return <DropdownItem key={key} label={value} />;
return (
<DropdownItem key={key} label={value} onClick={() => onSelect(value)} />
);
}
});
};
@ -74,19 +86,50 @@ const renderNestedData = (data: Record<string, any>) => {
// Main Multi-Level Dropdown Component
const MultiLevelDropdown = ({ data }: { data: Record<string, any> }) => {
const [open, setOpen] = useState(false);
const [selectedLabel, setSelectedLabel] = useState("Dropdown trigger");
const dropdownRef = useRef<HTMLDivElement>(null);
// Handle outer click to close the dropdown
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
// Handle selection of an item
const handleSelect = (selectedLabel: string) => {
setSelectedLabel(selectedLabel); // Update the dropdown trigger text
setOpen(false); // Close the dropdown
};
return (
<div className="multi-level-dropdown">
<div className="multi-level-dropdown" ref={dropdownRef}>
{/* Dropdown Trigger Button */}
<button
className="dropdown-button"
className={`dropdown-button ${open ? "open" : ""}`}
onClick={() => setOpen(!open)} // Toggle main menu on click
>
Dropdown trigger <span className="icon"></span>
{selectedLabel} <span className="icon"></span>
</button>
{/* Dropdown Menu */}
{open && <div className="dropdown-menu">{renderNestedData(data)}</div>}
{open && (
<div className="dropdown-menu">
<div className="dropdown-content">
{renderNestedData(data, handleSelect)}
</div>
</div>
)}
</div>
);
};

View File

@ -7,30 +7,68 @@ type HandleSaveTemplateProps = {
panelOrder: string[]; // Adjust the type based on actual data structure
widgets: any[]; // Replace `any` with the actual widget type
};
templates?: Template[];
};
// Generate a unique ID (placeholder function)
const generateUniqueId = (): string => {
return Math.random().toString(36).substring(2, 15);
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
};
// Refactored function
export const handleSaveTemplate = async ({
addTemplate,
selectedZone,
templates = [],
}: HandleSaveTemplateProps): Promise<void> => {
try {
// Check if the selected zone has any widgets
if (!selectedZone.widgets || selectedZone.widgets.length === 0) {
console.warn("Cannot save an empty template.");
return;
}
// Check if the template already exists
const isDuplicate = templates.some((template) => {
const isSamePanelOrder =
JSON.stringify(template.panelOrder) ===
JSON.stringify(selectedZone.panelOrder);
const isSameWidgets =
JSON.stringify(template.widgets) ===
JSON.stringify(selectedZone.widgets);
return isSamePanelOrder && isSameWidgets;
});
if (isDuplicate) {
console.warn("This template already exists.");
return;
}
// Capture visualization snapshot
const snapshot = await captureVisualization();
const template: Template = {
if (!snapshot) {
console.error("Failed to capture visualization snapshot.");
return;
}
// Create a new template
const newTemplate: Template = {
id: generateUniqueId(),
name: `Template ${Date.now()}`,
panelOrder: selectedZone.panelOrder,
widgets: selectedZone.widgets,
snapshot,
};
console.log('template: ', template);
addTemplate(template);
console.log("Saving template:", newTemplate);
// Save the template
try {
addTemplate(newTemplate);
} catch (error) {
console.error('Failed to save template:', error);
console.error("Failed to add template:", error);
}
} catch (error) {
console.error("Failed to save template:", error);
}
};

View File

@ -35,7 +35,7 @@ $highlight-accent-color-dark: #403e6a; // Highlighted accent for dark mode
$background-color: #fcfdfd; // Main background color
$background-color-dark: #19191d; // Main background color for dark mode
$background-color-secondary: #e1e0ff80; // Secondary background color
$background-color-secondary-dark: #1f1f2399; // Secondary background color for dark mode
$background-color-secondary-dark: #39394f99; // Secondary background color for dark mode
// Border colors
$border-color: #e0dfff; // Default border color

View File

@ -36,9 +36,11 @@
overflow-y: auto; // Optional: Enable scrolling if content exceeds height
left: 0;
top: 104%;
.option {
padding: 5px;
cursor: pointer;
flex-direction: row !important;
&:hover {
background-color: var(--primary-color); // Optional: Hover effect

View File

@ -0,0 +1,110 @@
@use "../../../abstracts/variables" as *;
@use "../../../abstracts/mixins" as *;
.floatingWidgets-wrapper {
display: flex;
flex-direction: column;
gap: 6px;
padding-top: 12px;
padding: 6px;
.floating {
min-height: 170px;
background: var(--background-color);
border: 1.23px solid var(--border-color);
box-shadow: 0px 4.91px 4.91px 0px #0000001c;
border-radius: $border-radius-medium;
padding: 12px 6px;
}
.working-state {
display: flex;
flex-direction: column;
gap: 6px;
.state-working-top {
display: flex;
}
}
}
.floatingWidgets-wrapper {
font-family: Arial, sans-serif;
color: #333;
}
.floating.working-state {
width: 100%;
height: 283px;
background: #f5f5f5;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 20px;
box-sizing: border-box;
}
.state-working-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
// flex-direction: column;
}
.state {
font-size: 24px;
font-weight: bold;
}
.working-status {
display: flex;
align-items: center;
gap: 8px;
}
.working {
font-size: 20px;
color: #4CAF50;
}
.dot {
display: inline-block;
width: 10px;
height: 10px;
background: #4CAF50;
border-radius: 50%;
}
.img img {
width: 150px;
height: 100px;
border-radius: 4px;
object-fit: cover;
}
.state-working-data {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.data-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
padding: 4px 0;
}
.data-key {
color: #666;
}
.data-value {
font-weight: bold;
color: #333;
}

View File

@ -78,14 +78,14 @@
display: flex;
flex-direction: column;
gap: 8px;
padding-right: 6px;
padding: 6px;
flex-wrap: nowrap;
overflow: auto;
.chart {
min-height: 170px;
background: var(--background-primary, #fcfdfd);
border: 1.23px solid var(--Grays-Gray-5, #e5e5ea);
background: var(--background-color);
border: 1.23px solid var(--border-color);
box-shadow: 0px 4.91px 4.91px 0px #0000001c;
border-radius: $border-radius-medium;
padding: 12px 6px;
@ -107,7 +107,7 @@
.stock {
padding: 13px 5px;
background-color: #e0dfff80;
background-color: var(--background-color-secondary);
border-radius: 6.33px;
display: flex;
justify-content: space-between;
@ -291,8 +291,46 @@
gap: 12px;
padding: 10px 12px;
.datas {
display: flex;
align-items: center;
justify-content: space-between;
.datas__class {
display: flex;
align-items: center;
.multi-level-dropdown {
min-width: 100px;
.dropdown-button {
display: flex;
justify-content: space-between;
gap: 6px;
}
}
}
.datas__class {
display: flex;
gap: 12px;
.datas__separator {}
.disable {
cursor: not-allowed;
pointer-events: none;
/* Disables all mouse interactions */
opacity: 0.5;
/* Optional: Makes the button look visually disabled */
}
}
}
.sideBarHeader {
color: #5c87df;
color: var(--accent-color);
border-bottom: 1px solid var(--border-color);
padding-bottom: 6px;
}
@ -388,7 +426,7 @@
width: 100%;
height: 150px;
background: #f0f0f0;
border-radius: 8px;
// border-radius: 8px;
}
.optionsContainer {
@ -523,93 +561,119 @@
}
}
/* Base styles */
.multi-level-dropdown {
position: relative;
display: inline-block;
text-align: left;
.dropdown-button {
background-color: #3b82f6; /* Blue background */
color: white;
padding: 0.5rem 1rem;
font-size: 0.875rem;
border: none;
border-radius: 0.375rem;
width: 100%;
background-color: var(--background-color) !important;
border: 1px solid var(--border-color) !important;
padding: 5px 10px;
// font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
transition: background-color 0.2s ease;
border-radius: 5px;
transition: background-color 0.3s ease;
&:hover {
background-color: #2563eb; /* Darker blue on hover */
background-color: #333333;
}
.icon {
margin-left: 0.5rem;
&.open {
background-color: #333333;
}
}
.dropdown-menu {
position: absolute;
top: calc(100% + 0.5rem); /* Add spacing below the button */
top: 100%;
left: 0;
width: 12rem;
background-color: white;
border: 1px solid #e5e7eb; /* Light gray border */
border-radius: 0.375rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow */
z-index: 10;
}
}
background-color: #ffffff;
border: 1px solid #cccccc;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
min-width: 200px;
overflow: auto;
max-height: 600px;
/* Dropdown Item */
.dropdown-item {
.dropdown-content {
display: flex;
flex-direction: column;
gap: 6px;
.nested-dropdown {
// &:first-child{
margin-left: 0;
// }
}
padding: 10px;
}
.dropdown-item {
display: block;
padding: 0.5rem 1rem;
font-size: 0.875rem;
color: #4b5563; /* Gray text */
padding: 5px 10px;
text-decoration: none;
color: #000000;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s ease;
transition: background-color 0.3s ease;
&:hover {
background-color: #f3f4f6; /* Light gray background on hover */
background-color: #f0f0f0;
}
}
}
/* Nested Dropdown */
.nested-dropdown {
position: relative;
.nested-dropdown {
margin-left: 20px;
.dropdown-trigger {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem;
font-size: 0.875rem;
color: #4b5563; /* Gray text */
justify-content: space-between;
padding: 5px 10px;
cursor: pointer;
transition: background-color 0.2s ease;
font-size: 14px;
color: #000000;
transition: background-color 0.3s ease;
&:hover {
background-color: #f3f4f6; /* Light gray background on hover */
background-color: #f0f0f0;
}
&.open {
background-color: #e0e0e0;
}
.icon {
margin-left: 0.5rem;
font-size: 12px;
margin-left: 5px;
}
}
.submenu {
position: absolute;
top: 0;
left: 100%; /* Position submenu to the right */
width: 12rem;
background-color: white;
border: 1px solid #e5e7eb; /* Light gray border */
border-radius: 0.375rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow */
z-index: 20;
margin-top: 5px;
padding-left: 20px;
border-left: 2px solid #cccccc;
display: flex;
flex-direction: column;
gap: 6px;
}
}
}
}

View File

@ -19,6 +19,7 @@
@use 'components/templates';
@use 'components/tools';
@use 'components/regularDropDown';
@use 'components/visualization/floating/energyConsumed';
// layout
@use 'layout/sidebar';

View File

@ -4,7 +4,7 @@
.realTime-viz {
background-color: var(--background-color);
border-radius: 20px;
box-shadow: 0px 4px 8px rgba(60, 60, 67, 0.1019607843);
box-shadow: $box-shadow-medium;
width: calc(100% - (320px + 270px + 80px));
height: 600px;
position: absolute;
@ -30,7 +30,7 @@
.zoon-wrapper {
display: flex;
background-color: #e0dfff80;
background-color: var(--background-color);
position: absolute;
bottom: 10px;
left: 50%;
@ -40,14 +40,14 @@
border-radius: 8px;
max-width: 80%;
overflow: auto;
max-width: calc(100% - 450px);
&::-webkit-scrollbar {
display: none;
}
.zone {
width: auto;
background-color: #fcfdfd;
background-color: var(--background-color);
border-radius: 6px;
padding: 4px 8px;
white-space: nowrap;
@ -83,43 +83,6 @@
}
}
@media (max-width: 768px) {
width: 90%; // Take even more width on very small screens
height: 400px; // Further reduce height
top: 45%; // Adjust vertical position slightly upward
.panel {
&.top-panel,
&.bottom-panel {
.panel-content {
flex-direction: column; // Stack panels vertically on small screens
.chart-container {
width: 100%; // Make charts full width
height: 150px; // Reduce chart height
}
}
}
}
}
@media (max-width: 480px) {
width: 95%; // Take almost full width on very small devices
height: 350px; // Further reduce height
top: 40%; // Move slightly higher for better visibility
.side-button-container {
flex-direction: row !important; // Force buttons into a row
gap: 4px; // Reduce spacing between buttons
&.top,
&.bottom {
left: 50%; // Center horizontally
transform: translateX(-50%);
}
}
}
.content-container {
display: flex;
height: 100vh;
@ -189,6 +152,7 @@
display: flex;
flex-direction: column;
gap: 10px;
background-color: var(--background-color);
&::-webkit-scrollbar {
display: none;
@ -216,18 +180,23 @@
}
}
&.top-panel,
&.bottom-panel {
left: 0;
right: 0;
.fullScreen {
background-color: red;
}
.panel-content {
display: flex;
flex-direction: row;
.chart-container {
height: 100%;
width: 230px;
width: 200px;
}
}
}
@ -247,7 +216,7 @@
.chart-container {
width: 100%;
height: 200px;
height: 180px;
}
}
@ -275,6 +244,15 @@
.icon {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 4px;
}
.active {
background-color: var(--accent-color);
}
&:hover {