ツールチップ位置決定

注記配置法

文脈

[15] ラベル配置問題は多くの分野に関係します。各分野で少しずつ要件が違います。

点への注記の配置

[4] 応用数理 32(4): 235-239 (2022) - 32_235.pdf, https://www.jstage.jst.go.jp/article/bjsiam/32/4/32_235/_pdf

[2] https://chuo-u.repo.nii.ac.jp/record/4398/files/1345_2428~~42~291.pdf

[8] F-3-4.pdf, , https://www.gisa-japan.org/content/files/conferences/proceedings/2011cd/papers/F-3-4.pdf

[7] digidepo_11175653_po_SEDK00278_PhDthesis.pdf, https://dl.ndl.go.jp/view/prepareDownload?itemId=info%3Andljp%2Fpid%2F11175653&contentNo=1

[9] https://chuo-u.repo.nii.ac.jp/record/4384/files/1345_2428~~42~277.pdf

[10] https://chuo-u.repo.nii.ac.jp/record/7042/files/nenpou45_242.pdf

[11] 位置誤差を含む空間データに基づいたバッファ操作の精度 - D-7-4.pdf, , https://www.gisa-japan.org/content/files/conferences/proceedings/2014cd/papers/D-7-4.pdf

線上の点への注記の配置

[3] Takahashi Research Group: A Zone-Based Approach for Placing Annotation Labels on Metro Maps, , https://web-ext.u-aizu.ac.jp/~shigeo/research/annotate/index-j.html

[12] Takahashi Research Group: Spatially Efficient Design of Annotated Metro Maps, , https://u-aizu.ac.jp/~shigeo/research/geospace/index-j.html

[13] D14.pdf, , http://www.csis.u-tokyo.ac.jp/csisdays2013/csisdays2013-ra-pdf/D14.pdf

[14] 高橋研究室: 地下鉄路線図に注釈を付ける際のメンタルマップの保持, , https://u-aizu.ac.jp/~shigeo/research/mental/index-j.html

[40] 多角線上の p2 の近く、距離 opts.delta の位置を、 前後の p1p3 の位置を勘案しつつ決定する JavaScript の実装例。

  function get (p1, p2, p3, opts) {
    let Q;
    {
      let d = opts.offset || 1;

      let p1p;
      let p3p;
      let pr = p1;
      if (p1.x === p2.x && p1.y === p2.y && p1.x === p3.x && p1.y === p3.y) {
        p1p = p3p = p2;
      } else if (p1.x === p2.x && p1.y === p2.y) {
        p1p = p3p = {x: p1.x + p3.x - p2.x, y: p1.y + p3.y - p2.y};
        pr = p3;
      } else if (p3.x === p2.x && p3.y === p2.y) {
        p3p = p1p = {x: p3.x + p1.x - p2.x, y: p3.y + p1.y - p2.y};
      } else {
        p1p = {
          x: p2.x + (p1.x - p2.x) / Math.sqrt ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2),
          y: p2.y + (p1.y - p2.y) / Math.sqrt ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
        };
        p3p = {
          x: p2.x + (p3.x - p2.x) / Math.sqrt ((p3.x - p2.x) ** 2 + (p3.y - p2.y) ** 2),
          y: p2.y + (p3.y - p2.y) / Math.sqrt ((p3.x - p2.x) ** 2 + (p3.y - p2.y) ** 2)
        };
      }

      const mx = (p1p.x + p3p.x) / 2;
      const my = (p1p.y + p3p.y) / 2;
      //let M = {x: mx, y: my};

      let ux;
      let uy;
      if (p2.x === mx) {
        if (p1.y === p3.y) {
          ux = 0;
          uy = 1;
        } else {
          const m1 = (p3.y - p1.y) / (p3.x - p1.x);
          const mL = -1 / m1;
          ux = 1 / Math.sqrt (1 + mL * mL);
          uy = mL / Math.sqrt (1 + mL * mL);
        }
      } else {
        const dx = mx - p2.x;
        const dy = my - p2.y;
        const m = dy / dx;
        ux = 1 / Math.sqrt (1 + m * m);
        uy = m / Math.sqrt (1 + m * m);
      }

      const Q1 = {x: p2.x + d * ux, y: p2.y + d * uy};
      const Q2 = {x: p2.x - d * ux, y: p2.y - d * uy};
      const dQ1p = Math.sqrt (Math.pow (Q1.x - pr.x, 2) + Math.pow (Q1.y - pr.y, 2));
      const dQ2p = Math.sqrt (Math.pow (Q2.x - pr.x, 2) + Math.pow (Q2.y - pr.y, 2));

      Q = dQ1p > dQ2p ? Q1 : Q2;
    }

    let angle = Math.atan2 (Q.y - p2.y, Q.x - p2.x) * (180 / Math.PI);

    angle += 22.5;
    if (angle < 0) angle += 360;
    angle %= 360;
    angle /= 45;

    let v;
    if (angle < 1) {
      v = {textAlign: "left", textBaseline: "middle", anchorPoint: Q};
    } else if (angle < 2) {
      v = {textAlign: "left", textBaseline: "top", anchorPoint: Q};
    } else if (angle < 3) {
      v = {textAlign: "center", textBaseline: "top", anchorPoint: Q};
    } else if (angle < 4) {
      v = {textAlign: "right", textBaseline: "top", anchorPoint: Q};
    } else if (angle < 5) {
      v = {textAlign: "right", textBaseline: "middle", anchorPoint: Q};
    } else if (angle < 6) {
      v = {textAlign: "right", textBaseline: "bottom", anchorPoint: Q};
    } else if (angle < 7) {
      v = {textAlign: "center", textBaseline: "bottom", anchorPoint: Q};
    } else {
      v = {textAlign: "left", textBaseline: "bottom", anchorPoint: Q};
    }

    return v;
  }

