first commit

This commit is contained in:
2025-04-03 14:27:30 +07:00
parent a519af9cf7
commit f7c93b1eb3
221 changed files with 26063 additions and 0 deletions

View File

@@ -0,0 +1,341 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show
BehaviorPosition,
ChartBehavior,
DatumLegend,
InsideJustification,
LegendEntry,
MeasureFormatter,
LegendDefaultMeasure,
OutsideJustification,
SelectionModelType,
TextStyleSpec;
import 'package:flutter/widgets.dart'
show BuildContext, EdgeInsets, Widget, hashValues;
import 'package:meta/meta.dart' show immutable;
import '../../chart_container.dart' show ChartContainerRenderObject;
import '../chart_behavior.dart'
show BuildableBehavior, ChartBehavior, GestureType;
import 'legend.dart' show TappableLegend;
import 'legend_content_builder.dart'
show LegendContentBuilder, TabularLegendContentBuilder;
import 'legend_layout.dart' show TabularLegendLayout;
/// Datum legend behavior for charts.
///
/// By default this behavior creates one legend entry per datum in the first
/// series rendered on the chart.
@immutable
class DatumLegend<D> extends ChartBehavior<D> {
static const defaultBehaviorPosition = common.BehaviorPosition.top;
static const defaultOutsideJustification =
common.OutsideJustification.startDrawArea;
static const defaultInsideJustification = common.InsideJustification.topStart;
final desiredGestures = new Set<GestureType>();
final common.SelectionModelType? selectionModelType;
/// Builder for creating custom legend content.
final LegendContentBuilder contentBuilder;
/// Position of the legend relative to the chart.
final common.BehaviorPosition position;
/// Justification of the legend relative to the chart
final common.OutsideJustification outsideJustification;
final common.InsideJustification insideJustification;
/// Whether or not the legend should show measures.
///
/// By default this is false, measures are not shown. When set to true, the
/// default behavior is to show measure only if there is selected data.
/// Please set [legendDefaultMeasure] to something other than none to enable
/// showing measures when there is no selection.
///
/// This flag is used by the [contentBuilder], so a custom content builder
/// has to choose if it wants to use this flag.
final bool showMeasures;
/// Option to show measures when selection is null.
///
/// By default this is set to none, so no measures are shown when there is
/// no selection.
final common.LegendDefaultMeasure? legendDefaultMeasure;
/// Formatter for measure value(s) if the measures are shown on the legend.
final common.MeasureFormatter? measureFormatter;
/// Formatter for secondary measure value(s) if the measures are shown on the
/// legend and the series uses the secondary axis.
final common.MeasureFormatter? secondaryMeasureFormatter;
/// Styles for legend entry label text.
final common.TextStyleSpec? entryTextStyle;
static const defaultCellPadding = const EdgeInsets.all(8.0);
/// Create a new tabular layout legend.
///
/// By default, the legend is place above the chart and horizontally aligned
/// to the start of the draw area.
///
/// [position] the legend will be positioned relative to the chart. Default
/// position is top.
///
/// [outsideJustification] justification of the legend relative to the chart
/// if the position is top, bottom, left, right. Default to start of the draw
/// area.
///
/// [insideJustification] justification of the legend relative to the chart if
/// the position is inside. Default to top of the chart, start of draw area.
/// Start of draw area means left for LTR directionality, and right for RTL.
///
/// [horizontalFirst] if true, legend entries will grow horizontally first
/// instead of vertically first. If the position is top, bottom, or inside,
/// this defaults to true. Otherwise false.
///
/// [desiredMaxRows] the max rows to use before layout out items in a new
/// column. By default there is no limit. The max columns created is the
/// smaller of desiredMaxRows and number of legend entries.
///
/// [desiredMaxColumns] the max columns to use before laying out items in a
/// new row. By default there is no limit. The max columns created is the
/// smaller of desiredMaxColumns and number of legend entries.
///
/// [showMeasures] show measure values for each series.
///
/// [legendDefaultMeasure] if measure should show when there is no selection.
/// This is set to none by default (only shows measure for selected data).
///
/// [measureFormatter] formats measure value if measures are shown.
///
/// [secondaryMeasureFormatter] formats measures if measures are shown for the
/// series that uses secondary measure axis.
factory DatumLegend({
common.BehaviorPosition? position,
common.OutsideJustification? outsideJustification,
common.InsideJustification? insideJustification,
bool? horizontalFirst,
int? desiredMaxRows,
int? desiredMaxColumns,
EdgeInsets? cellPadding,
bool? showMeasures,
common.LegendDefaultMeasure? legendDefaultMeasure,
common.MeasureFormatter? measureFormatter,
common.MeasureFormatter? secondaryMeasureFormatter,
common.TextStyleSpec? entryTextStyle,
}) {
// Set defaults if empty.
position ??= defaultBehaviorPosition;
outsideJustification ??= defaultOutsideJustification;
insideJustification ??= defaultInsideJustification;
cellPadding ??= defaultCellPadding;
// Set the tabular layout settings to match the position if it is not
// specified.
horizontalFirst ??= (position == common.BehaviorPosition.top ||
position == common.BehaviorPosition.bottom ||
position == common.BehaviorPosition.inside);
final layoutBuilder = horizontalFirst
? new TabularLegendLayout.horizontalFirst(
desiredMaxColumns: desiredMaxColumns, cellPadding: cellPadding)
: new TabularLegendLayout.verticalFirst(
desiredMaxRows: desiredMaxRows, cellPadding: cellPadding);
return new DatumLegend._internal(
contentBuilder:
new TabularLegendContentBuilder(legendLayout: layoutBuilder),
selectionModelType: common.SelectionModelType.info,
position: position,
outsideJustification: outsideJustification,
insideJustification: insideJustification,
showMeasures: showMeasures ?? false,
legendDefaultMeasure:
legendDefaultMeasure ?? common.LegendDefaultMeasure.none,
measureFormatter: measureFormatter,
secondaryMeasureFormatter: secondaryMeasureFormatter,
entryTextStyle: entryTextStyle);
}
/// Create a legend with custom layout.
///
/// By default, the legend is place above the chart and horizontally aligned
/// to the start of the draw area.
///
/// [contentBuilder] builder for the custom layout.
///
/// [position] the legend will be positioned relative to the chart. Default
/// position is top.
///
/// [outsideJustification] justification of the legend relative to the chart
/// if the position is top, bottom, left, right. Default to start of the draw
/// area.
///
/// [insideJustification] justification of the legend relative to the chart if
/// the position is inside. Default to top of the chart, start of draw area.
/// Start of draw area means left for LTR directionality, and right for RTL.
///
/// [showMeasures] show measure values for each series.
///
/// [legendDefaultMeasure] if measure should show when there is no selection.
/// This is set to none by default (only shows measure for selected data).
///
/// [measureFormatter] formats measure value if measures are shown.
///
/// [secondaryMeasureFormatter] formats measures if measures are shown for the
/// series that uses secondary measure axis.
factory DatumLegend.customLayout(
LegendContentBuilder contentBuilder, {
common.BehaviorPosition? position,
common.OutsideJustification? outsideJustification,
common.InsideJustification? insideJustification,
bool? showMeasures,
common.LegendDefaultMeasure? legendDefaultMeasure,
common.MeasureFormatter? measureFormatter,
common.MeasureFormatter? secondaryMeasureFormatter,
common.TextStyleSpec? entryTextStyle,
}) {
// Set defaults if empty.
position ??= defaultBehaviorPosition;
outsideJustification ??= defaultOutsideJustification;
insideJustification ??= defaultInsideJustification;
return new DatumLegend._internal(
contentBuilder: contentBuilder,
selectionModelType: common.SelectionModelType.info,
position: position,
outsideJustification: outsideJustification,
insideJustification: insideJustification,
showMeasures: showMeasures ?? false,
legendDefaultMeasure:
legendDefaultMeasure ?? common.LegendDefaultMeasure.none,
measureFormatter: measureFormatter,
secondaryMeasureFormatter: secondaryMeasureFormatter,
entryTextStyle: entryTextStyle,
);
}
DatumLegend._internal({
required this.contentBuilder,
this.selectionModelType,
required this.position,
required this.outsideJustification,
required this.insideJustification,
required this.showMeasures,
this.legendDefaultMeasure,
this.measureFormatter,
this.secondaryMeasureFormatter,
this.entryTextStyle,
});
@override
common.DatumLegend<D> createCommonBehavior() =>
new _FlutterDatumLegend<D>(this);
@override
void updateCommonBehavior(common.ChartBehavior commonBehavior) {
(commonBehavior as _FlutterDatumLegend).config = this;
}
/// All Legend behaviors get the same role ID, because you should only have
/// one legend on a chart.
@override
String get role => 'legend';
@override
bool operator ==(Object o) {
return o is DatumLegend &&
selectionModelType == o.selectionModelType &&
contentBuilder == o.contentBuilder &&
position == o.position &&
outsideJustification == o.outsideJustification &&
insideJustification == o.insideJustification &&
showMeasures == o.showMeasures &&
legendDefaultMeasure == o.legendDefaultMeasure &&
measureFormatter == o.measureFormatter &&
secondaryMeasureFormatter == o.secondaryMeasureFormatter &&
entryTextStyle == o.entryTextStyle;
}
@override
int get hashCode {
return Object.hash(
selectionModelType,
contentBuilder,
position,
outsideJustification,
insideJustification,
showMeasures,
legendDefaultMeasure,
measureFormatter,
secondaryMeasureFormatter,
entryTextStyle);
}
}
/// Flutter specific wrapper on the common Legend for building content.
class _FlutterDatumLegend<D> extends common.DatumLegend<D>
implements BuildableBehavior, TappableLegend {
DatumLegend config;
_FlutterDatumLegend(this.config)
: super(
selectionModelType: config.selectionModelType,
measureFormatter: config.measureFormatter,
secondaryMeasureFormatter: config.secondaryMeasureFormatter,
legendDefaultMeasure: config.legendDefaultMeasure,
) {
super.entryTextStyle = config.entryTextStyle;
}
@override
void updateLegend() {
(chartContext as ChartContainerRenderObject).requestRebuild();
}
@override
common.BehaviorPosition get position => config.position;
@override
common.OutsideJustification get outsideJustification =>
config.outsideJustification;
@override
common.InsideJustification get insideJustification =>
config.insideJustification;
@override
Widget build(BuildContext context) {
final hasSelection =
legendState.legendEntries.any((entry) => entry.isSelected);
// Show measures if [showMeasures] is true and there is a selection or if
// showing measures when there is no selection.
final showMeasures = config.showMeasures &&
(hasSelection ||
legendDefaultMeasure != common.LegendDefaultMeasure.none);
return config.contentBuilder
.build(context, legendState, this, showMeasures: showMeasures);
}
/// TODO: Maybe highlight the pie wedge.
@override
onLegendEntryTapUp(common.LegendEntry detail) {}
}

