1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-09 14:28:51 +00:00
Files
samples/web/charts/flutter/lib/src/chart_canvas.dart
2019-05-07 13:32:08 -07:00

443 lines
14 KiB
Dart

// 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:flutter_web_ui/ui.dart' as ui show Gradient, Shader;
import 'dart:math' show Point, Rectangle, max;
import 'package:charts_common/common.dart' as common
show
ChartCanvas,
CanvasBarStack,
CanvasPie,
Color,
FillPatternType,
GraphicsFactory,
StyleFactory,
TextElement,
TextDirection;
import 'package:flutter_web/material.dart';
import 'text_element.dart' show TextElement;
import 'canvas/circle_sector_painter.dart' show CircleSectorPainter;
import 'canvas/line_painter.dart' show LinePainter;
import 'canvas/pie_painter.dart' show PiePainter;
import 'canvas/point_painter.dart' show PointPainter;
import 'canvas/polygon_painter.dart' show PolygonPainter;
class ChartCanvas implements common.ChartCanvas {
/// Pixels to allow to overdraw above the draw area that fades to transparent.
static const double rect_top_gradient_pixels = 5;
final Canvas canvas;
final common.GraphicsFactory graphicsFactory;
final _paint = new Paint();
CircleSectorPainter _circleSectorPainter;
LinePainter _linePainter;
PiePainter _piePainter;
PointPainter _pointPainter;
PolygonPainter _polygonPainter;
ChartCanvas(this.canvas, this.graphicsFactory);
@override
void drawCircleSector(Point center, double radius, double innerRadius,
double startAngle, double endAngle,
{common.Color fill, common.Color stroke, double strokeWidthPx}) {
_circleSectorPainter ??= new CircleSectorPainter();
_circleSectorPainter.draw(
canvas: canvas,
paint: _paint,
center: center,
radius: radius,
innerRadius: innerRadius,
startAngle: startAngle,
endAngle: endAngle,
fill: fill,
stroke: stroke,
strokeWidthPx: strokeWidthPx);
}
@override
void drawLine(
{List<Point> points,
Rectangle<num> clipBounds,
common.Color fill,
common.Color stroke,
bool roundEndCaps,
double strokeWidthPx,
List<int> dashPattern}) {
_linePainter ??= new LinePainter();
_linePainter.draw(
canvas: canvas,
paint: _paint,
points: points,
clipBounds: clipBounds,
fill: fill,
stroke: stroke,
roundEndCaps: roundEndCaps,
strokeWidthPx: strokeWidthPx,
dashPattern: dashPattern);
}
@override
void drawPie(common.CanvasPie canvasPie) {
_piePainter ??= new PiePainter();
_piePainter.draw(canvas, _paint, canvasPie);
}
@override
void drawPoint(
{Point point,
double radius,
common.Color fill,
common.Color stroke,
double strokeWidthPx}) {
_pointPainter ??= new PointPainter();
_pointPainter.draw(
canvas: canvas,
paint: _paint,
point: point,
radius: radius,
fill: fill,
stroke: stroke,
strokeWidthPx: strokeWidthPx);
}
@override
void drawPolygon(
{List<Point> points,
Rectangle<num> clipBounds,
common.Color fill,
common.Color stroke,
double strokeWidthPx}) {
_polygonPainter ??= new PolygonPainter();
_polygonPainter.draw(
canvas: canvas,
paint: _paint,
points: points,
clipBounds: clipBounds,
fill: fill,
stroke: stroke,
strokeWidthPx: strokeWidthPx);
}
/// Creates a bottom to top gradient that transitions [fill] to transparent.
ui.Gradient _createHintGradient(double left, double top, common.Color fill) {
return new ui.Gradient.linear(
new Offset(left, top),
new Offset(left, top - rect_top_gradient_pixels),
[
new Color.fromARGB(fill.a, fill.r, fill.g, fill.b),
new Color.fromARGB(0, fill.r, fill.g, fill.b)
],
);
}
@override
void drawRect(Rectangle<num> bounds,
{common.Color fill,
common.FillPatternType pattern,
common.Color stroke,
double strokeWidthPx,
Rectangle<num> drawAreaBounds}) {
final drawStroke =
(strokeWidthPx != null && strokeWidthPx > 0.0 && stroke != null);
final strokeWidthOffset = (drawStroke ? strokeWidthPx : 0);
// Factor out stroke width, if a stroke is enabled.
final fillRectBounds = new Rectangle<num>(
bounds.left + strokeWidthOffset / 2,
bounds.top + strokeWidthOffset / 2,
bounds.width - strokeWidthOffset,
bounds.height - strokeWidthOffset);
switch (pattern) {
case common.FillPatternType.forwardHatch:
_drawForwardHatchPattern(fillRectBounds, canvas,
fill: fill, drawAreaBounds: drawAreaBounds);
break;
case common.FillPatternType.solid:
default:
// Use separate rect for drawing stroke
_paint.color = new Color.fromARGB(fill.a, fill.r, fill.g, fill.b);
_paint.style = PaintingStyle.fill;
// Apply a gradient to the top [rect_top_gradient_pixels] to transparent
// if the rectangle is higher than the [drawAreaBounds] top.
if (drawAreaBounds != null && bounds.top < drawAreaBounds.top) {
_paint.shader = _createHintGradient(drawAreaBounds.left.toDouble(),
drawAreaBounds.top.toDouble(), fill);
}
canvas.drawRect(_getRect(fillRectBounds), _paint);
break;
}
// [Canvas.drawRect] does not support drawing a rectangle with both a fill
// and a stroke at this time. Use a separate rect for the stroke.
if (drawStroke) {
_paint.color = new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b);
// Set shader to null if no draw area bounds so it can use the color
// instead.
_paint.shader = drawAreaBounds != null
? _createHintGradient(drawAreaBounds.left.toDouble(),
drawAreaBounds.top.toDouble(), stroke)
: null;
_paint.strokeJoin = StrokeJoin.round;
_paint.strokeWidth = strokeWidthPx;
_paint.style = PaintingStyle.stroke;
canvas.drawRect(_getRect(bounds), _paint);
}
// Reset the shader.
_paint.shader = null;
}
@override
void drawRRect(Rectangle<num> bounds,
{common.Color fill,
common.Color stroke,
num radius,
bool roundTopLeft,
bool roundTopRight,
bool roundBottomLeft,
bool roundBottomRight}) {
// Use separate rect for drawing stroke
_paint.color = new Color.fromARGB(fill.a, fill.r, fill.g, fill.b);
_paint.style = PaintingStyle.fill;
canvas.drawRRect(
_getRRect(bounds,
radius: radius,
roundTopLeft: roundTopLeft,
roundTopRight: roundTopRight,
roundBottomLeft: roundBottomLeft,
roundBottomRight: roundBottomRight),
_paint);
}
@override
void drawBarStack(common.CanvasBarStack barStack,
{Rectangle<num> drawAreaBounds}) {
// only clip if rounded rect.
// Clip a rounded rect for the whole region if rounded bars.
final roundedCorners = 0 < barStack.radius;
if (roundedCorners) {
canvas
..save()
..clipRRect(_getRRect(
barStack.fullStackRect,
radius: barStack.radius.toDouble(),
roundTopLeft: barStack.roundTopLeft,
roundTopRight: barStack.roundTopRight,
roundBottomLeft: barStack.roundBottomLeft,
roundBottomRight: barStack.roundBottomRight,
));
}
// Draw each bar.
for (var barIndex = 0; barIndex < barStack.segments.length; barIndex++) {
// TODO: Add configuration for hiding stack line.
// TODO: Don't draw stroke on bottom of bars.
final segment = barStack.segments[barIndex];
drawRect(segment.bounds,
fill: segment.fill,
pattern: segment.pattern,
stroke: segment.stroke,
strokeWidthPx: segment.strokeWidthPx,
drawAreaBounds: drawAreaBounds);
}
if (roundedCorners) {
canvas.restore();
}
}
@override
void drawText(common.TextElement textElement, int offsetX, int offsetY,
{double rotation = 0.0}) {
// Must be Flutter TextElement.
assert(textElement is TextElement);
final flutterTextElement = textElement as TextElement;
final textDirection = flutterTextElement.textDirection;
final measurement = flutterTextElement.measurement;
if (rotation != 0) {
// TODO: Remove once textAnchor works.
if (textDirection == common.TextDirection.rtl) {
offsetY += measurement.horizontalSliceWidth.toInt();
}
offsetX -= flutterTextElement.verticalFontShift;
canvas.save();
canvas.translate(offsetX.toDouble(), offsetY.toDouble());
canvas.rotate(rotation);
(textElement as TextElement)
.textPainter
.paint(canvas, new Offset(0.0, 0.0));
canvas.restore();
} else {
// TODO: Remove once textAnchor works.
if (textDirection == common.TextDirection.rtl) {
offsetX -= measurement.horizontalSliceWidth.toInt();
}
// Account for missing center alignment.
if (textDirection == common.TextDirection.center) {
offsetX -= (measurement.horizontalSliceWidth / 2).ceil();
}
offsetY -= flutterTextElement.verticalFontShift;
(textElement as TextElement)
.textPainter
.paint(canvas, new Offset(offsetX.toDouble(), offsetY.toDouble()));
}
}
@override
void setClipBounds(Rectangle<int> clipBounds) {
canvas
..save()
..clipRect(_getRect(clipBounds));
}
@override
void resetClipBounds() {
canvas.restore();
}
/// Convert dart:math [Rectangle] to Flutter [Rect].
Rect _getRect(Rectangle<num> rectangle) {
return new Rect.fromLTWH(
rectangle.left.toDouble(),
rectangle.top.toDouble(),
rectangle.width.toDouble(),
rectangle.height.toDouble());
}
/// Convert dart:math [Rectangle] and to Flutter [RRect].
RRect _getRRect(
Rectangle<num> rectangle, {
double radius,
bool roundTopLeft = false,
bool roundTopRight = false,
bool roundBottomLeft = false,
bool roundBottomRight = false,
}) {
final cornerRadius =
radius == 0 ? Radius.zero : new Radius.circular(radius);
return new RRect.fromLTRBAndCorners(
rectangle.left.toDouble(),
rectangle.top.toDouble(),
rectangle.right.toDouble(),
rectangle.bottom.toDouble(),
topLeft: roundTopLeft ? cornerRadius : Radius.zero,
topRight: roundTopRight ? cornerRadius : Radius.zero,
bottomLeft: roundBottomLeft ? cornerRadius : Radius.zero,
bottomRight: roundBottomRight ? cornerRadius : Radius.zero);
}
/// Draws a forward hatch pattern in the given bounds.
_drawForwardHatchPattern(
Rectangle<num> bounds,
Canvas canvas, {
common.Color background,
common.Color fill,
double fillWidthPx = 4.0,
Rectangle<num> drawAreaBounds,
}) {
background ??= common.StyleFactory.style.white;
fill ??= common.StyleFactory.style.black;
// Fill in the shape with a solid background color.
_paint.color = new Color.fromARGB(
background.a, background.r, background.g, background.b);
_paint.style = PaintingStyle.fill;
// Apply a gradient the background if bounds exceed the draw area.
if (drawAreaBounds != null && bounds.top < drawAreaBounds.top) {
_paint.shader = _createHintGradient(drawAreaBounds.left.toDouble(),
drawAreaBounds.top.toDouble(), background);
}
canvas.drawRect(_getRect(bounds), _paint);
// As a simplification, we will treat the bounds as a large square and fill
// it up with lines from the bottom-left corner to the top-right corner.
// Get the longer side of the bounds here for the size of this square.
final size = max(bounds.width, bounds.height);
final x0 = bounds.left + size + fillWidthPx;
final x1 = bounds.left - fillWidthPx;
final y0 = bounds.bottom - size - fillWidthPx;
final y1 = bounds.bottom + fillWidthPx;
final offset = 8;
final isVertical = bounds.height >= bounds.width;
_linePainter ??= new LinePainter();
// The "first" line segment will be drawn from the bottom left corner of the
// bounds, up and towards the right. Start the loop N iterations "back" to
// draw partial line segments beneath (or to the left) of this segment,
// where N is the number of offsets that fit inside the smaller dimension of
// the bounds.
final smallSide = isVertical ? bounds.width : bounds.height;
final start = -(smallSide / offset).round() * offset;
// Keep going until we reach the top or right of the bounds, depending on
// whether the rectangle is oriented vertically or horizontally.
final end = size + offset;
// Create gradient for line painter if top bounds exceeded.
ui.Shader lineShader;
if (drawAreaBounds != null && bounds.top < drawAreaBounds.top) {
lineShader = _createHintGradient(
drawAreaBounds.left.toDouble(), drawAreaBounds.top.toDouble(), fill);
}
for (int i = start; i < end; i = i + offset) {
// For vertical bounds, we need to draw lines from top to bottom. For
// bounds, we need to draw lines from left to right.
final modifier = isVertical ? -1 * i : i;
// Draw a line segment in the bottom right corner of the pattern.
_linePainter.draw(
canvas: canvas,
paint: _paint,
points: [
new Point(x0 + modifier, y0),
new Point(x1 + modifier, y1),
],
stroke: fill,
strokeWidthPx: fillWidthPx,
shader: lineShader);
}
}
@override
set drawingView(String viewName) {}
}