この実装例では、 p2 の周囲を8分割し、右側に置くべきならその左端 (上下中央) の位置を返し、左下に置くべきならその右上端の位置を返し、 といった感じで基準となる位置を返す。

判断基準は p2 を中心に p1, p3 が成す角度であり、 角度が大きい方の中央に向かって距離 opts.delta の位置が基準点となる。

ツールチップ表示位置の決定

[23] Anchor Positioning

[29] GUI におけるツールチップの配置 (位置決定) アルゴリズムはいろいろとノウハウが溜まっていそうなものですけど、 きっちり明文化したものって意外とないですねえ。

[30] それぞれの実装がアドホックというか発見的というか、 それぞれの方法で「ちょっとだけいい感じ」にやっているのに、 その「ちょっとだけいい感じ」がうまいこと共有知になっていないので、 結果として「まあまあいいけどなんかちょっとビミョーな感じ」の実装が氾濫しているような。

[31] 呼び出し側が位置を指定できるときは、基準に対して上下左右のどこかを選べることが多い感じでしょうかね。

[32] 基準はボタン等のGUI構成要素による場合と、マウスポインターの位置による場合、 両方による場合があります。

[33] 表示のきっかけとなるボタン等からマウスカーソル位置が外れたときに消えるタイプが一般的ですが、 そうでないタイプもあります。また、ツールチップ上にマウスカーソルを移動して操作できるタイプとそうでないタイプがあり、 操作できないタイプの方が一般的ですが、操作できるタイプもままあります。 問題となるのが、消えるタイプかつ操作できるタイプのときです。 消えない領域からツールチップ内へと直接マウスカーソルを移動できなければならない、 というツールチップ表示位置の制約が生じます。

[34] ツールチップ矩形が一般的ですが、吹き出し式とすることもあります。 その場合は吹き出し表示を考慮した位置決定が必要となります。

[35] ツールチップは基本的にそれ1つだけが同時表示され (例外もあり)、 他のツールチップの表示位置との関係の制約はありません。 また、背景となる他の部品等の配置との関係も普通は問題とされません。 (これが地図注記等より問題を簡単にしています。)

[36] ツールチップの配置は画面上の位置を考慮する必要があります。 伝統的には基準となるの位置の制約は受けず、からはみ出しても自由に配置できます。 ただし、Webページ内部の表示のように、が実質的に画面に相当する制約となることがあります。 また、スクロール位置との関係は気にする必要があります。

[37] ツールチップ内容のサイズが決まる前にツールチップの位置を決めなければならない場合もあり、 この性質を持つ場合問題が難しくなります。

線に沿った注記の配置

[1] https://chuo-u.repo.nii.ac.jp/record/14550/files/nenpou49_150.pdf

群に沿った注記の配置

[5] https://chuo-u.repo.nii.ac.jp/record/16681/files/nenpou20N8100012I.pdf

[6] https://ipsj.ixsq.nii.ac.jp/ej/?action=repository_action_common_download&item_id=216129&item_no=1&attribute_id=1&file_no=1

メモ