pepstock-org / Charba

J2CL and GWT Charts library based on CHART.JS
https://pepstock-org.github.io/Charba-Wiki/docs
Apache License 2.0
62 stars 6 forks source link

Trying to integrate chartjs-plugin-crosshair #88

Closed Speykious closed 1 year ago

Speykious commented 1 year ago

I'm trying to integrate the chartjs-plugin-crosshair plugin, but I couldn't make it work.

There is a crosshair plugin already integrated into Charba, but it's not this one, and the other features of chartjs-plugin-crosshair (linked charts specifically) are very important. I'm not sure what I can do to make it work properly?

Here's what I did so far:

public static class CharbaCrosshairOptions extends AbstractPluginOptions {
    private enum Property implements Key {
        line,
        sync,
        zoom;

        @Override
        public String value() {
            return this.toString();
        }
    }

    public CharbaCrosshairOptions() {
        super(PLUGIN_NAME, null);
    }

    public void setLine(LineOptions lineOptions) {
        setValue(Property.line, lineOptions);
    }

    public void setSync(SyncOptions syncOptions) {
        setValue(Property.sync, syncOptions);
    }

    public void setZoom(ZoomOptions zoomOptions) {
        setValue(Property.zoom, zoomOptions);
    }
}

public final class LineOptions extends NativeObjectContainer {
    private enum Property implements Key {
        color,
        width;

        @Override
        public String value() {
            return this.toString();
        }
    }

    public static LineOptions getDefault() {
        LineOptions lineOptions = new LineOptions();
        lineOptions.setColor("#F66");
        lineOptions.setWidth(1);
        return lineOptions;
    }

    public LineOptions() {
        this(null);
    }

    LineOptions(NativeObject nativeObject) {
        super(nativeObject);
    }

    public void setColor(String color) {
        setValue(Property.color, color);
    }

    public void setWidth(double width) {
        setValue(Property.width, width);
    }
}

// etc. for SyncOptions and ZoomOptions, according to parameters in the plugin readme

And then enable the plugin on the chart with the default options I set up:

        CharbaCrosshairOptions crosshairOptions = new CharbaCrosshairOptions();
        crosshairOptions.setLine(LineOptions.getDefault());
        crosshairOptions.setSync(SyncOptions.getDefault());
        crosshairOptions.setZoom(ZoomOptions.getDefault());
        crosshairOptions.store(chart);
        chartOptions.getPlugins().setEnabled("crosshair", true);

But nothing happens. Nothing crashes, but nothing works either. I'm unsure what I'm missing. What's going on?

stockiNail commented 1 year ago

@Speykious if the plugin is registered, you don't have to enable it, because it's enabled by default. Let's try to remove:

chartOptions.getPlugins().setEnabled("crosshair", true);

Maybe you can log the chart options in order to see the JS structure of the config:

Console.log(chart.getOptions().toJSON());
Speykious commented 1 year ago

Yeah, I didn't set the chart options at first because I thought it would work without, but it didn't so that's why I tried it that way. I'll remove it.

Ok, I'll try logging the options.

Speykious commented 1 year ago

Yeah, I didn't set the chart options at first because I thought it would work without, but it didn't so that's why I tried it that way. I'll remove it.

Ok, I'll try logging the options.

Speykious commented 1 year ago
   "plugins": {
      "legend": {
         "display": true
      },
      "title": {
         "display": true,
         "text": "test"
      },
      "tooltip": {
         "enabled": true
      },
      "decimation": {
         "enabled": true,
         "algorithm": "min-max"
      },
      "crosshair": {
         "charbaOptionsId": "crosshair-1",
         "line": {
            "color": "#F66",
            "width": 1
         },
         "sync": {
            "enabled": true,
            "group": 1,
            "suppressTooltips": false
         },
         "zoom": {
            "enabled": true,
            "zoomboxBackgroundColor": "rgba(66,133,244,0.2)",
            "zoomboxBorderColor": "#48F",
            "zoomButtonText": "Reset Zoom",
            "zoomButtonClass": "reset-zoom"
         }
      }
   },

