diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78e888c..50c1990 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,13 +8,22 @@ and the project aims to follow [Semantic Versioning](https://semver.org/).
## [Unreleased]
### Added
-- **Source-code slides** — a dark "code sheet" with per-language syntax
- highlighting, stored as a fenced code block.
-- **Charts** — bar, line, and pie chart slides. Data is entered in an in-app grid
- or imported from CSV; the spec is stored as JSON in a ```chart block. Data can
- stay inline or be linked to a CSV in a separate `data/` directory. Rendered
- natively in-app (preview, presenter, PDF, PPTX) and as self-contained SVG in
- the HTML export.
+- **Source-code slides** — a "code sheet" with per-language syntax highlighting,
+ stored as a fenced code block. Background and text colours are part of the style
+ profile, with a syntax-colouring toggle; turning it off renders the block in a
+ single colour (e.g. green on black for a CRT-terminal look).
+- **Charts** — bar, line, pie, and **spider/radar** chart slides. Data is entered
+ in an in-app grid or imported from CSV; the spec is stored as JSON in a ```chart
+ block. Data can stay inline or be linked to a CSV in a separate `data/`
+ directory. Rendered natively in-app (preview, presenter, PDF, PPTX) and as
+ self-contained SVG in the HTML export.
+ - Optional **min/max**: horizontal reference lines on bar/line charts, or a
+ fixed scale on spider/radar charts shown as a small legend beside the figure.
+ - **Legend hover** highlights the matching series (or pie slice). Line-chart
+ tooltips attach to the dot under the cursor (showing every overlapping dot),
+ and spider/radar points show a tooltip on hover too.
+- **Custom theme colours** — every style-profile colour can be entered as a custom
+ hex value in addition to the presets.
- **Per-slide TLP classification** — each slide can carry its own Traffic Light
Protocol level; slides classified stricter than the level the deck is shown at
are withheld when presenting and exporting.
diff --git a/README.md b/README.md
index b64ea77..4c694b9 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,8 @@ Built with Flutter for macOS, Windows, and Linux.
## Features
- **Structured slide editors** — dedicated editors per slide type: title, bullets, two-column bullets, bullets + image, single/two images, quote, table, section divider, image-only, video, audio, source code, charts, and free-form Markdown.
-- **Source-code slides** — a dark "code sheet" with syntax highlighting per language, stored as a fenced code block.
-- **Charts** — bar, line, and pie charts rendered natively (preview, presenter, PDF, PPTX) and as self-contained SVG in the HTML export. Data is entered in an in-app grid or imported from CSV; the spec is stored as JSON in the Markdown, with optional linking to a CSV kept in a tidy `data/` directory.
+- **Source-code slides** — a "code sheet" with per-language syntax highlighting, stored as a fenced code block. Background and text colours come from the style profile, with an optional syntax-colouring toggle (turn it off for a single-colour, CRT-terminal look).
+- **Charts** — bar, line, pie, and spider/radar charts rendered natively (preview, presenter, PDF, PPTX) and as self-contained SVG in the HTML export. Data is entered in an in-app grid or imported from CSV; the spec is stored as JSON in the Markdown, with optional linking to a CSV kept in a tidy `data/` directory. Optional min/max draw reference lines (bar/line) or fix the scale (radar); legend hover highlights a series, and line tooltips pick the dot under the cursor.
- **Live preview** — see each slide rendered as you edit, with inline Markdown, footers, and TLP (Traffic Light Protocol) marking. Free-Markdown slides render fenced code with syntax highlighting and `$…$` / `$$…$$` LaTeX math.
- **Traffic Light Protocol** — a deck-wide classification plus an optional **per-slide TLP level**; slides classified stricter than the level the deck is shown at are automatically withheld, both when presenting and exporting.
- **Fullscreen presenter** — keyboard-driven navigation, presenter view, blank screen, auto-advance, and a slide-grid overview.
@@ -20,7 +20,7 @@ Built with Flutter for macOS, Windows, and Linux.
- **Import / export** — round-trips Marp Markdown, imports existing slides, and exports to PDF, PPTX (with speaker notes), and a self-contained offline HTML deck (code highlighting, math, charts, and mermaid diagrams render in the browser). Decks are saved as a self-contained package with copied assets.
- **Productivity** — find & replace, slide finder, undo/redo, skip-slide state, multi-select with bulk copy-to-another-deck / delete / skip, and tabbed multi-deck editing. `Ctrl/Cmd+O` opens, `Ctrl/Cmd+S` saves.
- **Crash recovery** — automatic snapshots so work survives an unexpected exit.
-- **Theming** — customizable deck style profiles and app appearance (including a dark interface), a bundled Marp CSS theme (`assets/themes/ocideck.css`), and a bundled EB Garamond font (no network fetch).
+- **Theming** — customizable deck style profiles (deck and source-code colours via presets or custom hex, fonts, logo, footer) and app appearance (including a dark interface), a bundled Marp CSS theme (`assets/themes/ocideck.css`), and a bundled EB Garamond font (no network fetch).
- **Localized** — Dutch, English, Italian, German, French, Spanish, Frisian, and Papiamento.
## Requirements
diff --git a/docs/FILE_FORMAT.md b/docs/FILE_FORMAT.md
index f21c7c7..92de35d 100644
--- a/docs/FILE_FORMAT.md
+++ b/docs/FILE_FORMAT.md
@@ -135,6 +135,9 @@ JSON heeft deze velden (met standaardwaarden):
| `titleBackgroundColor` | `#1C2B47` | Achtergrond titelslide. |
| `titleTextColor` | `#FFFFFF` | Tekst op titel-/sectieslide. |
| `sectionBackgroundColor` | `#2E7D64` | Achtergrond sectieslide. |
+| `codeBackgroundColor` | `#282C34` | Achtergrond van broncode-slides. |
+| `codeTextColor` | `#ABB2BF` | Tekstkleur van broncode-slides. |
+| `codeHighlightSyntax` | `true` | Syntaxkleuring aan/uit. Uit = alles in één kleur (bijv. groen op zwart voor een CRT-look). |
| `logoPath` | `null` | Pad naar logo (relatief in `logos/`). |
| `logoPosition` | `bottom-right` | `top-left`/`top-right`/`bottom-left`/`bottom-right`. |
| `logoSize` | `96` | Logogrootte in px. |
@@ -314,15 +317,31 @@ openen wordt die weer ingelezen.
````markdown
```chart
{
- "type": "bar", // bar | line | pie
+ "type": "bar", // bar | line | pie | radar
"title": "Omzet",
"source": "data/omzet.csv", // optioneel; anders inline x/series
"x": ["Q1", "Q2"],
- "series": [ { "name": "2025", "data": [10, 14] } ]
+ "rowColors": ["#003399", "#FFCC00"], // optioneel; kleur per label (cirkel/radar)
+ "minBound": 0, // optioneel; niet bij pie
+ "maxBound": 20, // optioneel; niet bij pie
+ "series": [ { "name": "2025", "data": [10, 14], "color": "#2563EB" } ]
}
```
````
+Velden:
+
+- `type` — `bar`, `line`, `pie` of `radar` (spider). Standaard `bar`.
+- `x` — labels; bij `pie`/`radar` zijn dit de segmenten/assen (radar heeft er
+ minstens drie nodig).
+- `series` — genoemde reeksen met `data` (uitgelijnd op `x`) en optioneel een
+ `color` (hex). `pie` toont maximaal de eerste twee reeksen.
+- `rowColors` — optionele kleur per label (gebruikt door `pie`/`radar`).
+- `minBound` / `maxBound` — optioneel en alleen voor niet-`pie`. Bij `bar`/`line`
+ zijn het horizontale **referentielijnen**; bij `radar` bepalen ze de **schaal**
+ (binnenste/buitenste ring) met een gelijkmatige verdeling. Worden weggelaten
+ bij `pie`.
+
### Afbeeldingsgrootte (`imageSize`)
Eén integer-veld met typeafhankelijke betekenis: bij `image`/`title`/`quote` het
achtergrond-percentage (`![bg N%]`), bij `split` de paneelbreedte (geklemd
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
index fe65c35..f49dc48 100644
--- a/docs/USER_GUIDE.md
+++ b/docs/USER_GUIDE.md
@@ -20,8 +20,9 @@ Marp tools.
Add a slide and pick a type: **title**, **section** divider, **bullets**, **two
bullet columns**, **bullets + image**, **two images**, **large image**, **video**,
-**audio**, **quote**, **table**, **source code**, **chart**, and **free Markdown**.
-Each type has a dedicated editor on the left and a live preview on the right.
+**audio**, **quote**, **table**, **source code**, **chart** (bar, line, pie, or
+spider/radar), and **free Markdown**. Each type has a dedicated editor on the left
+and a live preview on the right.
Text fields support inline Markdown (`**bold**`, `*italic*`, `` `code` ``,
`[links](…)`). Free-Markdown slides also render fenced code with syntax
@@ -30,19 +31,30 @@ highlighting and `$…$` / `$$…$$` LaTeX math.
### Source-code slides
Choose a programming language for syntax highlighting (or "plain text") and paste
-your code. It renders as a dark "code sheet". Stored as a fenced code block in the
-Markdown.
+your code. It renders as a "code sheet" whose background and text colour come from
+the active **style profile**. Turn **syntax colouring** off to show the whole block
+in a single colour — e.g. bright green on black for a classic CRT-terminal look.
+Stored as a fenced code block in the Markdown.
### Charts
-Pick a type (**bar**, **line**, **pie**) and a title, then enter data in the grid:
-the first column is the labels, each further column is a named series. Use **Row**
-and **Series** to add data; the small ✕ removes a row/column.
+Pick a type (**bar**, **line**, **pie**, or **spider/radar**) and a title, then
+enter data in the grid: the first column is the labels, each further column is a
+named series. Use **Row** and **Series** to add data; the small ✕ removes a
+row/column. Each series and (for pie/radar) each label can be given its own colour.
- **CSV import** — click **CSV importeren**. You can either keep the data **in the
slide** (inline) or store it **as a CSV file**. A linked CSV lives in the deck's
`data/` directory and stays the source of truth (edit it in a spreadsheet); the
grid then shows it read-only until you **Ontkoppelen** (unlink).
+- **Min/max** (optional, bar/line/radar) — on bar and line charts these draw
+ horizontal **reference lines**; on a spider/radar chart they fix the **scale**
+ (centre to outer ring), shown as evenly spaced values in a small legend beside
+ the chart. Leave them empty to scale automatically.
+- **Reading values** — hovering a legend entry highlights its series (or pie
+ slice). On a line chart the tooltip belongs to the dot under the cursor and
+ shows every overlapping dot at once; on a spider/radar chart hovering a point
+ shows its value in a tooltip too.
- Charts render in the preview, presenter, PDF, and PPTX, and as inline SVG in the
HTML export.
@@ -100,8 +112,10 @@ Export to:
## Theming and language
-- **Style profiles** control deck colours, fonts, logo, and footer. The bundled
- Marp theme is `assets/themes/ocideck.css`.
+- **Style profiles** control deck colours (including the source-code background and
+ text, with an optional syntax-colouring toggle), fonts, logo, and footer. Every
+ colour can be picked from the presets or entered as a custom hex value. The
+ bundled Marp theme is `assets/themes/ocideck.css`.
- **App appearance** (including a dark interface) is configurable in settings.
- The interface is available in Dutch, English, Italian, German, French, Spanish,
Frisian, and Papiamento.
diff --git a/lib/services/marp_html_service.dart b/lib/services/marp_html_service.dart
index 25508c8..712132a 100644
--- a/lib/services/marp_html_service.dart
+++ b/lib/services/marp_html_service.dart
@@ -480,14 +480,21 @@ class MarpHtmlService {
);
}
- // Scale labels up the top spoke: lo at the centre, hi at the outer ring.
- for (var k = 0; k <= ticks; k++) {
+ // Scale legend on the right: hi at the top down to lo at the bottom, so the
+ // figure itself stays clean.
+ final legendX = cx + radius + 30;
+ for (var k = ticks; k >= 0; k--) {
final value = lo + span * k / ticks;
- final y = cy - radius * k / ticks;
- b.write(
- '${_num(value)}',
- );
+ final y = (cy - radius) + (2 * radius) * (ticks - k) / ticks;
+ b
+ ..write(
+ '',
+ )
+ ..write(
+ '${_num(value)}',
+ );
}
// Spokes and axis labels.
diff --git a/lib/widgets/slides/slide_preview.dart b/lib/widgets/slides/slide_preview.dart
index 3810af3..d7342b9 100644
--- a/lib/widgets/slides/slide_preview.dart
+++ b/lib/widgets/slides/slide_preview.dart
@@ -2212,6 +2212,10 @@ class _ChartPreviewState extends State<_ChartPreview> {
/// slice (category) index for pie charts. Null when nothing is hovered.
int? _hovered;
+ /// The radar vertex under the pointer, used to draw its tooltip. Null when not
+ /// hovering a point.
+ ({int series, int entry, double value, Offset offset})? _radarTouch;
+
void _setHover(int? index) {
if (_hovered != index) setState(() => _hovered = index);
}
@@ -2894,133 +2898,247 @@ class _ChartPreviewState extends State<_ChartPreview> {
}
final grid = textColor.withValues(alpha: 0.18);
final scale = radarScale(spec);
- final bg = _hexColor(profile.slideBackgroundColor);
return Padding(
- padding: EdgeInsets.symmetric(horizontal: w * 0.06, vertical: w * 0.012),
+ padding: EdgeInsets.symmetric(horizontal: w * 0.03, vertical: w * 0.012),
child: LayoutBuilder(
builder: (context, constraints) {
- // A square keeps fl_chart's centre/radius predictable so the tick
- // labels we overlay line up exactly with its grid rings.
- final side = math.min(constraints.maxWidth, constraints.maxHeight);
- final centerOffset = side / 2;
- final radius = centerOffset * 0.8; // matches RadarChartPainter
- final tickStyle = _applyFont(
- font,
- TextStyle(
- fontSize: w * 0.012 * _labelScale,
- color: textColor.withValues(alpha: 0.6),
- fontWeight: FontWeight.w600,
- ),
+ // Reserve a slim column on the right for the scale legend, then keep
+ // the chart square so fl_chart's centre/radius stay predictable.
+ final legendWidth = w * 0.075;
+ final available = constraints.maxWidth - legendWidth - w * 0.02;
+ final side = math.max(
+ 0.0,
+ math.min(available, constraints.maxHeight),
);
- return Center(
- child: SizedBox(
- width: side,
- height: side,
- child: Stack(
- children: [
- Positioned.fill(
- child: RadarChart(
- RadarChartData(
- dataSets: [
- for (var si = 0; si < spec.series.length; si++)
- RadarDataSet(
- dataEntries: [
- for (var xi = 0; xi < spec.x.length; xi++)
- RadarEntry(
- value: xi < spec.series[si].data.length
- ? spec.series[si].data[xi]
- : 0,
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Expanded(
+ child: Center(
+ child: SizedBox(
+ width: side,
+ height: side,
+ child: Stack(
+ children: [
+ Positioned.fill(
+ child: RadarChart(
+ RadarChartData(
+ dataSets: [
+ for (var si = 0; si < spec.series.length; si++)
+ RadarDataSet(
+ dataEntries: [
+ for (var xi = 0; xi < spec.x.length; xi++)
+ RadarEntry(
+ value:
+ xi < spec.series[si].data.length
+ ? spec.series[si].data[xi]
+ : 0,
+ ),
+ ],
+ fillColor: _seriesDisplayColor(
+ spec.series[si],
+ si,
+ ).withValues(alpha: _dimmed(si) ? 0.04 : 0.16),
+ borderColor: _seriesDisplayColor(
+ spec.series[si],
+ si,
+ ),
+ borderWidth:
+ w * (_hovered == si ? 0.0055 : 0.0035),
+ entryRadius:
+ w * (_hovered == si ? 0.006 : 0.004),
),
+ // Invisible anchor pinning the scale to [lo, hi]
+ // so the rings represent a fixed scale.
+ RadarDataSet(
+ dataEntries: [
+ for (var xi = 0; xi < spec.x.length; xi++)
+ RadarEntry(
+ value: xi == 0 ? scale.hi : scale.lo,
+ ),
+ ],
+ fillColor: Colors.transparent,
+ borderColor: Colors.transparent,
+ borderWidth: 0,
+ entryRadius: 0,
+ ),
],
- fillColor: _seriesDisplayColor(
- spec.series[si],
- si,
- ).withValues(alpha: _dimmed(si) ? 0.04 : 0.16),
- borderColor: _seriesDisplayColor(
- spec.series[si],
- si,
+ radarShape: RadarShape.polygon,
+ radarBackgroundColor: Colors.transparent,
+ radarBorderData: BorderSide(color: grid, width: 1),
+ gridBorderData: BorderSide(color: grid, width: 1),
+ tickBorderData: BorderSide(color: grid, width: 1),
+ tickCount: scale.ticks,
+ isMinValueAtCenter: true,
+ // The scale now lives in a side legend, so hide
+ // fl_chart's in-chart ring numbers.
+ ticksTextStyle: const TextStyle(
+ color: Colors.transparent,
+ fontSize: 0.001,
+ ),
+ titlePositionPercentageOffset: 0.14,
+ getTitle: (index, angle) => RadarChartTitle(
+ text: index < spec.x.length ? spec.x[index] : '',
+ ),
+ titleTextStyle: _applyFont(
+ font,
+ TextStyle(
+ fontSize: w * 0.0135 * _labelScale,
+ color: textColor.withValues(alpha: 0.88),
+ fontWeight: presentationMode
+ ? FontWeight.w600
+ : FontWeight.w500,
+ ),
+ ),
+ radarTouchData: RadarTouchData(
+ enabled: true,
+ touchSpotThreshold: (w * 0.02).clamp(8.0, 24.0)
+ .toDouble(),
+ mouseCursorResolver: (event, response) =>
+ _radarSpotFrom(response, spec) == null
+ ? SystemMouseCursors.basic
+ : SystemMouseCursors.click,
+ touchCallback: (event, response) {
+ final next =
+ event.isInterestedForInteractions
+ ? _radarSpotFrom(response, spec)
+ : null;
+ if (next != _radarTouch) {
+ setState(() => _radarTouch = next);
+ }
+ },
),
- borderWidth: w * (_hovered == si ? 0.0055 : 0.0035),
- entryRadius: w * (_hovered == si ? 0.006 : 0.004),
),
- // Invisible anchor pinning the scale to [lo, hi] so the
- // rings — and our labels — represent a fixed scale.
- RadarDataSet(
- dataEntries: [
- for (var xi = 0; xi < spec.x.length; xi++)
- RadarEntry(value: xi == 0 ? scale.hi : scale.lo),
- ],
- fillColor: Colors.transparent,
- borderColor: Colors.transparent,
- borderWidth: 0,
- entryRadius: 0,
- ),
- ],
- radarShape: RadarShape.polygon,
- radarBackgroundColor: Colors.transparent,
- radarBorderData: BorderSide(color: grid, width: 1),
- gridBorderData: BorderSide(color: grid, width: 1),
- tickBorderData: BorderSide(color: grid, width: 1),
- tickCount: scale.ticks,
- isMinValueAtCenter: true,
- // Hide fl_chart's own ring numbers; we draw labelled
- // ticks ourselves so any min/max scale reads correctly.
- ticksTextStyle: const TextStyle(
- color: Colors.transparent,
- fontSize: 0.001,
- ),
- titlePositionPercentageOffset: 0.14,
- getTitle: (index, angle) => RadarChartTitle(
- text: index < spec.x.length ? spec.x[index] : '',
- ),
- titleTextStyle: _applyFont(
- font,
- TextStyle(
- fontSize: w * 0.0135 * _labelScale,
- color: textColor.withValues(alpha: 0.88),
- fontWeight: presentationMode
- ? FontWeight.w600
- : FontWeight.w500,
+ duration: Duration.zero,
),
),
- radarTouchData: RadarTouchData(enabled: false),
- ),
- duration: Duration.zero,
+ if (_radarTouch != null)
+ _radarTooltip(spec, side, _radarTouch!),
+ ],
),
),
- // Evenly spaced scale labels up the top spoke: lo at centre,
- // hi at the outer ring, with equal steps between.
- for (var k = 0; k <= scale.ticks; k++)
- Positioned(
- left: centerOffset + w * 0.006,
- top:
- centerOffset -
- radius * k / scale.ticks -
- w * 0.01 * _labelScale,
- child: IgnorePointer(
- child: Container(
- padding: EdgeInsets.symmetric(
- horizontal: w * 0.004,
- ),
- color: bg.withValues(alpha: 0.7),
- child: Text(
- _fmtNum(scale.lo + (scale.hi - scale.lo) * k / scale.ticks),
- style: tickStyle,
- ),
- ),
- ),
- ),
- ],
+ ),
),
- ),
+ SizedBox(
+ width: legendWidth,
+ child: _radarScaleLegend(scale, textColor),
+ ),
+ ],
);
},
),
);
}
+ /// Extract the touched real-series vertex from a radar touch response,
+ /// ignoring the invisible scale anchor dataset.
+ ({int series, int entry, double value, Offset offset})? _radarSpotFrom(
+ RadarTouchResponse? response,
+ ChartSpec spec,
+ ) {
+ final spot = response?.touchedSpot;
+ if (spot == null) return null;
+ if (spot.touchedDataSetIndex < 0 ||
+ spot.touchedDataSetIndex >= spec.series.length) {
+ return null; // the anchor dataset, or out of range
+ }
+ return (
+ series: spot.touchedDataSetIndex,
+ entry: spot.touchedRadarEntryIndex,
+ value: spot.touchedRadarEntry.value,
+ offset: spot.offset,
+ );
+ }
+
+ /// A small floating tooltip for the hovered radar vertex, like the other
+ /// charts: the axis label, the series name and the value.
+ Widget _radarTooltip(
+ ChartSpec spec,
+ double side,
+ ({int series, int entry, double value, Offset offset}) touch,
+ ) {
+ final axis = touch.entry >= 0 && touch.entry < spec.x.length
+ ? spec.x[touch.entry]
+ : '';
+ final series = touch.series < spec.series.length
+ ? spec.series[touch.series].name
+ : '';
+ final label = series.isEmpty ? 'Reeks ${touch.series + 1}' : series;
+ final onLeftHalf = touch.offset.dx <= side / 2;
+ return Positioned(
+ left: onLeftHalf ? (touch.offset.dx + w * 0.012) : null,
+ right: onLeftHalf ? null : (side - touch.offset.dx + w * 0.012),
+ top: (touch.offset.dy - w * 0.03).clamp(0.0, math.max(0.0, side - 1)),
+ child: IgnorePointer(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(maxWidth: side * 0.6),
+ child: Container(
+ padding: EdgeInsets.symmetric(
+ horizontal: w * 0.012,
+ vertical: w * 0.006,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xFF0F172A),
+ borderRadius: BorderRadius.circular(w * 0.008),
+ boxShadow: const [
+ BoxShadow(color: Color(0x33000000), blurRadius: 6),
+ ],
+ ),
+ child: Text(
+ '${axis.isEmpty ? '' : '$axis\n'}$label: ${_fmtNum(touch.value)}',
+ maxLines: 3,
+ overflow: TextOverflow.ellipsis,
+ style: _tooltipStyle(),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ /// Vertical scale legend shown to the right of a radar chart: the tick values
+ /// from the outer ring (top) down to the centre (bottom), in a small font.
+ Widget _radarScaleLegend(
+ ({double lo, double hi, int ticks}) scale,
+ Color textColor,
+ ) {
+ final style = _applyFont(
+ font,
+ TextStyle(
+ fontSize: w * 0.012 * _labelScale,
+ color: textColor.withValues(alpha: 0.62),
+ fontWeight: FontWeight.w600,
+ ),
+ );
+ final tickColor = textColor.withValues(alpha: 0.3);
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ for (var k = scale.ticks; k >= 0; k--) ...[
+ if (k != scale.ticks) SizedBox(height: w * 0.018 * _labelScale),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(width: w * 0.012, height: 1, color: tickColor),
+ SizedBox(width: w * 0.006),
+ Flexible(
+ child: Text(
+ _fmtNum(scale.lo + (scale.hi - scale.lo) * k / scale.ticks),
+ style: style,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ],
+ );
+ }
+
/// Resolves the radar scale: a low/high pair plus an even tick count. Honours
/// the optional [ChartSpec.minBound]/[maxBound] and otherwise rounds the data
/// range to a tidy scale so the rings read as round numbers.
diff --git a/test/chart_preview_test.dart b/test/chart_preview_test.dart
index ec6a0c4..e6eef49 100644
--- a/test/chart_preview_test.dart
+++ b/test/chart_preview_test.dart
@@ -189,8 +189,8 @@ void main() {
// A crowded stack uses a smaller font than a single tooltip.
final single = touch.touchTooltipData.getTooltipItems([spots.first]);
expect(
- items[0]!.textStyle!.fontSize!,
- lessThan(single.single!.textStyle!.fontSize!),
+ items[0]!.textStyle.fontSize!,
+ lessThan(single.single!.textStyle.fontSize!),
);
});
@@ -324,12 +324,91 @@ void main() {
final anchor = radar.data.dataSets.last.dataEntries.map((e) => e.value);
expect(anchor.reduce((a, b) => a < b ? a : b), 0);
expect(anchor.reduce((a, b) => a > b ? a : b), 10);
- // Evenly spaced scale labels are drawn (0..10).
+ // The scale is shown in the side legend (0..10), not painted in the chart.
+ expect(radar.data.ticksTextStyle?.color, Colors.transparent);
expect(find.text('0'), findsWidgets);
expect(find.text('10'), findsOneWidget);
expect(tester.takeException(), isNull);
});
+ testWidgets('radar shows a tooltip for the hovered point', (tester) async {
+ const spec = ChartSpec(
+ type: ChartType.radar,
+ x: ['Snelheid', 'Kracht', 'Uithouding'],
+ series: [
+ ChartSeries(name: 'Alpha', data: [3, 4, 5]),
+ ChartSeries(name: 'Beta', data: [5, 2, 3]),
+ ],
+ );
+
+ await tester.pumpWidget(_host(spec));
+ await tester.pump();
+
+ final radar = tester.widget(find.byType(RadarChart));
+ final touch = radar.data.radarTouchData;
+ expect(touch.enabled, isTrue);
+
+ // Simulate hovering the second axis of the Beta series.
+ final dataSet = radar.data.dataSets[1];
+ final entry = dataSet.dataEntries[1];
+ touch.touchCallback!(
+ const FlPointerHoverEvent(PointerHoverEvent()),
+ RadarTouchResponse(
+ touchLocation: const Offset(20, 20),
+ touchedSpot: RadarTouchedSpot(
+ dataSet,
+ 1,
+ entry,
+ 1,
+ const FlSpot(1, 2),
+ const Offset(20, 20),
+ ),
+ ),
+ );
+ await tester.pump();
+
+ expect(find.text('Kracht\nBeta: 2'), findsOneWidget);
+ expect(tester.takeException(), isNull);
+ });
+
+ testWidgets('radar ignores touches on the invisible scale anchor', (
+ tester,
+ ) async {
+ const spec = ChartSpec(
+ type: ChartType.radar,
+ x: ['A', 'B', 'C'],
+ series: [
+ ChartSeries(name: 'Alpha', data: [3, 4, 5]),
+ ],
+ );
+
+ await tester.pumpWidget(_host(spec));
+ await tester.pump();
+
+ final radar = tester.widget(find.byType(RadarChart));
+ final anchorIndex = radar.data.dataSets.length - 1; // the anchor dataset
+ final anchor = radar.data.dataSets[anchorIndex];
+ radar.data.radarTouchData.touchCallback!(
+ const FlPointerHoverEvent(PointerHoverEvent()),
+ RadarTouchResponse(
+ touchLocation: Offset.zero,
+ touchedSpot: RadarTouchedSpot(
+ anchor,
+ anchorIndex,
+ anchor.dataEntries.first,
+ 0,
+ const FlSpot(0, 0),
+ Offset.zero,
+ ),
+ ),
+ );
+ await tester.pump();
+
+ // No tooltip for the anchor: only the legend value "5" exists.
+ expect(find.textContaining(': '), findsNothing);
+ expect(tester.takeException(), isNull);
+ });
+
testWidgets('radar chart asks for at least three labels', (tester) async {
const spec = ChartSpec(
type: ChartType.radar,