From dd54d36a602d2607a52cf221757879209e66d165 Mon Sep 17 00:00:00 2001 From: Brenno de Winter Date: Mon, 8 Jun 2026 14:04:47 +0200 Subject: [PATCH] Move radar scale to a side legend and add point tooltips - The radar/spider scale no longer clutters the figure: the evenly spaced tick values now sit in a slim legend beside the chart, both in the live preview and in the SVG/HTML export. - Hovering a radar point shows a tooltip (axis, series, value) like the other charts; the invisible scale-anchor dataset is ignored. - Refresh the documentation (README, user guide, file format, changelog) for all recent work: code-slide theming with custom hex colours, the spider/radar chart type, chart min/max, legend hover, and the chart tooltip behaviour. - Drop two redundant non-null assertions in the chart preview tests. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 23 +- README.md | 6 +- docs/FILE_FORMAT.md | 23 +- docs/USER_GUIDE.md | 32 ++- lib/services/marp_html_service.dart | 21 +- lib/widgets/slides/slide_preview.dart | 330 +++++++++++++++++--------- test/chart_preview_test.dart | 85 ++++++- 7 files changed, 383 insertions(+), 137 deletions(-) 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,