Code slides: - Theme code (broncode) background and text colours, with an optional syntax-colouring toggle. With it off the block renders monochrome, so a black background + bright green gives a classic CRT-screen look. - Colour pickers gained a custom hex entry so arbitrary colours (e.g. CRT green) can be set, not just presets. Exported HTML mirrors the code colours. Radar/spider charts: - Optional min/max now define the radar scale (centre/outer ring) instead of threshold lines. Evenly spaced, labelled tick rings are drawn in both the live preview and the SVG export so the scale is readable. A nice scale is derived from the data when no bounds are set. Line chart tooltips: - Detect the touched dot by true (x and y) distance instead of the x-only default, so the tooltip belongs to the point under the cursor. Overlapping dots all show, and the font shrinks a step when several stack. New UI strings are translated across all supported languages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
173 lines
5.6 KiB
Dart
173 lines
5.6 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:ocideck/models/chart.dart';
|
|
|
|
void main() {
|
|
test('chart palette starts with the EU flag colors', () {
|
|
expect(chartColorPalette.take(2), ['#003399', '#FFCC00']);
|
|
});
|
|
|
|
group('parseCsv', () {
|
|
test('reads header series names and labelled rows', () {
|
|
final (x, series) = parseCsv('\n, 2025, 2026\nQ1, 10, 12\nQ2, 14, 9\n');
|
|
expect(x, ['Q1', 'Q2']);
|
|
expect(series.map((s) => s.name), ['2025', '2026']);
|
|
expect(series[0].data, [10, 14]);
|
|
expect(series[1].data, [12, 9]);
|
|
});
|
|
|
|
test('non-numeric cells become 0', () {
|
|
final (x, series) = parseCsv(',A\nQ1,oops');
|
|
expect(x, ['Q1']);
|
|
expect(series.single.data, [0]);
|
|
});
|
|
});
|
|
|
|
group('ChartSpec', () {
|
|
test('round-trips inline data through the block JSON', () {
|
|
const spec = ChartSpec(
|
|
type: ChartType.line,
|
|
title: 'Omzet',
|
|
x: ['Q1', 'Q2'],
|
|
rowColors: ['#003399', '#FFCC00'],
|
|
series: [
|
|
ChartSeries(name: '2025', data: [10, 14], color: '#EF4444'),
|
|
],
|
|
);
|
|
final back = ChartSpec.parse(spec.toBlock());
|
|
expect(back.type, ChartType.line);
|
|
expect(back.title, 'Omzet');
|
|
expect(back.x, ['Q1', 'Q2']);
|
|
expect(back.rowColors, ['#003399', '#FFCC00']);
|
|
expect(back.series.single.name, '2025');
|
|
expect(back.series.single.data, [10, 14]);
|
|
expect(back.series.single.color, '#EF4444');
|
|
expect(back.hasInlineData, isTrue);
|
|
});
|
|
|
|
test('storage form drops inline data when a source is linked', () {
|
|
const spec = ChartSpec(
|
|
type: ChartType.bar,
|
|
title: 'Omzet',
|
|
source: 'data/omzet.csv',
|
|
x: ['Q1', 'Q2'],
|
|
rowColors: ['#003399', '#FFCC00'],
|
|
series: [
|
|
ChartSeries(name: '2025', data: [10, 14], color: '#10B981'),
|
|
],
|
|
);
|
|
final stored = ChartSpec.parse(spec.toBlock(forStorage: true));
|
|
expect(stored.source, 'data/omzet.csv');
|
|
expect(stored.hasInlineData, isFalse);
|
|
expect(stored.rowColors, ['#003399', '#FFCC00']);
|
|
expect(stored.series.single.color, '#10B981');
|
|
|
|
// The in-app/full form keeps the data.
|
|
final full = ChartSpec.parse(spec.toBlock());
|
|
expect(full.hasInlineData, isTrue);
|
|
});
|
|
|
|
test('withCsv fills x/series and keeps the source', () {
|
|
const spec = ChartSpec(
|
|
type: ChartType.bar,
|
|
source: 'data/o.csv',
|
|
rowColors: ['#003399', '#FFCC00'],
|
|
series: [ChartSeries(name: 'oud', data: [], color: '#10B981')],
|
|
);
|
|
final filled = spec.withCsv(',A,B\nJan,1,2\nFeb,3,4');
|
|
expect(filled.source, 'data/o.csv');
|
|
expect(filled.x, ['Jan', 'Feb']);
|
|
expect(filled.series.map((s) => s.name), ['A', 'B']);
|
|
expect(filled.series[1].data, [2, 4]);
|
|
expect(filled.series[0].color, '#10B981');
|
|
expect(filled.series[1].color, isNull);
|
|
expect(filled.rowColors, ['#003399', '#FFCC00']);
|
|
});
|
|
|
|
test('invalid colors are ignored while valid colors are normalized', () {
|
|
final valid = ChartSeries.fromJson({
|
|
'name': 'A',
|
|
'data': [1],
|
|
'color': 'ef4444',
|
|
});
|
|
final invalid = ChartSeries.fromJson({
|
|
'name': 'B',
|
|
'data': [2],
|
|
'color': 'red',
|
|
});
|
|
expect(valid.color, '#EF4444');
|
|
expect(invalid.color, isNull);
|
|
});
|
|
|
|
test('parse is tolerant of malformed JSON', () {
|
|
final spec = ChartSpec.parse('{ not json');
|
|
expect(spec.type, ChartType.bar);
|
|
expect(spec.hasInlineData, isFalse);
|
|
});
|
|
|
|
test('round-trips optional min/max bound lines for bar/line', () {
|
|
const spec = ChartSpec(
|
|
type: ChartType.line,
|
|
x: ['Q1'],
|
|
series: [ChartSeries(name: 'A', data: [10])],
|
|
minBound: 5,
|
|
maxBound: 20,
|
|
);
|
|
final back = ChartSpec.parse(spec.toBlock());
|
|
expect(back.minBound, 5);
|
|
expect(back.maxBound, 20);
|
|
});
|
|
|
|
test('bounds are dropped from a pie chart', () {
|
|
const spec = ChartSpec(
|
|
type: ChartType.pie,
|
|
x: ['Q1'],
|
|
series: [ChartSeries(name: 'A', data: [10])],
|
|
minBound: 5,
|
|
maxBound: 20,
|
|
);
|
|
expect(spec.supportsBounds, isFalse);
|
|
final back = ChartSpec.parse(spec.toBlock());
|
|
expect(back.minBound, isNull);
|
|
expect(back.maxBound, isNull);
|
|
});
|
|
|
|
test('round-trips a spider/radar chart type', () {
|
|
const spec = ChartSpec(
|
|
type: ChartType.radar,
|
|
x: ['Snelheid', 'Kracht', 'Uithouding'],
|
|
series: [
|
|
ChartSeries(name: 'A', data: [3, 4, 5]),
|
|
],
|
|
);
|
|
final back = ChartSpec.parse(spec.toBlock());
|
|
expect(back.type, ChartType.radar);
|
|
expect(back.x, ['Snelheid', 'Kracht', 'Uithouding']);
|
|
expect(back.series.single.data, [3, 4, 5]);
|
|
});
|
|
|
|
test('radar keeps bounds as a scale but never draws bound lines', () {
|
|
const spec = ChartSpec(
|
|
type: ChartType.radar,
|
|
x: ['A', 'B', 'C'],
|
|
series: [ChartSeries(name: 'A', data: [1, 2, 3])],
|
|
minBound: 1,
|
|
maxBound: 5,
|
|
);
|
|
expect(spec.supportsBounds, isTrue);
|
|
expect(spec.supportsBoundLines, isFalse);
|
|
final back = ChartSpec.parse(spec.toBlock());
|
|
expect(back.minBound, 1);
|
|
expect(back.maxBound, 5);
|
|
});
|
|
|
|
test('bar/line draw bound lines but pie does not', () {
|
|
const bar = ChartSpec(type: ChartType.bar);
|
|
const line = ChartSpec(type: ChartType.line);
|
|
const pie = ChartSpec(type: ChartType.pie);
|
|
expect(bar.supportsBoundLines, isTrue);
|
|
expect(line.supportsBoundLines, isTrue);
|
|
expect(pie.supportsBoundLines, isFalse);
|
|
expect(pie.supportsBounds, isFalse);
|
|
});
|
|
});
|
|
}
|