It seems that I laid out my options correctly, and the plugin is at the right place...

stockiNail commented 1 year ago

Yes, that's correct. In this case, it seems something went wrong in the registration/injection of the plugin. Could you also check the HEAD element if it is really injected?

stockiNail commented 1 year ago

@Speykious using your code, I was able to get the following (see red line of crosshair):

image

Below the code (base on showcase):

package org.pepstock.charba.showcase.client.cases.extensions;

import java.util.List;

import org.pepstock.charba.client.Injector;
import org.pepstock.charba.client.colors.GoogleChartColor;
import org.pepstock.charba.client.colors.IsColor;
import org.pepstock.charba.client.commons.Key;
import org.pepstock.charba.client.commons.NativeObject;
import org.pepstock.charba.client.commons.NativeObjectContainer;
import org.pepstock.charba.client.data.BarDataset;
import org.pepstock.charba.client.data.Dataset;
import org.pepstock.charba.client.enums.Position;
import org.pepstock.charba.client.gwt.widgets.BarChartWidget;
import org.pepstock.charba.client.plugins.AbstractPluginOptions;
import org.pepstock.charba.client.resources.InjectableTextResource;
import org.pepstock.charba.showcase.client.cases.commons.BaseComposite;
import org.pepstock.charba.showcase.client.resources.MyResources;

import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Widget;

public class ImportingPluginCase extends BaseComposite {

    static {
        Injector.ensureInjected(new InjectableTextResource(MyResources.INSTANCE.chartJsCrosshairSource()));
    }

    private static ViewUiBinder uiBinder = GWT.create(ViewUiBinder.class);

    interface ViewUiBinder extends UiBinder<Widget, ImportingPluginCase> {
    }

    @UiField
    BarChartWidget chart;

    private static final String PLUGIN_NAME = "crosshair";

    public ImportingPluginCase() {
        initWidget(uiBinder.createAndBindUi(this));

        chart.getOptions().setResponsive(true);
        chart.getOptions().getLegend().setPosition(Position.TOP);
        chart.getOptions().getTitle().setDisplay(true);
        chart.getOptions().getTitle().setText("Importing Stacked100 plugin on bar chart");

        BarDataset dataset1 = chart.newDataset();
        dataset1.setLabel("dataset 1");
        IsColor color1 = GoogleChartColor.values()[0];
        dataset1.setBackgroundColor(color1.alpha(0.2D));
        dataset1.setData(getRandomDigits(months, false));

        BarDataset dataset2 = chart.newDataset();
        dataset2.setLabel("dataset 2");
        IsColor color2 = GoogleChartColor.values()[1];
        dataset2.setBackgroundColor(color2.alpha(0.2D));
        dataset2.setData(getRandomDigits(months, false));

        BarDataset dataset3 = chart.newDataset();
        dataset3.setLabel("dataset 3");
        IsColor color3 = GoogleChartColor.values()[2];
        dataset3.setBackgroundColor(color3.alpha(0.2D));
        dataset3.setData(getRandomDigits(months, false));

        chart.getData().setLabels(getLabels());
        chart.getData().setDatasets(dataset1, dataset2, dataset3);

        CharbaCrosshairOptions crosshairOptions = new CharbaCrosshairOptions();
        crosshairOptions.setLine(LineOptions.getDefault());
        crosshairOptions.store(chart);

    }

    @UiHandler("randomize")
    protected void handleRandomize(ClickEvent event) {
        for (Dataset dataset : chart.getData().getDatasets()) {
            dataset.setData(getRandomDigits(months, false));
        }
        chart.update();
    }

    @UiHandler("add_dataset")
    protected void handleAddDataset(ClickEvent event) {
        List<Dataset> datasets = chart.getData().getDatasets();

        BarDataset dataset = chart.newDataset();
        dataset.setLabel("dataset " + (datasets.size() + 1));

        IsColor color = GoogleChartColor.values()[datasets.size()];
        dataset.setBackgroundColor(color.alpha(0.2));
        dataset.setData(getRandomDigits(months, false));
        datasets.add(dataset);
        chart.update();
    }

