didvan-app/lib/views/widgets/mini_chart.dart

265 lines
7.6 KiB
Dart

// ignore_for_file: deprecated_member_use
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:didvan/models/statistic_data/data.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
class MiniChart extends StatefulWidget {
final String label;
final double width;
final double height;
final Color? lineColor;
final double? changePercent;
final String? trend;
/// سری قیمت آماده برای رسم. اگر داده شود، نمودار مستقیما از این استفاده
/// می‌کند و هیچ fetchی از API انجام نمی‌شود (market-service جدید).
final List<double>? prices;
const MiniChart({
super.key,
required this.label,
this.width = 100,
this.height = 40,
this.lineColor,
this.changePercent,
this.trend,
this.prices,
});
@override
State<MiniChart> createState() => _MiniChartState();
}
class _MiniChartState extends State<MiniChart> {
List<Data> chartData = [];
bool isLoading = true;
bool hasError = false;
@override
void initState() {
super.initState();
// اگر prices از قبل داده شده، بدون fetch نمودار را می‌سازیم.
if (widget.prices != null && widget.prices!.isNotEmpty) {
chartData = widget.prices!
.map((v) => Data(
p: v.toString(),
h: '',
l: '',
d: '',
dp: 0,
dt: '',
t: '',
tEn: '',
tG: '',
ts: '',
))
.toList();
isLoading = false;
hasError = false;
} else {
_loadChartData();
}
}
Future<void> _loadChartData() async {
try {
final service = RequestService(
RequestHelper.statisticDetails(widget.label, 'weekly'),
);
await service.httpGet();
if (service.isSuccess) {
final result = service.result['data'];
final List<Data> data = [];
for (var i = 0; i < result.length; i++) {
data.add(Data.fromList(result[i]));
}
if (mounted) {
setState(() {
chartData = data.reversed.take(10).toList();
isLoading = false;
hasError = false;
});
}
} else {
if (mounted) {
setState(() {
isLoading = false;
hasError = true;
});
}
}
} catch (e) {
if (mounted) {
setState(() {
isLoading = false;
hasError = true;
});
}
}
}
double _stringToDouble(String value) {
try {
return double.parse(value.replaceAll(',', ''));
} catch (e) {
return 0.0;
}
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return SizedBox(
width: widget.width,
height: widget.height,
child: const Center(
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}
if (hasError || chartData.isEmpty) {
return SizedBox(
width: widget.width,
height: widget.height,
child: Container(
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: const Center(
child: Icon(
Icons.show_chart,
size: 16,
color: Colors.grey,
),
),
),
);
}
// The x-axis must run oldest → newest (left → right). If the incoming
// series happens to be in the opposite order, reverse it so the visual
// slope agrees with `changePercent` (the canonical direction from the
// API). Compare the endpoints' delta against the sign of `changePercent`.
List<Data> seriesLtr = chartData;
if (widget.changePercent != null &&
widget.changePercent != 0 &&
chartData.length > 1) {
final firstVal = _stringToDouble(chartData.first.p);
final lastVal = _stringToDouble(chartData.last.p);
final visualDelta = lastVal - firstVal;
final apiUp = widget.changePercent! > 0;
final visualUp = visualDelta > 0;
if (visualDelta != 0 && apiUp != visualUp) {
seriesLtr = chartData.reversed.toList();
}
}
final spots = <FlSpot>[];
double minY = double.infinity;
double maxY = double.negativeInfinity;
for (int i = 0; i < seriesLtr.length; i++) {
final value = _stringToDouble(seriesLtr[i].p);
spots.add(FlSpot(i.toDouble(), value));
if (value < minY) minY = value;
if (value > maxY) maxY = value;
}
final range = maxY - minY;
final absoluteValue = maxY.abs();
final padding = range == 0
? (absoluteValue == 0 ? 1.0 : absoluteValue * 0.01)
: range * 0.1;
minY -= padding;
maxY += padding;
// Determine trend (upward vs downward)
bool isPositive = false;
bool isNegative = false;
if (spots.length > 1) {
final double firstVal = spots.first.y;
final double lastVal = spots.last.y;
if (lastVal > firstVal) {
isPositive = true;
} else if (lastVal < firstVal) {
isNegative = true;
}
}
if (!isPositive && !isNegative) {
if (widget.changePercent != null && widget.changePercent != 0) {
if (widget.changePercent! > 0) isPositive = true;
if (widget.changePercent! < 0) isNegative = true;
}
if (widget.trend == 'high') isPositive = true;
if (widget.trend == 'low') isNegative = true;
}
// Green for upward (صعودی), Red for downward (نزولی)
final Color trendColor = isPositive
? const Color(0xff19a974)
: (isNegative
? const Color(0xffe53935)
: Theme.of(context).colorScheme.primary);
final Color effectiveLineColor = widget.lineColor ?? trendColor;
return SizedBox(
width: widget.width,
height: widget.height,
// Time-series charts must always run from oldest (left) to newest
// (right). The app's RTL Directionality otherwise mirrors the X axis.
child: Directionality(
textDirection: TextDirection.ltr,
child: LineChart(
LineChartData(
lineTouchData: const LineTouchData(enabled: false),
gridData: const FlGridData(show: false),
titlesData: const FlTitlesData(show: false),
borderData: FlBorderData(show: false),
minX: 0,
maxX: (chartData.length - 1).toDouble(),
minY: minY,
maxY: maxY,
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: false,
color: effectiveLineColor,
barWidth: 1.5,
isStrokeCapRound: false,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
effectiveLineColor.withOpacity(0.35),
const Color.fromARGB(10, 255, 255, 255),
],
stops: const [0.4, 1.0],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
aboveBarData: BarAreaData(show: false),
),
],
),
),
),
);
}
}