docker setup
# Conflicts: # .gitignore # backend/vite.config.js # frontend/package-lock.json
This commit is contained in:
parent
f01a0a967f
commit
c62234e1ca
546 changed files with 141382 additions and 757 deletions
519
dot-line-system/src/ConnectedDotsVisualization.ts
Normal file
519
dot-line-system/src/ConnectedDotsVisualization.ts
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
// Define interfaces
|
||||
export interface DotConfig {
|
||||
id: number;
|
||||
value: number;
|
||||
x: number;
|
||||
link?: string; // URL to navigate to when dot is clicked
|
||||
imageUrl?: string; // Image to display in tooltip
|
||||
title?: string; // Optional title for the tooltip
|
||||
description?: string; // Optional description for the tooltip
|
||||
}
|
||||
export interface Config {
|
||||
totalWidth: number;
|
||||
height: number;
|
||||
dotRadius: number;
|
||||
xUnitSize: number;
|
||||
tension: number;
|
||||
showGrid: boolean;
|
||||
tooltipWidth: number;
|
||||
tooltipHeight: number;
|
||||
}
|
||||
interface ControlPoints {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
}
|
||||
interface TooltipEdges {
|
||||
leftmost: number;
|
||||
rightmost: number;
|
||||
}
|
||||
export class ConnectedDotsVisualization {
|
||||
private config: Config;
|
||||
private dots: DotConfig[];
|
||||
private preloadedImages: Map<string, HTMLImageElement> = new Map();
|
||||
// DOM Elements
|
||||
private scrollContainer: HTMLElement;
|
||||
private svg: SVGElement;
|
||||
private gridGroup: SVGGElement;
|
||||
private curvePath: SVGPathElement;
|
||||
private dotsGroup: SVGGElement;
|
||||
private tooltipGroup: SVGGElement;
|
||||
// Active tooltip
|
||||
private activeTooltip: SVGElement | null = null;
|
||||
constructor(
|
||||
containerId: string,
|
||||
dots: DotConfig[],
|
||||
config?: Partial<Config>
|
||||
) {
|
||||
// Use the provided dots or empty array
|
||||
this.dots = dots || [];
|
||||
// Calculate the total width based on dots data
|
||||
const xUnitSize = config?.xUnitSize || 80;
|
||||
let calculatedWidth = 0;
|
||||
if (this.dots.length > 0) {
|
||||
// Find the minimum and maximum x values
|
||||
const minX = Math.min(...this.dots.map((dot) => dot.x));
|
||||
const maxX = Math.max(...this.dots.map((dot) => dot.x));
|
||||
// Calculate width based on the range of x values
|
||||
// Add padding on both sides (3 units on each side)
|
||||
calculatedWidth = (maxX - minX + 6) * xUnitSize;
|
||||
} else {
|
||||
calculatedWidth = 6 * xUnitSize; // Default width if no dots
|
||||
}
|
||||
// Default configuration
|
||||
this.config = {
|
||||
totalWidth: calculatedWidth,
|
||||
height: window.innerHeight,
|
||||
dotRadius: 6,
|
||||
xUnitSize: xUnitSize,
|
||||
tension: 0.5,
|
||||
showGrid: false,
|
||||
tooltipWidth: 128,
|
||||
tooltipHeight: 128,
|
||||
...config,
|
||||
};
|
||||
// Initialize DOM elements
|
||||
this.scrollContainer = document.getElementById(containerId) as HTMLElement;
|
||||
// Create SVG elements
|
||||
this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
this.gridGroup = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"g"
|
||||
);
|
||||
this.curvePath = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"path"
|
||||
);
|
||||
this.dotsGroup = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"g"
|
||||
);
|
||||
this.tooltipGroup = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"g"
|
||||
);
|
||||
// Initialize the visualization
|
||||
this.addStyles();
|
||||
this.initializeSVG();
|
||||
this.setupEventListeners();
|
||||
this.preloadImages();
|
||||
this.render();
|
||||
}
|
||||
private preloadImages(): void {
|
||||
// Extract all unique image URLs from dots
|
||||
const imageUrls: string[] = this.dots
|
||||
.filter((dot) => dot.imageUrl)
|
||||
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||
.map((dot) => dot.imageUrl!)
|
||||
.filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
|
||||
// Create a loading indicator (optional)
|
||||
const loadingCount = { current: 0, total: imageUrls.length };
|
||||
if (imageUrls.length > 0) {
|
||||
console.log(`Preloading ${imageUrls.length} images...`);
|
||||
}
|
||||
// Preload each image
|
||||
for (const url of imageUrls) {
|
||||
const img = new Image();
|
||||
// Optional loading events
|
||||
img.onload = () => {
|
||||
loadingCount.current++;
|
||||
if (loadingCount.current === loadingCount.total) {
|
||||
console.log("All images preloaded successfully");
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
loadingCount.current++;
|
||||
console.error(`Failed to preload image: ${url}`);
|
||||
};
|
||||
// Set src to start loading
|
||||
img.src = url;
|
||||
// Store in map for potential later use
|
||||
this.preloadedImages.set(url, img);
|
||||
}
|
||||
}
|
||||
private addStyles(): void {
|
||||
// Add necessary styles for tooltips and interactions
|
||||
const styleId = "connected-dots-styles";
|
||||
if (!document.getElementById(styleId)) {
|
||||
const style = document.createElement("style");
|
||||
style.id = styleId;
|
||||
// style.textContent = `
|
||||
|
||||
// `;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
private initializeSVG(): void {
|
||||
// Configure SVG
|
||||
this.svg.setAttribute("width", `${this.config.totalWidth}`);
|
||||
this.svg.setAttribute("height", `${this.config.height}`);
|
||||
this.svg.style.overflow = "visible";
|
||||
this.scrollContainer.appendChild(this.svg);
|
||||
// Configure grid group
|
||||
this.gridGroup.classList.add("grid");
|
||||
this.svg.appendChild(this.gridGroup);
|
||||
// Configure curve path
|
||||
this.curvePath.setAttribute("fill", "none");
|
||||
this.curvePath.setAttribute("stroke", "white");
|
||||
this.curvePath.setAttribute("stroke-width", "2");
|
||||
this.curvePath.setAttribute("stroke-linecap", "round");
|
||||
this.curvePath.classList.add("curve-path");
|
||||
this.svg.appendChild(this.curvePath);
|
||||
// Configure dots group
|
||||
this.svg.appendChild(this.dotsGroup);
|
||||
// Configure tooltip group (always on top)
|
||||
this.tooltipGroup.classList.add("tooltips");
|
||||
this.svg.appendChild(this.tooltipGroup);
|
||||
}
|
||||
private setupEventListeners(): void {
|
||||
// Event listeners removed as the controls were removed
|
||||
}
|
||||
private getDotX(x: number): number {
|
||||
return (x + 3) * this.config.xUnitSize;
|
||||
}
|
||||
private getDotY(value: number): number {
|
||||
const centerY = this.config.height / 1.625;
|
||||
// Calculate raw Y position
|
||||
// height of the amplitude
|
||||
const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7);
|
||||
// Calculate minimum Y position to ensure tooltip fits
|
||||
const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
|
||||
// Ensure Y is never less than minimum (never too high on screen)
|
||||
return Math.max(rawY, minY);
|
||||
}
|
||||
private calculateBezierControlPoints(
|
||||
dots: DotConfig[],
|
||||
index: number
|
||||
): ControlPoints {
|
||||
const tension = this.config.tension * 500; // Scale tension for Bezier curve Rundung Kurve
|
||||
// Get current point and its neighbors
|
||||
const curr = dots[index];
|
||||
const next = dots[index + 1];
|
||||
// Calculate control points for a smooth bezier curve
|
||||
const x1 = this.getDotX(curr.x) + tension;
|
||||
const y1 = this.getDotY(curr.value);
|
||||
const x2 = this.getDotX(next.x) - tension;
|
||||
const y2 = this.getDotY(next.value);
|
||||
return { x1, y1, x2, y2 };
|
||||
}
|
||||
private generateBezierPath(): string {
|
||||
if (this.dots.length < 2) return "";
|
||||
let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
|
||||
this.dots[0].value
|
||||
)}`;
|
||||
for (let i = 0; i < this.dots.length - 1; i++) {
|
||||
const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
|
||||
this.dots,
|
||||
i
|
||||
);
|
||||
const nextX = this.getDotX(this.dots[i + 1].x);
|
||||
const nextY = this.getDotY(this.dots[i + 1].value);
|
||||
path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
private drawGrid(): void {
|
||||
// Clear previous grid
|
||||
while (this.gridGroup.firstChild) {
|
||||
this.gridGroup.removeChild(this.gridGroup.firstChild);
|
||||
}
|
||||
if (!this.config.showGrid) return;
|
||||
// Horizontal grid lines
|
||||
for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
|
||||
const line = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"line"
|
||||
);
|
||||
line.setAttribute("x1", "0");
|
||||
line.setAttribute("y1", this.getDotY(value).toString());
|
||||
line.setAttribute("x2", this.config.totalWidth.toString());
|
||||
line.setAttribute("y2", this.getDotY(value).toString());
|
||||
line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
|
||||
line.setAttribute("stroke-width", "1");
|
||||
this.gridGroup.appendChild(line);
|
||||
const text = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"text"
|
||||
);
|
||||
text.setAttribute("x", "10");
|
||||
text.setAttribute("y", (this.getDotY(value) + 4).toString());
|
||||
text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
|
||||
text.setAttribute("font-size", "12");
|
||||
text.textContent = value.toString();
|
||||
this.gridGroup.appendChild(text);
|
||||
}
|
||||
// Vertical grid lines
|
||||
const numVertLines = Math.ceil(
|
||||
this.config.totalWidth / this.config.xUnitSize
|
||||
);
|
||||
for (let i = 0; i < numVertLines; i++) {
|
||||
const x = i * this.config.xUnitSize;
|
||||
const xValue = i - 3; // Starting from -3
|
||||
const line = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"line"
|
||||
);
|
||||
line.setAttribute("x1", x.toString());
|
||||
line.setAttribute("y1", "0");
|
||||
line.setAttribute("x2", x.toString());
|
||||
line.setAttribute("y2", this.config.height.toString());
|
||||
line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
|
||||
line.setAttribute("stroke-width", "1");
|
||||
this.gridGroup.appendChild(line);
|
||||
if (xValue !== 0) {
|
||||
const text = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"text"
|
||||
);
|
||||
text.setAttribute("x", x.toString());
|
||||
text.setAttribute("y", (this.config.height / 2 + 20).toString());
|
||||
text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
|
||||
text.setAttribute("font-size", "12");
|
||||
text.setAttribute("text-anchor", "middle");
|
||||
text.textContent = xValue.toString();
|
||||
this.gridGroup.appendChild(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
|
||||
const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
tooltip.classList.add("dot-tooltip");
|
||||
tooltip.setAttribute("data-dot-id", dot.id.toString());
|
||||
|
||||
// Calculate tooltip dimensions and position
|
||||
const tooltipWidth = 128; // Base width for your tooltip
|
||||
const tooltipHeight = (4 / 3) * tooltipWidth;
|
||||
const tooltipX = x - tooltipWidth / 2;
|
||||
let tooltipY = y - tooltipHeight - 10; // Positioned above the dot
|
||||
tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
|
||||
|
||||
// Create background rectangle
|
||||
const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
bg.setAttribute("x", tooltipX.toString());
|
||||
bg.setAttribute("y", tooltipY.toString());
|
||||
bg.setAttribute("width", tooltipWidth.toString());
|
||||
bg.setAttribute("height", tooltipHeight.toString());
|
||||
bg.setAttribute("rx", "0"); // Rounded corners
|
||||
bg.classList.add("tooltip-background");
|
||||
tooltip.appendChild(bg);
|
||||
|
||||
// Create foreignObject for the content
|
||||
const contentContainer = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"foreignObject"
|
||||
);
|
||||
contentContainer.setAttribute("x", tooltipX.toString());
|
||||
contentContainer.setAttribute("y", tooltipY.toString());
|
||||
contentContainer.setAttribute("width", tooltipWidth.toString());
|
||||
contentContainer.setAttribute("height", tooltipHeight.toString());
|
||||
|
||||
// Create a div to contain the content
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("tooltip-content");
|
||||
|
||||
// Add title if available
|
||||
if (dot.title) {
|
||||
const title = document.createElement("div");
|
||||
title.textContent = dot.title;
|
||||
title.classList.add("tooltip-title");
|
||||
div.appendChild(title);
|
||||
}
|
||||
|
||||
// Add description if available
|
||||
if (dot.description) {
|
||||
const desc = document.createElement("div");
|
||||
desc.textContent = dot.description;
|
||||
desc.classList.add("tooltip-description");
|
||||
div.appendChild(desc);
|
||||
}
|
||||
|
||||
// Add image if available
|
||||
// Create a container div
|
||||
const imageContainer = document.createElement("div");
|
||||
imageContainer.classList.add("image_container"); // Add image_container class
|
||||
|
||||
// Define a variable for handling case with or without link
|
||||
let imgWrapper: HTMLElement;
|
||||
|
||||
// if (dot.imageUrl) {
|
||||
if (dot.link) {
|
||||
const link = document.createElement("a");
|
||||
link.href = dot.link;
|
||||
link.target = "_self"; // Opens in the same window
|
||||
|
||||
const imgElement = document.createElement("img");
|
||||
imgElement.src = dot.imageUrl;
|
||||
imgElement.classList.add("tooltip-image");
|
||||
|
||||
// Append the image element to the link
|
||||
link.appendChild(imgElement);
|
||||
imgWrapper = link; // Use the link as the wrapper
|
||||
|
||||
// Add the event listener to the link
|
||||
link.addEventListener("click", () => {
|
||||
if (dot.link) {
|
||||
window.location.href = dot.link;
|
||||
} else {
|
||||
console.error("Dot has no link");
|
||||
throw new Error("Dot has no link");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const img = document.createElement("img");
|
||||
img.src = dot.imageUrl;
|
||||
img.classList.add("tooltip-image");
|
||||
imgWrapper = img; // Use the image directly as the wrapper
|
||||
}
|
||||
// } else {
|
||||
// console.error("Dot has no image URL");
|
||||
// throw new Error("Dot has no image URL");
|
||||
// }
|
||||
|
||||
// Append imageWrapper to the container
|
||||
imageContainer.appendChild(imgWrapper);
|
||||
|
||||
|
||||
// Append the image container to the main div
|
||||
div.appendChild(imageContainer);
|
||||
|
||||
const arrow = document.createElement("div");
|
||||
|
||||
arrow.classList.add("tooltip-arrow");
|
||||
|
||||
div.appendChild(arrow); // Append the arrow to the tooltip-content div
|
||||
|
||||
contentContainer.appendChild(div);
|
||||
tooltip.appendChild(contentContainer);
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
private showTooltip(dot: DotConfig, x: number, y: number): void {
|
||||
// Create tooltip
|
||||
const tooltip = this.createTooltip(dot, x, y);
|
||||
this.tooltipGroup.appendChild(tooltip);
|
||||
this.activeTooltip = tooltip;
|
||||
}
|
||||
private hideTooltip(): void {
|
||||
// This method is kept for compatibility but doesn't hide tooltips anymore
|
||||
}
|
||||
private drawCurve(): void {
|
||||
const pathData = this.generateBezierPath();
|
||||
this.curvePath.setAttribute("d", pathData);
|
||||
}
|
||||
private calculateTooltipEdges(): TooltipEdges {
|
||||
let leftmost = 0;
|
||||
let rightmost = 0;
|
||||
let firstTooltipFound = false;
|
||||
// If no dots with tooltips, return default values
|
||||
if (this.dots.length === 0) {
|
||||
return { leftmost: 0, rightmost: this.config.totalWidth };
|
||||
}
|
||||
// Calculate the leftmost and rightmost edges of all tooltips
|
||||
for (const dot of this.dots) {
|
||||
// Skip dots without tooltip content
|
||||
if (!dot.imageUrl && !dot.title && !dot.description) {
|
||||
continue;
|
||||
}
|
||||
const x = this.getDotX(dot.x);
|
||||
const tooltipWidth = this.config.tooltipWidth;
|
||||
const tooltipX = x - tooltipWidth / 2;
|
||||
if (!firstTooltipFound) {
|
||||
leftmost = tooltipX;
|
||||
rightmost = tooltipX + tooltipWidth;
|
||||
firstTooltipFound = true;
|
||||
} else {
|
||||
// Update leftmost and rightmost values
|
||||
leftmost = Math.min(leftmost, tooltipX);
|
||||
rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
|
||||
}
|
||||
}
|
||||
// If no dots with tooltips were found, use default values
|
||||
if (!firstTooltipFound) {
|
||||
return { leftmost: 0, rightmost: this.config.totalWidth };
|
||||
}
|
||||
return { leftmost, rightmost };
|
||||
}
|
||||
private drawDots(): void {
|
||||
// Clear previous dots
|
||||
while (this.dotsGroup.firstChild) {
|
||||
this.dotsGroup.removeChild(this.dotsGroup.firstChild);
|
||||
}
|
||||
// Clear previous tooltips
|
||||
while (this.tooltipGroup.firstChild) {
|
||||
this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
|
||||
}
|
||||
for (const dot of this.dots) {
|
||||
const x = this.getDotX(dot.x);
|
||||
const y = this.getDotY(dot.value);
|
||||
const circle = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"circle"
|
||||
);
|
||||
circle.setAttribute("cx", x.toString());
|
||||
circle.setAttribute("cy", y.toString());
|
||||
circle.setAttribute("r", this.config.dotRadius.toString());
|
||||
circle.setAttribute("fill", "white");
|
||||
circle.setAttribute("data-dot-id", dot.id.toString());
|
||||
circle.classList.add("dot");
|
||||
// Always show tooltip if it has content
|
||||
if (dot.imageUrl || dot.title || dot.description) {
|
||||
this.showTooltip(dot, x, y);
|
||||
}
|
||||
// Click event for navigation
|
||||
if (dot.link) {
|
||||
circle.addEventListener("click", () => {
|
||||
if (dot.link) {
|
||||
window.location.href = dot.link;
|
||||
} else {
|
||||
console.error("Dot has no link");
|
||||
throw new Error("Dot has no link");
|
||||
}
|
||||
});
|
||||
}
|
||||
this.dotsGroup.appendChild(circle);
|
||||
}
|
||||
}
|
||||
public render(): void {
|
||||
this.drawGrid();
|
||||
this.drawCurve();
|
||||
this.drawDots();
|
||||
// Calculate tooltip edges and set SVG width
|
||||
const { leftmost, rightmost } = this.calculateTooltipEdges();
|
||||
// Set the SVG width based on the rightmost tooltip edge
|
||||
if (rightmost > 0) {
|
||||
// Add some padding
|
||||
const padding = 40;
|
||||
this.config.totalWidth = rightmost + padding;
|
||||
this.svg.setAttribute("width", `${this.config.totalWidth}`);
|
||||
// Update grid width
|
||||
this.drawGrid();
|
||||
}
|
||||
}
|
||||
// Public API methods for external use
|
||||
public updateDots(newDots: DotConfig[]): void {
|
||||
this.dots = newDots;
|
||||
// Initial width calculation based on dot positions (for grid)
|
||||
if (this.dots.length > 0) {
|
||||
// Find the minimum and maximum x values
|
||||
const minX = Math.min(...this.dots.map((dot) => dot.x));
|
||||
const maxX = Math.max(...this.dots.map((dot) => dot.x));
|
||||
// Calculate width based on the range of x values
|
||||
// Add padding on both sides (3 units on each side)
|
||||
this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
|
||||
}
|
||||
// Render will calculate the tooltip edges and update the SVG width
|
||||
this.render();
|
||||
}
|
||||
public updateConfig(newConfig: Partial<Config>): void {
|
||||
this.config = { ...this.config, ...newConfig };
|
||||
this.render();
|
||||
}
|
||||
public resize(): void {
|
||||
this.config.height = window.innerHeight;
|
||||
this.svg.setAttribute("height", `${this.config.height}`);
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
320
dot-line-system/src/main.ts
Normal file
320
dot-line-system/src/main.ts
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
import {
|
||||
ConnectedDotsVisualization,
|
||||
type DotConfig,
|
||||
} from "./ConnectedDotsVisualization";
|
||||
|
||||
import '../src/style.css';
|
||||
|
||||
/*
|
||||
* If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output.
|
||||
*
|
||||
* For Vite:
|
||||
* - Place your images in the `public/images` directory instead of `src/images`.
|
||||
* - Reference them as `/images/filename.png` in your code.
|
||||
*
|
||||
* For Webpack:
|
||||
* - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory.
|
||||
*
|
||||
* For static hosting (e.g., GitHub Pages, Netlify):
|
||||
* - Make sure the images are in the output directory (e.g., `dist/images`) after build.
|
||||
*
|
||||
* Example for Vite:
|
||||
* Move images to `public/images` and update imageUrl paths to `/images/filename.png`.
|
||||
*/
|
||||
|
||||
// Sample dot configurations
|
||||
const sampleDots: DotConfig[] = [
|
||||
{
|
||||
id: 1,
|
||||
value: -1.8,
|
||||
x: -2,
|
||||
imageUrl:
|
||||
"/images/0_3.png",
|
||||
title: "Beginn des neuen Abenteuers",
|
||||
description: "01.10.2024",
|
||||
link: "/page1",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
value: 1.2,
|
||||
x: 0,
|
||||
imageUrl:
|
||||
"/images/0_2.png",
|
||||
title: "Omas Annis Geburtstag",
|
||||
description: "02.10.2024",
|
||||
link: "/page2",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
value: -0.6,
|
||||
x: 2,
|
||||
imageUrl:
|
||||
"/images/disco.png",
|
||||
title: "Konzertbesuch mit Freunden",
|
||||
description: "03.10.2024",
|
||||
link: "/page3",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
value:3,
|
||||
x: 4,
|
||||
imageUrl:
|
||||
"/images/pferd.png",
|
||||
title: "Wanderreiten in den Bergen",
|
||||
description: "04.10.2024",
|
||||
link: "/page4",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
value: 1,
|
||||
x: 6,
|
||||
imageUrl:
|
||||
"/images/gpt.png",
|
||||
title: "Ruhiger Tag zu Hause",
|
||||
description: "05.10.2024",
|
||||
link: "/page5",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
value: -3,
|
||||
x: 8,
|
||||
imageUrl:
|
||||
"/images/oma.png",
|
||||
title: "Oma Erna verstorben",
|
||||
description: "06.10.2024",
|
||||
link: "/page6",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
value: 1.5,
|
||||
x: 10,
|
||||
imageUrl:
|
||||
"/images/see.png",
|
||||
title: "Erholungsausflug zum See",
|
||||
description: "07.10.2024",
|
||||
link: "/page7",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
value: 0,
|
||||
x: 12,
|
||||
imageUrl:
|
||||
"/images/feier.png",
|
||||
title: "Kleine Wochenendsfeier",
|
||||
description: "08.10.2024",
|
||||
link: "/page8",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
value: 3,
|
||||
x: 14,
|
||||
imageUrl:
|
||||
"/images/hochzeit.png",
|
||||
title: "Hochzeit von Cousine Tatjana",
|
||||
description: "09.10.2024",
|
||||
link: "/page9",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
value: 1,
|
||||
x: 16,
|
||||
imageUrl:
|
||||
"/images/work.png",
|
||||
title: "Erster Tag im neuen Job",
|
||||
description: "10.10.2024",
|
||||
link: "/page10",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
value: -1.2,
|
||||
x: 18,
|
||||
imageUrl:
|
||||
"/images/klasse.png",
|
||||
title: "Klassentreffen nach vielen Jahren",
|
||||
description: "11.10.2024",
|
||||
link: "/page11",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
value: -0.6,
|
||||
x: 20,
|
||||
imageUrl:
|
||||
"/images/familie.png",
|
||||
title: "Familienabendessen",
|
||||
description: "12.10.2024",
|
||||
link: "/page12",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
value: 2.7,
|
||||
x: 22,
|
||||
imageUrl:
|
||||
"/images/kinobesuch.png",
|
||||
title: "Kinobesuch mit der ganzen Familie",
|
||||
description: "13.10.2024",
|
||||
link: "/page13",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
value: 0,
|
||||
x: 24,
|
||||
imageUrl:
|
||||
"/images/entspannung.png",
|
||||
title: "Entspannung",
|
||||
description: "14.10.2024",
|
||||
link: "/page14",
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
value: -2.9,
|
||||
x: 26,
|
||||
imageUrl: "/images/sonntag.png",
|
||||
title: "Geruhsamer Sonntag",
|
||||
description: "15.10.2024",
|
||||
link: "/page15",
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
value: 1.5,
|
||||
x: 28,
|
||||
imageUrl:
|
||||
"/images/kindergeburtstag.png",
|
||||
title: "Kindergeburtstag",
|
||||
description: "16.10.2024",
|
||||
link: "/page16",
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
value: 0,
|
||||
x: 30,
|
||||
imageUrl:
|
||||
"/images/familie2.png",
|
||||
title: "Spaziergang mit der Familie",
|
||||
description: "17.10.2024",
|
||||
link: "/page17",
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
value: 2.1,
|
||||
x: 32,
|
||||
imageUrl:
|
||||
"/images/grosseltern.png",
|
||||
title: "Familienfeier bei den Großeltern",
|
||||
description: "18.10.2024",
|
||||
link: "/page18",
|
||||
},
|
||||
];
|
||||
|
||||
// Wait for DOM to be fully loaded
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Initialize the visualization with the sample dots
|
||||
const visualization = new ConnectedDotsVisualization(
|
||||
"scroll-container",
|
||||
sampleDots,
|
||||
{
|
||||
// Optional custom configuration
|
||||
dotRadius: 8,
|
||||
// tooltipWidth: 100,
|
||||
// tooltipHeight: 100,
|
||||
}
|
||||
);
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener("resize", () => {
|
||||
visualization.resize();
|
||||
});
|
||||
|
||||
// Example of updating dots dynamically (if needed)
|
||||
/*
|
||||
const updateButton = document.createElement('button');
|
||||
updateButton.textContent = 'Update Data';
|
||||
updateButton.classList.add('button');
|
||||
updateButton.style.marginTop = '10px';
|
||||
document.body.appendChild(updateButton);
|
||||
|
||||
updateButton.addEventListener('click', () => {
|
||||
// Generate some new random data with image tooltips
|
||||
const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
value: Math.random() * 6 - 3, // Random value between -3 and 3
|
||||
x: i - 3,
|
||||
imageUrl: `https://picsum.photos/200/150?random=${i+10}`,
|
||||
title: `Point ${i+1}`,
|
||||
description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}`
|
||||
}));
|
||||
visualization.updateDots(newDots);
|
||||
});
|
||||
*/
|
||||
|
||||
const scrollContainer = document.querySelector(".scroll-container") as HTMLElement;
|
||||
|
||||
let isDown = false;
|
||||
let startX: number;
|
||||
let scrollLeft: number;
|
||||
|
||||
|
||||
// Mouse events
|
||||
scrollContainer.addEventListener("mousedown", (e) => {
|
||||
isDown = true;
|
||||
scrollContainer.classList.add("active");
|
||||
startX = e.pageX - scrollContainer.offsetLeft;
|
||||
scrollLeft = scrollContainer.scrollLeft;
|
||||
|
||||
// Remove smooth scrolling while dragging
|
||||
scrollContainer.classList.remove("smooth-scroll");
|
||||
});
|
||||
|
||||
scrollContainer.addEventListener("mouseleave", () => {
|
||||
if (!isDown) return;
|
||||
isDown = false;
|
||||
scrollContainer.classList.remove("active");
|
||||
|
||||
// Add smooth scrolling after dragging
|
||||
scrollContainer.classList.add("smooth-scroll");
|
||||
});
|
||||
|
||||
scrollContainer.addEventListener("mouseup", () => {
|
||||
if (!isDown) return;
|
||||
isDown = false;
|
||||
scrollContainer.classList.remove("active");
|
||||
|
||||
// Add smooth scrolling after dragging
|
||||
scrollContainer.classList.add("smooth-scroll");
|
||||
});
|
||||
|
||||
scrollContainer.addEventListener("mousemove", (e) => {
|
||||
if (!isDown) return;
|
||||
e.preventDefault();
|
||||
const x = e.pageX - scrollContainer.offsetLeft;
|
||||
const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling
|
||||
scrollContainer.scrollLeft = scrollLeft - walk;
|
||||
});
|
||||
|
||||
// Touch events
|
||||
scrollContainer.addEventListener("touchstart", (e) => {
|
||||
isDown = true;
|
||||
scrollContainer.classList.add("active");
|
||||
startX = e.touches[0].pageX - scrollContainer.offsetLeft;
|
||||
scrollLeft = scrollContainer.scrollLeft;
|
||||
|
||||
// Remove smooth scrolling while dragging
|
||||
scrollContainer.classList.remove("smooth-scroll");
|
||||
});
|
||||
|
||||
scrollContainer.addEventListener("touchend", () => {
|
||||
if (!isDown) return;
|
||||
isDown = false;
|
||||
scrollContainer.classList.remove("active");
|
||||
|
||||
// Add smooth scrolling after dragging
|
||||
scrollContainer.classList.add("smooth-scroll");
|
||||
});
|
||||
|
||||
scrollContainer.addEventListener("touchmove", (e) => {
|
||||
if (!isDown) return;
|
||||
e.preventDefault();
|
||||
const x = e.touches[0].pageX - scrollContainer.offsetLeft;
|
||||
const walk = (x - startX) * 3;
|
||||
scrollContainer.scrollLeft = scrollLeft - walk;
|
||||
});
|
||||
});
|
||||
203
dot-line-system/src/style.css
Normal file
203
dot-line-system/src/style.css
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
body {
|
||||
font-family: "Barlow Condensed", sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 6px 12px;
|
||||
background-color: #4f46e5;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.visualization-container {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-left: calc(-50vw + 50%);
|
||||
}
|
||||
|
||||
.gradient-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
min-width: 100%;
|
||||
z-index: -1;
|
||||
background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2);
|
||||
background-size: 200% 200%;
|
||||
animation: gradientAnimation 20s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradientAnimation {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
25% {
|
||||
background-position: 100% 0%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
|
||||
75% {
|
||||
background-position: 0% 100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
}
|
||||
|
||||
.median {
|
||||
position: fixed;
|
||||
top: 61.5%;
|
||||
left: 0;
|
||||
height: 1px;
|
||||
border-top: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
/* background-color: rgba(255, 255, 255, 0.2); */
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
scroll-behavior: smooth;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scroll-container:active {
|
||||
cursor: grabbing;
|
||||
/* Change cursor when active */
|
||||
}
|
||||
|
||||
.smooth-scroll {
|
||||
transition: scroll-left 0.5s ease-out; /* Add easing on scroll-left */
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.dot-tooltip .tooltip-background {
|
||||
fill: rgba(0, 0, 0, 0.0);
|
||||
}
|
||||
|
||||
.dot-tooltip .tooltip-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
/* Center vertically */
|
||||
align-items: center;
|
||||
/* Center horizontally */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: white;
|
||||
/* Text color */
|
||||
}
|
||||
|
||||
.dot-tooltip .image_container {
|
||||
margin-top: 8px;
|
||||
/* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */
|
||||
box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25);
|
||||
transition: box-shadow 0.25s ease-in-out;
|
||||
}
|
||||
|
||||
.dot-tooltip .image_container:hover {
|
||||
|
||||
box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8);
|
||||
/* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */
|
||||
transition: box-shadow 0.25s ease-in-out;
|
||||
|
||||
}
|
||||
|
||||
.dot-tooltip .tooltip-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
pointer-events: auto;
|
||||
|
||||
}
|
||||
|
||||
.dot-tooltip .tooltip-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 2px;
|
||||
text-align: center;
|
||||
text-wrap: balance;
|
||||
hyphens: auto;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.dot-tooltip .tooltip-description {
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.dot-tooltip .image_container {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dot-tooltip .tooltip-arrow {
|
||||
width: 1px;
|
||||
height: 30px;
|
||||
background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent);
|
||||
}
|
||||
|
||||
.dot {
|
||||
transition: r 0.2s ease, fill 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dot:hover {
|
||||
fill: rgba(255, 255, 255, 0.9);
|
||||
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
|
||||
}
|
||||
|
||||
.dot-tooltip {
|
||||
pointer-events: none;
|
||||
opacity: 1;
|
||||
/* Always visible */
|
||||
}
|
||||
|
||||
.tooltip-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
1
dot-line-system/src/typescript.svg
Normal file
1
dot-line-system/src/typescript.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
dot-line-system/src/vite-env.d.ts
vendored
Normal file
1
dot-line-system/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
Loading…
Add table
Add a link
Reference in a new issue