Advitum.de auf Google+

Events im Diagramm – Interaktive SVG-Diagramme im Web

Events im Diagramm – Interaktive SVG-Diagramme im Web
VN:F [1.9.22_1171]
Bewertung: 3.3/5 (3 Stimmen abgegeben)
Von am
Kategorien: JavaScript, Programmieren, Tutorials

Nachdem wir im letzten Artikel der Artikelserie ein Linien-Diagramm erstellt haben, kümmern wir uns in diesem Artikel darum, das Diagramm für den Nutzer interaktiv zu gestalten. Hier punktet ein SVG-Diagramm gegenüber einem Canvas oder gar einer Bilddatei: Wir können dem Nutzer recht einfach die Möglichkeit geben, mit dem Diagramm zu interagieren, um zum Beispiel genaue Werte abzulesen oder in eine tiefere Detail-Ansicht zu gelangen.

Im Folgenden gehe ich die Änderungen Schritt für Schritt durch.

Events im Diagramm – Interaktive SVG-Diagramme im Web, 3.3 out of 5 based on 3 ratings

Artikelserie: Interaktive SVG-Diagramme im Web

Dieser Artikel ist Teil einer mehrteiligen Artikelserie. Lies dir auch die restlichen Teile durch!

  1. Das Linien-Diagramm – Interaktive SVG-Diagramme im Web
  2. Events im Diagramm – Interaktive SVG-Diagramme im Web

Abonniere den RSS-Feed von Advitum, um keinen Artikel zu verpassen, oder folge mir über meine Facebook-Seite oder meinen Twitter-Kanal.

Das Resultat

So wird das Diagramm am Ende des Artikels aussehen. Optisch ändert sich nicht viel, allerdings kann der Nutzer nun mit der Maus über die Punkte fahren und sie anklicken.

Ausgangssituation

Im vorherigen Artikel haben wir schon ein Linien-Diagramm erstellt, dort findest Du auch den aktuellen Stand des Quellcodes. So sieht unser Diagramm momentan aus:

Tooltips zeigen detaillierte Daten

Die erste neue Funktion für unser interaktives Diagramm werden Tooltips sein. Beim überfahren mit der Maus wollen wir dem Nutzer die genauen Zahlen zeigen, die sich hinter dem Messwert verbergen. Bewegt der Nutzer die Maus über eine Spalte (also einen Tag), werden für diesen Tag die genauen Zahlen aller Messreihen angezeigt.

Dafür müssen wir als erstes die Position der Maus verfolgen.

function LineChart(container, data) {
	Chart.call(this);
	
	var chart = this;
	
	chart.container = container;
	chart.labels = data.labels;
	chart.lines = data.lines;
	chart.tooltip = null;
	
	chart.svgElement = chart.createSvgElement('svg', {
		style: 'font-family: Arial, sans-serif;'
	});
	chart.container.appendChild(chart.svgElement);
	
	window.addEventListener('resize', function() {
		chart.render();
	});
	
	chart.svgElement.addEventListener('mousemove', function(event) {
		
	});
	chart.svgElement.addEventListener('mouseleave', function(event) {
		// Tooltip löschen
	});
	
	chart.render();
}

Da die einzelnen Elemente im SVG reguläre DOM-Elemente sind, können wir ihnen auch einfach EventListener verpassen. Über das mousemove Event prüfen wir beim Bewegen der Maus, ob ein Tooltip angezeigt werden muss, beim mouseleave Event wird der Tooltip dann wieder entfernt.

var chartBounds = chart.svgElement.getBoundingClientRect();
chartBounds.top += document.body.scrollTop;
chartBounds.left += document.body.scrollLeft;
var mouse = {
	x: event.pageX - chartBounds.left,
	y: event.pageY - chartBounds.top
};

Im mousemove Event können wir die aktuelle Position der Maus leicht bestimmen. getBoundingClientRect() gibt die Position des Diagramms im Browserfenster zurück, diese muss dann allerdings noch mit der Scroll-Position verrechnet werden, um absolute Zahlen zu bekommen. In mouse speichern wir schließlich die Mausposition relativ zur linken oberen Ecke des Diagramms.

if(mouse.x > chart.padding.left && mouse.y > chart.padding.top && mouse.y < chart.height - chart.padding.bottom) {
	
} else {
	// Tooltip löschen
}

