]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Fix tooltip positioning issues (#8646)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Tue, 16 Mar 2021 12:06:05 +0000 (14:06 +0200)
committerGitHub <noreply@github.com>
Tue, 16 Mar 2021 12:06:05 +0000 (08:06 -0400)
* Fix tooltip positioning issues

* Update fixture, add npm run dev:ff

* Refactor determineXAlign

* Simplify more

* remove unneeded change

package.json
src/plugins/plugin.tooltip.js
test/fixtures/plugin.tooltip/opacity.js
test/fixtures/plugin.tooltip/opacity.png
test/fixtures/plugin.tooltip/point-style.js
test/fixtures/plugin.tooltip/point-style.png
test/fixtures/plugin.tooltip/positioning.js [new file with mode: 0644]
test/fixtures/plugin.tooltip/positioning.png [new file with mode: 0644]
test/specs/plugin.tooltip.tests.js

index 4de42753413597d09a6bd653a93ae9b3e894bb2f..19982c1427faa3d7eef716c6496ec8c27eee8228 100644 (file)
@@ -38,6 +38,7 @@
     "autobuild": "rollup -c -w",
     "build": "rollup -c",
     "dev": "karma start --auto-watch --no-single-run --browsers chrome --grep",
+    "dev:ff": "karma start --auto-watch --no-single-run --browsers firefox --grep",
     "docs": "cd docs && npm install && npm run build",
     "lint-js": "eslint \"samples/**/*.html\" \"samples/**/*.js\" \"src/**/*.js\" \"test/**/*.js\"",
     "lint-md": "markdownlint-cli2 \"**/*.md\" \"**/*.mdx\" \"#**/node_modules\"",
index fa6859b03ec1d07d81c6e4b2f26ef35736e35ab3..3d0643b40de9d575847b10f4cd5984e2516137dc 100644 (file)
@@ -3,7 +3,7 @@ import Element from '../core/core.element';
 import {each, noop, isNullOrUndef, isArray, _elementsEqual} from '../helpers/helpers.core';
 import {toFont, toPadding} from '../helpers/helpers.options';
 import {getRtlAdapter, overrideTextDirection, restoreTextDirection} from '../helpers/helpers.rtl';
-import {distanceBetweenPoints} from '../helpers/helpers.math';
+import {distanceBetweenPoints, _limitValue} from '../helpers/helpers.math';
 import {drawPoint} from '../helpers';
 
 /**
@@ -211,76 +211,67 @@ function getTooltipSize(tooltip, options) {
   return {width, height};
 }
 
-/**
- * Helper to get the alignment of a tooltip given the size
- */
-function determineAlignment(chart, options, size) {
-  const {x, y, width, height} = size;
-  const chartArea = chart.chartArea;
-  let xAlign = 'center';
-  let yAlign = 'center';
+function determineYAlign(chart, size) {
+  const {y, height} = size;
 
   if (y < height / 2) {
-    yAlign = 'top';
+    return 'top';
   } else if (y > (chart.height - height / 2)) {
-    yAlign = 'bottom';
+    return 'bottom';
   }
+  return 'center';
+}
 
-  let lf, rf; // functions to determine left, right alignment
-  const midX = (chartArea.left + chartArea.right) / 2;
-  const midY = (chartArea.top + chartArea.bottom) / 2;
+function doesNotFitWithAlign(xAlign, chart, options, size) {
+  const {x, width} = size;
+  const caret = options.caretSize + options.caretPadding;
+  if (xAlign === 'left' && x + width + caret > chart.width) {
+    return true;
+  }
 
-  if (yAlign === 'center') {
-    lf = (value) => value <= midX;
-    rf = (value) => value > midX;
-  } else {
-    lf = (value) => value <= (width / 2);
-    rf = (value) => value >= (chart.width - (width / 2));
+  if (xAlign === 'right' && x - width - caret < 0) {
+    return true;
   }
+}
 
-  // functions to determine if left/right alignment causes tooltip to go outside chart
-  const olf = (value) => value + width + options.caretSize + options.caretPadding > chart.width;
-  const orf = (value) => value - width - options.caretSize - options.caretPadding < 0;
-  // function to get the y alignment if the tooltip goes outside of the left or right edges
-  const yf = (value) => value <= midY ? 'top' : 'bottom';
+function determineXAlign(chart, options, size, yAlign) {
+  const {x, width} = size;
+  const {width: chartWidth, chartArea: {left, right}} = chart;
+  let xAlign = 'center';
 
-  if (lf(x)) {
+  if (yAlign === 'center') {
+    xAlign = x <= (left + right) / 2 ? 'left' : 'right';
+  } else if (x <= width / 2) {
     xAlign = 'left';
-
-    // Is tooltip too wide and goes over the right side of the chart.?
-    if (olf(x)) {
-      xAlign = 'center';
-      yAlign = yf(y);
-    }
-  } else if (rf(x)) {
+  } else if (x >= chartWidth - width / 2) {
     xAlign = 'right';
+  }
 
-    // Is tooltip too wide and goes outside left edge of canvas?
-    if (orf(x)) {
-      xAlign = 'center';
-      yAlign = yf(y);
-    }
+  if (doesNotFitWithAlign(xAlign, chart, options, size)) {
+    xAlign = 'center';
   }
 
+  return xAlign;
+}
+
+/**
+ * Helper to get the alignment of a tooltip given the size
+ */
+function determineAlignment(chart, options, size) {
+  const yAlign = options.yAlign || determineYAlign(chart, size);
+
   return {
-    xAlign: options.xAlign ? options.xAlign : xAlign,
-    yAlign: options.yAlign ? options.yAlign : yAlign
+    xAlign: options.xAlign || determineXAlign(chart, options, size, yAlign),
+    yAlign
   };
 }
 
-function alignX(size, xAlign, chartWidth) {
-  // eslint-disable-next-line prefer-const
+function alignX(size, xAlign) {
   let {x, width} = size;
   if (xAlign === 'right') {
     x -= width;
   } else if (xAlign === 'center') {
     x -= (width / 2);
-    if (x + width > chartWidth) {
-      x = chartWidth - width;
-    }
-    if (x < 0) {
-      x = 0;
-    }
   }
   return x;
 }
@@ -307,7 +298,7 @@ function getBackgroundPoint(options, size, alignment, chart) {
   const paddingAndSize = caretSize + caretPadding;
   const radiusAndPadding = cornerRadius + caretPadding;
 
-  let x = alignX(size, xAlign, chart.width);
+  let x = alignX(size, xAlign);
   const y = alignY(size, yAlign, paddingAndSize);
 
   if (yAlign === 'center') {
@@ -322,7 +313,10 @@ function getBackgroundPoint(options, size, alignment, chart) {
     x += radiusAndPadding;
   }
 
-  return {x, y};
+  return {
+    x: _limitValue(x, 0, chart.width - size.width),
+    y: _limitValue(y, 0, chart.height - size.height)
+  };
 }
 
 function getAlignedX(tooltip, align, options) {
index 4726fb832c703c88b63c4194630a0028201eced1..c6c4424ecb2cde6ab16f26c26f3db13d4c544ae3 100644 (file)
@@ -47,17 +47,18 @@ module.exports = {
       plugins: {
         legend: false,
         title: false,
-        filler: false
-      },
-      tooltips: {
-        mode: 'nearest',
-        intersect: false,
-        callbacks: {
-          label: function() {
-            return '\u200b';
+        filler: false,
+        tooltip: {
+          mode: 'nearest',
+          intersect: false,
+          callbacks: {
+            label: function() {
+              return '\u200b';
+            },
           }
-        }
+        },
       },
+
       layout: {
         padding: 15
       }
index 4ad9c4055fb107e1b2f70477944b868c4999f27b..e44b000f93889a121d458a1b40f4b50286244c02 100644 (file)
Binary files a/test/fixtures/plugin.tooltip/opacity.png and b/test/fixtures/plugin.tooltip/opacity.png differ
index f14dc83b5f07dbfd3d074950a79066d1034e0981..255e4f153b8013d91109bc457a4b4493384bd510 100644 (file)
@@ -30,17 +30,18 @@ module.exports = {
       plugins: {
         legend: false,
         title: false,
-        filler: false
-      },
-      tooltips: {
-        mode: 'nearest',
-        intersect: false,
-        usePointStyle: true,
-        callbacks: {
-          label: function() {
-            return '\u200b';
+        filler: false,
+        tooltip: {
+          mode: 'nearest',
+          intersect: false,
+          padding: 5,
+          usePointStyle: true,
+          callbacks: {
+            label: function() {
+              return '\u200b';
+            }
           }
-        }
+        },
       },
       layout: {
         padding: 15
index defb03359fc761e3f202676fb0c87f54c881f4c6..44662c6b007b9ba5f7eb67842fec045f1a96e2ba 100644 (file)
Binary files a/test/fixtures/plugin.tooltip/point-style.png and b/test/fixtures/plugin.tooltip/point-style.png differ
diff --git a/test/fixtures/plugin.tooltip/positioning.js b/test/fixtures/plugin.tooltip/positioning.js
new file mode 100644 (file)
index 0000000..c30ff3e
--- /dev/null
@@ -0,0 +1,71 @@
+const data = [];
+for (let x = 0; x < 3; x++) {
+  for (let y = 0; y < 3; y++) {
+    data.push({x, y});
+  }
+}
+
+module.exports = {
+  config: {
+    type: 'scatter',
+    data: {
+      datasets: [{
+        data,
+        backgroundColor: 'red',
+        radius: 8
+      }],
+    },
+    options: {
+      scales: {
+        x: {display: false},
+        y: {display: false}
+      },
+      plugins: {
+        legend: false,
+        title: false,
+        filler: false,
+        tooltip: {
+          mode: 'point',
+          intersect: true,
+          // spriteText: use white background to hide any gaps between fonts
+          backgroundColor: 'white',
+          borderColor: 'black',
+          borderWidth: 1,
+          callbacks: {
+            beforeLabel: () => 'before label',
+            label: () => 'label',
+            afterLabel: () => 'after1\nafter2\nafter3\nafter4\nafter5'
+          }
+        }
+      },
+    },
+    plugins: [{
+      afterDraw: function(chart) {
+        const canvas = chart.canvas;
+        const rect = canvas.getBoundingClientRect();
+        const meta = chart.getDatasetMeta(0);
+        let point, event;
+
+        for (let i = 0; i < data.length; i++) {
+          point = meta.data[i];
+          event = {
+            type: 'mousemove',
+            target: canvas,
+            clientX: rect.left + point.x,
+            clientY: rect.top + point.y
+          };
+          chart._handleEvent(event);
+          chart.tooltip.handleEvent(event);
+          chart.tooltip.draw(chart.ctx);
+        }
+      }
+    }]
+  },
+  options: {
+    spriteText: true,
+    canvas: {
+      height: 400,
+      width: 500
+    }
+  }
+};
diff --git a/test/fixtures/plugin.tooltip/positioning.png b/test/fixtures/plugin.tooltip/positioning.png
new file mode 100644 (file)
index 0000000..da095f7
Binary files /dev/null and b/test/fixtures/plugin.tooltip/positioning.png differ
index 9dbd198e62703c57f498152a6c62ad2d0c113077..2a1fe22fea7c013142963c59b466cec2f6fff917 100644 (file)
@@ -3,7 +3,7 @@ const tooltipPlugin = Chart.registry.getPlugin('tooltip');
 const Tooltip = tooltipPlugin._element;
 
 describe('Plugin.Tooltip', function() {
-  describe('auto', jasmine.fixture.specs('core.tooltip'));
+  describe('auto', jasmine.fixture.specs('plugin.tooltip'));
 
   describe('config', function() {
     it('should not include the dataset label in the body string if not defined', function() {