グラフとPDFレポートを生成する反応を使用して、Petpeteer


2019年9月に会社に連絡してレポートAPIを構築しました.この会社は、従業員に調査を送ることによって組織における健康とストレスのレベルを測定する製品を構築しています.
いくつかの会社のクライアントは、これらの調査に基づいてPDFレポートを生成する機能を要求した.各調査は、グラフで表示される複数の期間のデータを持つ質問の数が含まれます.グラフデータは、2つの方法で表示することができます:期間と期間の概要の期間のための調査データ.
私はその問題を解決する方法についてかなり自由に与えられました.

Note: The code snippets are trimmed for brevity and does not represent real data or architecture



要件
  • API終点は、Azure雲
  • で利用できなければなりません
    終点は、調査データとテンプレート名を受信しなければなりません
  • は、応答242479182のチャートとともに尋ねられる質問で、PDF文書を返します
    PDFファイルとの作業を動的にサーバー上で生成することができます挑戦することができます.PDFkit ( https://pdfkit.org )のようなライブラリがありますが、特に何を描くべきか、どこで、キャンバスAPIのようにどこに書かなければならないでしょう.
    const pdfKitDoc = new PDFDocument()
    questions.forEach((question, i) => {
      pdfKitDoc
        // what's the height of each question?
        .text(question.name, 0, i * ?)
        // draw charts and calculate these x and y values somehow
        .moveTo(100, 150)
        .lineTo(100, 250)
        .lineTo(200, 250)
        .fill('#FF3300')
    })
    
    これは、グラフを構築する楽しい方法ではありません.
    代わりに、静的なHTMLをレンダリングするために、テンプレートエンジンとして反応を使用することを選びました.反応を使用して、マージン、パドル、テキストなどのスタイリングに変更を行うには簡単ですし、我々は、位置決めやテキストの流れを心配する必要はありません.また、グラフを構築するための素晴らしい図書館を含む巨大な生態系の利点を得る.
    テンプレートは次のようになります.
    const Template = ({ questions }) => (
      <Layout>
        {questions.map(question => {
          const { type, data } = question.chart
          return (
            <Question key={question.id}>
              <QuestionHeader title={question.name} />
              <Chart type={type} data={data} />
            </Question>
          )
        })}
      </Layout>
    )
    
    つの制限は、それがJavaScriptを実行しているDOM環境に依存しているので、チャートを描くためにキャンバスを使うことができないということです.このアプローチで静的HTMLをレンダリングするだけです.幸いにもNIVO(https://nivo.rocks)SVGのサポートと美しいチャートを提供します.

    Note: It might be possible to use something like jsdom to work around this but that seems unnecessarily complex. Besides, SVG is great for drawing charts.


    これらのテンプレートをレンダリングするには、React.renderToStaticMarkupを使用します.
    export function renderTemplate({ data, language, title }) {
      return ReactDOMServer.renderToStaticMarkup(
        React.createElement(Template, { data, language, title })
      )
    }
    
    このHTMLページをPDFファイルに変換する必要があります.このためにGoogle Puppeteerを使用できます.

    人形を用いたPDFの生成
    Puppeteerは、サイトを訪問し、一般的にスクレーパーとして使用するか、エンドツーエンドのテストを実行しているDOMからデータを取得するように指示することができるヘッドレスクロームブラウザです.また、PDFファイルを作成するために使用することができます.
    次のように動作します.
    import puppeteer from 'puppeteer'
    
    export async function renderPDF(html: string) {
      const browser = await puppeteer.launch({
        args: ['--no-sandbox', '--disable-setuid-sandbox']
      })
      const page = await browser.newPage()
      // pass the html string as data text/html so we don't have to visit a url
      await page.goto(`data text/html,${html}`, { waitUntil: 'networkidle0' })
      const pdf = await page.pdf({ format: 'A4' })
      await browser.close()
      return pdf
    }
    
    時には、物事は予想通りスムーズに行かない.Google Puppeteerは、どんな色っぽい色がSVGで使われるならば、空のPDFを引き起こしているバグを持っているとわかります.これを解決するために、私は正規表現を使用して、HTMLのRGB値ですべての色の出現を置き換えました.
    // https://github.com/sindresorhus/hex-rgb
    import hexRgb from 'hex-rgb'
    export function hexToRgb(str: string) {
      const hexTest = /#[a-f\d]{3,6}/gim
      return str.replace(hexTest, hexColor => {
        const { red, green, blue } = hexRgb(hexColor)
        return `rgb(${red}, ${green}, ${blue})`
      })
    }
    

    マッピングデータ
    各質問の回答の種類を受け入れるように構成することができます.以下の種類があります:
    yes/no
  • のための
  • バイナリ
  • シングルチョイス
  • マルチチョイス
  • の範囲選択
  • コメントのための
  • のテキスト
    これらのタイプは、グラフタイプに関して両方のレポートで異なって表現される必要があります、また、それは時間の期間の上にデータを示すべきであるならば、テンプレートに従います、あるいは、集計された要約.
    // Questions have different answer types and should use different types of charts depending on template
    const chartMappers = {
      scale: {
        summary: (responses) => createGroupedBar(responses),
        periodic: (responses) => createPeriodicLine(responses)
      },
      single: {...},
      multi: {...},
      scale: {...},
      text: {...}
    }
    const templateMappers = {
      summary: periods => mergePeriods(periods),
      periodic: periods => flattenPeriods(periods)
    }
    export function mapSurveyToCharts({ survey, template }) {
      return {
        questions: survey.questions.map(question => {
          const responses = tempateMappers[template](question.periods)
          const chart = chartMappers[question.Type][template](responses)
          return {
            name: question.Title,
            chart: chart
          }
        })
      }
    }
    
    

    包み上げる
    我々は今必要なすべての作品を持って、ちょうど一緒にすべてを置く必要があります:
    export async function generateReport({ survey, template, language = 'en_US' }) {
      const data = mapSurveyToCharts({ survey, template })
      const html = renderTemplate({ data, language })
      /*
        Puppeteer is having issues with rendering SVGs with hex colors. Replace all with rgb(R, G, B).
        https://github.com/GoogleChrome/puppeteer/issues/2556
        */
      const replacedHTML = hexToRgb(html)
      const pdf = await renderPDF(replacedHTML)
    
      return pdf
    }
    
    別の方法でこれを解決しましたか.意味をなさない何か?あなたの考えやフィードバックを聞くのが大好きだ!