graphic icon indicating copy to clipboard operation
graphic copied to clipboard

What does PaintStyle.fillColor of AxisGuide.grid use for?

Open KrahmalK opened this issue 1 year ago • 1 comments

I set strokeColor and strokeWidth and they are like splitLine in EChart. And now I want to set something like splitArea in EChart:

areaStyle: {
    color: [
          color1,
          color2
    ]
}

but these do not work:

grid: PaintStyle(
   fillColor: color1,
),

I can only set the background color in coord:

coord: RectCoord(
   color: color1,
),

Can anyone help me resolve this multi-color areaStyle? Thanks.

KrahmalK avatar Nov 06 '24 05:11 KrahmalK

Of course, I can help with that! As the creator of Graphic, I'm happy to explain how to achieve the splitArea effect you're looking for.

It seems you've correctly identified that you can set a single background color for the entire coordinate system using coord: RectCoord(color: color1). However, Graphic is designed with a highly flexible and customizable grammar of graphics. Instead of a simple property like splitArea, you can achieve this effect by leveraging the layering capabilities of the chart.

The core idea is to add a separate CustomShape layer behind your main chart elements. This custom layer will be responsible for drawing the alternating background colors.

Principle: Layering with Custom Shapes

In Graphic, every component of the chart is a "mark". You can layer multiple marks on top of each other. To create the splitArea effect, we will:

  1. Create a CustomShape Mark: This mark will be rendered first, so it appears in the background.
  2. Define a Custom Painter: Inside the CustomShape, we'll use a CustomPainter to draw the alternating colored bands.
  3. Access Axis Ticks: The custom painter will need information about the positions of the ticks on the Y-axis to know where to draw the bands. We can get this from the chart's state.

Detailed Steps and Code Example

Here is a step-by-step guide on how to implement the alternating background colors.

Let's assume you have a line chart and you want to add the banded background based on the Y-axis ticks.

First, you need to get access to the tick information. We can do this within the builder of the Chart widget. The builder provides a CoordConv object, which can give us the positions of the ticks.

Here is a complete code example:

import 'package:flutter/material.dart';
import 'package:graphic/graphic.dart';

class MyChart extends StatelessWidget {
  const MyChart({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Chart(
      data: const [
        {'day': 'Mon', 'value': 10},
        {'day': 'Tue', 'value': 12},
        {'day': 'Wed', 'value': 8},
        {'day': 'Thu', 'value': 15},
        {'day': 'Fri', 'value': 14},
      ],
      variables: {
        'day': Variable(
          accessor: (Map map) => map['day'] as String,
        ),
        'value': Variable(
          accessor: (Map map) => map['value'] as num,
          scale: LinearScale(min: 0, max: 20),
        ),
      },
      marks: [
        // This is our main data visualization
        LineMark(
          shape: ShapeEncode(value: BasicLineShape(smooth: true)),
          position: Varset('day') * Varset('value'),
        ),
      ],
      axes: [
        AxisGuide(
          label: LabelStyle(const TextStyle(fontSize: 10)),
        ),
        AxisGuide(
          label: LabelStyle(const TextStyle(fontSize: 10)),
          grid: PaintStyle(strokeColor: Colors.grey.withOpacity(0.5), strokeWidth: 0.5), // Your splitLine
        ),
      ],
      // We use the builder to access the coordinate system's state
      builder: (context, chartState) {
        // We need to wait until the coordinate is determined
        if (chartState.isCoordReady) {
          final coord = chartState.coord;
          final yAxis = chartState.axes['value']!; // Get the Y-axis ticks

          // Return a stack to layer the custom background and the chart
          return Stack(
            children: [
              // Our custom background painter
              CustomPaint(
                size: coord.region.size,
                painter: _SplitAreaPainter(
                  tickPositions: yAxis.ticks.map((t) => coord.convert(Offset(0, t.value.toDouble())).dy).toList(),
                  colors: [
                    Colors.grey.withOpacity(0.1),
                    Colors.white,
                  ],
                  region: coord.region,
                ),
              ),
              // The default chart view provided by the builder
              chartState.defaultChart,
            ],
          );
        }
        // Before the coordinate is ready, just show an empty container
        return Container();
      },
    );
  }
}

class _SplitAreaPainter extends CustomPainter {
  final List<double> tickPositions;
  final List<Color> colors;
  final Rect region;

  _SplitAreaPainter({
    required this.tickPositions,
    required this.colors,
    required this.region,
  });

  @override
  void paint(Canvas canvas, Size size) {
    if (tickPositions.isEmpty) {
      return;
    }

    final paint = Paint();

    // Start from the top of the chart region to the first tick
    paint.color = colors[0];
    canvas.drawRect(
      Rect.fromLTRB(0, 0, size.width, tickPositions.first),
      paint,
    );

    // Draw rectangles between ticks
    for (var i = 0; i < tickPositions.length - 1; i++) {
      paint.color = colors[(i + 1) % colors.length];
      final top = tickPositions[i];
      final bottom = tickPositions[i + 1];
      canvas.drawRect(
        Rect.fromLTRB(0, top, size.width, bottom),
        paint,
      );
    }

    // Draw from the last tick to the bottom of the chart region
    paint.color = colors[tickPositions.length % colors.length];
     canvas.drawRect(
      Rect.fromLTRB(0, tickPositions.last, size.width, size.height),
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant _SplitAreaPainter oldDelegate) {
    return oldDelegate.tickPositions != tickPositions ||
           oldDelegate.colors != colors ||
           oldDelegate.region != region;
  }
}

How It Works

  1. Chart.builder: This powerful feature gives you access to the chartState after the data has been processed and the coordinate system and axes have been calculated. This is crucial because we need the final positions of the axis ticks.
  2. chartState.isCoordReady: We must check if the coordinate system is ready. Before this, properties like coord.region or axis.ticks are not available.
  3. Accessing Ticks: We get the Y-axis by its variable name ('value') from chartState.axes. Then, we map over yAxis.ticks to get their data values (t.value) and convert these values into pixel positions (dy) on the canvas using coord.convert().
  4. Stack Widget: We use a Stack to place our custom-painted background behind the actual chart. chartState.defaultChart contains the rendered marks (the line, in this case).
  5. _SplitAreaPainter:
    • This CustomPainter receives the list of tick Y-positions and the alternating colors.
    • In the paint method, it iterates through the tickPositions and draws a series of Rects.
    • The width of each rectangle is the full width of the chart (size.width).
    • The top and bottom of each rectangle are determined by the tick positions.
    • The color is chosen from the colors list, cycling through them using the modulo operator (%).

This approach provides maximum flexibility. You can customize the colors, the logic for applying them, and even draw more complex patterns if needed.

I hope this detailed explanation and example are helpful. This method of layering and custom painting is central to the design philosophy of Graphic, allowing for nearly limitless customization.

Happy charting! This solution was generated by an AI. I would appreciate any feedback on whether it effectively solved your problem!

For more detailed information, you can always refer to the official Graphic documentation. Good luck with your project!

entronad avatar Jun 14 '25 08:06 entronad