View File

@@ -0,0 +1,22 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' show LegendEntry, LegendTapHandling;
abstract class TappableLegend<T, D> {
/// Delegates handling of legend entry clicks according to the configured
/// [LegendTapHandling] strategy.
onLegendEntryTapUp(LegendEntry detail);
}

View File

@@ -0,0 +1,92 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show Legend, LegendState, SeriesLegend;
import 'package:flutter/widgets.dart' show BuildContext, hashValues, Widget;
import 'legend.dart';
import 'legend_entry_layout.dart';
import 'legend_layout.dart';
/// Strategy for building a legend content widget.
abstract class LegendContentBuilder {
const LegendContentBuilder();
Widget build(BuildContext context, common.LegendState legendState,
common.Legend legend,
{bool showMeasures});
}
/// Base strategy for building a legend content widget.
///
/// Each legend entry is passed to a [LegendLayout] strategy to create a widget
/// for each legend entry. These widgets are then passed to a
/// [LegendEntryLayout] strategy to create the legend widget.
abstract class BaseLegendContentBuilder implements LegendContentBuilder {
/// Strategy for creating one widget or each legend entry.
LegendEntryLayout get legendEntryLayout;
/// Strategy for creating the legend content widget from a list of widgets.
///
/// This is typically the list of widgets from legend entries.
LegendLayout get legendLayout;
@override
Widget build(BuildContext context, common.LegendState legendState,
common.Legend legend,
{bool showMeasures = false}) {
final entryWidgets = legendState.legendEntries.map((entry) {
var isHidden = false;
if (legend is common.SeriesLegend) {
isHidden = legend.isSeriesHidden(entry.series.id);
}
return legendEntryLayout.build(
context, entry, legend as TappableLegend, isHidden,
showMeasures: showMeasures);
}).toList();
return legendLayout.build(context, entryWidgets);
}
}
// TODO: Expose settings for tabular layout.
/// Strategy that builds a tabular legend.
///
/// [legendEntryLayout] custom strategy for creating widgets for each legend
/// entry.
/// [legendLayout] custom strategy for creating legend widget from list of
/// widgets that represent a legend entry.
class TabularLegendContentBuilder extends BaseLegendContentBuilder {
final LegendEntryLayout legendEntryLayout;
final LegendLayout legendLayout;
TabularLegendContentBuilder(
{LegendEntryLayout? legendEntryLayout, LegendLayout? legendLayout})
: this.legendEntryLayout =
legendEntryLayout ?? const SimpleLegendEntryLayout(),
this.legendLayout =
legendLayout ?? new TabularLegendLayout.horizontalFirst();
@override
bool operator ==(Object o) {
return o is TabularLegendContentBuilder &&
legendEntryLayout == o.legendEntryLayout &&
legendLayout == o.legendLayout;
}
@override
int get hashCode => Object.hash(legendEntryLayout, legendLayout);
}

