|
@@ -49,7 +49,11 @@ class _StatisticalChartsPageState extends State<StatisticalChartsPage> {
|
|
|
|
|
|
|
|
void _addRow({String initialLabel = '新数据', String initialValue = '0'}) {
|
|
void _addRow({String initialLabel = '新数据', String initialValue = '0'}) {
|
|
|
final n = _rowCount + 1;
|
|
final n = _rowCount + 1;
|
|
|
- final label = ToolField('第 $n 项标签', hint: '数据名', keyboard: TextInputType.text);
|
|
|
|
|
|
|
+ final label = ToolField(
|
|
|
|
|
+ '第 $n 项标签',
|
|
|
|
|
+ hint: '数据名',
|
|
|
|
|
+ keyboard: TextInputType.text,
|
|
|
|
|
+ );
|
|
|
final value = ToolField('第 $n 项数值', hint: '例如 10');
|
|
final value = ToolField('第 $n 项数值', hint: '例如 10');
|
|
|
label.controller.text = initialLabel;
|
|
label.controller.text = initialLabel;
|
|
|
value.controller.text = initialValue;
|
|
value.controller.text = initialValue;
|
|
@@ -57,9 +61,9 @@ class _StatisticalChartsPageState extends State<StatisticalChartsPage> {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void _onAddRow() => setState(() {
|
|
void _onAddRow() => setState(() {
|
|
|
- _addRow();
|
|
|
|
|
- _runToken++;
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ _addRow();
|
|
|
|
|
+ _runToken++;
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
void _onRemoveRow() {
|
|
void _onRemoveRow() {
|
|
|
if (_rowCount <= 1) return;
|
|
if (_rowCount <= 1) return;
|
|
@@ -86,105 +90,104 @@ class _StatisticalChartsPageState extends State<StatisticalChartsPage> {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
Widget _intro() => Column(
|
|
Widget _intro() => Column(
|
|
|
- crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ const SizedBox(height: 8),
|
|
|
|
|
+ Wrap(
|
|
|
|
|
+ spacing: 8,
|
|
|
|
|
+ runSpacing: 8,
|
|
|
children: [
|
|
children: [
|
|
|
- const Text('图表类型'),
|
|
|
|
|
- const SizedBox(height: 8),
|
|
|
|
|
- Wrap(
|
|
|
|
|
- spacing: 8,
|
|
|
|
|
- runSpacing: 8,
|
|
|
|
|
- children: [
|
|
|
|
|
- for (var i = 0; i < _chartTypes.length; i++)
|
|
|
|
|
- ChoiceChip(
|
|
|
|
|
- label: Text(_chartTypes[i]),
|
|
|
|
|
- selected: _type == i,
|
|
|
|
|
- onSelected: (_) => _selectType(i),
|
|
|
|
|
- ),
|
|
|
|
|
- ],
|
|
|
|
|
- ),
|
|
|
|
|
- if (_type == 1) ...[
|
|
|
|
|
- const SizedBox(height: 12),
|
|
|
|
|
- Row(
|
|
|
|
|
- children: [
|
|
|
|
|
- const Text('曲线类型:'),
|
|
|
|
|
- const SizedBox(width: 8),
|
|
|
|
|
- ChoiceChip(
|
|
|
|
|
- label: const Text('直线'),
|
|
|
|
|
- selected: !_curved,
|
|
|
|
|
- onSelected: (_) => _setCurved(false),
|
|
|
|
|
- ),
|
|
|
|
|
- const SizedBox(width: 8),
|
|
|
|
|
- ChoiceChip(
|
|
|
|
|
- label: const Text('光滑曲线'),
|
|
|
|
|
- selected: _curved,
|
|
|
|
|
- onSelected: (_) => _setCurved(true),
|
|
|
|
|
- ),
|
|
|
|
|
- ],
|
|
|
|
|
|
|
+ for (var i = 0; i < _chartTypes.length; i++)
|
|
|
|
|
+ ChoiceChip(
|
|
|
|
|
+ label: Text(_chartTypes[i]),
|
|
|
|
|
+ selected: _type == i,
|
|
|
|
|
+ onSelected: (_) => _selectType(i),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ if (_type == 1) ...[
|
|
|
|
|
+ const SizedBox(height: 12),
|
|
|
|
|
+ Row(
|
|
|
|
|
+ children: [
|
|
|
|
|
+ const Text('曲线类型:'),
|
|
|
|
|
+ const SizedBox(width: 8),
|
|
|
|
|
+ ChoiceChip(
|
|
|
|
|
+ label: const Text('直线'),
|
|
|
|
|
+ selected: !_curved,
|
|
|
|
|
+ onSelected: (_) => _setCurved(false),
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(width: 8),
|
|
|
|
|
+ ChoiceChip(
|
|
|
|
|
+ label: const Text('光滑曲线'),
|
|
|
|
|
+ selected: _curved,
|
|
|
|
|
+ onSelected: (_) => _setCurved(true),
|
|
|
),
|
|
),
|
|
|
],
|
|
],
|
|
|
- const SizedBox(height: 12),
|
|
|
|
|
- Row(
|
|
|
|
|
- children: [
|
|
|
|
|
- OutlinedButton.icon(
|
|
|
|
|
- onPressed: _onAddRow,
|
|
|
|
|
- icon: const Icon(Icons.add),
|
|
|
|
|
- label: const Text('添加数据'),
|
|
|
|
|
- ),
|
|
|
|
|
- const SizedBox(width: 12),
|
|
|
|
|
- OutlinedButton.icon(
|
|
|
|
|
- onPressed: _rowCount > 1 ? _onRemoveRow : null,
|
|
|
|
|
- icon: const Icon(Icons.remove),
|
|
|
|
|
- label: const Text('删除数据'),
|
|
|
|
|
- ),
|
|
|
|
|
- ],
|
|
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ const SizedBox(height: 12),
|
|
|
|
|
+ Row(
|
|
|
|
|
+ children: [
|
|
|
|
|
+ OutlinedButton.icon(
|
|
|
|
|
+ onPressed: _onAddRow,
|
|
|
|
|
+ icon: const Icon(Icons.add),
|
|
|
|
|
+ label: const Text('添加数据'),
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(width: 12),
|
|
|
|
|
+ OutlinedButton.icon(
|
|
|
|
|
+ onPressed: _rowCount > 1 ? _onRemoveRow : null,
|
|
|
|
|
+ icon: const Icon(Icons.remove),
|
|
|
|
|
+ label: const Text('删除数据'),
|
|
|
),
|
|
),
|
|
|
],
|
|
],
|
|
|
- );
|
|
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
@override
|
|
@override
|
|
|
Widget build(BuildContext context) => ToolScaffold(
|
|
Widget build(BuildContext context) => ToolScaffold(
|
|
|
- key: ValueKey<int>(_runToken),
|
|
|
|
|
- title: '统计图表绘制',
|
|
|
|
|
- autoRun: true,
|
|
|
|
|
- fields: _fields,
|
|
|
|
|
- intro: _intro(),
|
|
|
|
|
- compute: (session) async {
|
|
|
|
|
- // 把输入行读成 (标签, 数值)。这里没有数学计算,只是把输入变成坐标,
|
|
|
|
|
- // 所以解析留在 Dart 侧;非数字就抛中文异常。
|
|
|
|
|
- final labels = <String>[];
|
|
|
|
|
- final values = <double>[];
|
|
|
|
|
- for (var i = 0; i < _rowCount; i++) {
|
|
|
|
|
- final label = _labelAt(i).text;
|
|
|
|
|
- final raw = _valueAt(i).text;
|
|
|
|
|
- if (label.isEmpty && raw.isEmpty) continue;
|
|
|
|
|
- if (raw.isEmpty) throw Exception('第 ${i + 1} 行的数值不能为空');
|
|
|
|
|
- final v = double.tryParse(raw);
|
|
|
|
|
- if (v == null || !v.isFinite) {
|
|
|
|
|
- throw Exception('第 ${i + 1} 行的「$raw」不是有效数字');
|
|
|
|
|
- }
|
|
|
|
|
- values.add(v);
|
|
|
|
|
- labels.add(label.isEmpty ? '第${i + 1}项' : label);
|
|
|
|
|
- }
|
|
|
|
|
- if (values.isEmpty) {
|
|
|
|
|
- throw Exception('请至少填写一行有效数据');
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ key: ValueKey<int>(_runToken),
|
|
|
|
|
+ title: '统计图表绘制',
|
|
|
|
|
+ autoRun: true,
|
|
|
|
|
+ fields: _fields,
|
|
|
|
|
+ intro: _intro(),
|
|
|
|
|
+ compute: (session) async {
|
|
|
|
|
+ // 把输入行读成 (标签, 数值)。这里没有数学计算,只是把输入变成坐标,
|
|
|
|
|
+ // 所以解析留在 Dart 侧;非数字就抛中文异常。
|
|
|
|
|
+ final labels = <String>[];
|
|
|
|
|
+ final values = <double>[];
|
|
|
|
|
+ for (var i = 0; i < _rowCount; i++) {
|
|
|
|
|
+ final label = _labelAt(i).text;
|
|
|
|
|
+ final raw = _valueAt(i).text;
|
|
|
|
|
+ if (label.isEmpty && raw.isEmpty) continue;
|
|
|
|
|
+ if (raw.isEmpty) throw Exception('第 ${i + 1} 行的数值不能为空');
|
|
|
|
|
+ final v = double.tryParse(raw);
|
|
|
|
|
+ if (v == null || !v.isFinite) {
|
|
|
|
|
+ throw Exception('第 ${i + 1} 行的「$raw」不是有效数字');
|
|
|
|
|
+ }
|
|
|
|
|
+ values.add(v);
|
|
|
|
|
+ labels.add(label.isEmpty ? '第${i + 1}项' : label);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (values.isEmpty) {
|
|
|
|
|
+ throw Exception('请至少填写一行有效数据');
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- final chart = switch (_type) {
|
|
|
|
|
- 1 => _lineChart(values, labels),
|
|
|
|
|
- 2 => _pieChart(values),
|
|
|
|
|
- 3 => _radarChart(values, labels),
|
|
|
|
|
- _ => _barChart(values, labels),
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ final chart = switch (_type) {
|
|
|
|
|
+ 1 => _lineChart(values, labels),
|
|
|
|
|
+ 2 => _pieChart(values),
|
|
|
|
|
+ 3 => _radarChart(values, labels),
|
|
|
|
|
+ _ => _barChart(values, labels),
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- return <ToolOutput>[
|
|
|
|
|
- ToolChart(
|
|
|
|
|
- _chartTypes[_type],
|
|
|
|
|
- // fl_chart 需要一个确定高度,否则在无界的高度里会报错。
|
|
|
|
|
- SizedBox(height: 320, child: chart),
|
|
|
|
|
- ),
|
|
|
|
|
- ];
|
|
|
|
|
- },
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ return <ToolOutput>[
|
|
|
|
|
+ ToolChart(
|
|
|
|
|
+ _chartTypes[_type],
|
|
|
|
|
+ // fl_chart 需要一个确定高度,否则在无界的高度里会报错。
|
|
|
|
|
+ SizedBox(height: 320, child: chart),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ];
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
/// 坐标轴的"好看"上限:最大值再留 20% 余量;全零或全负时给个 1,免得轴退化。
|
|
/// 坐标轴的"好看"上限:最大值再留 20% 余量;全零或全负时给个 1,免得轴退化。
|
|
|
static double _niceMax(List<double> values) {
|
|
static double _niceMax(List<double> values) {
|
|
@@ -196,97 +199,97 @@ class _StatisticalChartsPageState extends State<StatisticalChartsPage> {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
static SideTitles _bottomTitles(List<String> labels) => SideTitles(
|
|
static SideTitles _bottomTitles(List<String> labels) => SideTitles(
|
|
|
- showTitles: true,
|
|
|
|
|
- getTitlesWidget: (value, meta) {
|
|
|
|
|
- final index = value.toInt();
|
|
|
|
|
- if (index < 0 || index >= labels.length) {
|
|
|
|
|
- return const SizedBox.shrink();
|
|
|
|
|
- }
|
|
|
|
|
- return Padding(
|
|
|
|
|
- padding: const EdgeInsets.only(top: 8),
|
|
|
|
|
- child: Text(
|
|
|
|
|
- labels[index],
|
|
|
|
|
- style: const TextStyle(fontSize: 10),
|
|
|
|
|
- maxLines: 2,
|
|
|
|
|
- overflow: TextOverflow.ellipsis,
|
|
|
|
|
- ),
|
|
|
|
|
- );
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ showTitles: true,
|
|
|
|
|
+ getTitlesWidget: (value, meta) {
|
|
|
|
|
+ final index = value.toInt();
|
|
|
|
|
+ if (index < 0 || index >= labels.length) {
|
|
|
|
|
+ return const SizedBox.shrink();
|
|
|
|
|
+ }
|
|
|
|
|
+ return Padding(
|
|
|
|
|
+ padding: const EdgeInsets.only(top: 8),
|
|
|
|
|
+ child: Text(
|
|
|
|
|
+ labels[index],
|
|
|
|
|
+ style: const TextStyle(fontSize: 10),
|
|
|
|
|
+ maxLines: 2,
|
|
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
|
|
+ ),
|
|
|
);
|
|
);
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
Widget _barChart(List<double> values, List<String> labels) => BarChart(
|
|
Widget _barChart(List<double> values, List<String> labels) => BarChart(
|
|
|
- BarChartData(
|
|
|
|
|
- alignment: BarChartAlignment.spaceAround,
|
|
|
|
|
- maxY: _niceMax(values),
|
|
|
|
|
- barTouchData: BarTouchData(
|
|
|
|
|
- enabled: true,
|
|
|
|
|
- touchTooltipData: const BarTouchTooltipData(),
|
|
|
|
|
- ),
|
|
|
|
|
- titlesData: FlTitlesData(
|
|
|
|
|
- show: true,
|
|
|
|
|
- bottomTitles: AxisTitles(sideTitles: _bottomTitles(labels)),
|
|
|
|
|
- leftTitles: const AxisTitles(
|
|
|
|
|
- sideTitles: SideTitles(showTitles: true, reservedSize: 40),
|
|
|
|
|
- ),
|
|
|
|
|
- ),
|
|
|
|
|
- gridData: const FlGridData(show: true),
|
|
|
|
|
- borderData: FlBorderData(show: true),
|
|
|
|
|
- barGroups: <BarChartGroupData>[
|
|
|
|
|
- for (var i = 0; i < values.length; i++)
|
|
|
|
|
- BarChartGroupData(
|
|
|
|
|
- x: i,
|
|
|
|
|
- barRods: [
|
|
|
|
|
- BarChartRodData(
|
|
|
|
|
- toY: values[i],
|
|
|
|
|
- color: Colors.primaries[i % Colors.primaries.length],
|
|
|
|
|
- width: 16,
|
|
|
|
|
- ),
|
|
|
|
|
- ],
|
|
|
|
|
- ),
|
|
|
|
|
- ],
|
|
|
|
|
|
|
+ BarChartData(
|
|
|
|
|
+ alignment: BarChartAlignment.spaceAround,
|
|
|
|
|
+ maxY: _niceMax(values),
|
|
|
|
|
+ barTouchData: BarTouchData(
|
|
|
|
|
+ enabled: true,
|
|
|
|
|
+ touchTooltipData: const BarTouchTooltipData(),
|
|
|
|
|
+ ),
|
|
|
|
|
+ titlesData: FlTitlesData(
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ bottomTitles: AxisTitles(sideTitles: _bottomTitles(labels)),
|
|
|
|
|
+ leftTitles: const AxisTitles(
|
|
|
|
|
+ sideTitles: SideTitles(showTitles: true, reservedSize: 40),
|
|
|
),
|
|
),
|
|
|
- );
|
|
|
|
|
|
|
+ ),
|
|
|
|
|
+ gridData: const FlGridData(show: true),
|
|
|
|
|
+ borderData: FlBorderData(show: true),
|
|
|
|
|
+ barGroups: <BarChartGroupData>[
|
|
|
|
|
+ for (var i = 0; i < values.length; i++)
|
|
|
|
|
+ BarChartGroupData(
|
|
|
|
|
+ x: i,
|
|
|
|
|
+ barRods: [
|
|
|
|
|
+ BarChartRodData(
|
|
|
|
|
+ toY: values[i],
|
|
|
|
|
+ color: Colors.primaries[i % Colors.primaries.length],
|
|
|
|
|
+ width: 16,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
Widget _lineChart(List<double> values, List<String> labels) => LineChart(
|
|
Widget _lineChart(List<double> values, List<String> labels) => LineChart(
|
|
|
- LineChartData(
|
|
|
|
|
- lineTouchData: const LineTouchData(enabled: true),
|
|
|
|
|
- gridData: const FlGridData(show: true),
|
|
|
|
|
- titlesData: FlTitlesData(
|
|
|
|
|
|
|
+ LineChartData(
|
|
|
|
|
+ lineTouchData: const LineTouchData(enabled: true),
|
|
|
|
|
+ gridData: const FlGridData(show: true),
|
|
|
|
|
+ titlesData: FlTitlesData(
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ bottomTitles: AxisTitles(sideTitles: _bottomTitles(labels)),
|
|
|
|
|
+ leftTitles: const AxisTitles(
|
|
|
|
|
+ sideTitles: SideTitles(showTitles: true, reservedSize: 40),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ borderData: FlBorderData(show: true),
|
|
|
|
|
+ minX: 0,
|
|
|
|
|
+ maxX: values.length > 1 ? (values.length - 1).toDouble() : 1,
|
|
|
|
|
+ minY: 0,
|
|
|
|
|
+ maxY: _niceMax(values),
|
|
|
|
|
+ lineBarsData: <LineChartBarData>[
|
|
|
|
|
+ LineChartBarData(
|
|
|
|
|
+ spots: <FlSpot>[
|
|
|
|
|
+ for (var i = 0; i < values.length; i++)
|
|
|
|
|
+ FlSpot(i.toDouble(), values[i]),
|
|
|
|
|
+ ],
|
|
|
|
|
+ isCurved: _curved,
|
|
|
|
|
+ color: Colors.blue,
|
|
|
|
|
+ barWidth: 4,
|
|
|
|
|
+ belowBarData: BarAreaData(show: false),
|
|
|
|
|
+ dotData: FlDotData(
|
|
|
show: true,
|
|
show: true,
|
|
|
- bottomTitles: AxisTitles(sideTitles: _bottomTitles(labels)),
|
|
|
|
|
- leftTitles: const AxisTitles(
|
|
|
|
|
- sideTitles: SideTitles(showTitles: true, reservedSize: 40),
|
|
|
|
|
- ),
|
|
|
|
|
- ),
|
|
|
|
|
- borderData: FlBorderData(show: true),
|
|
|
|
|
- minX: 0,
|
|
|
|
|
- maxX: values.length > 1 ? (values.length - 1).toDouble() : 1,
|
|
|
|
|
- minY: 0,
|
|
|
|
|
- maxY: _niceMax(values),
|
|
|
|
|
- lineBarsData: <LineChartBarData>[
|
|
|
|
|
- LineChartBarData(
|
|
|
|
|
- spots: <FlSpot>[
|
|
|
|
|
- for (var i = 0; i < values.length; i++)
|
|
|
|
|
- FlSpot(i.toDouble(), values[i]),
|
|
|
|
|
- ],
|
|
|
|
|
- isCurved: _curved,
|
|
|
|
|
- color: Colors.blue,
|
|
|
|
|
- barWidth: 4,
|
|
|
|
|
- belowBarData: BarAreaData(show: false),
|
|
|
|
|
- dotData: FlDotData(
|
|
|
|
|
- show: true,
|
|
|
|
|
- getDotPainter: (spot, percent, barData, index) =>
|
|
|
|
|
- FlDotCirclePainter(
|
|
|
|
|
|
|
+ getDotPainter: (spot, percent, barData, index) =>
|
|
|
|
|
+ FlDotCirclePainter(
|
|
|
radius: 4,
|
|
radius: 4,
|
|
|
color: Colors.blue,
|
|
color: Colors.blue,
|
|
|
strokeWidth: 2,
|
|
strokeWidth: 2,
|
|
|
strokeColor: Colors.white,
|
|
strokeColor: Colors.white,
|
|
|
),
|
|
),
|
|
|
- ),
|
|
|
|
|
- ),
|
|
|
|
|
- ],
|
|
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
- );
|
|
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
Widget _pieChart(List<double> values) {
|
|
Widget _pieChart(List<double> values) {
|
|
|
var total = 0.0;
|
|
var total = 0.0;
|