diff --git a/AI_HANDOVER.md b/AI_HANDOVER.md index fab5979..5d04a00 100644 --- a/AI_HANDOVER.md +++ b/AI_HANDOVER.md @@ -116,7 +116,9 @@ Aktuelle Button- und Sichtbarkeitslogik pro Umfrage: - Nur sichtbar, wenn `published = 0` - `Ergebnisse anzeigen` - Immer verfügbar, sofern Results-Seite konfiguriert ist -- Aktionslinks (`Bearbeiten`, `Öffentliche Ansicht`, `Ergebnisse anzeigen`, `Entwurfs-Ansicht`) öffnen bewusst **im selben Tab** (kein `target="_blank"` mehr), weil der Kunde die vielen neuen Tabs als störend meldete. +- Tab-Verhalten (bewusste Produktentscheidung nach Kundenrückmeldung): + - `Bearbeiten` öffnet **im selben Tab** (eigentlicher Arbeitsschritt). + - `Öffentliche Ansicht`, `Entwurfs-Ansicht` und `Ergebnisse anzeigen` öffnen **im neuen Fenster** (`target="_blank"`), damit die Editor-/Listenansicht erhalten bleibt. - `Umfrage veröffentlichen` - Eigene Aktion in der Liste - Nach Veröffentlichung disabled und textlich als veröffentlicht markiert @@ -236,7 +238,9 @@ Aktuelle Ergebnislogik: - Optionsstatistik plus Kreisdiagramm - `range` - Minimum, Maximum, Durchschnitt plus Verteilung und Linienchart - - Die Verteilung deckt die **komplette Skala** von `rangeMin` bis `rangeMax` in `rangeStep`-Schritten ab (inkl. Werte ohne Antworten), damit die X-Achse linear und vollständig ist. Antworten außerhalb der konfigurierten Grenzen erweitern die Skala. Siehe `SurveyResultsViewService::buildRangeScale`. + - **Diagramm** (`distribution`): deckt die **komplette Skala** von `rangeMin` bis `rangeMax` in `rangeStep`-Schritten ab (inkl. Werte ohne Antworten), damit die X-Achse linear und vollständig ist. Antworten außerhalb der konfigurierten Grenzen erweitern die Skala. Siehe `SurveyResultsViewService::buildRangeScale`. + - **Werte-Liste/Tabelle** (`distributionRows`): zeigt bewusst nur die **tatsächlich beantworteten** Werte (aufsteigend). So bläht sich die Ergebnisseite bei großen Skalen (z. B. Alter 1–100 mit Schrittweite 1) nicht mit hunderten Nullzeilen auf. Die vollständige Skala bleibt trotzdem im Diagramm sichtbar. Online-Liste, PDF-Tabelle und die SVG-/Chart.js-Diagramme greifen entsprechend auf `distributionRows` (Liste) bzw. `distribution` (Diagramm) zu. + - Bei großen Skalen werden die X-Achsen-Beschriftungen ausgedünnt (Chart.js `autoSkip`/`maxTicksLimit`, SVG jede n-te Beschriftung) und die Punkt-Marker verkleinert bzw. ab >40 Punkten weggelassen. Die PDF-Range-Tabelle hat keine Farb-Swatch-Spalte mehr (nur Wert/Anzahl/Anteil), weil die Kopplung an ein Vollskalen-Diagramm nicht mehr sinnvoll ist. - `text` - Kein Pie-Chart - Gruppierte Freitextantworten: jede unterschiedliche Antwort **einmal** mit exakter Häufigkeit (`responseSummary`, häufigste zuerst, kein Top-5-Limit). Prozentangaben und die frühere Doppel-Auflistung wurden bewusst entfernt. diff --git a/contao/templates/frontend/member_survey_list.html.twig b/contao/templates/frontend/member_survey_list.html.twig index a82135a..c3938ab 100644 --- a/contao/templates/frontend/member_survey_list.html.twig +++ b/contao/templates/frontend/member_survey_list.html.twig @@ -70,7 +70,7 @@ {% endif %} {% endif %} {% if survey.publicUrl and survey.published %} - {{ 'survey.list.public_view'|trans }} + {{ 'survey.list.public_view'|trans }} {% endif %} {% if survey.copyUrl and survey.published %} {% endif %} {% if survey.draftUrl and not survey.published %} - {{ 'survey.list.draft_view'|trans }} + {{ 'survey.list.draft_view'|trans }} {% endif %} {% if survey.resultsUrl %} - {{ 'survey.list.show_results'|trans }} + {{ 'survey.list.show_results'|trans }} {% endif %}
diff --git a/contao/templates/frontend/show_survey_results.html.twig b/contao/templates/frontend/show_survey_results.html.twig index c9184e0..16e79c6 100644 --- a/contao/templates/frontend/show_survey_results.html.twig +++ b/contao/templates/frontend/show_survey_results.html.twig @@ -93,11 +93,11 @@
- {% for option in question.distribution %} + {% for option in question.distributionRows|default([]) %}
{{ option.label|striptags|trim }} - {{ option.count }} + {{ 'survey.results.answer_count'|trans({'%count%': option.count}) }} {{ option.percentage }}%
diff --git a/src/Resources/views/pdf/survey_results.html.twig b/src/Resources/views/pdf/survey_results.html.twig index ae721e0..c94872f 100644 --- a/src/Resources/views/pdf/survey_results.html.twig +++ b/src/Resources/views/pdf/survey_results.html.twig @@ -232,9 +232,6 @@ {% if question.pdfChartDataUri %}
Diagramm zu {{ question.question|striptags|trim }} - {% if question.type == 'range' and question.pdfChartLegend %} -
{{ 'survey.pdf.chart_hint'|trans }}
- {% endif %}
{% endif %} @@ -279,30 +276,28 @@
- - - - - - - - - - - {% for option in question.pdfChartLegend %} + {% if question.distributionRows|default([]) %} +
{{ 'survey.pdf.color'|trans }}{{ 'survey.pdf.value'|trans }}{{ 'survey.pdf.count'|trans }}{{ 'survey.pdf.share'|trans }}
+ - - - - + + + - {% else %} - - - - {% endfor %} - -
{{ option.label|striptags|trim }}{{ option.count }}{{ option.percentage }}%{{ 'survey.pdf.value'|trans }}{{ 'survey.pdf.count'|trans }}{{ 'survey.pdf.share'|trans }}
{{ 'survey.pdf.no_ratings'|trans }}
+ + + {% for option in question.distributionRows %} + + {{ option.label|striptags|trim }} + {{ option.count }} + {{ option.percentage }}% + + {% endfor %} + + + {% else %} +

{{ 'survey.pdf.no_ratings'|trans }}

+ {% endif %} {% else %} {# Eine Tabelle statt Doppel-Auflistung: jede Antwort einmal, mit exakter Anzahl statt Prozentwerten. #} diff --git a/src/Service/SurveyResultsPdfService.php b/src/Service/SurveyResultsPdfService.php index e457f0a..537f29b 100644 --- a/src/Service/SurveyResultsPdfService.php +++ b/src/Service/SurveyResultsPdfService.php @@ -361,7 +361,16 @@ final class SurveyResultsPdfService $xLabels = []; $yGrid = []; - for ($tick = 0; $tick <= $maxValue; ++$tick) { + // Große Skalen (z. B. Alter 1–100): X-Beschriftung ausdünnen und die + // Punkt-Marker verkleinern bzw. weglassen, damit das Diagramm lesbar bleibt. + $pointCount = count($values); + $labelStep = max(1, (int) ceil($pointCount / 12)); + $lastIndex = $pointCount - 1; + $pointRadius = $pointCount > 40 ? 0.0 : ($pointCount > 20 ? 3.0 : 5.5); + + $yStep = max(1, (int) ceil($maxValue / 6)); + + for ($tick = 0; $tick <= $maxValue; $tick += $yStep) { $ratio = $maxValue > 0 ? $tick / $maxValue : 0; $y = $paddingTop + $plotHeight - ($ratio * $plotHeight); $yGrid[] = sprintf('', $paddingLeft, $y, $width - $paddingRight, $y); @@ -373,8 +382,14 @@ final class SurveyResultsPdfService $y = $paddingTop + $plotHeight - (($value / $maxValue) * $plotHeight); $polylinePoints[] = sprintf('%.2f,%.2f', $x, $y); $areaPolylinePoints[] = sprintf('%.2f,%.2f', $x, $y); - $pointCircles[] = sprintf('', $x, $y, $this->escapeXml((string) ($pointColors[$index] ?? '#EC7C32'))); - $xLabels[] = sprintf('%s', $x, $height - 16, $this->escapeXml($labels[$index])); + + if ($pointRadius > 0.0) { + $pointCircles[] = sprintf('', $x, $y, $pointRadius, $this->escapeXml((string) ($pointColors[$index] ?? '#EC7C32'))); + } + + if (0 === $index % $labelStep || $index === $lastIndex) { + $xLabels[] = sprintf('%s', $x, $height - 16, $this->escapeXml($labels[$index])); + } } $areaPoints = sprintf( diff --git a/src/Service/SurveyResultsViewService.php b/src/Service/SurveyResultsViewService.php index 228cb29..050a598 100644 --- a/src/Service/SurveyResultsViewService.php +++ b/src/Service/SurveyResultsViewService.php @@ -202,19 +202,30 @@ final class SurveyResultsViewService $valueCounts[$numericValue] = ($valueCounts[$numericValue] ?? 0) + 1; } - // Die komplette Skala abbilden (inklusive Werte ohne Antworten), damit - // die X-Achse linear und vollständig ist – nicht nur die Werte, die - // tatsächlich geantwortet wurden. - $distribution = []; + // Diagramm: die komplette Skala abbilden (inklusive Werte ohne + // Antworten), damit die X-Achse linear und vollständig ist. + $fullScale = []; foreach ($this->buildRangeScale($result['_rangeConfig'] ?? null, $valueCounts) as $scalePoint) { - $distribution[(string) $scalePoint] = $valueCounts[$scalePoint] ?? 0; + $fullScale[(string) $scalePoint] = $valueCounts[$scalePoint] ?? 0; + } + + // Liste/Tabelle: nur tatsächlich beantwortete Werte, aufsteigend. + // So bläht sich die Ergebnisseite bei großen Skalen (z. B. Alter + // 1–100) nicht mit hunderten Nullzeilen auf. Die vollständige Skala + // bleibt weiterhin im Diagramm sichtbar. + ksort($valueCounts, SORT_NUMERIC); + $answeredRows = []; + + foreach ($valueCounts as $value => $count) { + $answeredRows[(string) $value] = $count; } $result['minimum'] = [] !== $numericValues ? min($numericValues) : null; $result['maximum'] = [] !== $numericValues ? max($numericValues) : null; $result['average'] = [] !== $numericValues ? round(array_sum($numericValues) / count($numericValues), 2) : null; - $result['distribution'] = $this->buildOptionStats($distribution); + $result['distribution'] = $this->buildOptionStats($fullScale); + $result['distributionRows'] = $this->buildOptionStats($answeredRows); $result['chart'] = $this->buildRangeChartConfig($result['distribution']); break; @@ -402,6 +413,11 @@ final class SurveyResultsViewService $pointStyles = ['circle', 'rectRounded', 'triangle', 'star', 'rectRot', 'crossRot']; $palette = $this->buildChartPalette(count($distribution)); + $pointCount = count($distribution); + + // Bei großen Skalen (z. B. Alter 1–100) die Punkt-Marker verkleinern bzw. + // ausblenden, damit sich die Linie nicht in überlappenden Punkten verliert. + $pointRadius = $pointCount > 40 ? 0 : ($pointCount > 20 ? 3 : 7); return [ 'type' => 'line', @@ -414,10 +430,10 @@ final class SurveyResultsViewService 'backgroundColor' => 'rgba(236, 124, 50, 0.12)', 'pointBackgroundColor' => array_column($palette, 'border'), 'pointBorderColor' => '#ffffff', - 'pointBorderWidth' => 3, + 'pointBorderWidth' => $pointRadius > 0 ? 3 : 0, 'pointHoverBorderWidth' => 3, - 'pointRadius' => 7, - 'pointHoverRadius' => 9, + 'pointRadius' => $pointRadius, + 'pointHoverRadius' => $pointRadius > 0 ? $pointRadius + 2 : 4, 'pointStyle' => array_map( static fn (int $index): string => $pointStyles[$index % count($pointStyles)], array_keys($distribution) @@ -452,6 +468,11 @@ final class SurveyResultsViewService ], 'ticks' => [ 'color' => '#555E68', + // Achsenbeschriftung bei großen Skalen ausdünnen, damit + // sich die Zahlen nicht überlappen. + 'autoSkip' => true, + 'maxTicksLimit' => 12, + 'maxRotation' => 0, 'font' => [ 'family' => 'PT Sans Narrow', 'weight' => '600',