View File

@@ -0,0 +1,146 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common;
import 'package:charts_flutter/src/util/color.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart'
show GestureDetector, GestureTapUpCallback, TapUpDetails, Theme;
import '../../symbol_renderer.dart';
import 'legend.dart' show TappableLegend;
/// Strategy for building one widget from one [common.LegendEntry].
abstract class LegendEntryLayout {
Widget build(BuildContext context, common.LegendEntry legendEntry,
TappableLegend legend, bool isHidden,
{bool showMeasures});
}
/// Builds one legend entry as a row with symbol and label from the series.
///
/// If directionality from the chart context indicates RTL, the symbol is placed
/// to the right of the text instead of the left of the text.
class SimpleLegendEntryLayout implements LegendEntryLayout {
const SimpleLegendEntryLayout();
Widget createSymbol(BuildContext context, common.LegendEntry legendEntry,
TappableLegend legend, bool isHidden) {
// TODO: Consider allowing scaling the size for the symbol.
// A custom symbol renderer can ignore this size and use their own.
final materialSymbolSize = new Size(12.0, 12.0);
final entryColor = legendEntry.color;
final color = entryColor == null ? null : ColorUtil.toDartColor(entryColor);
// Get the SymbolRendererBuilder wrapping a common.SymbolRenderer if needed.
final SymbolRendererBuilder symbolRendererBuilder =
legendEntry.symbolRenderer! is SymbolRendererBuilder
? legendEntry.symbolRenderer! as SymbolRendererBuilder
: new SymbolRendererCanvas(
legendEntry.symbolRenderer!, legendEntry.dashPattern);
return new GestureDetector(
child: symbolRendererBuilder.build(
context,
size: materialSymbolSize,
color: color,
enabled: !isHidden,
),
onTapUp: makeTapUpCallback(context, legendEntry, legend));
}
Widget createLabel(BuildContext context, common.LegendEntry legendEntry,
TappableLegend legend, bool isHidden) {
TextStyle style =
_convertTextStyle(isHidden, context, legendEntry.textStyle);
return new GestureDetector(
child: new Text(legendEntry.label, style: style),
onTapUp: makeTapUpCallback(context, legendEntry, legend));
}
Widget createMeasureValue(BuildContext context,
common.LegendEntry legendEntry, TappableLegend legend, bool isHidden) {
return new GestureDetector(
child: new Text(legendEntry.formattedValue!),
onTapUp: makeTapUpCallback(context, legendEntry, legend));
}
@override
Widget build(BuildContext context, common.LegendEntry legendEntry,
TappableLegend legend, bool isHidden,
{bool showMeasures = false}) {
final rowChildren = <Widget>[];
// TODO: Allow setting to configure the padding.
final padding = new EdgeInsets.only(right: 8.0); // Material default.
final symbol = createSymbol(context, legendEntry, legend, isHidden);
final label = createLabel(context, legendEntry, legend, isHidden);
final measure = showMeasures
? createMeasureValue(context, legendEntry, legend, isHidden)
: null;
rowChildren.add(symbol);
rowChildren.add(new Container(padding: padding));
rowChildren.add(label);
if (measure != null) {
rowChildren.add(new Container(padding: padding));
rowChildren.add(measure);
}
// Row automatically reverses the content if Directionality is rtl.
return new Row(children: rowChildren);
}
GestureTapUpCallback makeTapUpCallback(BuildContext context,
common.LegendEntry legendEntry, TappableLegend legend) {
return (TapUpDetails d) {
legend.onLegendEntryTapUp(legendEntry);
};
}
bool operator ==(Object other) => other is SimpleLegendEntryLayout;
int get hashCode {
return this.runtimeType.hashCode;
}
/// Convert the charts common TextStlyeSpec into a standard TextStyle, while
/// reducing the color opacity to 26% if the entry is hidden.
///
/// For non-specified values, override the hidden text color to use the body 1
/// theme, but allow other properties of [Text] to be inherited.
TextStyle _convertTextStyle(
bool isHidden, BuildContext context, common.TextStyleSpec? textStyle) {
Color? color = textStyle?.color != null
? ColorUtil.toDartColor(textStyle!.color!)
: null;
if (isHidden) {
// Use a default color for hidden legend entries if none is provided.
color ??= Theme.of(context).textTheme.bodyMedium!.color;
color = color!.withOpacity(0.26);
}
return new TextStyle(
inherit: true,
fontFamily: textStyle?.fontFamily,
fontSize: textStyle?.fontSize != null
? textStyle!.fontSize!.toDouble()
: null,
color: color);
}
}

