Modified Bubble Chart, Added Images to Speech, Added Speeches Tab, modified filter, loading of speeches for individual politicians is quicker

This commit is contained in:
vysitor 2025-03-19 22:46:58 +01:00
parent 253a6ed78b
commit 4f3ee4ef51
25 changed files with 377 additions and 139 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
src/.DS_Store vendored

Binary file not shown.

BIN
src/main/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -621,7 +621,7 @@ public class MongoDBHandler {
List<Document> speeches = speechesCollection.find().into(new ArrayList<>());
List<Speech> result = new ArrayList<>();
for (Document speech : speeches) {
result.add(new Speech_MongoDB_Impl(speech));
result.add(new Speech_MongoDB_Impl(speech, true));
}
return result;
@ -631,7 +631,7 @@ public class MongoDBHandler {
List<Document> speeches = speechesCollection.find(filter).into(new ArrayList<>());
List<Speech> result = new ArrayList<>();
for (Document speech : speeches) {
result.add(new Speech_MongoDB_Impl(speech));
result.add(new Speech_MongoDB_Impl(speech, true));
}
return result;

View file

@ -18,6 +18,7 @@ import org.texttechnologylab.project.gruppe_05_1.domain.speaker.Membership;
import org.texttechnologylab.project.gruppe_05_1.domain.speech.SpeechMetaData;
import org.texttechnologylab.project.gruppe_05_1.util.GeneralUtils;
import org.texttechnologylab.project.gruppe_05_1.util.Logger;
import org.texttechnologylab.project.gruppe_05_1.util.PPRUtils;
import org.texttechnologylab.project.gruppe_05_1.xml.speeches.Interfaces.Speech;
import java.io.IOException;
@ -421,7 +422,43 @@ public class MongoPprUtils {
List<Document> docs = getSpeechCollection().find(filter).into(new ArrayList<>());
for (Document doc : docs) {
speeches.add(new Speech_MongoDB_Impl(doc));
speeches.add(new Speech_MongoDB_Impl(doc, true));
}
return speeches;
}
/**
* Liefert alle Reden zurück
* Die Auswahl kann durch einen (textuellen) Filter eingeschränkt werden
* @param filter
* @return
*/
public static List<Speech> getSpeeches(String filter) {
List<Speech> speeches = new ArrayList<>();
MongoCursor<Document> cursor;
if (filter== null || filter.isBlank()) {
cursor = getSpeechCollection().find().iterator();
} else {
String pattern = ".*" + filter + ".*";
Document searchDocument = new Document("$or", List.of(
new Document("speakerName", new Document("$regex", pattern).append("$options", "i")),
new Document("fraction", new Document("$regex", pattern).append("$options", "i")),
new Document("speechKey", new Document("$regex", pattern).append("$options", "i"))
));
cursor = getSpeechCollection().find(searchDocument).cursor();
}
try {
while (cursor.hasNext()) {
speeches.add(new Speech_MongoDB_Impl(cursor.next(), false));
}
} catch (Throwable t) {
Logger.error(String.valueOf(t));
} finally {
cursor.close();
}
return speeches;
@ -448,18 +485,7 @@ public class MongoPprUtils {
// aus "sessions" Collection
String dateTimeString = getSessionDateTime(sessionId);
if (dateTimeString != null) {
md.setDateTimeString(dateTimeString);
LocalDateTime tmp = GeneralUtils.parseDateTime(dateTimeString, "dd.MM.yyyy HH:mm");
if (tmp == null) {
tmp = GeneralUtils.parseDateTime(dateTimeString, "dd.MM.yyyy H:mm");
if (tmp == null) {
Logger.error(dateTimeString + " could not be parsed");
}
}
md.setDateTime(tmp);
}
augmentSpeechMetaDataFromSession(sessionId, md);
// aus "agendaItems" Collection
int agendaItemId = speech.getAgendaItemId();
@ -472,16 +498,84 @@ public class MongoPprUtils {
// Sortiere nach Datum, absteigend
speechMetaDataList.sort((md1, md2) -> {
try {
return md2.getDateTime().compareTo(md1.getDateTime());
} catch (NullPointerException e) {
return 0;
}
if ((md2.getDateTime() == null) && (md1.getDateTime()) == null) return 0;
if (md2.getDateTime() == null) return -1;
if (md1.getDateTime() == null) return 1;
return md2.getDateTime().compareTo(md1.getDateTime());
});
return speechMetaDataList;
}
/**
* Liefert Metadaten (aber keine Inhalte!) für alle Reden zurück.
* Die Auswahl kann durch einen (textuellen) Filter eingeschränkt werden
* Als Metadaten zählen das Datum, Agenda-ID etc.
* @param filter
* @return
*/
public static List<SpeechMetaData> getSpeechesMetadata(String filter) {
List<SpeechMetaData> speechMetaDataList = new ArrayList<>();
List<Speech> speeches = MongoPprUtils.getSpeeches(filter);
for (Speech speech : speeches) {
SpeechMetaData md = new SpeechMetaData();
md.setSpeechKey(speech.getSpeechKey());
md.setSpeechId(speech.getSpeechId());
md.setSpeakerId(speech.getSpeakerId());
md.setSpeakerName(speech.getSpeakerName());
md.setFraktion(speech.getFraction());
if (md.getFraktion() == null) {md.setFraktion(PPRUtils.PARTEILOS_KUERZEL);}
int sessionId = speech.getSessionId();
md.setSessionId(sessionId);
// aus "sessions" Collection
augmentSpeechMetaDataFromSession(sessionId, md);
// aus "agendaItems" Collection
int agendaItemId = speech.getAgendaItemId();
String agendaTitel = getAgendaTitle(sessionId, agendaItemId);
md.setAgendaTitle(agendaTitel);
speechMetaDataList.add(md);
}
// Sortiere nach Datum, absteigend
speechMetaDataList.sort((md1, md2) -> {
if ((md2.getDateTime() == null) && (md1.getDateTime()) == null) return 0;
if (md2.getDateTime() == null) return -1;
if (md1.getDateTime() == null) return 1;
return md2.getDateTime().compareTo(md1.getDateTime());
});
return speechMetaDataList;
}
/**
* Füge Rede-Metadaten (welche in der Session-Collection stehen) der Rede hinzu.
* Achtung: Redezeit ist in der Datenbank in unterschiedlichen Formaten vorhanden.
* @param sessionId
* @param md
*/
public static void augmentSpeechMetaDataFromSession(int sessionId, SpeechMetaData md) {
String dateTimeString = getSessionDateTime(sessionId);
if (dateTimeString != null) {
md.setDateTimeString(dateTimeString);
for (String format : Arrays.asList("dd.MM.yyyy HH:mm",
"dd.MM.yyyy H:mm",
"dd.MM.yyyy HH.mm",
"dd.MM.yyyy H.mm")) {
LocalDateTime tmp = GeneralUtils.parseDateTime(dateTimeString,format);
if (tmp != null) {
md.setDateTime(tmp);
return;
}
}
Logger.error(dateTimeString + " could not be parsed");
}
}
/**
* Liefert das Datum und die Uhrzeit einer Sitzung zurück
@ -522,11 +616,14 @@ public class MongoPprUtils {
* @return
*/
public static HtmlSpeech getSpeechByKey(String key) {
System.out.println(key);
System.out.println(key); // TODO: remove when no longer needed
Document filter = new Document("speechKey", key);
Document speechDoc = getSpeechCollection().find(filter).first();
System.out.println(getSpeechCollection().find().filter(Filters.eq("speechKey", key)).first());
System.out.println("SpeechDoc "+ speechDoc);
System.out.println(getSpeechCollection().find().filter(Filters.eq("speechKey", key)).first()); // TODO: remove when no longer needed
System.out.println("SpeechDoc "+ speechDoc); // TODO: remove when no longer needed
if (speechDoc == null) {
Logger.error("Rede " + key + " nicht gefunden");
}
return new HtmlSpeech(speechDoc);
}

View file

@ -13,7 +13,7 @@ import org.texttechnologylab.project.gruppe_05_1.xml.speeches.Interfaces.Speech;
import java.util.List;
public class Speech_MongoDB_Impl extends Speech_File_Impl implements Speech {
public Speech_MongoDB_Impl(Document mongoDocument) {
public Speech_MongoDB_Impl(Document mongoDocument, boolean includeContent) {
super(
mongoDocument.getInteger("sessionId"),
mongoDocument.getInteger("agendaItemId"),
@ -24,23 +24,23 @@ public class Speech_MongoDB_Impl extends Speech_File_Impl implements Speech {
mongoDocument.getString("speechKey")
);
for (Document content : (List<Document>) mongoDocument.get("speechContents")) {
switch (content.getString("type")) {
case "line":
this.addContent(new Line_MongoDB_Impl(content));
break;
case "comment":
this.addContent(new Comment_MongoDB_Impl(content));
break;
case "speaker":
this.addContent(new Speaker_MongoDB_Impl(content));
break;
default:
throw new IllegalArgumentException("Unknown content type: " + content.getString("type"));
if (includeContent) {
for (Document content : (List<Document>) mongoDocument.get("speechContents")) {
switch (content.getString("type")) {
case "line":
this.addContent(new Line_MongoDB_Impl(content));
break;
case "comment":
this.addContent(new Comment_MongoDB_Impl(content));
break;
case "speaker":
this.addContent(new Speaker_MongoDB_Impl(content));
break;
default:
throw new IllegalArgumentException("Unknown content type: " + content.getString("type"));
}
}
}
}
public String getFullText() {

View file

@ -2,9 +2,11 @@ package org.texttechnologylab.project.gruppe_05_1.domain.html;
import org.bson.Document;
import org.texttechnologylab.project.gruppe_05_1.database.MongoDBHandler;
import org.texttechnologylab.project.gruppe_05_1.database.MongoPprUtils;
import org.texttechnologylab.project.gruppe_05_1.domain.nlp.NlpInfo;
import org.texttechnologylab.project.gruppe_05_1.domain.nlp.Token;
import org.texttechnologylab.project.gruppe_05_1.domain.nlp.Topic;
import org.texttechnologylab.project.gruppe_05_1.domain.speech.SpeechMetaData;
import java.util.ArrayList;
import java.util.List;
@ -15,6 +17,8 @@ public class HtmlSpeech {
String speechKey;
String speakerName;
String fraction;
String dateTimeString; // aus "sessions" Collection
String agendaTitle; // aus "agendaItems" Collection
List<SpeechContent> content = new ArrayList<>();
NlpInfo nlp = null;
@ -35,6 +39,18 @@ public class HtmlSpeech {
}
}
// ergänzen um Datum, Uhrzeit und Agendapunkt der Rede
SpeechMetaData md = new SpeechMetaData();
int sessionId = doc.getInteger("sessionId");
md.setSessionId(sessionId);
MongoPprUtils.augmentSpeechMetaDataFromSession(sessionId, md);
dateTimeString = md.getDateTimeString();
int agendaItemId = doc.getInteger("agendaItemId");
String title = MongoPprUtils.getAgendaTitle(sessionId, agendaItemId);
agendaTitle = title;
// Ergänzung um NLP-Informationen
Document nlpDoc = (Document) doc.get("analysisResults");
nlp = readNlpInfo(nlpDoc);
}
@ -86,6 +102,13 @@ public class HtmlSpeech {
public void setFraction(String fraction) {
this.fraction = fraction;
}
public String getDateTimeString() {return dateTimeString;}
public void setDateTimeString(String dateTimeString) {this.dateTimeString = dateTimeString;}
public String getAgendaTitle() {return agendaTitle;}
public void setAgendaTitle(String agendaTitle) {this.agendaTitle = agendaTitle;}
public List<SpeechContent> getContent() {
return content;
@ -112,13 +135,14 @@ public class HtmlSpeech {
if (this == o) return true;
if (!(o instanceof HtmlSpeech that)) return false;
return Objects.equals(speechKey, that.speechKey) && Objects.equals(speakerName, that.speakerName)
&& Objects.equals(fraction, that.fraction) && Objects.equals(content, that.content)
&& Objects.equals(fraction, that.fraction) && Objects.equals(dateTimeString, that.dateTimeString)
&& Objects.equals(agendaTitle, that.agendaTitle) && Objects.equals(content, that.content)
&& Objects.equals(nlp, that.nlp);
}
@Override
public int hashCode() {
return Objects.hash(speechKey, speakerName, fraction, content, nlp);
return Objects.hash(speechKey, speakerName, fraction, dateTimeString, agendaTitle, content, nlp);
}
@Override
@ -127,6 +151,8 @@ public class HtmlSpeech {
.add("speechKey='" + speechKey + "'")
.add("speakerName='" + speakerName + "'")
.add("fraction='" + fraction + "'")
.add("dateTimeString='" + dateTimeString + "'")
.add("agendaTitle='" + agendaTitle + "'")
.add("content=" + content)
.add("nlp=" + nlp)
.toString();

View file

@ -12,6 +12,8 @@ public class SpeechMetaData {
String speechKey; // z.B. "ID2011400300"
int speechId; // TODO: nötig?
int speakerId;
String speakerName;
String fraktion;
int sessionId; // TODO: nötig?
// aus "sessions" Collection
@ -45,6 +47,14 @@ public class SpeechMetaData {
this.speakerId = speakerId;
}
public String getSpeakerName() {return speakerName;}
public void setSpeakerName(String speakerName) {this.speakerName = speakerName;}
public String getFraktion() {return fraktion;}
public void setFraktion(String fraktion) {this.fraktion = fraktion;}
public int getSessionId() {
return sessionId;
}
@ -81,12 +91,15 @@ public class SpeechMetaData {
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SpeechMetaData that)) return false;
return speechId == that.speechId && speakerId == that.speakerId && sessionId == that.sessionId && Objects.equals(speechKey, that.speechKey) && Objects.equals(dateTime, that.dateTime) && Objects.equals(dateTimeString, that.dateTimeString) && Objects.equals(agendaTitle, that.agendaTitle);
return speechId == that.speechId && speakerId == that.speakerId && sessionId == that.sessionId
&& Objects.equals(speechKey, that.speechKey) && Objects.equals(speakerName, that.speakerName)
&& Objects.equals(fraktion, that.fraktion) && Objects.equals(dateTime, that.dateTime)
&& Objects.equals(dateTimeString, that.dateTimeString) && Objects.equals(agendaTitle, that.agendaTitle);
}
@Override
public int hashCode() {
return Objects.hash(speechKey, speechId, speakerId, sessionId, dateTime, dateTimeString, agendaTitle);
return Objects.hash(speechKey, speechId, speakerId, speakerName, fraktion, sessionId, dateTime, dateTimeString, agendaTitle);
}
@Override
@ -95,6 +108,8 @@ public class SpeechMetaData {
.add("speechKey='" + speechKey + "'")
.add("speechId=" + speechId)
.add("speakerId=" + speakerId)
.add("speakerName='" + speakerName + "'")
.add("fraktion='" + fraktion + "'")
.add("sessionId=" + sessionId)
.add("dateTime=" + dateTime)
.add("dateTimeString='" + dateTimeString + "'")

View file

@ -109,30 +109,4 @@ public class ParlamentarierController {
ctx.render("parlamentarierDetails.ftl", attributes);
}
/**
* Lösche alle Abgeordnete.
* @param ctx JavaLin-Context
*/
@OpenApi(
summary = "Lösche alle Parlamentarier",
description = "Lösche alle Parlamentarier aus der Datenbank",
operationId = "deleteAllParlamentarier",
path = "/deleteParlamentarier",
methods = HttpMethod.DELETE,
tags = {"Parlamentarier"},
responses = {
@OpenApiResponse(status = "204", content = {@OpenApiContent(from = Parlamentarier[].class)})
})
public static void deleteAllParlamentarier(Context ctx) {
MongoPprUtils.truncateSpeakerCollection();
List<Parlamentarier> parlamentarier = MongoPprUtils.getAllParlamentarier("");
Map<String, Object> attributes = new HashMap<>();
attributes.put("parlamentarier", parlamentarier);
attributes.put("filter", "filter");
ctx.render("parlamentarier.ftl", attributes);
}
}

View file

@ -58,11 +58,12 @@ public class RESTHandler {
// Parlamentarier
app.get("/", FrontEndController::getHomepage);
app.get("/members", FrontEndController::getAllParlamentarier);
app.get("/portfolio/{id}", FrontEndController::getParlamentarierDetails);
app.delete("/deleteParlamentarier", ParlamentarierController::deleteAllParlamentarier);
app.get("/portfolio/{id}", ParlamentarierController::getParlamentarierDetails);
// Reden
app.get("/reden/{id}", FrontEndController::listSpeeches); // zeige Reden eines Parlamentariers an
app.get("/reden/{id}/{redeId}", FrontEndController::showSpeech); // zeige eine bestimmte Rede des Parlamentariers an
app.get("/reden/{id}/{redeId}", SpeechController::showSpeech); // zeige eine bestimmte Rede des Parlamentariers an
app.get("/reden", SpeechController::listAllSpeeches); // zeige alle Reden an (Filtern möglich)
}
}

View file

@ -59,7 +59,6 @@ public class RESTHandlerOld {
// Parlamentarier
app.get("/", ParlamentarierController::getAllParlamentarier);
app.get("/portfolio/{id}", ParlamentarierController::getParlamentarierDetails);
app.delete("/deleteParlamentarier", ParlamentarierController::deleteAllParlamentarier);
// Reden
app.get("/reden/{id}", SpeechController::listSpeeches); // zeige Reden eines Parlamentariers an

View file

@ -1,5 +1,6 @@
package org.texttechnologylab.project.gruppe_05_1.rest;
import de.tudarmstadt.ukp.dkpro.core.api.syntax.type.constituent.S;
import io.javalin.http.Context;
import io.javalin.openapi.*;
import org.texttechnologylab.project.gruppe_05_1.database.MongoPprUtils;
@ -8,14 +9,14 @@ import org.texttechnologylab.project.gruppe_05_1.domain.html.ParlamentarierDetai
import org.texttechnologylab.project.gruppe_05_1.domain.nlp.Token;
import org.texttechnologylab.project.gruppe_05_1.domain.nlp.Topic;
import org.texttechnologylab.project.gruppe_05_1.domain.speech.SpeechMetaData;
import org.texttechnologylab.project.gruppe_05_1.util.Logger;
import org.texttechnologylab.project.gruppe_05_1.xml.speeches.Interfaces.Speech;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
import static org.texttechnologylab.project.gruppe_05_1.util.PPRUtils.listFractionsFromMembers;
public class SpeechController {
/**
* Liste alle Reden eines Parlamentariers an
@ -65,6 +66,7 @@ public class SpeechController {
@OpenApiResponse(status = "200", content = {@OpenApiContent(from = Speech.class)})
})
public static void showSpeech(Context ctx) {
String parlamentarierId = ctx.pathParam("id");
String redeId = ctx.pathParam("redeId");
Map<String, Object> attributes = new HashMap<>();
@ -72,6 +74,10 @@ public class SpeechController {
HtmlSpeech speech = MongoPprUtils.getSpeechByKey(redeId);
attributes.put("s", speech);
// Foto des Abgeordnetes
String picture = MongoPprUtils.getParlamentarierPictureByID(parlamentarierId);
attributes.put("picture", picture);
// NLP: Topic
if ((speech.getNlp() != null) && (speech.getNlp().getTopics() != null)) {
Map<String, Double> topics = Topic.condenseTopicInformation(speech.getNlp().getTopics()); // Daten "verdichten"...
@ -107,4 +113,39 @@ public class SpeechController {
ctx.render("speech.ftl", attributes);
}
@OpenApi(
summary = "Liste alle Reden (Filtern ist möglich)",
description = "Liste alle Reden. Man kann nach Freitext (MdB Name, Partei/Fraktion) oder nach Thema (Topic) filtern",
operationId = "listAllSpeeches",
path = "/reden",
methods = HttpMethod.GET,
tags = {"Rede"},
queryParams = {
@OpenApiParam(name = "filter", description = "Full-Text-Filter. Kann Vorname, Nachname oder Partei filtern", required = false),
// TODO: Topic Filter
},
responses = {
@OpenApiResponse(status = "200", content = {@OpenApiContent(from = Speech[].class)})
})
public static void listAllSpeeches(Context ctx) {
String filter = ctx.queryParam("filter");
Logger.info("Filter: '" + filter + "'");
List<SpeechMetaData> speechMetaDataList = MongoPprUtils.getSpeechesMetadata(filter);
Map<String, Object> attributes = new HashMap<>();
attributes.put("speechesMetaDataList", speechMetaDataList);
// Filtern nach Text
attributes.put("filter", filter == null || filter.isBlank() ? null : filter);
// Filtern nach Partei/Fraktion
attributes.put("parties", listFractionsFromMembers(MongoPprUtils.getAllParlamentarier("")));
// Filtern nach Topics - TODO
List<String> topics = Arrays.asList("International", "Government", "Labor", "Economy", "Public");
attributes.put("topics", topics);
ctx.render("showAllSpeechesList.ftl", attributes);
}
}

View file

@ -126,6 +126,7 @@ th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
border: 1px solid #ddd;
}
th {

View file

@ -1,14 +1,29 @@
<div class="filter-form">
<form method="GET" action="/members">
<form name="searchForm" action="${formAction}" method="GET">
<input type="text" name="firstName" placeholder="First Name">
<input type="text" name="name" placeholder="Last Name">
<select id="party" name="party">
<option value="">All</option>
<#-- Iterate over the parties list and create an option for each one -->
<#list parties as party>
<option value="${party}">${party}</option>
</#list>
</select>
<#if parties??>
<select id="party" name="party">
<option value="">Partei/Fraktion</option>
<#-- Iterate over the parties list and create an option for each one -->
<#list parties as party>
<option value="${party}">${party}</option>
</#list>
</select>
</#if>
<#if topics??>
<select id="topic" name="topic">
<option value="">Topic</option>
<#-- Iterate over the topics list and create an option for each one -->
<#list topics as topic>
<option value="${topic}">${topic}</option>
</#list>
</select>
</#if>
<button type="submit">Search</button>
</form>
</div>

View file

@ -3,9 +3,10 @@
<nav>
<p></p>
<a href="/">Home</a>
<a href="/members">Members</a>
<a href="/export">Export</a>
<a href="/about">About</a>
<a href="/members">Parlamentarier</a>
<a href="/reden">Reden</a>
<a href="/export">Exportieren</a>
<a href="/about">Über</a>
<p></p>
</nav>
</header>

View file

@ -5,20 +5,13 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Abgeordnete Übersicht</title>
<link rel="stylesheet" href="styles.css">
<style type="text/css">
th, td {
padding: 12px;
text-align: center; /* Center-aligns both header and data cells */
border: 1px solid #ddd;
}
</style>
</head>
<#include "header.ftl">
<body>
<h2>Abgeordnete (alphabetisch sortiert)</h2>
<main>
<#assign formAction = "/members">
<#include "filterForm.ftl">
<br>

View file

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="de">
<head>
<link rel="stylesheet" href="/index.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alle Reden</title>
</head>
<#include "header.ftl">
<body>
<h2>Reden</h2>
<main>
<br>
<#assign formAction = "/reden">
<#include "filterForm.ftl">
<br><br>
<section>
<table>
<thead>
<tr>
<th>Datum</th>
<th>Name</th>
<th>Fraktion</th>
<th>Sitzung / Agenda</th>
</tr>
</thead>
<tbody>
<#list speechesMetaDataList as redeMd>
<tr>
<td>${redeMd.dateTimeString}</td>
<td>${redeMd.speakerName}</td>
<td>${redeMd.fraktion}</td>
<td><a href="/reden/${redeMd.speakerId}/${redeMd.speechKey}">${redeMd.sessionId} / ${redeMd.agendaTitle}</a></td>
</tr>
</#list>
</tbody>
</table>
</section>
</main>
</body>
<#include "footer.ftl">
</html>

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="de">
<head>
<link rel="stylesheet" href="/index.css">
<link rel="stylesheet" href="index.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rede von ${s.speakerName} <#if s.fraction??> (${s.fraction}) </#if></title>
@ -16,11 +16,26 @@
<#include "header.ftl">
<body>
<script src="https://d3js.org/d3.v7.min.js"></script>
<h1>Rede von ${s.speakerName} <#if s.fraction??> (${s.fraction}) </#if> </h1>
<h1>
Rede von ${s.speakerName}
<#if s.fraction??> (${s.fraction}) </#if>
<#if s.dateTimeString??> vom ${s.dateTimeString} </#if>
</h1>
<br>
<h2>Rede ${s.speechKey} </h2>
<h2>
Rede ${s.speechKey}
<#if s.agendaTitle??> / Agendapunkt: ${s.agendaTitle} </#if>
</h2>
<br>
<#if picture??>
<img style="max-width: 400px; height: auto;" src="data:image/jpeg;base64,${picture}" alt="Foto von ${s.speakerName}" />
<#else>
<h2>(kein Foto verfügbar)</h2>
</#if>
<br> <br>
<#list s.content as c>
<#include "speechContent.ftl">

View file

@ -1,52 +1,65 @@
<svg id="topicsBubblechart"></svg>
<svg id="topicsBubblechart"></svg>
<script>
<script>
var topicsData = [
<#list condenseTopicInformation as topicTuple>
{ topic: "${topicTuple.topic}", score: ${topicTuple.score?string?replace(',', '.')}} <#sep>,
</#list>
];
var topicsData = [
<#list condenseTopicInformation as topicTuple>
{ topic: "${topicTuple.topic}", score: "${topicTuple.score?string?replace(',', '.')}"} <#sep>,
</#list>
];
const topics_bc_width = 1000;
const topics_bc_height = 800;
const topics_bc_width = 1000;
const topics_bc_height = 800;
const minScore = Math.min(...topicsData.map(item => item.score));
const maxScore = Math.max(...topicsData.map(item => item.score));
fillOpacity = 0.5;
fillOpacity = 0.5;
var svg = d3.select("#topicsBubblechart")
.attr("width", topics_bc_width)
.attr("height", topics_bc_height)
var svg = d3.select("#topicsBubblechart")
.attr("width", topics_bc_width)
.attr("height", topics_bc_height)
var topics_bc_color = d3.scaleOrdinal(d3.schemeCategory10);
var topics_bc_size = d3.scaleLinear().domain([1, 10]).range([30, 100]);
var topics_bc_color = d3.scaleOrdinal(d3.schemeCategory10);
var topics_bc_size = d3.scaleLinear().domain([minScore, maxScore]).range([30, 70]);
var bubbles = svg.selectAll("circle")
.data(topicsData)
.enter().append("circle")
.attr("r", d => topics_bc_size(d.score))
.attr("fill", d => topics_bc_color(d.topic))
.attr("fill-opacity", fillOpacity);
var bubbles = svg.selectAll("circle")
.data(topicsData)
.enter().append("circle")
.attr("r", d => topics_bc_size(d.score))
.attr("fill", d => topics_bc_color(d.topic))
.attr("fill-opacity", fillOpacity);
var topics_labels = svg.selectAll("text")
.data(topicsData)
.enter().append("text")
.attr("text-anchor", "middle")
.attr("font-size", d => (10 * d.score) + "px")
.attr("fill", "#000")
.text(d => d.topic);
var topics_labels = svg.selectAll("text")
.data(topicsData)
.enter().append("text")
.attr("text-anchor", "middle")
.attr("font-size", d => Math.max(12, 10 * Math.sqrt(d.score)) + "px")
.attr("fill", "#000")
.text(d => d.topic)
.on("mouseover", function(event, d) {
d3.select(this)
.transition()
.duration(200)
.attr("font-size", 25 + "px"); // Increase font size on hover
})
.on("mouseout", function(event, d) {
d3.select(this)
.transition()
.duration(200)
.attr("font-size", d => Math.max(12, 10 * Math.sqrt(d.score)) + "px"); // Reset font size after hover
});
var topics_bc_simulation = d3.forceSimulation(topicsData)
.force("charge", d3.forceManyBody().strength(5))
.force("center", d3.forceCenter(topics_bc_width / 2, topics_bc_height / 2))
.force("collision", d3.forceCollide(d => topics_bc_size(d.score) + 25))
.force("x", d3.forceX(topics_bc_width / 2).strength(0.1))
.force("y", d3.forceY(topics_bc_height / 2).strength(0.1));
var topics_bc_simulation = d3.forceSimulation(topicsData)
.force("charge", d3.forceManyBody().strength(5))
.force("center", d3.forceCenter(topics_bc_width / 2, topics_bc_height / 2))
.force("collision", d3.forceCollide(d => topics_bc_size(d.score) + 25))
.force("x", d3.forceX(topics_bc_width / 2).strength(0.1))
.force("y", d3.forceY(topics_bc_height / 2).strength(0.1));
topics_bc_simulation.on("tick", () => {
bubbles.attr("cx", d => d.x).attr("cy", d => d.y);
topics_labels.attr("x", d => d.x).attr("y", d => d.y);
});
</script>
topics_bc_simulation.on("tick", () => {
bubbles.attr("cx", d => d.x).attr("cy", d => d.y);
topics_labels.attr("x", d => d.x).attr("y", d => d.y);
});
</script>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before After
Before After