Blog

Build beautiful charts for mobile using Flutter with D3.js in one day
AllFlutter

Build beautiful charts for mobile using Flutter with D3.js in one day

Rating 4.78 / 5 based on 4 reviews
MobileAppMVP
9.09.2024

You’re tasked with creating an amazing animated chart for your Flutter project. You decide to explore what Flutter offers and come across the awesome library fl_chart. And boom—you find that the chart types are limited. Sure, you could build a new chart type from scratch, but that would take time and resources.

I have an another solution for you that you can implement in just one day. Scroll to the bottom for source code!

Idea behind

The overall approach is as follows. I will use the D3.js framework, a powerful JavaScript library known for creating highly customizable, data-driven visualizations. D3.js allows developers to manipulate documents based on data, making it ideal for crafting dynamic and interactive charts that can respond to user interactions and data updates in real time.

Next, I will prepare the chart to ensure dynamic visualization capabilities and create a static view using Bun and Elysia. Bun, a fast JavaScript runtime, will help handle server-side rendering and dependencies efficiently, while Elysia will provide a streamlined environment to build and optimize our assets for the WebView.

Elysia is said to be 21x faster than Express

Finally, I will use the Flutter WebView widget to display container, embedding the D3.js-powered chart directly within the Flutter application. The results are impressive, combining the rich interactivity of D3.js with the smooth performance of Flutter. Best of all, the entire implementation process is surprisingly quick and straightforward.

At first, the idea of using foreign (for Dart/Flutter developers) frameworks might seem intimidating. However, for a regular Dart/Flutter developer, the skills required are not too complex to understand and implement this integration.

Tools

Steps

Choose D3.js charts

When choosing the right type of chart for your Flutter project, it’s important to consider both the data you’re visualizing and the experience you want to deliver to users. For dynamic and engaging charts, D3.js offers a wide variety of options that can be integrated seamlessly using WebView in Flutter.

For my example app, I chose Streamgraph and Zoomable Circles.

Create bun application

To get started with integrating D3.js into your Flutter app, the first step is to initialize a Bun application. To initialize a Bun app, you need to set up a new project directory and install the necessary dependencies. Start by running the command in terminal, which will guide you through creating a new Bun project.

bun init

This command sets up the project structure and generates essential files like package.json, which you can customize according to your requirements.

Add Elysia for routes and static files

Once Bun app is initialized, the next step is to add the necessary packages to serve static files and build API routes. For this, we use Elysia, a lightweight and efficient web framework for Bun. To get started, run the command.

bun add elysia

This will install Elysia and add it to project’s dependencies, enabling you to create fast and concise server-side routes with minimal overhead. After adding Elysia, you’ll need to install the @elysiajs/static plugin to serve static assets like HTML, CSS, and JavaScript files. Run the command to include this plugin in project.

bun add @elysiajs/static

It provides a simple and effective way to serve D3.js visualizations and other static content directly from Bun server. With these, you can easily set up a powerful backend environment that handles both API requests and static file serving, perfectly complementing Flutter app’s front-end WebView integration.

At the end you should have similar output in package.json. Note, I added scripts section specifying the index.ts path. It selects the entry point for the app and allows running the app by the following commands.

bun install
bun run dev
{
  "name": "d3view",
  "module": "index.ts",
  "type": "module",
  "scripts": {
    "dev": "bun run --hot index.ts"
  },
  "devDependencies": {
    "@types/bun": "latest"
  },
  "peerDependencies": {
    "typescript": "^5.0.0"
  },
  "dependencies": {
    "@elysiajs/static": "^1.1.1",
    "elysia": "1.1.12"
  }
}

Initialize the server

To set up and clarify index.ts file, you need to ensure that Bun server is properly configured to serve static files and specific routes for D3.js. First, you import the necessary modules: staticPlugin from @elysiajs/static for serving static content and Elysia from elysia to create server instance.

Next, you initialize a new server instance by creating an Elysia object and use the .use() method to apply the staticPlugin(). This plugin will handle the serving of static files from a designated directory, making D3.js assets accessible via an URL.

import staticPlugin from "@elysiajs/static";
import { Elysia } from "elysia";

const server = new Elysia().use(staticPlugin());

Then, you define two specific routes, /example-1 and /example-2, using the .get() method of the server instance. These routes respond to GET requests by serving HTML files from the public directory. This way, you can create different routes for different examples, making it easy to serve multiple D3.js charts from Bun server.

Finally, you call server.listen(3000) to start the server on port 3000. With this setup, everything is configured to serve both static files and specific D3.js visualizations, ready to be embedded in a Flutter WebView for a rich, interactive experience.

import staticPlugin from "@elysiajs/static";
import { Elysia } from "elysia";

const server = new Elysia().use(staticPlugin());

server.get("/example-1", () => Bun.file("public/example-1.html"));
server.get("/example-2", () => Bun.file("public/example-2.html"));

server.listen(3000);