View File

@@ -0,0 +1,162 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show min;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// Strategy for building legend from legend entry widgets.
abstract class LegendLayout {
Widget build(BuildContext context, List<Widget> legendEntryWidgets);
}
/// Layout legend entries in tabular format.
class TabularLegendLayout implements LegendLayout {
/// No limit for max rows or max columns.
static const _noLimit = -1;
/// Default EdgeInsets for padding rows to the max column count
static const defaultCellPadding = const EdgeInsets.all(8.0);
final bool isHorizontalFirst;
final int desiredMaxRows;
final int desiredMaxColumns;
final EdgeInsets? cellPadding;
TabularLegendLayout._internal(
{required this.isHorizontalFirst,
required this.desiredMaxRows,
required this.desiredMaxColumns,
this.cellPadding});
/// Layout horizontally until columns exceed [desiredMaxColumns].
///
/// [desiredMaxColumns] the max columns to use before laying out items in a
/// new row. By default there is no limit. The max columns created is the
/// smaller of desiredMaxColumns and number of legend entries.
///
/// [cellPadding] the [EdgeInsets] for each widget.
factory TabularLegendLayout.horizontalFirst({
int? desiredMaxColumns,
EdgeInsets? cellPadding,
}) {
return new TabularLegendLayout._internal(
isHorizontalFirst: true,
desiredMaxRows: _noLimit,
desiredMaxColumns: desiredMaxColumns ?? _noLimit,
cellPadding: cellPadding,
);
}
/// Layout vertically, until rows exceed [desiredMaxRows].
///
/// [desiredMaxRows] the max rows to use before layout out items in a new
/// column. By default there is no limit. The max columns created is the
/// smaller of desiredMaxRows and number of legend entries.
///
/// [cellPadding] the [EdgeInsets] for each widget.
factory TabularLegendLayout.verticalFirst({
int? desiredMaxRows,
EdgeInsets? cellPadding,
}) {
return new TabularLegendLayout._internal(
isHorizontalFirst: false,
desiredMaxRows: desiredMaxRows ?? _noLimit,
desiredMaxColumns: _noLimit,
cellPadding: cellPadding,
);
}
@override
Widget build(BuildContext context, List<Widget> legendEntries) {
final paddedLegendEntries = ((cellPadding == null)
? legendEntries
: legendEntries
.map((entry) => new Padding(padding: cellPadding!, child: entry))
.toList());
return isHorizontalFirst
? _buildHorizontalFirst(paddedLegendEntries)
: _buildVerticalFirst(paddedLegendEntries);
}
@override
bool operator ==(o) =>
o is TabularLegendLayout &&
desiredMaxRows == o.desiredMaxRows &&
desiredMaxColumns == o.desiredMaxColumns &&
isHorizontalFirst == o.isHorizontalFirst &&
cellPadding == o.cellPadding;
@override
int get hashCode => Object.hash(
desiredMaxRows, desiredMaxColumns, isHorizontalFirst, cellPadding);
Widget _buildHorizontalFirst(List<Widget> legendEntries) {
final maxColumns = (desiredMaxColumns == _noLimit)
? legendEntries.length
: min(legendEntries.length, desiredMaxColumns);
final rows = <TableRow>[];
for (var i = 0; i < legendEntries.length; i += maxColumns) {
rows.add(new TableRow(
children: legendEntries
.sublist(i, min(i + maxColumns, legendEntries.length))
.toList()));
}
return _buildTableFromRows(rows);
}
Widget _buildVerticalFirst(List<Widget> legendEntries) {
final maxRows = (desiredMaxRows == _noLimit)
? legendEntries.length
: min(legendEntries.length, desiredMaxRows);
final rows =
new List.generate(maxRows, (_) => new TableRow(children: <Widget>[]));
for (var i = 0; i < legendEntries.length; i++) {
rows[i % maxRows].children!.add(legendEntries[i]);
}
return _buildTableFromRows(rows);
}
Table _buildTableFromRows(List<TableRow> rows) {
final padWidget = Padding(padding: cellPadding ?? defaultCellPadding);
// Pad rows to the max column count, because each TableRow in a table is
// required to have the same number of children.
final columnCount = rows
.map((r) => r.children!.length)
.fold<int>(0, (max, current) => (current > max) ? current : max);
for (var i = 0; i < rows.length; i++) {
final rowChildren = rows[i].children;
final padCount = columnCount - rowChildren!.length;
if (padCount > 0) {
rowChildren
.addAll(new Iterable<Padding>.generate(padCount, (_) => padWidget));
}
}
// TODO: Investigate other means of creating the tabular legend
// Sizing the column width using [IntrinsicColumnWidth] is expensive per
// Flutter's documentation, but has to be used if the table is desired to
// have a width that is tight on each column.
return new Table(
children: rows, defaultColumnWidth: new IntrinsicColumnWidth());
}
}

