notebook

都内でWEB系エンジニアやってます。

PixelaのグラフをGatsby製のブログに埋め込む

PixelaのグラフをGatsby(React)で表示させたい

素直にsvgをobjectタグで読み込むだけだとツールチップが表示されないのでせっかくなら表示させたい

(※前提の参考リンクをよく読めばiframeで良くないか?という話になるが、今回はmswを使って開発しているときはローカルだけで完結させたい、がiframeはmswではモックできないという事情により色々面倒なことをやっている)

前提知識

はてなブログに Pixela グラフを埋め込んで、さらにツールチップを表示させる方法 - えいのうにっき

blog.a-know.me

はてなブログへの埋め込み方法は上記

popoverを表示するためのライブラリとしてtippyjsを使うことを前提としている

Pixelaが返すSVGの中のrectタグ中にdata-tippy-contentというプロパティがありその中にpopoverで表示されるコンテンツが入っている

tippyjs側では特定の属性の内容をツールチップの内容とする仕様になっている

  • popoverの中身

頑張って一瞬でコピーした

<div class="tippy-popper" role="tooltip" id="tippy-92" x-placement="top" style="z-index: 9999; visibility: hidden; transition-duration: 0ms; position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(542px, 6020px, 0px);"><div class="tippy-tooltip dark-theme" data-size="regular" data-animation="shift-away" data-state="hidden" style="transition-duration: 275ms; top: 0px;"><div class="tippy-arrow" style="left: 77px;"></div><div class="tippy-content" data-state="hidden" style="transition-duration: 275ms;"><div>143 views on 2021-09-19</div></div></div></div>
  • rect(当日)
<rect class="each-day" rx="2" ry="2" width="10" height="10" x="0" y="0" fill="#d5eaff" data-count="6" data-date="2021-09-19" data-unit="view(s)" data-retina="true" data-retinaday="20210919" data-index="1" tabindex="0"></rect>

一個不具合というかわからないが当日のデータはdata-tippy-contentにデータが入ってこないっぽい

  • rect(昨日以前)
<rect data-tippy-content="6 view(s) on 2021-09-18" class="each-day" rx="2" ry="2" width="10" height="10" x="1" y="72" fill="#d5eaff" data-count="6" data-date="2021-09-18" data-unit="view(s)" data-retina="true" data-retinaday="20210918" data-index="2" tabindex="0"></rect>

なるほど

Reactでどうやってツールチップを実現するか

tippyjsを前提としているならreactでtippyjsを使えるようなライブラリがあれば良さそう

tippyjs作者と同じ方がReact用のライブラリも作っているようなのでそれを見にいってREADMEをいくつか試してみた

atomiks/tippyjs-react: React component for Tippy.js (official)

github.com

tippyjs単体での使用方法は下記

Constructor | Tippy.js

atomiks.github.io

色々試してみたが力不足のためtippyjs-reactを用いてPixelaのグラフの中のデータをツールチップに表示させるところまで実装できなかった

下記読んで見たissue

Question - Programmatically create tippies on spans inserted with 'dangerouslySetInnerHTML' · Issue #98 · atomiks/tippyjs-react

Is there a way to use css selectors like with tippy.js? · Issue #170 · atomiks/tippyjs-react

Doesn't accept target property · Issue #39 · atomiks/tippyjs-react

tippyを呼び出して直接実行することはできそう

ということで、useEffect内でtippyを実行するよう試してみたが

import {tippy} from '@tippyjs/react'
.....
.....

  useEffect(() => {
    tippy('.each-day', {arrow: true})
  })

.....
.....

return (
    <>
      <div>
        <div className="each-day" data-tippy-content="aaaa 1">a</div>
        <div className="each-day" data-tippy-content="bbbb 1">b</div>
        <div className="each-day" data-tippy-content="cccc 1">c</div>
      </div>
      <object
        type="image/svg+xml"
        data="https://pixe.la/v1/users/swfz/graphs/til-pv-dev?mode=short"
      ></object>
    </>
)

SVGで呼び出した各rectにはeach-dayクラスが存在するはずだが反応せず…

同様のCSSクラス名を設定した子要素には反応した

これはobjectで読み込んだSVGがiframeなどと同様に子コンテンツ扱いされているからのよう

Doesn't accept target property · Issue #39 · atomiks/tippyjs-react

CSSセレクタで中身を取得したい場合次のような感じで取得できる

const element = document.querySelector('.selector-in-parent-content').contentWindow.document.querySelector('.selector-in-child-content');

これをCSSセレクタ一発で取得できるか少し調べたが見つからなかったので断念

ということでどうしたもんかなと考えたが次の案くらいしか思い浮かばなかった

案1 fetchとdangerouslySetInnerHTMLでSVGのレスポンスをそのまま突っ込む

  • fetchでSVGデータを取得する
  • dangerouslySetInnerHTMLでHTMLを入れ込む
  • jQueryなどでの使用法と同様にtippyを実行する

ツールチップの要素などはtippyが実行してDOM操作する形になるのでReactの管理対象外になるはず

なので正直気持ちの良いものではない

案2 fetchとcloneElementなどを使ってDOMを書き換えツールチップを動作させる

  • fetchでSVGデータを取得する
  • cloneElementなどを駆使し、Tippyタグが動作するようにDOMを書き換える

工夫すればできそうだけどsvgの中身まで把握しておかないといけないし結構たいへんそう…

案3 objectタグでレンダリングしているSVGの中でtippyjsを実行する

  • そもそもできるのか不明

このへんまで調べてそんなに時間使えないし案1で良いか…ということで

まずは動かすところまで持っていく!!

結局次のような感じになった

案1でやってみた

import React, { useState, useEffect } from "react"
import fetch from "node-fetch"
import { tippy } from "@tippyjs/react"
import "tippy.js/dist/tippy.css"
import DOMPurify from "dompurify"

const Pixela = () => {
  const [pixelaSvg, setPixelaSvg] = useState("")

  useEffect(() => {
    const fetchPixelaSvg = async () => {
      const res = await fetch(
        "https://pixe.la/v1/users/swfz/graphs/til-pageviews?mode=short"
      )
      const html: string = await res.text()

      setPixelaSvg(DOMPurify.sanitize(html))
      tippy(".each-day", { arrow: true })
    }
    fetchPixelaSvg()
  }, [])

  return (
    <>
      <div
        dangerouslySetInnerHTML={{
          __html: pixelaSvg,
        }}
      ></div>
      <div
        style={{
          textAlign: `right`,
        }}
      >
        Powered by{" "}
        <a href="https://pixe.la/" target="_blank">
          Pixela
        </a>
      </div>
    </>
  )
}

f:id:swfz:20211015024241p:plain

まとめ

  • PixelaのグラフをGatsby(React)で表示してツールチップまで表示できるようにした
  • Reactの中の世界でツールチップを管理することを断念した
  • 他案はまた別な機会で挑戦したい