Der Tooltip soll nur angezeigt werden, wenn die Maus sich über der tatsächlichen Diagramm-Fläche befindet, aber nicht über den Beschriftungen oder der Legende. Daher prüfen wir nun, ob die Mausposition innerhalb der Diagramm-Fläche liegt.

var datumIndex = Math.max(0, Math.min(chart.labels.length - 1, Math.round((mouse.x - chart.padding.left - 20) / chart.axis.x.stepSize)));

Anschließend rechnen wir aus, über welcher Spalte sich die Maus befindet. Da die Punkte auf der X-Achse immer den gleichen Abstand haben, können wir das mit einer einfachen Formel erledigen.

if(chart.tooltip === null || chart.tooltip.datumIndex !== datumIndex) {
	
}

Nun noch ein Sonderfall: Wenn der richtige Tooltip schon angezeigt wird, brauchen wir ihn nicht neu anzeigen. Daher werden wir später das aktive Tooltip in chart.tooltip speichern und gleichen hier den Spalten-Index ab. So müssen wir den Tooltip nur neu darstellen, wenn es wirklich nötig ist.

chart.tooltip = {
	datumIndex: datumIndex,
	element: chart.createSvgElement('g', {
		'pointer-events': 'none'
	}),
	line: chart.createSvgElement('line', {
		x1: chart.padding.left + 20 + datumIndex * chart.axis.x.stepSize,
		y1: chart.padding.top,
		x2: chart.padding.left + 20 + datumIndex * chart.axis.x.stepSize,
		y2: chart.height - chart.padding.bottom,
		stroke: '#c0c0c0'
	})
};
chart.svgElement.insertBefore(chart.tooltip.line, chart.svgElement.firstChild);
chart.svgElement.appendChild(chart.tooltip.element);

Jetzt können wir das Tooltip-Element schon hinzufügen. Genau genommen fügen wir zwei Elemente hinzu: einmal den Tooltip selbst, und eine Linie, die dem Nutzer zeigt, in welcher Spalte er sich gerade befindet.

Wir speichern die beiden Elemente und den Spalten-Index in chart.tooltip, damit wir später noch darauf zugreifen können.

Das Tooltip-Element ist bisher einfach nur eine leere Gruppe. Das Linien-Element ist eine line. Die Linie geht vom oberen bis zum unteren Rand der Diagrammfläche.

Damit die Linie im Hintergrund liegt, fügen wir sie an den Anfang des SVG-Elements hinzu. Der Tooltip soll im Vordergrund liegen, daher wird er ans Ende eingefügt.

removeTooltip: {
	value: function() {
		var chart = this;
		
		if(chart.tooltip !== null) {
			chart.svgElement.removeChild(chart.tooltip.element);
			chart.svgElement.removeChild(chart.tooltip.line);
			chart.tooltip = null;
		}
	}
}

Wir schreiben jetzt noch schnell eine Funktion, die die Tooltip-Elemente wieder entfernt. Diese können wir dann an den entsprechenden Stellen einfügen.

Die Linie verhält sich schon wie erwartet, also stimmt bis hierhin schonmal alles. Ein kleiner Zwischenstand:

function LineChart(container, data) {
	Chart.call(this);
	
	var chart = this;
	
	chart.container = container;
	chart.labels = data.labels;
	chart.lines = data.lines;
	chart.tooltip = null;
	
	chart.svgElement = chart.createSvgElement('svg', {
		style: 'font-family: Arial, sans-serif;'
	});
	chart.container.appendChild(chart.svgElement);
	
	window.addEventListener('resize', function() {
		chart.render();
	});
	
	chart.svgElement.addEventListener('mousemove', function(event) {
		var chartBounds = chart.svgElement.getBoundingClientRect();
		chartBounds.top += document.body.scrollTop;
		chartBounds.left += document.body.scrollLeft;
		
		var mouse = {
			x: event.pageX - chartBounds.left,
			y: event.pageY - chartBounds.top
		};
		
		if(mouse.x > chart.padding.left && mouse.y > chart.padding.top && mouse.y < chart.height - chart.padding.bottom) {
			var datumIndex = Math.max(0, Math.min(chart.labels.length - 1, Math.round((mouse.x - chart.padding.left - 20) / chart.axis.x.stepSize)));
			
			if(chart.tooltip === null || chart.tooltip.datumIndex !== datumIndex) {
				chart.removeTooltip();
				
				chart.tooltip = {
					datumIndex: datumIndex,
					element: chart.createSvgElement('g', {
						'pointer-events': 'none'
					}),
					line: chart.createSvgElement('line', {
						x1: chart.padding.left + 20 + datumIndex * chart.axis.x.stepSize,
						y1: chart.padding.top,
						x2: chart.padding.left + 20 + datumIndex * chart.axis.x.stepSize,
						y2: chart.height - chart.padding.bottom,
						stroke: '#c0c0c0'
					})
				};
				
				chart.svgElement.insertBefore(chart.tooltip.line, chart.svgElement.firstChild);
				chart.svgElement.appendChild(chart.tooltip.element);
			}
		} else {
			chart.removeTooltip();
		}
	});
	chart.svgElement.addEventListener('mouseleave', function(event) {
		chart.removeTooltip();
	});
	
	chart.render();
}

LineChart.prototype = Object.create(Chart.prototype, {
	constructor: {
		value: LineChart
	},
	render: {
		value: function() {
			var chart = this;
			
			chart.width = chart.container.clientWidth;
			chart.height = 500;
			
			chart.svgElement.setAttribute('width', chart.width);
			chart.svgElement.setAttribute('height', chart.height);
			chart.svgElement.setAttribute('viewBox', '0 0 ' + chart.width + ' ' + chart.height);
			
			while(chart.svgElement.childNodes.length) {
				chart.svgElement.removeChild(chart.svgElement.firstChild);
			}
			
			chart.renderLegend();
			chart.renderAxis();
			chart.renderLines();
		}
	},
	renderLegend: {
		value: function() {
			var chart = this;
			
			chart.legend = chart.createSvgElement('g');
			chart.svgElement.appendChild(chart.legend);
			
			var offset = 0;
			for(var index = 0; index < chart.lines.length; index++) {
				var legendItemElement = chart.createSvgElement('g', {
					transform: 'translate(' + offset + ', 0)'
				});
				chart.legend.appendChild(legendItemElement);
				
				var circleElement = chart.createSvgElement('circle', {
					cx: 10,
					cy: 8,
					r: 3.5,
					fill: chart.colors[index % chart.colors.length],
					stroke: 'none'
				});
				legendItemElement.appendChild(circleElement);
				
				var textElement = chart.createSvgElement('text', {
					x: 20,
					y: 13,
					'font-size': 14
				});
				legendItemElement.appendChild(textElement);
				
				textElement.textContent = chart.lines[index].title;
				
				offset += legendItemElement.getBBox().width + 20;
			}
		}
	},
	renderAxis: {
		value: function() {
			var chart = this;
			
			chart.axis = {
				x: {
					nthLabel: 1
				},
				y: {
					maximum: 1,
					step: 1
				}
			};
			
			for(var lineIndex = 0; lineIndex < chart.lines.length; lineIndex++) {
				for(var datumIndex = 0; datumIndex < chart.lines[lineIndex].data.length; datumIndex++) {
					chart.axis.y.maximum = Math.max(chart.axis.y.maximum, chart.lines[lineIndex].data[datumIndex]);
				}
			}
			
			while(chart.axis.y.step * 7 < chart.axis.y.maximum) {
				chart.axis.y.step *= 10;
			}
			if(chart.axis.y.step % 2 === 0 && chart.axis.y.step * 3.5 > chart.axis.y.maximum) {
				chart.axis.y.step /= 2;
			}
			
			chart.axis.y.maximum = Math.ceil(chart.axis.y.maximum / chart.axis.y.step) * chart.axis.y.step;
			
			chart.axis.x.element = chart.createSvgElement('g');
			chart.svgElement.appendChild(chart.axis.x.element);
			
			chart.axis.y.element = chart.createSvgElement('g');
			chart.svgElement.appendChild(chart.axis.y.element);
			
			for(var index = 0; index < chart.labels.length; index++) {
				chart.axis.x.element.appendChild(chart.renderXAxisLabel(chart.labels[index]));
			}
			
			for(var y = 0; y <= chart.axis.y.maximum; y += chart.axis.y.step) {
				chart.axis.y.element.appendChild(chart.renderYAxisLabel(chart.formatNumber(y)));
			}
			
			chart.padding = {
				top: chart.legend.getBBox().height + 20,
				left: -1 * Math.min(chart.axis.x.element.getBBox().x + 20, chart.axis.y.element.getBBox().x - 8),
				bottom: chart.axis.x.element.getBBox().height
			};
			
			chart.axis.x.element.setAttribute('transform', 'translate(0, ' + (chart.height - chart.padding.bottom) + ')');
			chart.axis.x.stepSize = chart.labels.length > 1 ? (chart.width - chart.padding.left - 40) / (chart.labels.length - 1) : 20;
			
			while(chart.axis.x.stepSize * chart.axis.x.nthLabel < 20) {
				chart.axis.x.nthLabel++;
			}
			
			if(chart.axis.x.nthLabel > 1) {
				for(var index = chart.axis.x.element.childNodes.length - 1; index > 0; index--) {
					if(index % chart.axis.x.nthLabel) {
						chart.axis.x.element.removeChild(chart.axis.x.element.childNodes[index]);
					}
				}
			}
			
			for(var index = 0; index < chart.axis.x.element.childNodes.length; index++) {
				chart.axis.x.element.childNodes[index].setAttribute('transform', 'translate(' + (chart.padding.left + 20 + chart.axis.x.nthLabel * index * chart.axis.x.stepSize) + ', 0)');
			}
			
			chart.axis.y.element.setAttribute('transform', 'translate(' + chart.padding.left + ', 0)');
			chart.axis.y.stepSize = (500 - chart.padding.bottom - chart.padding.top) / (chart.axis.y.maximum / chart.axis.y.step);
			for(var index = 0; index < chart.axis.y.element.childNodes.length; index++) {
				chart.axis.y.element.childNodes[index].setAttribute('transform', 'translate(0, ' + (500 - chart.padding.bottom - index * chart.axis.y.stepSize) + ')');
			}
		}
	},
	renderXAxisLabel: {
		value: function(label) {
			var chart = this;
			
			var labelElement = chart.createSvgElement('g');
			
			var lineElement = chart.createSvgElement('line', {
				x1: 0,
				y1: 0,
				x2: 0,
				y2: 5,
				stroke: '#c0c0c0'
			});
			labelElement.appendChild(lineElement);
			
			var textElement = chart.createSvgElement('text', {
				x: -15,
				y: 7,
				'font-size': 11,
				'text-anchor': 'end',
				transform: 'rotate(-45 0 0)'
			});
			textElement.textContent = label;
			labelElement.appendChild(textElement);
			
			return labelElement;
		}
	},
	renderYAxisLabel: {
		value: function(label) {
			var chart = this;
			
			var labelElement = chart.createSvgElement('g');
			
			var lineElement = chart.createSvgElement('line', {
				x1: 0,
				y1: 0,
				x2: chart.width,
				y2: 0,
				stroke: '#c0c0c0'
			});
			labelElement.appendChild(lineElement);
			
			var textElement = chart.createSvgElement('text', {
				x: -8,
				y: 4,
				'font-size': 11,
				'text-anchor': 'end'
			});
			textElement.textContent = label;
			labelElement.appendChild(textElement);
			
			return labelElement;
		}
	},
	renderLines: {
		value: function() {
			var chart = this;
			
			for(var lineIndex = 0; lineIndex < chart.lines.length; lineIndex++) {
				chart.lines[lineIndex].element = chart.createSvgElement('g', {
					transform: 'translate(' + (chart.padding.left + 20) + ', ' + (500 - chart.padding.bottom) + ')'
				});
				chart.svgElement.appendChild(chart.lines[lineIndex].element);
				
				var points = [];
				chart.lines[lineIndex].circles = [];
				
				for(var datumIndex = 0; datumIndex < chart.lines[lineIndex].data.length; datumIndex++) {
					var point = {
						x: datumIndex * chart.axis.x.stepSize,
						y: -1 * chart.lines[lineIndex].data[datumIndex] * (chart.axis.y.stepSize / chart.axis.y.step),
					};
					points.push(point.x + ',' + point.y);
					
					if(chart.axis.x.stepSize >= 10) {
						var circleElement = chart.createSvgElement('circle', {
							cx: point.x,
							cy: point.y,
							r: 3.5,
							fill: chart.colors[lineIndex % chart.colors.length],
							stroke: 'none'
						});
						chart.lines[lineIndex].element.appendChild(circleElement);
						chart.lines[lineIndex].circles.push(circleElement);
					}
				}
				
				var lineElement = chart.createSvgElement('polyline', {
					points: points.join(' '),
					stroke: chart.colors[lineIndex % chart.colors.length],
					'stroke-width': 2,
					fill: 'none'
				});
				chart.lines[lineIndex].element.appendChild(lineElement);
				chart.lines[lineIndex].line = lineElement;
			}
		}
	},
	removeTooltip: {
		value: function() {
			var chart = this;
			
			if(chart.tooltip !== null) {
				chart.svgElement.removeChild(chart.tooltip.element);
				chart.svgElement.removeChild(chart.tooltip.line);
				chart.tooltip = null;
			}
		}
	}
});

