reactjstypescriptchartsamchartsamcharts5

How can I trigger a mouse click and drag event in amCharts v5?


I am using amCharts v5 to produce a similar function to this amCharts v3 example.

In this example it starts an event with "chart.isMouseDown" property and changes bullets' position according to cursor's position. When the mouse left click is released the event finishes. After this, if user wants to drag bullets he/she clicks on bullets and drags them to trigger another event. I couldn't find a similar event in amCharts v5 to understand whether the chart is clicked and the click is not released until a certain point.

In my code, I have an XYChart, a XYCursor to work with this chart. A Series to have the data and two axis. As far as I have searched there is a "click" event for the XYChart but it fires once. I want to understand if the click is held and when the click is released. I reached a similar usage by using "pointerover" event for the XYChart and "cursormoved" and "cursorhidden" events for the XYCursor but it is not exactly what I want.

Below, you can find the example for in order to have an idea. handleDraw is the method which I want to be triggered with the mouse click and released afterwards. handleDrag is the aftermath method which allows bullets to be dragged:

useLayoutEffect(() => {
let root = am5.Root.new(id);
root.utc = true;
let mouseIsDown = false;

let chart = root.container.children.push(
  am5xy.XYChart.new(root, {
    panX: false,
    panY: false,
    pinchZoomX: false,
    pinchZoomY: false,
    interactive: true,
    interactiveChildren: true,
  })
);

chart.events.on("pointerover", function (e) {
  handleDraw(e);
});
chart.events.on("pointerdown", function (e) {
  handleDraw(e);
});

let cursor = chart.set("cursor", am5xy.XYCursor.new(root, {}));
cursor.lineY.set("visible", true);
cursor.lineX.set("visible", true);

var xAxisM = chart.xAxes.push(
  am5xy.DateAxis.new(root, {
    baseInterval: { timeUnit: "month", count: 1 },
    markUnitChange: false,
    extraMin: 0.02,
    extraMax: 0.02,
    tooltip: am5.Tooltip.new(root, { position: "relative" }),
    tooltipDateFormat: "MM/yy",
    marginTop: 10,
    renderer: am5xy.AxisRendererX.new(root, {
      minGridDistance: 20,
    }),
    dateFormats: { month: "MM" },
    periodChangeDateFormats: { month: "MM" },
  })
);

xAxisM.get("renderer").labels.template.setAll({
  fill: am5.color("#19214d"),
  scale: 0.7,
});

var xAxisY = chart.xAxes.push(
  am5xy.DateAxis.new(root, {
    baseInterval: {
      timeUnit: "year",
      count: 1,
    },
    renderer: am5xy.AxisRendererX.new(root, {
      minGridDistance: 20,
      strokeOpacity: 0,
      fill: am5.color("#19214d"),
    }),
    dateFormats: { year: "yyyy" },
    periodChangeDateFormats: { year: "yyyy" },
  })
);

xAxisY.get("renderer").grid.template.setAll({
  forceHidden: true,
});

xAxisY.get("renderer").labels.template.setAll({
  fill: am5.color("#19214d"),
  scale: 0.7,
});

let yAxis = chart.yAxes.push(
  am5xy.ValueAxis.new(root, {
    renderer: am5xy.AxisRendererY.new(root, {}),
    marginRight: 10,
  })
);

yAxis.get("renderer").labels.template.setAll({
  scale: 0.7,
  fill: am5.color("#19214d"),
});

let marketSeries = chart.series.push(
  am5xy.LineSeries.new(root, {
    name: "Market Series",
    xAxis: xAxisM,
    yAxis: yAxis,
    valueYField: "value",
    valueXField: "date",
    tooltip: am5.Tooltip.new(root, {
      labelText: "{valueY}",
    }),
    fill: am5.color(marketValues.color),
    stroke: am5.color(marketValues.color),
    width: 4,
  })
);

let marketSeriesY = chart.series.push(
  am5xy.LineSeries.new(root, {
    name: "Market Series",
    xAxis: xAxisY,
    yAxis: yAxis,
    valueYField: "value",
    valueXField: "date",
    forceHidden: true,
  })
);

let userSeries = chart.series.push(
  am5xy.LineSeries.new(root, {
    name: "User Series",
    xAxis: xAxisM,
    yAxis: yAxis,
    valueYField: "value",
    valueXField: "date",
    tooltip: am5.Tooltip.new(root, {
      labelText: "{valueY}",
    }),
    fill: am5.color(userValues.color),
    stroke: am5.color(userValues.color),
  })
);

userSeries.strokes.template.setAll({
  templateField: "strokeSettings",
});

marketSeries.bullets.push(function () {
  let bulletCircle = am5.Circle.new(root, {
    radius: 2,
    stroke: am5.color(marketValues.color),
    strokeWidth: 1.5,
  });

  return am5.Bullet.new(root, {
    sprite: bulletCircle,
  });
});

userSeries.bullets.push(function () {
  let bulletCircle = am5.Circle.new(root, {
    radius: 2,
    strokeWidth: 1.5,
    templateField: "bulletSettings",
  });

  return am5.Bullet.new(root, {
    sprite: bulletCircle,
  });
});

userSeries.bullets.push(() => {
  let bulletCircle = am5.Circle.new(root, {
    radius: 6,
    fillOpacity: 0,
    fill: userSeries.get("fill"),
    draggable: true,
    cursorOverStyle: "pointer",
  });

  bulletCircle.events.on("dragged", function (e) {
    handleDrag(e);
  });

  bulletCircle.events.on("dragstop", function (e) {
    handleChangeUserValues(userSeries);
  });

  return am5.Bullet.new(root, {
    sprite: bulletCircle,
  });
});

const handleDrag = (e: any) => {
  let point = chart.plotContainer.toLocal(e.point);

  let date: any = xAxisM.positionToValue(
    xAxisM.coordinateToPosition(point.x)
  );
  date = dayjs(date);
  if (date.isBefore(dayjs())) {
    return;
  }
  let value = round(
    yAxis.positionToValue(yAxis.coordinateToPosition(point.y)),
    2
  );

  let dataItem = e.target.dataItem;
  dataItem.set("valueY", value);
  dataItem.set("valueYWorking", value);
};

cursor.events.on("cursormoved", function (e) {
  console.log("merhaba");
  mouseIsDown = true;
});
cursor.events.on("cursorhidden", function (e) {
  console.log("merhaba stop");
  mouseIsDown = false;
  handleChangeUserValues(userSeries);
});


const userData: any = [];
userValues.data.forEach((value: any, index: number) => {
  let date = xValues.data[index];
  date = dayjs(date, DATE_FORMAT);

  userData.push({
    value: value,
    date: xValues.data[index],
    bulletSettings: {
      stroke: date.isBefore(dayjs())
        ? am5.color("#9e9e9e")
        : am5.color(userValues.color),
      fill: date.isBefore(dayjs()) && am5.color("#9e9e9e"),
    },
    strokeSettings: {
      stroke: dayjs().isSameOrBefore(date)
        ? am5.color(userValues.color)
        : am5.color("#9e9e9e"),
    },
  });
});

const marketData: any = [];
marketValues.data.forEach((value: any, index: number) => {
  marketData.push({ value: value, date: xValues.data[index] });
});

marketSeries.data.processor = am5.DataProcessor.new(root, {
  numericFields: ["value"],
  dateFields: ["date"],
  dateFormat: "dd.MM.yyyy",
});

userSeries.data.processor = am5.DataProcessor.new(root, {
  numericFields: ["value"],
  dateFields: ["date"],
  dateFormat: "dd.MM.yyyy",
});

marketSeries.data.setAll(marketData);
marketSeriesY.data.setAll(marketData);
userSeries.data.setAll(userData);

const handleDraw = (e: any) => {
  if (mouseIsDown) {
    if (chart.get("cursor")) {
      let x = cursor.getPrivate("positionX");
      let y = cursor.getPrivate("positionY");
      let dateX: any = undefined;
      let valueY = undefined;

      if (!isNil(x))
        dateX = xAxisM.positionToDate(xAxisM.toAxisPosition(x));

      if (!isNil(y))
        valueY = round(yAxis.positionToValue(yAxis.toAxisPosition(y)), 2);

      let i = 0;
      let chartDate = dayjs(dateX);
      let fixingDate = dayjs(userData[i].date);

      while (chartDate.isAfter(fixingDate) && i < userData.length - 1) {
        fixingDate = dayjs(userData[i].date);
        i += 1;
      }

      let firstDiff = chartDate.diff(fixingDate); 
      let secondDiff = chartDate.diff(userData[i].date); 

      if (Math.abs(firstDiff) <= Math.abs(secondDiff)) {
        let index = userData.findIndex((data: any) => {
          let date = dayjs(data.date);
          return fixingDate.isSame(date);
        });

        if (!isNil(index)) {
          userData[index].value = valueY;
          userSeries.data.setAll(userData);
        }
      } else {
        let index = userData.findIndex((data: any) => {
          let date = dayjs(data.date);
          return dayjs(userData[i].date).isSame(date);
        });

        if (!isNil(index)) {
          userData[index].value = valueY;
          userSeries.data.setAll(userData);
        }
      }
    }
  }
};

return () => {
  root.dispose();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userValues]);

Solution

  • Here is the code:

    // manipulating with mouse code
    var isDown = false;
    
    // register down
    chart.plotContainer.events.on("pointerdown", function() {
      isDown = true;
    })
    // register up
    chart.plotContainer.events.on("globalpointerup", function() {
      isDown = false;
    })
    
    chart.plotContainer.events.on("globalpointermove", function(e) {
      // if pointer is down
      if (isDown) {
        // get tooltip data item 
        var tooltipDataItem = series.get("tooltipDataItem");
        if (tooltipDataItem) {
          if (e.originalEvent) {
    
            var position = yAxis.coordinateToPosition(chart.plotContainer.toLocal(e.point).y);
            var value = yAxis.positionToValue(position);
            // need to set bot working and original value
            tooltipDataItem.set("valueY", value);
            tooltipDataItem.set("valueYWorking", value);
          }
        }
      }
    })
    

    Working examples:

    https://www.amcharts.com/demos/manipulate-chart-data-with-mouse/ https://www.amcharts.com/demos/drag-and-change-column-value/

    I just register pointerdown/pointerup and then use tooltipDataItem of a series to identify which data item is closes to the cursor. Note, a XYCursor must be added to the chart in order this to work.