Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Linked Charts with Charba: small date intervals can be a problem #89

Closed
Speykious opened this issue Feb 21, 2023 · 10 comments
Closed

Linked Charts with Charba: small date intervals can be a problem #89

Speykious opened this issue Feb 21, 2023 · 10 comments

Comments

@Speykious
Copy link

I want to be able to create linked time series charts with Charba, where zooming and panning in one zooms in the other.

The previous issue #88 derived into a discussion about this, so I wanted to properly continue it here.
When doing my tests to synchronize charts, I realized that it wasn't working because of the random dataset example I was working with.

With the following code:

private TimeSeriesLineChartWidget createLineChart() {
    TimeSeriesLineChartWidget chart = new TimeSeriesLineChartWidget();

    TimeSeriesLineOptions chartOptions = chart.getOptions();
    chartOptions.setResponsive(true);
    chartOptions.setAspectRatio(3.5);
    chartOptions.setMaintainAspectRatio(true);
    chartOptions.getLegend().setDisplay(true);
    chartOptions.getTitle().setDisplay(true);
    chartOptions.getTitle().setText("oui");
    chartOptions.getTooltips().setEnabled(true);
    chartOptions.setAnimationEnabled(false);
    chartOptions.getDecimation().setEnabled(true);
    chartOptions.getDecimation().setAlgorithm(DecimationAlgorithm.MIN_MAX);

    // tooltip interaction options
    Interaction interaction = chartOptions.getInteraction();
    interaction.setMode(InteractionMode.NEAREST);
    interaction.setAxis(InteractionAxis.X);
    interaction.setIntersect(false);

    // axes options
    CartesianTimeSeriesAxis xAxis = chartOptions.getScales().getTimeAxis();
    xAxis.getTitle().setDisplay(true);
    xAxis.getTitle().setText("Time");
    xAxis.getTicks().setSource(TickSource.DATA);
    xAxis.getTime().setUnit(TimeUnit.SECOND);
    xAxis.getTime().getDisplayFormats().setDisplayFormat(TimeUnit.SECOND, "m’ss”");
    xAxis.getTime().getDisplayFormats().setDisplayFormat(TimeUnit.MINUTE, "H:mm:ss");
    xAxis.getTime().getDisplayFormats().setDisplayFormat(TimeUnit.HOUR, "H:mm:ss");

    xAxis.setMin((ScaleContext context) -> {
        return _minDate;
    });

    xAxis.setMax((ScaleContext context) -> {
        return _maxDate;
    });

    CartesianLinearAxis yAxis = chartOptions.getScales().getLinearAxis();
    yAxis.getTitle().setDisplay(true);
    yAxis.getTitle().setText("Fromage");
    yAxis.setDisplay(true);
    yAxis.setBeginAtZero(true);

    // zoom options
    ZoomOptions zoomOptions = new ZoomOptions();
    zoomOptions.getPan().setEnabled(true);
    zoomOptions.getPan().setModifierKey(ModifierKey.ALT);
    zoomOptions.getPan().setMode(Mode.X);
    zoomOptions.getZoom().setMode(Mode.X);
    zoomOptions.getZoom().getDrag().setEnabled(true);
    zoomOptions.getZoom().getWheel().setEnabled(true);
    zoomOptions.getZoom().getWheel().setSpeed(0.3);
    zoomOptions.getZoom().getWheel().setModifierKey(ModifierKey.ALT);
    zoomOptions.getZoom().getPinch().setEnabled(true);
    zoomOptions.store(chart);

    zoomOptions.getZoom().setCompletedCallback((ZoomContext context) -> {
        CartesianTimeSeriesAxis timeAxis = chartOptions.getScales().getTimeAxis();
        ScaleItem scaleAxis = chart.getNode().getScales().getItems().get(timeAxis.getId().value());

        for (TimeSeriesLineChartWidget rawChart : _rawCharts) {
            if (rawChart == chart)
                continue;
            _minDate = scaleAxis.getMinAsDate();
            _maxDate = scaleAxis.getMaxAsDate();
            rawChart.update();
        }
    });

    long start = new Date().getTime();

    TimeSeriesItem[] data = new TimeSeriesItem[20];
    for (int i = 0; i < data.length; i++)
        data[i] = new TimeSeriesItem(new Date((long) i + start), Random.nextDouble());

    // dataset
    TimeSeriesLineDataset dataset = chart.newDataset();
    dataset.setLabel("fromage");
    dataset.setBorderColor(Color.CHARBA);
    dataset.setBorderWidth(1);
    dataset.setPointRadius(0);
    dataset.setParsing(false);
    dataset.setTimeSeriesData(data);

    chart.getData().setDatasets(dataset);

    _rawCharts.add(chart);
    return chart;
}

as soon as I try to zoom, the synchronization completely breaks and the second graph shows nothing (the actual graph is there, but hidden far to the right).
image

I realized after hours of debugging, with the help of a coworker, that the dataset may have been too short and too compact - maybe I was losing precision on the dates?

And so I tried the code above, with these modifications:

     TimeSeriesItem[] data = new TimeSeriesItem[20];
     for (int i = 0; i < data.length; i++)