Als nächstes brauchen wir das Tooltip-Element.

var textElement = chart.createSvgElement('text', {
	x: 10,
	y: 21,
	'font-size': 12
});
textElement.textContent = chart.labels[datumIndex];
chart.tooltip.element.appendChild(textElement);

Die erste Zeile im Tooltip soll das Label der Spalte sein, also das formatierte Datum.

for(var lineIndex = 0; lineIndex < chart.lines.length; lineIndex++) {
	var textElement = chart.createSvgElement('text', {
		x: 10,
		y: 21 + (lineIndex + 1) * 16,
		'font-size': 12
	});
	chart.tooltip.element.appendChild(textElement);
	
	var titleElement = chart.createSvgElement('tspan', {
		fill: chart.colors[lineIndex % chart.colors.length]
	});
	textElement.appendChild(titleElement);
	titleElement.textContent = chart.lines[lineIndex].title + ': ';
	
	var valueElement = chart.createSvgElement('tspan', {
		'font-weight': 'bold'
	});
	textElement.appendChild(valueElement);
	valueElement.textContent = chart.formatNumber(chart.lines[lineIndex].data[datumIndex]);
}

Außerdem wollen wir die Daten jeder Datenreihe darstellen. Dafür zeigen wir den Namen der Datenreihe und den Wert der Datenreihe in der aktiven Spalte. Um den Namen der Datenreihe einzufärben, nutzen wir tspan Elemente.

var box = chart.tooltip.element.getBBox();
var boxElement = chart.createSvgElement('rect', {
	x: 0,
	y: 0,
	width: box.width + 20,
	height: box.height + 20,
	rx: 3,
	ry: 3,
	fill: 'rgba(255, 255, 255, .9)',
	stroke: '#c0c0c0'
});
chart.tooltip.element.insertBefore(boxElement, chart.tooltip.element.firstChild);

Außerdem brauchen wir noch eine Box im Hintergrund, um dem Tooltip einen Hintergrund und Rahmen zu geben.

var left = chart.padding.left + 20 + datumIndex * chart.axis.x.stepSize - box.width - 30;
var top = mouse.y - (box.height + 20) / 2;
if(left < 0) {
	left += box.width + 40;
}
top = Math.max(chart.padding.top, Math.min(500 - chart.padding.bottom - box.height - 20, top));
chart.tooltip.element.setAttribute('transform', 'translate(' + left + ', ' + top + ')');

Nun zur Position des Tooltips: Die vertikale Position machen wir vom Mauszeiger abhängig. So sollte der Tooltip immer auf einer Höhe mit dem Mauszeiger sein. Die horizontale Position berechnen wir anhand der Spalte, damit der Tooltip immer links neben der vertikalen Linie steht.

Sonderfall: Wenn der Tooltip links aus dem Diagramm herausragen würde, positionieren wir es stattdessen rechts von der Linie.

Hover und Klick

Nehmen wir an, hinter den einzelnen Messpunkten gibt es weitere Informationen. Beispielsweise könnte sich beim Klick auf den Datenpunkt eine genaue Auswertung oder eine Übersicht über den Tag öffnen. Daher machen wir die Datenpunkte jetzt einfach klickbar. Und damit man sieht, dass beim Klicken was passiert, heben wir die Punkte beim Hover etwas hervor.

renderLines: {
	value: function() {
		var chart = this;
		
		for(var lineIndex = 0; lineIndex < chart.lines.length; lineIndex++) {
			chart.lines[lineIndex].element = chart.createSvgElement('g', {
				transform: 'translate(' + (chart.padding.left + 20) + ', ' + (500 - chart.padding.bottom) + ')'
			});
			chart.svgElement.appendChild(chart.lines[lineIndex].element);
			
			var points = [];
			chart.lines[lineIndex].circles = [];
			
			for(var datumIndex = 0; datumIndex < chart.lines[lineIndex].data.length; datumIndex++) {
				var point = {
					x: datumIndex * chart.axis.x.stepSize,
					y: -1 * chart.lines[lineIndex].data[datumIndex] * (chart.axis.y.stepSize / chart.axis.y.step),
				};
				points.push(point.x + ',' + point.y);
				
				if(chart.axis.x.stepSize >= 10) {
					var circleElement = chart.createSvgElement('circle', {
						cx: point.x,
						cy: point.y,
						r: 3.5,
						fill: chart.colors[lineIndex % chart.colors.length],
						stroke: 'none',
						cursor: 'pointer'
					});
					chart.lines[lineIndex].element.appendChild(circleElement);
					chart.lines[lineIndex].circles.push(circleElement);
					
					circleElement.addEventListener('mouseenter', function(event) {
						event.target.setAttribute('r', 4.5);
					});
					circleElement.addEventListener('mouseleave', function(event) {
						event.target.setAttribute('r', 3.5);
					});
					circleElement.addEventListener('click', function(event) {
						// Klick-Event
					});
				}
			}
			
			var lineElement = chart.createSvgElement('polyline', {
				points: points.join(' '),
				stroke: chart.colors[lineIndex % chart.colors.length],
				'stroke-width': 2,
				fill: 'none'
			});
			chart.lines[lineIndex].element.insertBefore(lineElement, chart.lines[lineIndex].element.firstChild);
			chart.lines[lineIndex].line = lineElement;
		}
	}
},

In der Funktion renderLines() hängen wir an die Punkte drei EventListener an: mouseenter, mouseleave und click. Beim mouseenter vergrößern wir den Radius des Punktes, beim mouseleave verkleinern wir ihn wieder auf den Originalwert. So wird der Punkt beim Überfahren mit der Maus hervorgehoben. Nebenbei geben wir den Punkten noch die Eigenschaft cursor: 'pointer'.

Außerdem passen wir den Code noch etwas an, damit die Linie vor den Punkten eingefügt wird, statt hinter den Punkten.

Im Klick-Event könnten wir nun noch den Klick auf den Punkt abfangen, um beispielsweise auf eine Detailseite weiterzuleiten. Da wir aber keine Detailseite haben, spare ich mir das an dieser Stelle.

Fazit: SVG ist super für interaktivität

So sieht nun unser Endresultat aus. Der Nutzer kann nun mit dem Diagramm interagieren.

SVG-Grafiken sind super für Interaktivität. Auf die Elemente kann man mit JavaScript zugreifen und ihnen EventListeners anhängen. So kann man in diesem Fall Diagrammen sehr einfach einen Mehrwert geben.

Im nächsten Artikel schauen wir uns einen neuen Diagramm-Typen an.

Events im Diagramm – Interaktive SVG-Diagramme im Web, 3.3 out of 5 based on 3 ratings

Nächster Artikel der Serie

Dies ist der (vorerst) letzte Artikel in dieser Serie. Bald folgen aber bestimmt neue Artikel!

Abonniere den RSS-Feed von Advitum, um keinen Artikel zu verpassen, oder folge mir über meine Facebook-Seite oder meinen Twitter-Kanal.

Jetzt seid ihr dran!

Teilt eure Meinung mit uns in den Kommentaren, gebt eine Bewertung für diesen Artikel ab und teilt ihn in Social Networks!

Über

Ich bin ein junger Webdesigner und Programmierer aus Siegen und blogge auf Advitum.de über meine Erfahrungen im Web. Meine Themenschwerpunkte liegen im Bereich der Web-Entwicklung mit PHP, JavaScript, HTML und anderen Script-, Programmier- und Markup-Sprachen, der Nutzung von Content Management System wie Typo3, Wordpress etc. und der Effekt-Hascherei mit Photoshop. Seit 2008 blogge ich auf Advitum.de – mal mehr, mal weniger regelmäßig – über alles, was mich so interessiert. Wenn dir mein Blog gefällt, freue ich mich immer sehr über Feedback in Form von Kommentaren und E-Mails.

Kommentare zu diese Artikel

Schreibe jetzt einen Kommentar!

Diese Artikel könnten dir auch gefallen