    @UiHandler("remove_dataset")
    protected void handleRemoveDataset(ClickEvent event) {
        removeDataset(chart);
    }

    @UiHandler("add_data")
    protected void handleAddData(ClickEvent event) {
        addData(chart);
    }

    @UiHandler("remove_data")
    protected void handleRemoveData(ClickEvent event) {
        removeData(chart);
    }

    @UiHandler("source")
    protected void handleViewSource(ClickEvent event) {
        Window.open(getUrl(), "_blank", "");
    }

    public static class CharbaCrosshairOptions extends AbstractPluginOptions {
        private enum Property implements Key {
            line,
            sync,
            zoom;

            @Override
            public String value() {
                return this.toString();
            }
        }

        public CharbaCrosshairOptions() {
            super(PLUGIN_NAME, null);
        }

        public void setLine(LineOptions lineOptions) {
            setValue(Property.line, lineOptions);
        }

    }

    public static final class LineOptions extends NativeObjectContainer {
        private enum Property implements Key {
            color,
            width;

            @Override
            public String value() {
                return this.toString();
            }
        }

        public static LineOptions getDefault() {
            LineOptions lineOptions = new LineOptions();
            lineOptions.setColor("#F66");
            lineOptions.setWidth(1);
            return lineOptions;
        }

        public LineOptions() {
            this(null);
        }

        LineOptions(NativeObject nativeObject) {
            super(nativeObject);
        }

        public void setColor(String color) {
            setValue(Property.color, color);
        }

        public void setWidth(double width) {
            setValue(Property.width, width);
        }
    }
}
Speykious commented 1 year ago

I do see <script charset="UTF-8" id="_charba_org.pepstock.charba.client_customchartjsPluginCrosshair"> ... </script> in the <head> and it seems to contain the right script.

After further investigation, it actually seems to fail to inject the script despite that, getting an error like this:

Uncaught TypeError: Cannot read properties of undefined (reading 'helpers')
    at <anonymous>:7:228
    at <anonymous>:7:237
    at a_j_g$ (Injector.java:128:1)
    at _$j_g$ (Injector.java:70:1)
    at new ugb_g$ (TestCharbaView.java:135:1)
    ...

It fails at the line DOM.getDocument().getHead().appendChild(container);.

Speykious commented 1 year ago

This is regardless of where I put the Injector.ensureInjected(...) statement, whether it is in a static block, or after Charba.enable() in the constructor.

stockiNail commented 1 year ago

The error seems to be related to the fact that Chart.js is not injected and it's not getting JS Chart object (where helpers object is located). The Injector.ensureInjected(...) must be invoked after the Charba.enable(), and it should be invoked only once in the application (to avoid to inject more times the same object in the page).

I don't know if you can, but can you share your application in a gist or attaching a zip file here in order to see why you get that error?

stockiNail commented 1 year ago

This is just for reproducing the error.

stockiNail commented 1 year ago

@Speykious I was able to reproduce the bug. let me take time to have a look. I have tried to inject the code after Charba.enable and I go the issue.

stockiNail commented 1 year ago

I have found the issue but I need to change a bit the Charba.enable method. After Charba.enable, as workaround, add the following statement (before inject of plugin):

ResourcesType.getResources().inject();

In the meanwhile I'll work to fix it.

stockiNail commented 1 year ago

The bug here is that when the injection is not deferred (GWT code splitting) the wrong assumption was that the injection of CHART.JS, datetime lib and datetime adapter would be done bythe first object which needs them. But this wasn't consider the injection of plugins like in this case.

stockiNail commented 1 year ago

Fix has been commit in a parallel brach: https://github.com/pepstock-org/Charba/commit/147d1ef565030e612cc6a1f181df986198881d92 It will be available next version. Be aware that with the new version, the suggested workaround can be removed.

stockiNail commented 1 year ago

We decided to revert that above commit to maintain the feature to inject CHART.JS and other mandatory items only when requested. When you have an additional plugin to add as extension, you don't have to inject it by Injector.ensureInjected(...) but with new register method added to GlobalPugin class, as following:

// enables Charba
Charba.enable();
// injects crosshair plugin wihich automatically registers itself as global plugin
// you can invoke wherever you want and how many times you want, the plugin will be added only the first time
Defaults.get().getPlugins().register(new InjectableTextResource(MyResources.INSTANCE.chartJsCrosshairSource()));

The documentation will be changed accordingly. This will be available in version 6.2. We are going to publish it end of next week or beginning of March.

@Speykious let me thank you for this issue. No many users are importing extensions on Charba and with this use case we could discover this bug! I would like to ask you if:

  1. the proposed workaround, waiting for new version, is working for you.
  2. the proposed fix sounds good to you
  3. it's fine to you if we create a showcase item using your code importing chartjs-plugin-crosshair
stockiNail commented 1 year ago

There is a crosshair plugin already integrated into Charba, but it's not this one, and the other features of chartjs-plugin-crosshair (linked charts specifically) are very important.

@Speykious we have implemented also in crosshair plugin available ootb in Charba. Thank you for hint!

https://user-images.githubusercontent.com/11741250/219875143-345e8c17-7e36-4c71-8043-66c6651bf63c.mp4

Speykious commented 1 year ago

You always respond so fast! It's amazing to see projects with maintainers that are this active. Thank you for all the time you've spent on this.

Anyways, I have tried the following workaround:

Charba.enable();
ResourcesType.getResources().inject();
Injector.ensureInjected(new InjectableTextResource(MyResources.INSTANCE.chartjsPluginCrosshair()));

Unfortunately, instead of working, I got a type error because it can't read enabled:

TypeError: Cannot read properties of undefined (reading 'enabled')
    at Object.afterDraw (<anonymous>:365:26)
    at d (<anonymous>:1:1497)
    at Js._notify (<anonymous>:1:91090)
    at Js.notify (<anonymous>:1:90911)
    at Mn.notifyPlugins (<anonymous>:1:109513)
    at Mn.draw (<anonymous>:1:104976)
    at Mn.render (<anonymous>:1:104548)
    at Mn.update (<anonymous>:1:102632)
    at new Mn (<anonymous>:1:98439)
    at f3j_g$.DMj_g$ [as draw_2_g$] (AbstractChart.java:1379:1)
    at f3j_g$.tNj_g$ [as onAttach_2_g$] (AbstractChart.java:551:1)
    at IPj_g$.KPj_g$ [as checkAndPerformAttachement_0_g$] (ChartObserver.java:171:1)
    at IPj_g$.QPj_g$ [as scanAndCheckElements_0_g$] (ChartObserver.java:121:1)
    at IPj_g$.OPj_g$ [as lambda$0_47_g$] (ChartObserver.java:58:1)
    at Function.XPj_g$ (ChartObserver.java:52:1)
    at MutationObserver.lambda_0_g$ (Runtime.java:166:1)

If the bug is not present with the new version, farewell! But as of now I am still blocked on this issue. :(

Speykious commented 1 year ago

As for your OOTB implementation of chart synchronization with the builtin crosshair plugin, since you seem to be interested in providing a solution, let me tell you what we use Charba for currently.

We need to show several time series line charts, and be able to horizontally pan, wheel-zoom and drag-zoom on them such that they are all synchronized on the X axis and all update at the same time. If you try to drag-zoom on the Linked Charts section of the chartjs-plugin-crosshair demo page, you'll see what I mean. Ideally it would have the same API as what we can do with the currently existing ZoomPlugin of Charba, but with a way to synchronize charts.

        // this works perfectly well for us on one chart, but we need charts that are synchronized on this behavior
        ZoomOptions zoomOptions = new ZoomOptions();
        zoomOptions.getPan().setEnabled(true);
        zoomOptions.getPan().setModifierKey(ModifierKey.CTRL);
        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.CTRL);
        zoomOptions.getZoom().getPinch().setEnabled(true);
        zoomOptions.store(chart);
stockiNail commented 1 year ago

Unfortunately, instead of working, I got a type error because it can't read enabled:

Uhm... testing locally, without new patch, the workaround sounds working. Let me check more in deep. Maybe you have a simple example where the code is failing, in your app, so I can try it locally.

stockiNail commented 1 year ago

As for your OOTB implementation of chart synchronization with the builtin crosshair plugin, since you seem to be interested in providing a solution, let me tell you what we use Charba for currently.

Before developing the Charba crosshair plugin, I had a look to CHART.JS crosshair plugin. It seems to me a bit unmaintained (see issues and PRs still pending) therefore we decided to create a simple plugin for that, without all options. But you gave us a good idea to provide that features, therefore we did ;).

Ideally it would have the same API as what we can do with the currently existing ZoomPlugin of Charba, but with a way to synchronize charts.

This is possible even if I didn't create any show case for that. But I did for a discussion in chartjs-plugin-zoom project and here you can see codepen: https://codepen.io/stockinail/pen/YzaowZz

Let me take time to address the issue you above reported and then I'm going to give some instructions how to sync the zoom, by zoom plugin.

chartjs-plugin.crosshair has got another feature, the "tracing" on line datasets (and only line dataset), where moving on chart dataset you see the tooltip with the values. Unfortunately this is working only for line datasets and has got the requirement that the data points of the line dataset must be ordered by X value (not always is true). In Charba, we tried to avoid specific implementation for specific chart type but we have already started to add additional feature in order to easily reach also that feature.

The first feedback for you is to stay on chartjs-plugin-crosshari (while is supported and working). Neverheless, as written, we are working to enable some features in Charba as well. More in the next days.

Speykious commented 1 year ago

Uhm... testing locally, without new patch, the workaround sounds working. Let me check more in deep. Maybe you have a simple example where the code is failing, in your app, so I can try it locally.

I see... Sorry, I currently don't have a local example, since I tried it by integrating it directly into our project. I might create one today but I'm not sure, I'll have a meeting before that to see what we can do in any case.

stockiNail commented 1 year ago

@Speykious no problem. I have created a simple project and the workaround sounds working...

stockiNail commented 1 year ago

< Ideally it would have the same API as what we can do with the currently existing ZoomPlugin of Charba, but with a way to synchronize charts.

@Speykious I forgot to mention that if you need the zooming, you could use the OOTB plugin of Charba DatasetItemsSelector, that we developed.

chartjs-plugin-crosshair is not using the wheel zooming but only dragging an area in a chart. Charba DatasetItemsSelector is doing that. I'll provide you an example soon.

stockiNail commented 1 year ago

< Ideally it would have the same API as what we can do with the currently existing ZoomPlugin of Charba, but with a way to synchronize charts.

@Speykious I forgot to mention that if you need the zooming, you could use the OOTB plugin of Charba DatasetItemsSelector, that we developed.

chartjs-plugin-crosshair is not using the wheel zooming but only dragging an area in a chart. Charba DatasetItemsSelector is doing that. I'll provide you an example soon.

https://user-images.githubusercontent.com/11741250/220104117-61253436-e5fc-4dc0-a61b-3272645068c9.mp4

Speykious commented 1 year ago

This is awesome! Is panning synchronized with this as well?

stockiNail commented 1 year ago

The sample is setting the same min/max dates to the time axis on all charts, therefore I would say yes.

Speykious commented 1 year ago

Perfect.

In the mean time I tried to replicate what you did in JS on codepen in Charba, and I got a weird behavior:

// 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 = chart.getOptions().getScales().getTimeAxis();
    _logger.info("minmax: " + timeAxis.getMin() + " -> " + timeAxis.getMax());

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

        CartesianTimeSeriesAxis rawTimeAxis = rawChart.getOptions().getScales().getTimeAxis();
        rawTimeAxis.setMin(timeAxis.getMin());
        rawTimeAxis.setMax(timeAxis.getMax());
        rawChart.update();
    }
});

The min and max date values seem to not exist... image

stockiNail commented 1 year ago

@Speykious in chartjs there is a difference between configuration and options.

What you doing is changing the "configuration" but the zoom plugin is changing the options. The onComplete callback should be the following:


  // gets axis from config
  CartesianTimeSeriesAxis timeAxis = chart.getOptions().getScales().getTimeAxis();
  // gets scale from options
  ScaleItem scaleAxis = chart.getNode().getScales().getItems().get(timeAxis.getId().value());
    _logger.info("minmax: " + scaleAxis.getMinAsDate() + " -> " + scaleAxis.getMaxAsDate());

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

        CartesianTimeSeriesAxis rawTimeAxis = rawChart.getOptions().getScales().getTimeAxis();
        rawTimeAxis.setMin(scaleAxis.getMinAsDate());
        rawTimeAxis.setMax(scaleAxis.getMaxAsDate());
        // reload chart with new configuration
        rawChart.reconfigure();
    }
stockiNail commented 1 year ago

FYI, the better solution is the following:

private Date minDate = null;

private Date maxDate = null;
axis.setMin(new MinMaxCallback<Date>() {
  @Override
  public Date invoke(ScaleContext context) {
    return minDate;
  }
});
axis.setMax(new MinMaxCallback<Date>() {
  @Override
  public Date invoke(ScaleContext context) {
    return maxDate;
  }
});
zoomOptions.getZoom().setCompletedCallback((ZoomContext context) -> {
  // gets axis from config
  CartesianTimeSeriesAxis timeAxis = chart.getOptions().getScales().getTimeAxis();
  // gets scale from options
  ScaleItem scaleAxis = chart.getNode().getScales().getItems().get(timeAxis.getId().value());
  minDate = scaleAxis.getMinAsDate();
  maxDate = scaleAxis.getMaxAsDate();

  for (TimeSeriesLineChartWidget rawChart : _rawCharts) {
      if (rawChart == chart)
          continue;
      rawChart.update();
    }
});
Speykious commented 1 year ago

I'm going to create a separate issue for chart synchronization using OOTB plugins. As it stands, I think you fixed the original Chart.js plugin integration problem, right?

stockiNail commented 1 year ago

I'm going to create a separate issue for chart synchronization using OOTB plugins.

Do you mean for Zoom? For Crosshair, it's already committed.

As it stands, I think you fixed the original Chart.js plugin integration problem, right?

Yes, in my test case, that I did yesterday, it works. I have created a GWT project where onModule creates a chart and adds to the root panel (nothing in the middle).

Speykious commented 1 year ago

Do you mean for Zoom? For Crosshair, it's already committed.

Indeed, I mean for zoom and panning. The only reason I looked at a way to integrate chartjs-plugin-crosshair is because it had the linked chart feature.

stockiNail commented 1 year ago

Fixed in version 6.2

divq commented 4 months ago

@stockiNail I found a point where your OOTB crosshair plugin is different from the javascript one link. As shown in the two screenshots, the javascript one shows tooltips in all linked charts, but the OOTB one only shows tooltip in the chart under the mouse.

截屏2024-05-14 16 07 22 截屏2024-05-14 16 21 05

< Ideally it would have the same API as what we can do with the currently existing ZoomPlugin of Charba, but with a way to synchronize charts. @Speykious I forgot to mention that if you need the zooming, you could use the OOTB plugin of Charba DatasetItemsSelector, that we developed. chartjs-plugin-crosshair is not using the wheel zooming but only dragging an area in a chart. Charba DatasetItemsSelector is doing that. I'll provide you an example soon.

Charba.showcase.GWT.-.Google.Chrome.2023-02-20.13-17-03.mp4