Refine range results and restore new-tab view links
Zweite Runde Kundenrückmeldung: - Range-Ergebnisse: Das Diagramm behält die komplette lineare Skala (rangeMin..rangeMax), aber die Werte-Liste/Tabelle zeigt nur noch die tatsächlich beantworteten Werte. Sonst würde eine große Skala (z. B. Alter 1-100, Schrittweite 1) die Ergebnisseite mit hunderten Nullzeilen fluten. Neues Feld distributionRows für die Liste; distribution bleibt fürs Diagramm. - Große Skalen: X-Achsenbeschriftung wird ausgedünnt (Chart.js autoSkip/ maxTicksLimit, SVG jede n-te Beschriftung), Punkt-Marker verkleinert bzw. ab >40 Punkten weggelassen. Auch die Y-Ticks im PDF-SVG werden gedeckelt. - PDF-Range-Tabelle ohne Farb-Swatch-Spalte (nur Wert/Anzahl/Anteil), da die Kopplung an ein Vollskalen-Diagramm nicht mehr sinnvoll ist. - Aktionslinks in der Frontend-Liste: Öffentliche Ansicht, Entwurfs-Ansicht und Ergebnisse anzeigen öffnen wieder im neuen Fenster (target=_blank); nur Bearbeiten bleibt im selben Tab. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+6
-2
@@ -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.
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if survey.publicUrl and survey.published %}
|
||||
<a class="survey-button secondary" href="{{ survey.publicUrl }}">{{ 'survey.list.public_view'|trans }}</a>
|
||||
<a class="survey-button secondary" href="{{ survey.publicUrl }}" target="_blank" rel="noreferrer">{{ 'survey.list.public_view'|trans }}</a>
|
||||
{% endif %}
|
||||
{% if survey.copyUrl and survey.published %}
|
||||
<button
|
||||
@@ -82,10 +82,10 @@
|
||||
>{{ 'survey.list.copy_link'|trans }}</button>
|
||||
{% endif %}
|
||||
{% if survey.draftUrl and not survey.published %}
|
||||
<a class="survey-button secondary" href="{{ survey.draftUrl }}">{{ 'survey.list.draft_view'|trans }}</a>
|
||||
<a class="survey-button secondary" href="{{ survey.draftUrl }}" target="_blank" rel="noreferrer">{{ 'survey.list.draft_view'|trans }}</a>
|
||||
{% endif %}
|
||||
{% if survey.resultsUrl %}
|
||||
<a class="survey-button secondary" href="{{ survey.resultsUrl }}">{{ 'survey.list.show_results'|trans }}</a>
|
||||
<a class="survey-button secondary" href="{{ survey.resultsUrl }}" target="_blank" rel="noreferrer">{{ 'survey.list.show_results'|trans }}</a>
|
||||
{% endif %}
|
||||
<form method="post" class="survey-question-action-form">
|
||||
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||
|
||||
@@ -93,11 +93,11 @@
|
||||
</div>
|
||||
|
||||
<div class="survey-results-list">
|
||||
{% for option in question.distribution %}
|
||||
{% for option in question.distributionRows|default([]) %}
|
||||
<div class="survey-results-row">
|
||||
<div class="survey-meta">
|
||||
<strong>{{ option.label|striptags|trim }}</strong>
|
||||
<span class="survey-badge neutral">{{ option.count }}</span>
|
||||
<span class="survey-badge neutral">{{ 'survey.results.answer_count'|trans({'%count%': option.count}) }}</span>
|
||||
<span class="survey-badge neutral">{{ option.percentage }}%</span>
|
||||
</div>
|
||||
<div class="survey-progress">
|
||||
|
||||
@@ -232,9 +232,6 @@
|
||||
{% if question.pdfChartDataUri %}
|
||||
<div class="chart-box chart-{{ question.pdfChartType ?: 'default' }}">
|
||||
<img src="{{ question.pdfChartDataUri }}" alt="Diagramm zu {{ question.question|striptags|trim }}">
|
||||
{% if question.type == 'range' and question.pdfChartLegend %}
|
||||
<div class="chart-hint">{{ 'survey.pdf.chart_hint'|trans }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -279,30 +276,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if question.distributionRows|default([]) %}
|
||||
<table class="stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="swatch-col">{{ 'survey.pdf.color'|trans }}</th>
|
||||
<th>{{ 'survey.pdf.value'|trans }}</th>
|
||||
<th class="numeric">{{ 'survey.pdf.count'|trans }}</th>
|
||||
<th class="numeric">{{ 'survey.pdf.share'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for option in question.pdfChartLegend %}
|
||||
{% for option in question.distributionRows %}
|
||||
<tr>
|
||||
<td><span class="swatch" style="background: {{ option.fillColor }}; border-color: {{ option.borderColor }};"></span></td>
|
||||
<td>{{ option.label|striptags|trim }}</td>
|
||||
<td class="numeric">{{ option.count }}</td>
|
||||
<td class="numeric">{{ option.percentage }}%</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">{{ 'survey.pdf.no_ratings'|trans }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>{{ 'survey.pdf.no_ratings'|trans }}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{# Eine Tabelle statt Doppel-Auflistung: jede Antwort einmal, mit
|
||||
exakter Anzahl statt Prozentwerten. #}
|
||||
|
||||
@@ -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('<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" stroke="#d7dee8" stroke-width="1" />', $paddingLeft, $y, $width - $paddingRight, $y);
|
||||
@@ -373,9 +382,15 @@ final class SurveyResultsPdfService
|
||||
$y = $paddingTop + $plotHeight - (($value / $maxValue) * $plotHeight);
|
||||
$polylinePoints[] = sprintf('%.2f,%.2f', $x, $y);
|
||||
$areaPolylinePoints[] = sprintf('%.2f,%.2f', $x, $y);
|
||||
$pointCircles[] = sprintf('<circle cx="%.2f" cy="%.2f" r="5.5" fill="%s" stroke="#ffffff" stroke-width="2" />', $x, $y, $this->escapeXml((string) ($pointColors[$index] ?? '#EC7C32')));
|
||||
|
||||
if ($pointRadius > 0.0) {
|
||||
$pointCircles[] = sprintf('<circle cx="%.2f" cy="%.2f" r="%.1f" fill="%s" stroke="#ffffff" stroke-width="2" />', $x, $y, $pointRadius, $this->escapeXml((string) ($pointColors[$index] ?? '#EC7C32')));
|
||||
}
|
||||
|
||||
if (0 === $index % $labelStep || $index === $lastIndex) {
|
||||
$xLabels[] = sprintf('<text x="%.2f" y="%.2f" text-anchor="middle" font-size="11" font-family="DejaVu Sans" fill="#555e68">%s</text>', $x, $height - 16, $this->escapeXml($labels[$index]));
|
||||
}
|
||||
}
|
||||
|
||||
$areaPoints = sprintf(
|
||||
'%.2f,%.2f %s %.2f,%.2f',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user