feature/app-theming-and-code-slides #2
7 changed files with 383 additions and 137 deletions
23
CHANGELOG.md
23
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -480,12 +480,19 @@ 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(
|
||||
'<text x="${cx + 6}" y="${y - 2}" font-size="11" '
|
||||
final y = (cy - radius) + (2 * radius) * (ticks - k) / ticks;
|
||||
b
|
||||
..write(
|
||||
'<line x1="$legendX" y1="$y" x2="${legendX + 8}" y2="$y" '
|
||||
'stroke="#cbd5e1" stroke-width="1"/>',
|
||||
)
|
||||
..write(
|
||||
'<text x="${legendX + 12}" y="${y + 4}" font-size="12" '
|
||||
'fill="#94a3b8">${_num(value)}</text>',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,27 +2898,25 @@ 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(
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: side,
|
||||
height: side,
|
||||
|
|
@ -2929,7 +2931,8 @@ class _ChartPreviewState extends State<_ChartPreview> {
|
|||
dataEntries: [
|
||||
for (var xi = 0; xi < spec.x.length; xi++)
|
||||
RadarEntry(
|
||||
value: xi < spec.series[si].data.length
|
||||
value:
|
||||
xi < spec.series[si].data.length
|
||||
? spec.series[si].data[xi]
|
||||
: 0,
|
||||
),
|
||||
|
|
@ -2942,15 +2945,19 @@ class _ChartPreviewState extends State<_ChartPreview> {
|
|||
spec.series[si],
|
||||
si,
|
||||
),
|
||||
borderWidth: w * (_hovered == si ? 0.0055 : 0.0035),
|
||||
entryRadius: w * (_hovered == si ? 0.006 : 0.004),
|
||||
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.
|
||||
// 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),
|
||||
RadarEntry(
|
||||
value: xi == 0 ? scale.hi : scale.lo,
|
||||
),
|
||||
],
|
||||
fillColor: Colors.transparent,
|
||||
borderColor: Colors.transparent,
|
||||
|
|
@ -2965,8 +2972,8 @@ class _ChartPreviewState extends State<_ChartPreview> {
|
|||
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.
|
||||
// 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,
|
||||
|
|
@ -2985,39 +2992,150 @@ class _ChartPreviewState extends State<_ChartPreview> {
|
|||
: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
radarTouchData: RadarTouchData(enabled: false),
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
// 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,
|
||||
if (_radarTouch != null)
|
||||
_radarTooltip(spec, side, _radarTouch!),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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.004,
|
||||
horizontal: w * 0.012,
|
||||
vertical: w * 0.006,
|
||||
),
|
||||
color: bg.withValues(alpha: 0.7),
|
||||
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: tickStyle,
|
||||
),
|
||||
),
|
||||
style: style,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<RadarChart>(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<RadarChart>(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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue