Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 65 additions & 28 deletions lib/chart/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,8 @@ class ChartState extends WindowState {
SeriesData? seriesData = DataCenter().getSeriesData(seriesInfo.id);
if (seriesData == null) {
// Data not loaded yet, skip this series silently
developer.log("Data not yet loaded for series '${seriesInfo.name}'", name: "rubin_chart.workspace");
developer.log("Data not yet loaded for series '${seriesInfo.name}'",
name: "rubintv.chart.workspace");
continue;
}

Expand All @@ -315,11 +316,11 @@ class ChartState extends WindowState {
} catch (e) {
if (e is DataConversionException) {
developer.log("Failed to convert series '${seriesInfo.name}': ${e.message}",
name: "rubin_chart.workspace");
name: "rubintv.chart.workspace");
reportError("Chart rendering error: ${e.message}");
} else {
developer.log("Unexpected error converting series '${seriesInfo.name}': $e",
name: "rubin_chart.workspace", error: e);
name: "rubintv.chart.workspace", error: e);
}
// Skip this series and continue with others
}
Expand All @@ -344,19 +345,28 @@ class ChartState extends WindowState {
/// Create a [ChartState] from a JSON object.
@override
factory ChartState.fromJson(Map<String, dynamic> json) {
return ChartState(
id: UniqueId.fromString(json["id"]),
series: Map.fromEntries((json["series"] as List<dynamic>).map((e) {
try {
Map<SeriesId, SeriesInfo> series = Map.fromEntries((json["series"] as List<dynamic>).map((e) {
SeriesInfo seriesInfo = SeriesInfo.fromJson(e);
return MapEntry(seriesInfo.id, seriesInfo);
})),
axisInfo: List<ChartAxisInfo>.from(json["axisInfo"].map((e) => ChartAxisInfo.fromJson(e))),
legend: json["legend"] == null ? null : Legend.fromJson(json["legend"]),
useGlobalQuery: json["useGlobalQuery"],
windowType: WindowTypes.fromString(json["windowType"]),
tool: MultiSelectionTool.fromString(json["tool"]),
resetController: StreamController<ResetChartAction>.broadcast(),
);
}));

return ChartState(
id: UniqueId.fromString(json["id"]),
series: series,
axisInfo: List<ChartAxisInfo>.from(json["axisInfo"].map((e) => ChartAxisInfo.fromJson(e))),
legend: json["legend"] == null ? null : Legend.fromJson(json["legend"]),
useGlobalQuery: json["useGlobalQuery"],
windowType: WindowTypes.fromString(json["windowType"]),
tool: MultiSelectionTool.fromString(json["tool"]),
resetController: StreamController<ResetChartAction>.broadcast(),
);
} catch (e, stackTrace) {
developer.log("Error in ChartState.fromJson: $e",
name: "rubintv.chart.base", error: e, stackTrace: stackTrace);
developer.log("Full JSON: $json", name: "rubintv.chart.base");
rethrow;
}
}
}

Expand Down Expand Up @@ -404,8 +414,30 @@ class ChartBloc extends WindowBloc<ChartState> {
add(ChartReceiveMessageEvent(message));
});

/// Subscribe to selection controller to update when points are selected.
developer.log("Subscribing chart ${state.id} to selection controller", name: "rubintv.chart.base");
ControlCenter().selectionController.subscribe(state.id, (Object? origin, Set<Object> dataPoints) {
if (origin == state.id) {
return;
}
developer.log("Processing selection update for chart ${state.id}", name: "rubintv.chart.base");
// TODO: Handle selection update in chart
});

/// Subscribe to drill down controller.
developer.log("Subscribing chart ${state.id} to drill down controller", name: "rubintv.chart.base");
ControlCenter().drillDownController.subscribe(state.id, (Object? origin, Set<Object> dataPoints) {
if (origin == state.id) {
return;
}
developer.log("Processing drill down update for chart ${state.id}", name: "rubintv.chart.base");
// TODO: Handle drill down update in chart
});

/// Reload the data if the global query or global dayObs changes.
_globalQuerySubscription = ControlCenter().globalQueryStream.listen((GlobalQuery? query) {
developer.log("Chart ${state.id} received global query update: dayObs=${query?.dayObs}",
name: "rubintv.chart.base");
for (SeriesInfo series in state._series.values) {
add(UpdateSeriesEvent(
series: series,
Expand All @@ -415,6 +447,8 @@ class ChartBloc extends WindowBloc<ChartState> {
}
});

developer.log("Chart bloc created and subscribed to streams", name: "rubintv.chart.base");

/// Change the selection tool.
on<UpdateMultiSelect>((event, emit) {
emit(state.copyWith(tool: event.tool));
Expand Down Expand Up @@ -513,25 +547,25 @@ class ChartBloc extends WindowBloc<ChartState> {
int columns = event.message["content"]["data"].length;
developer.log(
"received $columns columns and $rows rows for ${event.message["requestId"]} in series $seriesId",
name: "rubin_chart.workspace");
name: "rubintv.chart.workspace");

// Add validation to ensure series exists and data is not empty
final seriesInfo = state.series[seriesId];
if (seriesInfo == null) {
developer.log("Series $seriesId not found in state", name: "rubin_chart.workspace");
developer.log("Series $seriesId not found in state", name: "rubintv.chart.workspace");
return;
}

final data = event.message["content"]["data"] as Map<String, dynamic>?;
if (data == null || data.isEmpty) {
developer.log("Received empty data for series $seriesId", name: "rubin_chart.workspace");
developer.log("Received empty data for series $seriesId", name: "rubintv.chart.workspace");
return;
}

// Validate that all expected columns are present
final plotColumns = List<String>.from(event.message["content"]["columns"].map((e) => e));
if (plotColumns.isEmpty) {
developer.log("No plot columns received for series $seriesId", name: "rubin_chart.workspace");
developer.log("No plot columns received for series $seriesId", name: "rubintv.chart.workspace");
return;
}

Expand Down Expand Up @@ -566,12 +600,12 @@ class ChartBloc extends WindowBloc<ChartState> {
));
}
} else {
developer.log("Could not find pending series $seriesId", name: "rubin_chart.workspace");
developer.log("Pending series: ${_pendingRowCountChecks.keys}", name: "rubin_chart.workspace");
developer.log("Could not find pending series $seriesId", name: "rubintv.chart.workspace");
developer.log("Pending series: ${_pendingRowCountChecks.keys}", name: "rubintv.chart.workspace");
}
} catch (e) {
developer.log("Error processing count message: $e", name: "rubin_chart.workspace");
developer.log("Message content: ${event.message["content"]}", name: "rubin_chart.workspace");
developer.log("Error processing count message: $e", name: "rubintv.chart.workspace");
developer.log("Message content: ${event.message["content"]}", name: "rubintv.chart.workspace");
}
}
emit(state.copyWith());
Expand Down Expand Up @@ -633,6 +667,7 @@ class ChartBloc extends WindowBloc<ChartState> {
on<SynchDataEvent>((event, emit) {
// When loading from a file, we don't want to trigger the global query update unnecessarily
if (!event.skipGlobalUpdate && event.globalQuery != null) {
developer.log("Updating global query in ControlCenter", name: "rubintv.chart.base");
// Update the global query in the control center which will notify all charts
ControlCenter().updateGlobalQuery(GlobalQuery(
query: event.globalQuery,
Expand All @@ -643,14 +678,14 @@ class ChartBloc extends WindowBloc<ChartState> {
}

for (SeriesInfo series in state._series.values) {
developer.log("Synching data for series ${series.id}", name: "rubintv.chart.base.dart");
developer.log("Synching data for series ${series.id}", name: "rubintv.chart.base");
_fetchSeriesData(
series: series,
globalQuery: event.globalQuery,
dayObs: event.dayObs,
);
developer.log("synch request sent", name: "rubintv.chart.base.dart");
}
developer.log("Data sync requests sent for ${state._series.length} series", name: "rubintv.chart.base");
});

on<DeleteSeriesEvent>((event, emit) {
Expand Down Expand Up @@ -725,7 +760,7 @@ class ChartBloc extends WindowBloc<ChartState> {
on<ConfirmRowCountEvent>((event, emit) {
if (state.pendingRowCountDialog != null) {
developer.log("User confirmed row count dialog, proceeding with series",
name: "rubin_chart.workspace");
name: "rubintv.chart.workspace");

final dialogInfo = state.pendingRowCountDialog!;

Expand All @@ -744,7 +779,7 @@ class ChartBloc extends WindowBloc<ChartState> {
/// Handle user canceling the row count dialog
on<CancelRowCountEvent>((event, emit) {
if (state.pendingRowCountDialog != null) {
developer.log("User canceled row count dialog", name: "rubin_chart.workspace");
developer.log("User canceled row count dialog", name: "rubintv.chart.workspace");

// Simply clear the dialog without proceeding
emit(state.copyWith(pendingRowCountDialog: null));
Expand Down Expand Up @@ -785,7 +820,7 @@ class ChartBloc extends WindowBloc<ChartState> {

// Check that the series has the correct number of columns and axes
if (series.fields.length != state.axisInfo.length) {
developer.log("bad axes", name: "rubin_chart.core.chart.dart");
developer.log("bad axes", name: "rubintv.core.chart.dart");
return null;
}

Expand Down Expand Up @@ -876,7 +911,7 @@ class ChartBloc extends WindowBloc<ChartState> {
if (workspace.info?.instrument?.schema == null) {
throw ArgumentError("The chosen instrument has no schema");
}
developer.log("New series fields: ${series.fields}", name: "rubin_chart.core.chart.dart");
developer.log("New series fields: ${series.fields}", name: "rubintv.core.chart.dart");
return showDialog(
context: context,
builder: (BuildContext context) => Dialog(
Expand Down Expand Up @@ -1008,6 +1043,8 @@ class ChartBloc extends WindowBloc<ChartState> {

@override
Future<void> close() async {
ControlCenter().selectionController.unsubscribe(state.id);
ControlCenter().drillDownController.unsubscribe(state.id);
await _subscription.cancel();
await _globalQuerySubscription.cancel();
return super.close();
Expand Down
79 changes: 66 additions & 13 deletions lib/chart/series.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
///
/// You should have received a copy of the GNU General Public License
/// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:developer' as developer;

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
Expand Down Expand Up @@ -161,24 +162,76 @@ class SeriesInfo {
"marker": marker?.toJson(),
"errorBars": errorBars?.toJson(),
"axes": axes.map((e) => e.toJson()).toList(),
"fields": fields.entries.map((entry) => [entry.key.toJson(), entry.value.toJson()]).toList(),
"fields": Map<String, dynamic>.fromEntries(
fields.entries.map((entry) => MapEntry(entry.key.toJson(), entry.value.toJson()))),
"query": query?.toJson(),
};
}

/// Create a [SeriesInfo] from a JSON object.
static SeriesInfo fromJson(Map<String, dynamic> json) {
return SeriesInfo(
id: SeriesId.fromString(json["id"]),
name: json["name"],
marker: json["marker"] == null ? null : Marker.fromJson(json["marker"]),
errorBars: json["errorBars"] == null ? null : ErrorBars.fromJson(json["errorBars"]),
axes: (json["axes"] as List).map((e) => AxisId.fromJson(e)).toList(),
fields: Map.fromEntries((json["fields"] as List).map((e) {
List<dynamic> entry = e;
return MapEntry(AxisId.fromJson(entry[0]), SchemaField.fromJson(entry[1]));
})),
query: json["query"] == null ? null : QueryExpression.fromJson(json["query"]),
);
try {
Map<AxisId, SchemaField> fields = {};

if (json["fields"] is List) {
// Handle old format: list of pairs
developer.log("Processing fields as list format", name: "rubintv.chart.series");
for (dynamic fieldItem in json["fields"]) {
if (fieldItem is List && fieldItem.length == 2) {
var axisData = fieldItem[0];
var fieldData = fieldItem[1];

developer.log("Processing field pair: $axisData -> $fieldData", name: "rubintv.chart.series");

if (axisData != null && fieldData != null) {
AxisId axisId = AxisId.fromJson(axisData);
SchemaField field = SchemaField.fromJson(fieldData as Map<String, dynamic>);
fields[axisId] = field;
developer.log("Field processed from list: $axisId -> ${field.name}",
name: "rubintv.chart.series");
} else {
developer.log("Skipping null field pair: $axisData -> $fieldData",
name: "rubintv.chart.series");
}
} else {
developer.log("Invalid field item format: $fieldItem", name: "rubintv.chart.series");
}
}
} else if (json["fields"] is Map) {
// Handle new format: map
developer.log("Processing fields as map format", name: "rubintv.chart.series");
Map<String, dynamic> fieldsMap = json["fields"] as Map<String, dynamic>;
for (MapEntry<String, dynamic> entry in fieldsMap.entries) {
developer.log("Processing field: ${entry.key} -> ${entry.value}", name: "rubintv.chart.series");
if (entry.value != null) {
AxisId axisId = AxisId.fromJson(entry.key);
SchemaField field = SchemaField.fromJson(entry.value as Map<String, dynamic>);
fields[axisId] = field;
developer.log("Field processed from map: ${axisId} -> ${field.name}",
name: "rubintv.chart.series");
} else {
developer.log("Skipping null field value for key: ${entry.key}", name: "rubintv.chart.series");
}
}
} else {
developer.log("Unknown fields format: ${json["fields"].runtimeType}", name: "rubintv.chart.series");
throw ArgumentError("Unknown fields format in JSON");
}

return SeriesInfo(
id: SeriesId.fromString(json["id"]),
name: json["name"],
axes: List<AxisId>.from(json["axes"].map((e) => AxisId.fromJson(e))),
fields: fields,
query: json.containsKey("query") && json["query"] != null
? QueryExpression.fromJson(json["query"])
: null,
);
} catch (e, stackTrace) {
developer.log("Error in SeriesInfo.fromJson: $e",
name: "rubintv.chart.series", error: e, stackTrace: stackTrace);
developer.log("Full JSON: $json", name: "rubintv.chart.series");
rethrow;
}
}
}
8 changes: 6 additions & 2 deletions lib/editors/series.dart
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,8 @@ class ColumnEditorState extends State<ColumnEditor> {
@override
Widget build(BuildContext context) {
if (_field != null) {
_table = _field!.schema;
// Ensure we're using the table instance from the database, not a deserialized copy
_table = widget.databaseSchema.tables[_field!.schema.name];
}

List<DropdownMenuItem<TableSchema>> tableEntries = [];
Expand Down Expand Up @@ -376,7 +377,10 @@ class ColumnEditorState extends State<ColumnEditor> {
onChanged: (TableSchema? newTable) {
setState(() {
_table = newTable;
_field = _table!.fields.values.first;
// Ensure we get the field from the new table instance
if (_table != null && _table!.fields.isNotEmpty) {
_field = _table!.fields.values.first;
}
});
widget.onChanged(_field);
},
Expand Down
Loading