View File

@@ -0,0 +1,383 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show
BehaviorPosition,
ChartBehavior,
InsideJustification,
LegendEntry,
LegendTapHandling,
MeasureFormatter,
LegendDefaultMeasure,
OutsideJustification,
SeriesLegend,
SelectionModelType,
TextStyleSpec;
import 'package:collection/collection.dart' show ListEquality;
import 'package:flutter/widgets.dart'
show BuildContext, EdgeInsets, Widget, hashValues;
import 'package:meta/meta.dart' show immutable;
import '../../chart_container.dart' show ChartContainerRenderObject;
import '../chart_behavior.dart'
show BuildableBehavior, ChartBehavior, GestureType;
import 'legend.dart' show TappableLegend;
import 'legend_content_builder.dart'
show LegendContentBuilder, TabularLegendContentBuilder;
import 'legend_layout.dart' show TabularLegendLayout;
/// Series legend behavior for charts.
@immutable
class SeriesLegend<D> extends ChartBehavior<D> {
static const defaultBehaviorPosition = common.BehaviorPosition.top;
static const defaultOutsideJustification =
common.OutsideJustification.startDrawArea;
static const defaultInsideJustification = common.InsideJustification.topStart;
final desiredGestures = new Set<GestureType>();
final common.SelectionModelType? selectionModelType;
/// Builder for creating custom legend content.
final LegendContentBuilder contentBuilder;
/// Position of the legend relative to the chart.
final common.BehaviorPosition position;
/// Justification of the legend relative to the chart
final common.OutsideJustification outsideJustification;
final common.InsideJustification insideJustification;
/// Whether or not the legend should show measures.
///
/// By default this is false, measures are not shown. When set to true, the
/// default behavior is to show measure only if there is selected data.
/// Please set [legendDefaultMeasure] to something other than none to enable
/// showing measures when there is no selection.
///
/// This flag is used by the [contentBuilder], so a custom content builder
/// has to choose if it wants to use this flag.
final bool showMeasures;
/// Option to show measures when selection is null.
///
/// By default this is set to none, so no measures are shown when there is
/// no selection.
final common.LegendDefaultMeasure? legendDefaultMeasure;
/// Formatter for measure value(s) if the measures are shown on the legend.
final common.MeasureFormatter? measureFormatter;
/// Formatter for secondary measure value(s) if the measures are shown on the
/// legend and the series uses the secondary axis.
final common.MeasureFormatter? secondaryMeasureFormatter;
/// Styles for legend entry label text.
final common.TextStyleSpec? entryTextStyle;
static const defaultCellPadding = const EdgeInsets.all(8.0);
final List<String>? defaultHiddenSeries;
/// Create a new tabular layout legend.
///
/// By default, the legend is place above the chart and horizontally aligned
/// to the start of the draw area.
///
/// [position] the legend will be positioned relative to the chart. Default
/// position is top.
///
/// [outsideJustification] justification of the legend relative to the chart
/// if the position is top, bottom, left, right. Default to start of the draw
/// area.
///
/// [insideJustification] justification of the legend relative to the chart if
/// the position is inside. Default to top of the chart, start of draw area.
/// Start of draw area means left for LTR directionality, and right for RTL.
///
/// [horizontalFirst] if true, legend entries will grow horizontally first
/// instead of vertically first. If the position is top, bottom, or inside,
/// this defaults to true. Otherwise false.
///
/// [desiredMaxRows] the max rows to use before layout out items in a new
/// column. By default there is no limit. The max columns created is the
/// smaller of desiredMaxRows and number of legend entries.
///
/// [desiredMaxColumns] the max columns to use before laying out items in a
/// new row. By default there is no limit. The max columns created is the
/// smaller of desiredMaxColumns and number of legend entries.
///
/// [defaultHiddenSeries] lists the IDs of series that should be hidden on
/// first chart draw.
///
/// [showMeasures] show measure values for each series.
///
/// [legendDefaultMeasure] if measure should show when there is no selection.
/// This is set to none by default (only shows measure for selected data).
///
/// [measureFormatter] formats measure value if measures are shown.
///
/// [secondaryMeasureFormatter] formats measures if measures are shown for the
/// series that uses secondary measure axis.
factory SeriesLegend({
common.BehaviorPosition? position,
common.OutsideJustification? outsideJustification,
common.InsideJustification? insideJustification,
bool? horizontalFirst,
int? desiredMaxRows,
int? desiredMaxColumns,
EdgeInsets? cellPadding,
List<String>? defaultHiddenSeries,
bool? showMeasures,
common.LegendDefaultMeasure? legendDefaultMeasure,
common.MeasureFormatter? measureFormatter,
common.MeasureFormatter? secondaryMeasureFormatter,
common.TextStyleSpec? entryTextStyle,
}) {
// Set defaults if empty.
position ??= defaultBehaviorPosition;
outsideJustification ??= defaultOutsideJustification;
insideJustification ??= defaultInsideJustification;
cellPadding ??= defaultCellPadding;
// Set the tabular layout settings to match the position if it is not
// specified.
horizontalFirst ??= (position == common.BehaviorPosition.top ||
position == common.BehaviorPosition.bottom ||
position == common.BehaviorPosition.inside);
final layoutBuilder = horizontalFirst
? new TabularLegendLayout.horizontalFirst(
desiredMaxColumns: desiredMaxColumns, cellPadding: cellPadding)
: new TabularLegendLayout.verticalFirst(
desiredMaxRows: desiredMaxRows, cellPadding: cellPadding);
return new SeriesLegend._internal(
contentBuilder:
new TabularLegendContentBuilder(legendLayout: layoutBuilder),
selectionModelType: common.SelectionModelType.info,
position: position,
outsideJustification: outsideJustification,
insideJustification: insideJustification,
defaultHiddenSeries: defaultHiddenSeries,
showMeasures: showMeasures ?? false,
legendDefaultMeasure:
legendDefaultMeasure ?? common.LegendDefaultMeasure.none,
measureFormatter: measureFormatter,
secondaryMeasureFormatter: secondaryMeasureFormatter,
entryTextStyle: entryTextStyle);
}
/// Create a legend with custom layout.
///
/// By default, the legend is place above the chart and horizontally aligned
/// to the start of the draw area.
///
/// [contentBuilder] builder for the custom layout.
///
/// [position] the legend will be positioned relative to the chart. Default
/// position is top.
///
/// [outsideJustification] justification of the legend relative to the chart
/// if the position is top, bottom, left, right. Default to start of the draw
/// area.
///
/// [insideJustification] justification of the legend relative to the chart if
/// the position is inside. Default to top of the chart, start of draw area.
/// Start of draw area means left for LTR directionality, and right for RTL.
///
/// [defaultHiddenSeries] lists the IDs of series that should be hidden on
/// first chart draw.
///
/// [showMeasures] show measure values for each series.
///
/// [legendDefaultMeasure] if measure should show when there is no selection.
/// This is set to none by default (only shows measure for selected data).
///
/// [measureFormatter] formats measure value if measures are shown.
///
/// [secondaryMeasureFormatter] formats measures if measures are shown for the
/// series that uses secondary measure axis.
factory SeriesLegend.customLayout(
LegendContentBuilder contentBuilder, {
common.BehaviorPosition? position,
common.OutsideJustification? outsideJustification,
common.InsideJustification? insideJustification,
List<String>? defaultHiddenSeries,
bool? showMeasures,
common.LegendDefaultMeasure? legendDefaultMeasure,
common.MeasureFormatter? measureFormatter,
common.MeasureFormatter? secondaryMeasureFormatter,
common.TextStyleSpec? entryTextStyle,
}) {
// Set defaults if empty.
position ??= defaultBehaviorPosition;
outsideJustification ??= defaultOutsideJustification;
insideJustification ??= defaultInsideJustification;
return new SeriesLegend._internal(
contentBuilder: contentBuilder,
selectionModelType: common.SelectionModelType.info,
position: position,
outsideJustification: outsideJustification,
insideJustification: insideJustification,
defaultHiddenSeries: defaultHiddenSeries,
showMeasures: showMeasures ?? false,
legendDefaultMeasure:
legendDefaultMeasure ?? common.LegendDefaultMeasure.none,
measureFormatter: measureFormatter,
secondaryMeasureFormatter: secondaryMeasureFormatter,
entryTextStyle: entryTextStyle,
);
}
SeriesLegend._internal({
required this.contentBuilder,
this.selectionModelType,
required this.position,
required this.outsideJustification,
required this.insideJustification,
this.defaultHiddenSeries,
required this.showMeasures,
this.legendDefaultMeasure,
this.measureFormatter,
this.secondaryMeasureFormatter,
this.entryTextStyle,
});
@override
common.SeriesLegend<D> createCommonBehavior() =>
new _FlutterSeriesLegend<D>(this);
@override
void updateCommonBehavior(common.ChartBehavior commonBehavior) {
(commonBehavior as _FlutterSeriesLegend).config = this;
}
/// All Legend behaviors get the same role ID, because you should only have
/// one legend on a chart.
@override
String get role => 'legend';
@override
bool operator ==(Object o) {
return o is SeriesLegend &&
selectionModelType == o.selectionModelType &&
contentBuilder == o.contentBuilder &&
position == o.position &&
outsideJustification == o.outsideJustification &&
insideJustification == o.insideJustification &&
new ListEquality().equals(defaultHiddenSeries, o.defaultHiddenSeries) &&
showMeasures == o.showMeasures &&
legendDefaultMeasure == o.legendDefaultMeasure &&
measureFormatter == o.measureFormatter &&
secondaryMeasureFormatter == o.secondaryMeasureFormatter &&
entryTextStyle == o.entryTextStyle;
}
@override
int get hashCode {
return Object.hash(
selectionModelType,
contentBuilder,
position,
outsideJustification,
insideJustification,
defaultHiddenSeries,
showMeasures,
legendDefaultMeasure,
measureFormatter,
secondaryMeasureFormatter,
entryTextStyle);
}
}
/// Flutter specific wrapper on the common Legend for building content.
class _FlutterSeriesLegend<D> extends common.SeriesLegend<D>
implements BuildableBehavior, TappableLegend {
SeriesLegend config;
_FlutterSeriesLegend(this.config)
: super(
selectionModelType: config.selectionModelType,
measureFormatter: config.measureFormatter,
secondaryMeasureFormatter: config.secondaryMeasureFormatter,
legendDefaultMeasure: config.legendDefaultMeasure,
) {
super.defaultHiddenSeries = config.defaultHiddenSeries;
super.entryTextStyle = config.entryTextStyle;
}
@override
void updateLegend() {
(chartContext as ChartContainerRenderObject).requestRebuild();
}
@override
common.BehaviorPosition get position => config.position;
@override
common.OutsideJustification get outsideJustification =>
config.outsideJustification;
@override
common.InsideJustification get insideJustification =>
config.insideJustification;
@override
Widget build(BuildContext context) {
final hasSelection = legendState.legendEntries != null &&
legendState.legendEntries.any((entry) => entry.isSelected);
// Show measures if [showMeasures] is true and there is a selection or if
// showing measures when there is no selection.
final showMeasures = config.showMeasures &&
(hasSelection ||
legendDefaultMeasure != common.LegendDefaultMeasure.none);
return config.contentBuilder
.build(context, legendState, this, showMeasures: showMeasures);
}
@override
onLegendEntryTapUp(common.LegendEntry detail) {
switch (legendTapHandling) {
case common.LegendTapHandling.hide:
_hideSeries(detail);
break;
case common.LegendTapHandling.none:
default:
break;
}
}
/// Handles tap events by hiding or un-hiding entries tapped in the legend.
///
/// Tapping on a visible series in the legend will hide it. Tapping on a
/// hidden series will make it visible again.
void _hideSeries(common.LegendEntry detail) {
final seriesId = detail.series.id;
// Handle the event by toggling the hidden state of the target.
if (isSeriesHidden(seriesId)) {
showSeries(seriesId);
} else {
hideSeries(seriesId);
}
// Redraw the chart to actually hide hidden series.
chart.redraw(skipLayout: true, skipAnimation: false);
}
}