0

I've been playing around with the CatmullRomSpline class from Flutter the past few days. Drawing points with is trivial, however, I've been looking for information on interpolating between two sets of values and haven't had much luck.

to give a better overview here is the first plot of offsets: 1

This is the next plot of offsets that I would like to morph/animate to:

2

Here is the code I have so far:

import 'dart:ui';

import 'package:curves/data.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;

import 'models/price_plot.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Home(),
    );
  }
}

class Home extends StatefulWidget {
  const Home({
    Key? key,
  }) : super(key: key);

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
  late Animation<List<Offset>> animation;
  late AnimationController controller;

  List<Offset> controlPoints = [];

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

    WidgetsBinding.instance.addPostFrameCallback((_) {
      controlPoints = buildControlPoints(hourItems);

      setState(() {});
    });
  }

  List<Offset> buildControlPoints(List<List<double>> items) {
    final max = items.map((x) => x.last).fold<double>(
          items.first.last,
          math.max,
        );
    final min = items.map((x) => x.last).fold<double>(
          items.first.last,
          math.min,
        );

    final range = max - min;

    final priceMap = {
      for (var v in items)
        v.last: ((max - v.last) / range) * MediaQuery.of(context).size.height,
    };

    final pricePlots =
        priceMap.entries.map((e) => PricePlot(e.value.toInt(), e.key)).toList();

    return controlPoints = List.generate(
      pricePlots.length,
      (index) {
        return Offset(
          calculateXOffset(
              MediaQuery.of(context).size.width, pricePlots, index),
          pricePlots[index].yAxis.toDouble(),
        );
      },
    ).toList();
  }

  double calculateXOffset(double width, List<PricePlot> list, int index) {
    if (index == (list.length - 1)) {
      return width;
    }

    return index == 0 ? 0 : width ~/ list.length * (index + 1);
  }

  void _startAnimation() {
    controller.stop();
    controller.reset();
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("App"),
      ),
      body: Stack(
        children: [
          CustomPaint(
            painter: SplinePainter(controlPoints),
            size: Size(MediaQuery.of(context).size.width,
                MediaQuery.of(context).size.height),
          ),
          FloatingActionButton(onPressed: () {
            _startAnimation();
          })
        ],
      ),
    );
  }
}

class SplinePainter extends CustomPainter {
  final List<Offset> items;

  SplinePainter(this.items);

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawPaint(Paint()..color = Colors.white);

    if (items.isEmpty) {
      return;
    }

    final spline = CatmullRomSpline(items);

    final bezierPaint = Paint()
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 2
      ..color = Colors.blue;

    canvas.drawPoints(
      PointMode.points,
      spline.generateSamples(tolerance: 1e-17).map((e) => e.value).toList(),
      bezierPaint,
    );
  }

  @override
  bool shouldRepaint(SplinePainter oldDelegate) => true;
}

Daniel Hakimi
  • 1,153
  • 9
  • 16

1 Answers1

0

not coding in your environment but I would try:

  1. lets have p0[n0] and p1[n1] control points for your curves

  2. let n = max(n0,n1)

  3. resample the less sampled curve to n points

  4. linearly interpolate between corresponding curve points

    p[i] = p0[i] + (p1[i]-p0[i])*t
    

    where t is linear parameter in range <0.0,1.0>

So to animate just render p[n] with t=0.0 wait a bit, increase tby small amount render again and so on until you hit t=1.0 ...

If you share your control point I would make a simple C++ example and grab a GIF animation but without I would need to extract the points from image which is a problem as you did not mark control point in your plot... and the extraction is much more work than the resampling and animation itself...

After your comments and looking at the link of yours its obvious you do not have 2 datasets but just one and just changing the sampling rate ...

So I taken the more dense data you provided and create second set by skipping points ... so booth sets covering the same time interval just have different number of points but still uniformly sampled...

Putting all together I created a small C++/VCL example of doing this:

//$$---- Form CPP ----
//---------------------------------------------------------------------------
#include <vcl.h>
#include "List.h"
#include "str.h"
#pragma hdrstop
#include "win_main.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
// https://stackoverflow.com/a/66565537/2521214
//---------------------------------------------------------------------------
int xs,ys;              // screen resolution
Graphics::TBitmap *bmp; // back buffer bitmap for rendering
const int N=1024;
List<double> p0,p1,p;
double x0,y0,x1,y1,mx,my;
//---------------------------------------------------------------------------
void dat_load(List<double> &p,AnsiString filename)
    {
    BYTE *dat;
    int hnd,siz,adr,i;
    AnsiString s,ss;
    p.reset();
    hnd=FileOpen(filename,fmOpenRead);
    if (hnd==-1) return;
    siz=FileSeek(hnd,0,2);
        FileSeek(hnd,0,0);
    dat=new BYTE[siz];
    if (dat==NULL){ FileClose(hnd); return; }
    FileRead(hnd,dat,siz);
    FileClose(hnd);
    for (adr=0;adr<siz;)
        {
        s=txt_load_str('[',']',dat,siz,adr,true);
        if (s=="") break;
        if (s[1]=='[') s[1]=' ';
        i=1;
        p.add(str2num(str_load_str(s,i,true)));
        p.add(str2num(str_load_str(s,i,true)));
        if (s[s.Length()]==']') break;;
        }
    delete[] dat;
    }
//---------------------------------------------------------------------------
void dat_getpnt(double &x,double &y,double *p,int n,double t)
    {
    int i0,i1,i2,i3;
    double tt,ttt,a0,a1,a2,a3,d1,d2;
    // bount parameter
    if (t<0.0) t=0.0;
    if (t>  n) t=n;
    // control points indexes
    i1=floor(t); i1&=0xFFFFFFFE;
    t=(t-i1)*0.5; tt*t*t; ttt=tt*t;
             if (i1>=n) i1=n-2;
    i0=i1-2; if (i0< 0) i0=  0;
    i2=i1+2; if (i2>=n) i2=n-2;
    i3=i2+2; if (i3>=n) i3=n-2;
    // cubic x
    d1=0.5*(p[i2]-p[i0]);
    d2=0.5*(p[i3]-p[i1]);
    a0=p[i1];
    a1=d1;
    a2=(3.0*(p[i2]-p[i1]))-(2.0*d1)-d2;
    a3=d1+d2+(2.0*(-p[i2]+p[i1]));
    x=a0+a1*t+a2*t*t+a3*t*t*t;
    // cubic y
    i0++; i1++; i2++; i3++;
    d1=0.5*(p[i2]-p[i0]);
    d2=0.5*(p[i3]-p[i1]);
    a0=p[i1];
    a1=d1;
    a2=(3.0*(p[i2]-p[i1]))-(2.0*d1)-d2;
    a3=d1+d2+(2.0*(-p[i2]+p[i1]));
    y=a0+a1*t+a2*t*t+a3*t*t*t;
    }
//---------------------------------------------------------------------------
void dat_draw(double *p,int n)
    {
    int e;
    double x,y,t,N=n;
    for (e=1,t=0.0;e;t+=0.1)
        {
        if (t>=N){ e=0; t=N; }
        dat_getpnt(x,y,p,n,t);
        x=x-x0; x*=mx;
        y=y-y0; y*=my;
        if (e==1){ bmp->Canvas->MoveTo(x,y); e=2; }
        else       bmp->Canvas->LineTo(x,y);
        }
    }
//---------------------------------------------------------------------------
void dat_draw(double *p0,int n0,double *p1,int n1,double t)
    {
    int i,N;
    double x,y,xx0,yy0,xx1,yy1,t0,t1,dt0,dt1;
    N=n0; if (N<n1) N=n1;
    dt0=double(n0)/double(N-1);
    dt1=double(n1)/double(N-1);
    for (t0=t1=0.0,i=0;i<N;i++,t0+=dt0,t1+=dt1)
        {
        dat_getpnt(xx0,yy0,p0,n0,t0);
        dat_getpnt(xx1,yy1,p1,n1,t1);
        x=xx0+((xx1-xx0)*t);
        y=yy0+((yy1-yy0)*t);
        x=x-x0; x*=mx;
        y=y-y0; y*=my;
        if (i==0) bmp->Canvas->MoveTo(x,y);
        else      bmp->Canvas->LineTo(x,y);
        }
    }
//---------------------------------------------------------------------------
void draw() // just render of my App
    {
    bmp->Canvas->Brush->Color=clWhite;
    bmp->Canvas->FillRect(TRect(0,0,xs,ys));

    bmp->Canvas->Pen->Color=clLtGray; dat_draw(p0.dat,p0.num);
    bmp->Canvas->Pen->Color=clLtGray; dat_draw(p1.dat,p1.num);
    static double t=0.0,dt=0.1;
    bmp->Canvas->Pen->Color=clBlue;   dat_draw(p0.dat,p0.num,p1.dat,p1.num,t);
    t+=dt; if ((dt>0.0)&&(t>=1.0)){ t=1.0; dt=-dt; }
           if ((dt<0.0)&&(t<=0.0)){ t=0.0; dt=-dt; }

    Form1->Canvas->Draw(0,0,bmp);
//  bmp->SaveToFile("out.bmp");
    }
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner) // init of my app
    {
    // init backbuffer
    bmp=new Graphics::TBitmap;
    bmp->HandleType=bmDIB;
    bmp->PixelFormat=pf32bit;

    // load data
    dat_load(p0,"in.txt");
    // compute BBOX and create smaller dataset
    int i; double x,y;
    for (i=0;i<p0.num;)
        {
        x=p0[i]; i++;
        y=p0[i]; i++;
        if (i==2){ x0=x; y0=y; x1=x; y1=y; }
        if (x0>x) x0=x; if (x1<x) x1=x;
        if (y0>y) y0=y; if (y1<y) y1=y;
        if (i%60==2){ p1.add(x); p1.add(y); }
        }
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender) // not important just destructor of my App
    {
    delete bmp;
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormResize(TObject *Sender) // not important just resize event
    {
    xs=ClientWidth;
    ys=ClientHeight;
    bmp->Width=xs;
    bmp->Height=ys;
    mx=xs/(x1-x0);
    my=ys/(y1-y0);
    draw();
    }
//-------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender) // not important just repaint event
    {
    draw();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::Timer1Timer(TObject *Sender)
    {
    draw();
    }
//---------------------------------------------------------------------------

You can ignore most of the stuff, the only important parts of code are functions:

void dat_getpnt(double &x,double &y,double *p,int n,double t);
void dat_draw(double *p0,int n0,double *p1,int n1,double t);

the first just obtains point on piecewise cubic curve p[n] with parameter t=<0.0,n>. The other one uses the first function to obtain corresponding points on both curve and linearly interpolate between them with parameter t=<0.0,1.0> to render interpolated curve.

The function void draw(); render the graph and animate the interpolation parameter ... its called in timer with 100ms interval...

Beware my data is just 1D array of x,y coordinates so n0,n1 are twice the number of points !!!

Here preview:

preview

The graph transition in the link of yours adds also animation of scale and scroll of time (x axis) but that is just a matter of changing view parameters ...

Spektre
  • 49,595
  • 11
  • 110
  • 380
  • Thanks for the information! Here is where I get the sample data from: https://www.coindesk.com/pf/api/v3/content/fetch/chart-api?query=%7B%22end_date%22%3A%222022-06-28T12%3A07%22%2C%22iso%22%3A%22BTC%22%2C%22ohlc%22%3Afalse%2C%22start_date%22%3A%222022-06-27T12%3A07%22%7D&d=175&_website=coindesk – Daniel Hakimi Jun 28 '22 at 12:10
  • @DanielHakimi that is single or both curves? – Spektre Jun 28 '22 at 12:15
  • Apologies, here is the other data set: https://www.coindesk.com/pf/api/v3/content/fetch/chart-api?query=%7B%22end_date%22%3A%222022-06-28T12%3A22%22%2C%22iso%22%3A%22BTC%22%2C%22ohlc%22%3Afalse%2C%22start_date%22%3A%222021-06-28T12%3A22%22%7D&d=175&_website=coindesk – Daniel Hakimi Jun 28 '22 at 12:23
  • Just incase you want to fiddle with more data: https://www.coindesk.com/price/bitcoin/ – Daniel Hakimi Jun 28 '22 at 12:25
  • @DanielHakimi how do you want to handle time? those 2 datasets overlaps only a little the 1 day step is just very small part of the 1 minute set ... so you want to interpolate only corresponding time? – Spektre Jun 28 '22 at 13:03
  • Have you seen the coin base app? If you pick one of the charts and switch between the times it “morphs” to the new data points. Like you said, the data set on a lower time-frame has less points than a higher time-frame. Do you know how Coinbase interpolates between smaller sets of data to larger seamlessly? – Daniel Hakimi Jun 28 '22 at 17:52
  • @DanielHakimi As I see it that is not animation between 2 datasets .. there is just one dataset and its just redrawed with different time scale ... – Spektre Jun 28 '22 at 20:07
  • @DanielHakimi Add C++/VCL code example and preview – Spektre Jun 29 '22 at 08:53