Add the Streamgraph chart

Start by creating a public folder in the root directory of Bun project. This folder will hold all the static files that server will serve, including the HTML files for D3.js.

Inside the public folder, create a new file named example-1.html. This name corresponds to the path you defined in index.ts file for serving the HTML content. This file will be responsible for rendering the Streamgraph chart. Begin by setting up a basic HTML structure that includes a head section with meta tags, and a link to the D3.js library. Here’s the basic setup you’ll need.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title> Your title</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 0;
        padding: 0;
        overflow: hidden; // Disables scrolling
        display: flex;
        align-items: center;
        justify-content: center;
      }
      svg {
        display: block;
        max-width: 100%;
        max-height: 100%;
      }
    </style>
  </head>

Now, go to Streamgraph example chart and and copy the script provided there. Paste the script directly inside the <body>. You may also need to copy and paste data section. Play and adjust until you find the best output.

<!doctype html>
<html lang="en">
  <head>
     .......
  </head>
  <body>
    <script>
               //// PASTE HERE
    </script>
  </body>
</html>

To see the chart live, you need to run the Bun server. Open terminal and navigate to project directory. Then run the following commands.

bun install
bun run dev

Open web browser and enter the following URL.

http://localhost:3000/example-1

Add Zoomable Circles chart

Repeat the steps you used to add the Streamgraph chart. This time, you’ll implement a Zoomable Circles. chart. Begin by creating an HTML file named example-2.html inside the public folder to serve the new visualization and prepare basic structure.

Add the data source needed for the Zoomable Circles chart. Create a new JSON file named flare-2.json inside the public folder. This file should contain the hierarchical data that will be used to create the zoomable visualization. Data can be downloaded from example’s page. Then, in example-2.html file, add the D3.js script to load and render the Zoomable Circles chart.

<script>
  d3.json("./public/flare-2.json").then(function (data) {
    const width = 640;
    const height = 480;

    // D3.js Zoomable Circles Chart Implementation
    // (Insert the D3.js script to create zoomable circles using the loaded 'data')
  });
</script>

To enable communication between Flutter and the JavaScript running in WebView, you will use a Flutter JavaScript Channel. This will allow you to send messages from Flutter to the JavaScript, such as adjusting the color scale of the chart.

At the top of script inside <body>, add the following method to create a FlutterChannel object.

<script>
  window.FlutterChannel = {
    postMessage: function (message) {
      // Adjust colors based on the message from Flutter  
      console.log("Received message from Flutter:", message);  
      
      if (message.startsWith("setColorScale:")) {
        const scaleValue = parseFloat(message.split(":")[1]);
        window.updateColorScale(scaleValue);
      }
    },
  };
</script>

Now, implement the JavaScript function to change the colors of the chart dynamically based on input from the Flutter app. Add the following script to file.

<script>
  window.updateColorScale = function (value) {
    const colorScale = d3
      .scaleLinear()
      .domain([0, 5])
      .range([`hsl(${(120 * value) % 360},80%,80%)`, `hsl(${(240 * value) % 360},30%,40%)`])
      .interpolate(d3.interpolateHcl);

    updateColors();
  };

  function updateColors() {
    node.attr("fill", (d) => (d.children ? colorScale(d.depth) : "white"));
    svg.style("background", colorScale(0));
  }

  window.FlutterChannel = {
    postMessage: function (message) {
      console.log("Received message from Flutter:", message);  // Adjust colors based on the message from Flutter

      if (message.startsWith("setColorScale:")) {
        const scaleValue = parseFloat(message.split(":")[1]);
        window.updateColorScale(scaleValue);
      }
    },
  };
</script>

To see the Zoomable Circles chart live with the color-changing functionality open browser and navigate to.

http://localhost:3000/example-2

Create new Flutter project

Just follow the basic steps for project creation and prepare layout. Open terminal and run the following command to create a new Flutter project.

flutter create example

Add webview to pubspec.yaml.

name: example
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0

dependencies:
  webview_flutter: 4.9.0