-        data[i] = new TimeSeriesItem(new Date((long) i + start), Random.nextDouble());
+        data[i] = new TimeSeriesItem(new Date(((long) i) * 17271 + start), Random.nextDouble());

And now it works!
image

I'm glad I managed to fix my problem, but I was wondering why the dates I generated created this problem in the first place. For our use-case, it will probably not be relevant, but it's probably important to mention here that such a thing is happening.

@Speykious Speykious changed the title Linked Charts with Charba: small datasets can be a problem Linked Charts with Charba: small date intervals can be a problem Feb 21, 2023
@stockiNail
Copy link
Contributor

@Speykious thank you very much. I think Charba cannot do many things to solve but I wanted to test it and have a look to JS plugin (having already done some PRs overthere). Maybe the plugin (JS I meant) can be fixed.

@stockiNail
Copy link
Contributor

stockiNail commented Feb 21, 2023

@Speykious as shared, here is a sample javascript based where the defect you found is present also using natively the zoom plugin.

https://codepen.io/stockinail/pen/RwYabEv

EDIT: removing truncation, the codepen is working correctly. Therefore it sounds working well

@stockiNail
Copy link
Contributor

@Speykious the issue is that the zoom plugin (but in whole CHART.JS world) the dates are managed as numbers (epoch). Therefore the zoom in a chart instance set, for example, min = 2.5 but when the date object is created, 0.5 is lost (in the other chart instance there is the difference). This is the reason why it doesn't work now and why with higher numbers as time, it works better.
Let me think what I can do for that.

@stockiNail
Copy link
Contributor

@Speykious I have found the solution, but unfortunately it could be a breaking change for Charba.

Currently the callback for min and max in time/timeSeries axis accepts only Date as return value. I have changed locally, changing the generics type from Date to Object, in order to accept also double, and it works.

image

The data that I have loaded have got epoch from 0 to 20.

The _minDate and _maxDate are now double and onComplete, I read min and max as double (instead of Date) from the scale.

I think I can try to add it to next version (minor) even if it's a breaking change... Hopefully it could not create a big mess... But we have to think about.

@Speykious
Copy link
Author

I assume it is a breaking change because of the method type changing. But fundamentally, if we continue to use Dates, it shouldn't change, no?

@stockiNail
Copy link
Contributor

The breaking change is only in MinMaxCallback class where the generics for time axis will not be a Date (as is today) but an Object. In this way the user can return a Date or a Number and it will be stored as number for min or max. Today a number is already stored but the time of a Date.

That said, the breaking change is ONLY for users who are not using the lambda to define the scriptable options.
To work with time axis, we can continue (must!) working with Date, nothing changes here. Only for MinMaxCallback.
In your use case, where you want to keep aligned more charts by Zoom plugin, the scriptable option should return a number (and not a Date anymore). This is the only change.

Nevertheless, I'm thinking to add it in next version, anyway, because many users are using lambda and nothing change for them.

I'm attaching here the code, FYI, see my comments related to Date --> double:

package org.pepstock.charba.elemento.client;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.pepstock.charba.client.Charba;
import org.pepstock.charba.client.callbacks.ScaleContext;
import org.pepstock.charba.client.colors.Color;
import org.pepstock.charba.client.configuration.CartesianLinearAxis;
import org.pepstock.charba.client.configuration.CartesianTimeSeriesAxis;
import org.pepstock.charba.client.configuration.TimeSeriesLineOptions;
import org.pepstock.charba.client.data.TimeSeriesItem;
import org.pepstock.charba.client.data.TimeSeriesLineDataset;
import org.pepstock.charba.client.enums.Bounds;
import org.pepstock.charba.client.enums.ModifierKey;
import org.pepstock.charba.client.enums.TickSource;
import org.pepstock.charba.client.enums.TimeUnit;
import org.pepstock.charba.client.gwt.widgets.TimeSeriesLineChartWidget;
import org.pepstock.charba.client.items.ScaleItem;
import org.pepstock.charba.client.zoom.ZoomContext;
import org.pepstock.charba.client.zoom.ZoomOptions;
import org.pepstock.charba.client.zoom.ZoomPlugin;
import org.pepstock.charba.client.zoom.enums.Mode;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.Random;
import com.google.gwt.user.client.ui.RootPanel;

public class Main implements EntryPoint {

	private double _minDate = Double.NaN; // <-- as double instead of Date
	private double _maxDate = Double.NaN; // <-- as double instead of Date

	private List<TimeSeriesLineChartWidget> _rawCharts = new ArrayList<>();

	@Override
	public void onModuleLoad() {

		Charba.enable();

		ZoomPlugin.enable();
		TimeSeriesLineChartWidget chart1 = createLineChart();
		TimeSeriesLineChartWidget chart2 = createLineChart();

		_rawCharts.add(chart1);
		_rawCharts.add(chart2);

		RootPanel.get().add(chart1);
		RootPanel.get().add(chart2);
	}

