diff --git a/index.html b/index.html
index 5ab93f82a7de017dcd363d9f472d7af071fcef7b..599c69fa9c64fc36f2fb78da7546f7a2d726f148 100644
--- a/index.html
+++ b/index.html
@@ -13,6 +13,11 @@
         <div id="bubble-chart-wrapper">
 
         </div>
+        <div id="zoom-wrapper">
+        <button id="zoom-out">-</button>
+        <input type="range" min="1" max="100" value="50" class="slider" id="zoom-slider" />
+        <button id="zoom-in">+</button>
+        </div>
       </div>
       <div class="right">
         <h2>Vergleichen</h2>
diff --git a/src/charts/areaChart.ts b/src/charts/areaChart.ts
index fcc871d195c716a5151b527e108175882ff7a3b4..cbd4a01ad4dfdec99b03791b30630a1ee4731ca6 100644
--- a/src/charts/areaChart.ts
+++ b/src/charts/areaChart.ts
@@ -59,6 +59,7 @@ export default class AreaChart extends AxisChart<Date, number> {
         vis.chart.append('g')
             .attr('class', 'axis y-axis')
             .call(vis.yAxis)
+            .call(vis.yAxis)
     }
 
     updateVis(data: any[]): void {
diff --git a/src/charts/bubbleChart.ts b/src/charts/bubbleChart.ts
index 32f93ab7e609ff6ba9184028b7b9bf7fede3b221..dfc0e3af48e1b0ac53fcdb863779d56e0da5d69d 100644
--- a/src/charts/bubbleChart.ts
+++ b/src/charts/bubbleChart.ts
@@ -1,18 +1,42 @@
 import * as d3 from "d3";
-import Chart, {ChartConfigParam} from "@/charts/chart.ts";
+import Chart, {ChartConfig, ChartConfigParam} from "@/charts/chart.ts";
+
+
+export type BubbleChartConfig = ChartConfig & {
+    groupAccessor: (d: any) => string,
+    sizeAccessor: (d: any) => number,
+    colorAccessor: (d: any) => number | null,
+    zoomExtent: [number, number],
+    onZoom?: (event: any) => void
+}
+
+export type BubbleChartConfigParam = ChartConfigParam & Partial<BubbleChartConfig>
 
-export type BubbleChartConfigParam = ChartConfigParam
 
 
 export default class BubbleChart extends Chart {
-    groupAccessor: (d: any) => string = () => 'default';
-    sizeAccessor: (d: any) => number = () => 10;
-    colorAccessor: (d: any) => number | null = () => null;
     chartId: string = 'bubbleChart';
     chart: any
+    zoom: any;
+    config: BubbleChartConfig
 
     constructor(data: any[], _config: BubbleChartConfigParam) {
-        super(data, _config)
+        super(data, _config as ChartConfigParam)
+
+        this.config = {
+            ..._config,
+            parentElement: typeof _config.parentElement === 'string' ? document.querySelector(_config.parentElement) as HTMLElement : _config.parentElement,
+            containerWidth: _config.containerWidth || 500,
+            containerHeight: _config.containerHeight || 140,
+            margin: _config.margin || { top: 10, bottom: 30, right: 10, left: 30 },
+            tooltipPadding: _config.tooltipPadding || 15,
+            groupAccessor: _config.groupAccessor || (() => 'default'),
+            sizeAccessor: _config.sizeAccessor || (() => 5),
+            colorAccessor: _config.colorAccessor || (() => null),
+            zoomExtent: _config.zoomExtent || [0.5, 20],
+        }
+
+        this.initVis()
     }
 
     initVis() {
@@ -30,6 +54,17 @@ export default class BubbleChart extends Chart {
 
         vis.chart = svg.append('g')
             .attr('transform', `translate(${vis.config.margin.left},${vis.config.margin.top})`)
+
+        vis.zoom = d3.zoom()
+            .scaleExtent(vis.config.zoomExtent ?? [0.5, 20])
+            .on('zoom', (event) => {
+                vis.chart.attr('transform', event.transform);
+                vis.config.onZoom?.(event)
+            });
+
+        vis.chart.append('input')
+
+        svg.call(vis.zoom);
     }
 
     updateVis(data: any[]): void {
@@ -39,22 +74,114 @@ export default class BubbleChart extends Chart {
     renderVis(): void {
         let vis: BubbleChart = this;
 
+        vis.config.groupAccessor = (d: any) => {
+            return d['league']
+        }
+
         const chart = vis.chart
 
-        const parsedData = vis.isUngrouped(vis.data) ? [vis.data] : vis.data.reduce((acc: {[key: string]: any;}, data: any) => {
-            (acc[data[vis.groupAccessor(data)]] = acc[data[vis.groupAccessor(data)]] || []).push(data)
-        }, {})
+        const groupedData = vis.getGroupedData()
+
+        for (const groupIndex in groupedData)
+        {
+            const group = groupedData[groupIndex]
 
-        for (const group of parsedData) {
-            chart.append('circle')
-                .data(group)
-                .attr('fill', 'rgba(62,187,228,0.3)')
+            d3.forceSimulation(group.children)
+                .force("charge", d3.forceManyBody().strength(5))
+                .force("center", d3.forceCenter(vis.width() / 2, vis.height() / 2))
+                .force("collision", d3.forceCollide().radius(d => vis.config.sizeAccessor(d) + 1))
+                .on("tick", ticked);
+
+            const nodes = chart.selectAll('circle')
+                .data(group.children)
+                .enter()
+                .append('circle')
+                .attr('fill', 'var(--primary)')
                 .attr('stroke', 'none')
                 .attr('stroke-width', 2)
+                .attr('r', (d: any) => vis.config.sizeAccessor(d))
+                .attr('cx', vis.width() / 2)
+                .attr('cy', vis.height() / 2)
+                .on('mouseover', (_: Event, d: any) => {
+                    d3.select('#tooltip')
+                        .style('display', 'block')
+                        .html(`<table>
+                            <tr><th>Name</th><td>${d['player']}</td></tr>
+                            <tr><th>Liga</th><td>${d['league']}</td></tr>
+                            <tr><th>Position</th><td>${d['pos']}</td></tr>
+                            <tr><th>Nationalität</th><td>${d['nation']}</td></tr>
+                            <tr><th>Team</th><td>${d['team']}</td></tr>
+                            </table>`);
+                })
+                .on('mousemove', (event: any) => {
+                    d3.select('#tooltip')
+                        .style('left', (event.pageX + vis.config.tooltipPadding) + 'px')
+                        .style('top', (event.pageY + vis.config.tooltipPadding) + 'px')
+                })
+                .on('mouseleave', () => {
+                    d3.select('#tooltip').style('display', 'none');
+                })
+
+            function ticked() {
+                nodes.attr("cx", (d: any) => d.x)
+                    .attr("cy", (d: any) => d.y);
+            }
+        }
+    }
+
+    get zoomLevel() {
+        return this.zoom.scale()
+    }
+
+    zoomIn() {
+        this.chart.transition()
+            .duration(300)
+            .call(this.zoom.scaleBy, 1.3)
+    }
+
+    zoomOut() {
+        this.chart.transition()
+            .duration(300)
+            .call(this.zoom.scaleBy, 0.75)
+    }
+
+    zoomTo(zoomLevel: number) {
+        this.chart.call(this.zoom.scaleTo, zoomLevel)
+    }
+
+    private getGroupCenter(index: number, totalGroupLength: number) {
+        const angle = index * (2 * Math.PI / totalGroupLength)
+        return {
+            x: Math.cos(angle) * 100,
+            y: Math.sin(angle) * 100
         }
     }
 
-    isUngrouped(data: any[]): boolean {
-        return data.every((d: any) => this.groupAccessor(d) === 'default')
+    private getGroupedData() : {label: string, children: []}[] {
+        if (this.isUngrouped(this.data)) {
+            return [{
+                label: '',
+                children: this.data
+            }]
+        }
+
+        return this.data.reduce((acc: [{label: string, children: any[]}], data: any) => {
+            const groupLabel = this.config.groupAccessor(data)
+
+            const groupIndex = acc.findIndex(it => it.label === data[groupLabel])
+            if (groupIndex >= 0) {
+                acc[groupIndex].children.push(data)
+            } else {
+                acc.push({
+                    label: data[this.config.groupAccessor(groupLabel)],
+                    children: [data]
+                })
+            }
+            return acc
+        }, [])
+    }
+
+    private isUngrouped(data: any[]): boolean {
+        return data.every((d: any) => this.config.groupAccessor(d) === 'default')
     }
 }
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
index 9abdd749b7a40d360d2f6d6566d6f6dc67040f23..4f6434152e7b4ee385bb8c4e669d4cd082c5bff1 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,17 +1,49 @@
 import "@/styles/index.scss"; // imports the default styles
-import * as d3 from "d3-fetch";
+import {dsv} from "d3-fetch";
 import BubbleChart from "@/charts/bubbleChart.ts";
 
 // const radarChartWrapper = document.querySelector('#radar-chart-wrapper') as HTMLDivElement;
 const bubbleChartWrapper = document.querySelector('#bubble-chart-wrapper') as HTMLDivElement;
+const zoomExtent: [number, number] = [1, 5]
+let bubbleChart: BubbleChart | null = null
 
-d3.csv('data/Out_2.csv').then(data => {
-    const bubbleChart = new BubbleChart(data, {
+let sliderBlocked = false
+const zoomSlider = document.querySelector('#zoom-slider') as HTMLInputElement
+zoomSlider.min = zoomExtent[0].toString()
+zoomSlider.max = zoomExtent[1].toString()
+zoomSlider.step = '0.1'
+zoomSlider.oninput = (event) => {
+    if (bubbleChart) {
+        const newZoom = parseFloat((event.target as HTMLInputElement).value)
+        bubbleChart.zoomTo(newZoom)
+    }
+}
+
+dsv(';', 'data/Out_2.csv').then(data => {
+    const filteredData = data.slice(0,1300)
+    bubbleChart = new BubbleChart(filteredData, {
         parentElement: bubbleChartWrapper,
         containerWidth: 800,
-        containerHeight: 600,
-        margin: {top: 20, right: 20, bottom: 20, left: 20}
+        containerHeight: 800,
+        margin: {top: 20, right: 20, bottom: 20, left: 20},
+        zoomExtent,
+        onZoom: (event) => {
+            if (sliderBlocked) return
+            zoomSlider.value = event.transform.k.toString()
+        }
     })
 
     bubbleChart.renderVis()
 })
+
+const zoomInButton = document.querySelector('#zoom-in') as HTMLButtonElement
+zoomInButton.onclick = () => updateZoomLevel('+')
+
+const zoomOutButton = document.querySelector('#zoom-out') as HTMLButtonElement
+zoomOutButton.onclick = () => updateZoomLevel('-')
+
+function updateZoomLevel(to: '+' | '-') {
+    if (bubbleChart) {
+       to === '+' ? bubbleChart.zoomIn() : bubbleChart.zoomOut()
+    }
+}
\ No newline at end of file
diff --git a/src/styles/index.scss b/src/styles/index.scss
index c577690af561e39683a72e3daabb41d88da67a54..631fe69689e0c8c95a5d20db695e365785b14eb9 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -78,24 +78,40 @@ h1 {
   gap: 1rem;
 
   .left, .right {
+    position: relative;
     flex: 1;
     text-align: center;
+
+    #zoom-wrapper {
+      position: absolute;
+      bottom: 1rem;
+      right: 0;
+      width: 400px;
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      gap: 1rem;
+    }
   }
 }
 
 button {
-  @include neumorphism;
   padding: 0.5rem 1rem;
   border: none;
   cursor: pointer;
   transition: transform 0.2s;
+  font-size: 1.5rem;
+  background: linear-gradient(145deg, var(--primary), var(--secondary));
+  color: white;
+  border-radius: 0.5rem;
+  min-width: 3rem;
 
   &:hover {
     transform: scale(1.05);
   }
 }
 
-input {
+input[type="text"] {
   border-radius: .5rem;
   box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
   padding: 0.5rem;