Open the lib/main.dart file and prepare layout. I will use a Column widget with two Expanded widgets to display the Streamgraph and Zoomable Circles charts on the main page. Replace the existing code with the following.

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

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: SafeArea(
          child: Padding(
            padding: EdgeInsets.symmetric(horizontal: 50.0),
            child: Column(
              mainAxisSize: MainAxisSize.max,
              children: [
                Expanded(child: Streamgraph()),
                SizedBox(height: 40),
                Expanded(child: ZoomableCircles()),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Define a Streamgraph widget to render the chart using a WebView.

class Streamgraph extends StatefulWidget {
  const Streamgraph({super.key});

  @override
  State<Streamgraph> createState() => _StreamgraphState();
}

class _StreamgraphState extends State<Streamgraph> {
  final _controller = WebViewController();

  @override
  void initState() {
    super.initState();

    _controller
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(Colors.transparent)
      ..loadRequest(Uri.parse('http://localhost:3000/example-1'));
  }

  @override
  void dispose() {
    super.dispose();
    _controller.clearCache();
    _controller.clearLocalStorage();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 100.0),
      child: WebViewWidget(controller: _controller),
    );
  }
}

Define a ZoomableCircles widget to render the chart and enable interaction via a JavaScript channel:

class ZoomableCircles extends StatefulWidget {
  const ZoomableCircles({super.key});

  @override
  State<ZoomableCircles> createState() => _ZoomableCirclesState();
}

class _ZoomableCirclesState extends State<ZoomableCircles> {
  final _controller = WebViewController();
  final _javaScriptChannelName = 'FlutterChannel';
  var _sliderValue = 0.1;

  @override
  void initState() {
    super.initState();

    _controller
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(Colors.transparent)
      ..loadRequest(Uri.parse('http://localhost:3000/example-2'));
  }

  @override
  void dispose() {
    super.dispose();
    _controller.clearCache();
    _controller.clearLocalStorage();
    _controller.removeJavaScriptChannel(_javaScriptChannelName);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        SizedBox(
          height: 250,
          child: WebViewWidget(controller: _controller),
        ),
        Slider.adaptive(
          value: _sliderValue,
          onChanged: (val) {
            _controller.runJavaScript("""        window.FlutterChannel.postMessage('setColorScale:$val');
            """);

            setState(() {
              _sliderValue = val;
            });
          },
        ),
      ],
    );
  }
}

The Slider.adaptive widget allows the user to select a value from a range. In this specific implementation, the slider is used to adjust the color scale of the Zoomable Circles chart by sending a message from Flutter to the JavaScript running inside the WebView.

Inside the onChanged callback, I send a message to the JavaScript code running in the WebView. It communicates to the JavaScript environment that the color scale should be updated. The value val is appended to the message and will be parsed by the JavaScript code in the HTML file.

_controller.runJavaScript("""
window.FlutterChannel.postMessage('setColorScale:$val');
""");

Testing

To test the integration of the Streamgraph and Zoomable Circles charts within your Flutter application, you need to ensure that both the Flutter app and the Bun server are running properly. Start by launching the Bun server to serve the HTML files containing the D3.js.

Next, run the Flutter application on an emulator or a connected device using the flutter run command. Once the app is running, you should see two sections: one displaying the Streamgraph chart and the other the Zoomable Circles chart.

Providing data

Providing data to a D3.js chart in a Flutter app can be done either by passing parameters using a FlutterChannel to communicate between Flutter and JavaScript or by URL parameter. The former approach is straightforward but limited in flexibility, while the latter offers more control and real-time interactivity at the cost of increased complexity.

Development of charts

However, while this tutorial provides an example, you may want to develop directly with D3.js for more customized visualizations. You can add D3.js as a package, use it as vanilla HTML or integrate it in other ways. For larger projects, I recommend installing D3.js via a package manager and creating your project files accordingly for better maintainability and scalability.

For more detailed guidance, I suggest referring to the official D3.js official docs.

Pros and cons

The approach has significant advantages: it enables quick development of highly dynamic and interactive charts without needing to build them from scratch in Flutter. D3.js is a powerful and widely-used framework for data visualization, offering a vast array of customizable chart types. By leveraging WebView, you can easily integrate these sophisticated visualizations into your Flutter app, providing an engaging user experience. Overall, while the setup may be more complex, the benefits of rapid development and access to high-quality, customizable charts outweigh the downsides for many projects.

On the downside, it requires the app to be online to fetch data from the Bun server, and developers must have a basic understanding of JavaScript to manipulate D3.js effectively.

Deployment

The Bun server can be deployed using cloud platforms such as Heroku, Coolify, or any Docker-based solution. Heroku provides a straightforward platform for deploying Node-like environments and handles scaling automatically. Docker offers flexibility for containerized deployments, allowing you to run the Bun server on any cloud provider that supports Docker, such as AWS or DigitalOcean. Coolify is another alternative that simplifies the deployment process and integrates well with Docker.

If needed, I can provide a detailed tutorial in a second part, covering step-by-step instructions for deploying with Docker, Heroku, or Coolify, along with setting up CI/CD pipelines for a smooth deployment process.

Source

Please visit: https://github.com/codigee-devs/d3-in-flutter

Rate this article
4.78 / 5 based on 4 reviews
Maksym Kulicki
Maksym KulickiCTO

NEWSLETTER


Get latest insights ideas and inspiration


NEWSLETTER
Get latest insights, ideas and inspiration

Take your app development and management further with Codigee

HIRE US

Let's make something together.


If you have any questions about a new project or other inquiries, feel free to contact us. We will get back to you as soon as possible.

We await your application.


At Codigee, we are looking for technology enthusiasts who like to work in a unique atmosphere and derive pure satisfaction from new challenges. Don't wait and join the ranks of Flutter leaders now!

We are using cookies. Learn more