	private TimeSeriesLineChartWidget createLineChart() {
		TimeSeriesLineChartWidget chart = new TimeSeriesLineChartWidget();

		TimeSeriesLineOptions chartOptions = chart.getOptions();
		chartOptions.setResponsive(true);
		chartOptions.setAspectRatio(5);
		chartOptions.setMaintainAspectRatio(true);
		chartOptions.getLegend().setDisplay(true);
		chartOptions.getTitle().setDisplay(true);
		chartOptions.getTitle().setText("oui");
		chartOptions.getTooltips().setEnabled(true);

		// axes options
		CartesianTimeSeriesAxis xAxis = chartOptions.getScales().getTimeAxis();
		xAxis.getTitle().setDisplay(true);
		xAxis.getTitle().setText("Time");
		xAxis.getTicks().setSource(TickSource.DATA);
		xAxis.getTime().setUnit(TimeUnit.MILLISECOND);
		xAxis.getTime().getDisplayFormats().setDisplayFormat(TimeUnit.SECOND, "m’ss”");
		xAxis.getTime().getDisplayFormats().setDisplayFormat(TimeUnit.MINUTE, "H:mm:ss");
		xAxis.getTime().getDisplayFormats().setDisplayFormat(TimeUnit.HOUR, "H:mm:ss");

		xAxis.setMin((ScaleContext context) -> {
			return _minDate; // <-- return double value instead of Date
		});

		xAxis.setMax((ScaleContext context) -> {
			return _maxDate; // <-- return double value instead of Date
		});
		CartesianLinearAxis yAxis = chartOptions.getScales().getLinearAxis();
		yAxis.getTitle().setDisplay(true);
		yAxis.getTitle().setText("Fromage");
		yAxis.setDisplay(true);
		yAxis.setBeginAtZero(true);

		// zoom options
		ZoomOptions zoomOptions = new ZoomOptions();
		zoomOptions.getPan().setEnabled(false);
		zoomOptions.getPan().setModifierKey(ModifierKey.ALT);
		zoomOptions.getPan().setMode(Mode.X);
		zoomOptions.getZoom().setMode(Mode.X);
		zoomOptions.getZoom().getDrag().setEnabled(false);
		zoomOptions.getZoom().getWheel().setEnabled(true);
		zoomOptions.getZoom().getWheel().setSpeed(0.3);
		zoomOptions.getZoom().getPinch().setEnabled(false);
		zoomOptions.store(chart);

		zoomOptions.getZoom().setCompletedCallback((ZoomContext context) -> {
			CartesianTimeSeriesAxis timeAxis = chartOptions.getScales().getTimeAxis();
			ScaleItem scaleAxis = chart.getNode().getScales().getItems().get(timeAxis.getId().value());
			_minDate = scaleAxis.getMin(); // <-- get double value instead of Date
			_maxDate = scaleAxis.getMax(); // <-- get double value instead of Date

			for (TimeSeriesLineChartWidget rawChart : _rawCharts) {
				if (rawChart == chart)
				  continue;
				rawChart.update();
			}
		});

		TimeSeriesItem[] data = new TimeSeriesItem[20];
		for (int i = 0; i < data.length; i++)
			data[i] = new TimeSeriesItem(new Date((long) i), Random.nextDouble());

		// dataset
		TimeSeriesLineDataset dataset = chart.newDataset();
		dataset.setLabel("fromage");
		dataset.setBorderColor(Color.CHARBA);
		dataset.setBorderWidth(1);
		dataset.setPointRadius(3);
		dataset.setTimeSeriesData(data);

		chart.getData().setDatasets(dataset);

		_rawCharts.add(chart);
		return chart;
	}

}

@stockiNail
Copy link
Contributor

@Speykious I had more time yesterday to go in deep. You don't need new Charba version to link more charts.
The key point is that all chart instances must be changed by the plugin and not setting the min or max to all others. Therefore we need to use the zoom plugin api to keep aligned all charts instance.

If you can try the following:

  1. remove scriptable options to min and max (not needed) from time axis.
  2. remove _minDate and _maxDate (not needed)
  3. use the following onComplete code:
zoomOptions.getZoom().setCompletedCallback((ZoomContext context) -> {
        CartesianTimeSeriesAxis timeAxis = chartOptions.getScales().getTimeAxis();
        ScaleItem scaleAxis = chart.getNode().getScales().getItems().get(timeAxis.getId().value());
	double minDate = scaleAxis.getMin();
	double maxDate = scaleAxis.getMax();
	ScaleRange range = new ScaleRange(minDate, maxDate);

	for (IsChart rawChart : CHARTS) {
		if (rawChart == cCahrt)
		  continue;
		ZoomPlugin.zoomScale(rawChart, axis.getId(), range); // <-- zoom the other charts
	}
});

@Speykious
Copy link
Author

That works too! Thanks for your time.

@stockiNail
Copy link
Contributor

That works too! Thanks for your time.

The last code is also solving the problem related to the reset of the zoom. With the other version, it didn't work.
FYI, I'll add a show case related to zooming on linked charts.
Thank you too!

I'll close the issue when version 6.2 will be released. The same for #88.

@stockiNail
Copy link
Contributor

Fixed in version 6.2

Added a show case with linked charts instances and zooming: Ext. plugins --> Zoom Plugin --> Zoom wheel grouping

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants