diff --git a/.circleci/config.yml b/.circleci/config.yml
deleted file mode 100644
index 9c373616b26..00000000000
--- a/.circleci/config.yml
+++ /dev/null
@@ -1,606 +0,0 @@
-version: 2.1
-orbs:
- browser-tools: circleci/browser-tools@2.4.0
-
-# Inspired by:
-# https://github.com/CircleCI-Public/circleci-demo-workflows/blob/workspace-forwarding/.circleci/config.yml
-# https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs
-#
-# For list of official CircleCI node.js images, go to:
-# https://hub.docker.com/r/cimg/node/tags/
-
-jobs:
- install-and-cibuild:
- docker:
- - image: cimg/node:18.20.4
- working_directory: ~/plotly.js
- steps:
- - checkout
- - run:
- name: Set up build environment
- command: .circleci/env_build.sh
- - run:
- name: Pretest
- command: npm run pretest
- - run:
- name: CI-Build
- command: npm run cibuild
- - run:
- name: Delete git
- command: rm -rf .git
- - persist_to_workspace:
- root: /home/circleci
- paths:
- - plotly.js
-
- timezone-jasmine:
- docker:
- # need '-browsers' version to test in real (xvfb-wrapped) browsers
- - image: cimg/node:18.20.4-browsers
- working_directory: ~/plotly.js
- steps:
- - run: sudo apt-get update
- - browser-tools/install_browser_tools:
- install_firefox: false
- install_geckodriver: false
- install_chrome: true
- chrome_version: "136.0.7103.113"
- - attach_workspace:
- at: ~/
- - run:
- name: Run hover_label test in UTC timezone
- environment:
- TZ: "UTC"
- command: date && npm run test-jasmine hover_label
- - run:
- name: Run hover_label test in Europe/Berlin timezone
- environment:
- TZ: "Europe/Berlin"
- command: date && npm run test-jasmine hover_label
- - run:
- name: Run hover_label test in Asia/Tokyo timezone
- environment:
- TZ: "Asia/Tokyo"
- command: date && npm run test-jasmine hover_label
- - run:
- name: Run hover_label test in America/Toronto timezone
- environment:
- TZ: "America/Toronto"
- command: date && npm run test-jasmine hover_label
-
- no-gl-jasmine:
- docker:
- # need '-browsers' version to test in real (xvfb-wrapped) browsers
- - image: cimg/node:18.20.4-browsers
- environment:
- # Alaska time (arbitrary timezone to test date logic)
- TZ: "America/Anchorage"
- parallelism: 4
- working_directory: ~/plotly.js
- steps:
- - run: sudo apt-get update
- - browser-tools/install_browser_tools:
- install_firefox: false
- install_geckodriver: false
- install_chrome: true
- chrome_version: "136.0.7103.113"
- - attach_workspace:
- at: ~/
- - run:
- name: Run jasmine tests (part A)
- command: .circleci/test.sh no-gl-jasmine
-
- webgl-jasmine:
- docker:
- # need '-browsers' version to test in real (xvfb-wrapped) browsers
- - image: cimg/node:18.20.4-browsers
- environment:
- # Alaska time (arbitrary timezone to test date logic)
- TZ: "America/Anchorage"
- parallelism: 8
- working_directory: ~/plotly.js
- steps:
- - run: sudo apt-get update
- - browser-tools/install_browser_tools:
- install_firefox: false
- install_geckodriver: false
- install_chrome: true
- chrome_version: "136.0.7103.113"
- - attach_workspace:
- at: ~/
- - run:
- name: Run jasmine tests (part B)
- command: .circleci/test.sh webgl-jasmine
-
- virtual-webgl-jasmine:
- docker:
- # need '-browsers' version to test in real (xvfb-wrapped) browsers
- - image: cimg/node:18.20.4-browsers
- environment:
- # Alaska time (arbitrary timezone to test date logic)
- TZ: "America/Anchorage"
- parallelism: 8
- working_directory: ~/plotly.js
- steps:
- - run: sudo apt-get update
- - browser-tools/install_browser_tools:
- install_firefox: false
- install_geckodriver: false
- install_chrome: true
- chrome_version: "136.0.7103.113"
- - attach_workspace:
- at: ~/
- - run:
- name: Run jasmine tests (part B)
- command: .circleci/test.sh virtual-webgl-jasmine
-
- webgl-jasmine-chromeLatest:
- docker:
- # need '-browsers' version to test in real (xvfb-wrapped) browsers
- - image: cimg/node:18.20.4-browsers
- environment:
- # Alaska time (arbitrary timezone to test date logic)
- TZ: "America/Anchorage"
- parallelism: 8
- working_directory: ~/plotly.js
- steps:
- - browser-tools/install_browser_tools: &browser-versions
- install_firefox: false
- install_geckodriver: false
- install_chrome: true
- chrome_version: "143.0.7499.192" # TEMPORARY pin until WebGL issues with 144 are resolved
- - attach_workspace:
- at: ~/
- - run:
- name: Run jasmine tests (part B)
- command: .circleci/test.sh webgl-jasmine
-
- flaky-no-gl-jasmine:
- docker:
- # need '-browsers' version to test in real (xvfb-wrapped) browsers
- - image: cimg/node:18.20.4-browsers
- environment:
- # Alaska time (arbitrary timezone to test date logic)
- TZ: "America/Anchorage"
- working_directory: ~/plotly.js
- steps:
- - run: sudo apt-get update
- - browser-tools/install_browser_tools:
- install_firefox: false
- install_geckodriver: false
- install_chrome: true
- chrome_version: "136.0.7103.113"
- - attach_workspace:
- at: ~/
- - run:
- name: Run jasmine tests (part C)
- command: .circleci/test.sh flaky-no-gl-jasmine
-
- bundle-jasmine:
- docker:
- # need '-browsers' version to test in real (xvfb-wrapped) browsers
- - image: cimg/node:18.20.4-browsers
- environment:
- # Alaska time (arbitrary timezone to test date logic)
- TZ: "America/Anchorage"
- working_directory: ~/plotly.js
- steps:
- - run: sudo apt-get update
- - browser-tools/install_browser_tools:
- install_firefox: false
- install_geckodriver: false
- install_chrome: true
- chrome_version: "136.0.7103.113"
- - attach_workspace:
- at: ~/
- - run:
- name: Run jasmine tests (part D)
- command: .circleci/test.sh bundle-jasmine
-
- mathjax-firefoxLatest:
- docker:
- # need '-browsers' version to test in real (xvfb-wrapped) browsers
- - image: cimg/node:18.20.4-browsers
- environment:
- # Alaska time (arbitrary timezone to test date logic)
- TZ: "America/Anchorage"
- working_directory: ~/plotly.js
- steps:
- - browser-tools/install_browser_tools:
- install_chrome: false
- install_chromedriver: false
- - attach_workspace:
- at: ~/
- - run:
- name: Test MathJax on firefox-latest
- command: .circleci/test.sh mathjax-firefox
-
- make-baselines-virtual-webgl:
- parallelism: 8
- docker:
- - image: cimg/python:3.12.11
- working_directory: ~/plotly.js
- steps:
- - attach_workspace:
- at: ~/
- - run: sudo apt-get update
- - run:
- name: Install kaleido, plotly.io and required fonts
- command: .circleci/env_image.sh
- - run:
- name: Create png files using virtual-webgl & WebGL v1
- command: .circleci/test.sh make-baselines-virtual-webgl
- - persist_to_workspace:
- root: ~/
- paths:
- - plotly.js
-
- make-baselines-mathjax3:
- docker:
- - image: cimg/python:3.12.11
- working_directory: ~/plotly.js
- steps:
- - attach_workspace:
- at: ~/
- - run: sudo apt-get update
- - run:
- name: Install kaleido, plotly.io and required fonts
- command: .circleci/env_image.sh
- - run:
- name: Create mathjax v3 png files
- command: .circleci/test.sh make-baselines-mathjax3
- - persist_to_workspace:
- root: ~/
- paths:
- - plotly.js
-
- make-baselines:
- parallelism: 12
- docker:
- - image: cimg/python:3.12.11
- working_directory: ~/plotly.js
- steps:
- - attach_workspace:
- at: ~/
- - run: sudo apt-get update
- - run:
- name: Install kaleido, plotly.io and required fonts
- command: .circleci/env_image.sh
- - run:
- name: Create all png files
- command: .circleci/test.sh make-baselines
- - persist_to_workspace:
- root: ~/
- paths:
- - plotly.js
-
- make-baselines-b64:
- parallelism: 12
- docker:
- - image: cimg/python:3.12.11
- working_directory: ~/plotly.js
- steps:
- - attach_workspace:
- at: ~/
- - run: sudo apt-get update
- - run:
- name: Install kaleido, plotly.io and required fonts
- command: .circleci/env_image.sh
- - run:
- name: Create all png files
- command: .circleci/test.sh make-baselines-b64
- - persist_to_workspace:
- root: ~/
- paths:
- - plotly.js
-
- test-baselines:
- docker:
- - image: cimg/node:18.20.4
- working_directory: ~/plotly.js
- steps:
- - attach_workspace:
- at: ~/
- - run:
- name: Compare pixels
- command: .circleci/test.sh test-image ; find build -maxdepth 1 -type f -delete
- - image-diff-message
- - store_artifacts:
- path: build
- destination: /
-
- test-baselines-virtual-webgl:
- docker:
- - image: cimg/node:18.20.4
- working_directory: ~/plotly.js
- steps:
- - attach_workspace:
- at: ~/
- - run:
- name: Compare pixels
- command: .circleci/test.sh test-image-virtual-webgl ; find build -maxdepth 1 -type f -delete
- - image-diff-message
- - store_artifacts:
- path: build
- destination: /
-
- test-baselines-b64:
- docker:
- - image: cimg/node:18.20.4
- working_directory: ~/plotly.js
- steps:
- - attach_workspace:
- at: ~/
- - run:
- name: Compare pixels
- command: .circleci/test.sh test-image ; find build -maxdepth 1 -type f -delete
- - image-diff-message
- - store_artifacts:
- path: build
- destination: /
-
- test-baselines-mathjax3:
- docker:
- - image: cimg/node:18.20.4
- working_directory: ~/plotly.js
- steps:
- - attach_workspace:
- at: ~/
- - run:
- name: Compare pixels of mathjax v3 baselines
- command: .circleci/test.sh test-image-mathjax3
- - image-diff-message
- - store_artifacts:
- path: build
- destination: /
-
- make-exports:
- docker:
- - image: cimg/python:3.12.11
- working_directory: ~/plotly.js
- steps:
- - attach_workspace:
- at: ~/
- - run: sudo apt-get update
- - run:
- name: Install kaleido, plotly.io and required fonts
- command: .circleci/env_image.sh
- - run:
- name: Install poppler-utils to have pdftops for exporting eps
- command: |
- sudo apt-get update --allow-releaseinfo-change
- sudo apt-get install poppler-utils
- - run:
- name: Create svg, jpg, jpeg, webp, pdf and eps files
- command: sudo python3 test/image/make_exports.py
- - persist_to_workspace:
- root: ~/
- paths:
- - plotly.js
-
- test-exports:
- docker:
- - image: cimg/node:18.20.4
- working_directory: ~/plotly.js
- steps:
- - attach_workspace:
- at: ~/
- - run:
- name: Test export sizes
- command: node test/image/export_test.js ; find build -maxdepth 1 -type f -delete
- - store_artifacts:
- path: build
- destination: /
-
- mock-validation:
- docker:
- - image: cimg/node:18.20.4
- working_directory: ~/plotly.js
- steps:
- - attach_workspace:
- at: ~/
- - run:
- name: Test validation using node.js and jsdom
- command: npm run test-plain-obj
- - run:
- name: Validate mocks
- command: npm run test-mock
-
- source-syntax:
- docker:
- - image: cimg/node:18.20.4
- working_directory: ~/plotly.js
- steps:
- - attach_workspace:
- at: ~/
- - run:
- name: Run syntax tests on source files
- command: .circleci/test.sh source-syntax
-
- publish-dist: &publish-dist
- docker:
- - image: cimg/node:18.20.4
- working_directory: ~/plotly.js
- steps:
- - checkout
- - run:
- name: Set up build environment
- command: .circleci/env_build.sh
- - run:
- name: Preview CHANGELOG for next release (only on master)
- command: |
- if [ $CIRCLE_BRANCH == "master" ]
- then npm run use-draftlogs && git --no-pager diff --color-words CHANGELOG.md || true
- fi
- - run:
- name: Set draft version in package.json
- command: |
- node --eval "var fs = require('fs'); var inOut = './package.json'; var data = JSON.parse(fs.readFileSync(inOut)); var a = process.argv; data.version = a[a.length - 1].replace('v', ''); fs.writeFileSync(inOut, JSON.stringify(data, null, 2) + '\n');" `git describe`
- - run:
- name: View package.json diff between previous and next releases (including above draft version change)
- command: git --no-pager diff --color-words tags/$(git describe --tags --abbrev=0) package.json || true
- - run:
- name: Build dist/
- command: npm run build
- - store_artifacts:
- path: dist
- destination: dist
- - run:
- name: View dist/README.md diff between previous and next releases (including new bundle sizes)
- command: git --no-pager diff --color-words tags/$(git describe --tags --abbrev=0) dist/README.md || true
- - run:
- name: Preview plot-schema diff between previous and next releases (only on master)
- command: |
- if [ $CIRCLE_BRANCH == "master" ]
- then git --no-pager diff tags/$(git describe --tags --abbrev=0) dist/plot-schema.json || true
- fi
- - run:
- name: Pack tarball
- command: |
- npm pack
- version=$(node --eval "console.log(require('./package.json').version)")
- mv plotly.js-$version.tgz plotly.js.tgz
- - store_artifacts:
- path: plotly.js.tgz
- destination: /plotly.js.tgz
- - run:
- name: Show URLs to build files
- command: |
- PROJECT_NUM=45646037
- echo https://$CIRCLE_BUILD_NUM-$PROJECT_NUM-gh.circle-artifacts.com/0/plotly.js.tgz
- echo https://$CIRCLE_BUILD_NUM-$PROJECT_NUM-gh.circle-artifacts.com/0/dist/plotly.js
- echo https://$CIRCLE_BUILD_NUM-$PROJECT_NUM-gh.circle-artifacts.com/0/dist/plotly.min.js
- echo https://$CIRCLE_BUILD_NUM-$PROJECT_NUM-gh.circle-artifacts.com/0/dist/plot-schema.json
- - run:
- name: Test plot-schema.json diff - If failed, after (npm start) you could run (npm run schema && git add test/plot-schema.json && git commit -m "update plot-schema diff")
- command: diff --unified --color dist/plot-schema.json test/plot-schema.json
-
- publish-dist-node-v22:
- <<: *publish-dist
- docker:
- - image: cimg/node:22.14.0
-
- test-stackgl-bundle:
- docker:
- - image: cimg/node:18.20.4
- working_directory: ~/plotly.js
- steps:
- - checkout
- - run:
- name: Set up build environment
- command: cd stackgl_modules && npm ci
- - run:
- name: Bundle bundle-stackgl/index.js
- command: cd stackgl_modules && cp index.js INDEX.js && npm run bundle-stackgl
- - run:
- name: Test stackgl_modules/index.js diff - If failed please remember this file in auto generated and you should not modify it directly until a dependency change. To suggest changes please submit pull request to the relevant dependency.
- command: diff --unified --color stackgl_modules/INDEX.js stackgl_modules/index.js
- - store_artifacts:
- path: stackgl_modules/index.js
- destination: stackgl_modules/index.js
-
- test-topojson-build:
- docker:
- - image: cimg/node:18.20.4
- working_directory: ~/plotly.js
- steps:
- - checkout
- - run:
- name: Set up build environment
- command: cd topojson && npm ci
- - run:
- name: Build topojson
- command: cd topojson && mv dist dist_backup && npm run build
- - run:
- name: Compare existing files with newly built files. Any difference is a failure. A failure might mean that the source data changed.
- command: diff -qr topojson/dist topojson/dist_backup
- - run:
- name: Compress artifacts
- command: tar -cvzf topojson.tar topojson/dist
- - store_artifacts:
- path: topojson.tar
-
-commands:
- image-diff-message:
- steps:
- - run:
- name: IMAGE DIFF DETECTED - SEE NOTE BELOW
- when: on_fail
- command: |
- echo "Image Diff Detected: baseline images may need to be updated. Run 'tasks/circleci_image_artifact_download.sh' to download the baseline images generated by this job."
- echo "Add the new images to 'test/image/baselines/' and commit them to this pull request."
-
-workflows:
- version: 2
- build-and-test:
- max_auto_reruns: 1
- jobs:
- - install-and-cibuild
- - timezone-jasmine:
- requires:
- - install-and-cibuild
- - bundle-jasmine:
- requires:
- - install-and-cibuild
- - mathjax-firefoxLatest:
- requires:
- - install-and-cibuild
- - no-gl-jasmine:
- requires:
- - install-and-cibuild
- - webgl-jasmine:
- requires:
- - install-and-cibuild
- - virtual-webgl-jasmine:
- requires:
- - install-and-cibuild
- - webgl-jasmine-chromeLatest:
- requires:
- - install-and-cibuild
- - flaky-no-gl-jasmine:
- requires:
- - install-and-cibuild
- - make-baselines-virtual-webgl:
- requires:
- - install-and-cibuild
- - test-baselines-virtual-webgl:
- requires:
- - make-baselines-virtual-webgl
- - make-baselines-mathjax3:
- requires:
- - install-and-cibuild
- - test-baselines-mathjax3:
- requires:
- - make-baselines-mathjax3
- - make-baselines-b64:
- requires:
- - install-and-cibuild
- - test-baselines-b64:
- requires:
- - make-baselines-b64
- - make-baselines:
- requires:
- - install-and-cibuild
- - test-baselines:
- requires:
- - make-baselines
- - make-exports:
- requires:
- - install-and-cibuild
- - test-exports:
- requires:
- - make-exports
- - mock-validation:
- requires:
- - install-and-cibuild
- - source-syntax:
- requires:
- - install-and-cibuild
-
- - publish-dist
-
- - publish-dist-node-v22
-
- - test-stackgl-bundle
-
- - test-topojson-build
diff --git a/.circleci/download_google_fonts.py b/.circleci/download_google_fonts.py
deleted file mode 100644
index 8dc9dd7daa6..00000000000
--- a/.circleci/download_google_fonts.py
+++ /dev/null
@@ -1,82 +0,0 @@
-import os
-
-import requests
-
-dir_out = ".circleci/fonts/truetype/googleFonts/"
-
-
-def download(repo, family, types, overwrite=True):
- for t in types:
- name = family + t + ".ttf"
- url = repo + name + "?raw=true"
- out_file = dir_out + name
- print("Getting: ", url)
- if os.path.exists(out_file) and not overwrite:
- print(" => Already exists: ", out_file)
- continue
- req = requests.get(url, allow_redirects=False)
- if req.status_code != 200:
- # If we get a redirect, print an error so that we know to update the URL
- if req.status_code == 302 or req.status_code == 301:
- new_url = req.headers.get("Location")
- print(f" => Redirected -- please update URL to: {new_url}")
- raise RuntimeError(f"""
-Download failed.
-Status code: {req.status_code}
-Message: {req.reason}
-""")
- open(out_file, "wb").write(req.content)
-
-
-download(
- "https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansMono/hinted/ttf/",
- "NotoSansMono",
- ["-Regular", "-Bold"],
-)
-
-download(
- "https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSans/hinted/ttf/",
- "NotoSans",
- ["-Regular", "-Italic", "-Bold"],
-)
-
-download(
- "https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSerif/hinted/ttf/",
- "NotoSerif",
- [
- "-Regular",
- "-Italic",
- "-Bold",
- "-BoldItalic",
- ],
-)
-
-download(
- "https://raw.githubusercontent.com/google/fonts/refs/heads/main/ofl/oldstandardtt/",
- "OldStandard",
- ["-Regular", "-Italic", "-Bold"],
-)
-
-download(
- "https://raw.githubusercontent.com/google/fonts/refs/heads/main/ofl/ptsansnarrow/",
- "PT_Sans-Narrow-Web",
- ["-Regular", "-Bold"],
-)
-
-download(
- "https://raw.githubusercontent.com/impallari/Raleway/refs/heads/master/fonts/v3.000%20Fontlab/TTF/",
- "Raleway",
- ["-Regular", "-Regular-Italic", "-Bold", "-Bold-Italic"],
-)
-
-download(
- "https://raw.githubusercontent.com/googlefonts/roboto-2/refs/heads/main/src/hinted/",
- "Roboto",
- ["-Regular", "-Italic", "-Bold", "-BoldItalic"],
-)
-
-download(
- "https://raw.githubusercontent.com/expo/google-fonts/refs/heads/main/font-packages/gravitas-one/400Regular/",
- "GravitasOne",
- ["_400Regular"],
-)
diff --git a/.circleci/env_image.sh b/.circleci/env_image.sh
deleted file mode 100755
index 17e2f5bfa26..00000000000
--- a/.circleci/env_image.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/sh
-set -e
-# install required fonts
-sudo apt-get install fonts-liberation2 fonts-open-sans fonts-noto-cjk fonts-noto-color-emoji
-
-# install pip
-sudo curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
-sudo python3 get-pip.py
-
-# install additional fonts
-sudo python3 -m pip install requests
-sudo python3 .circleci/download_google_fonts.py
-sudo cp -r .circleci/fonts/ /usr/share/
-sudo apt install fontconfig
-sudo fc-cache -f
-
-# install kaleido & plotly
-sudo python3 -m pip install kaleido==0.2.1 plotly==6.2.0 --progress-bar off
-
-# install numpy i.e. to convert arrays to typed arrays
-sudo python3 -m pip install numpy==1.24.2
diff --git a/.github/actions/run-xvfb/action.yml b/.github/actions/run-xvfb/action.yml
new file mode 100644
index 00000000000..36b2d96fcfc
--- /dev/null
+++ b/.github/actions/run-xvfb/action.yml
@@ -0,0 +1,13 @@
+name: 'Run with Xvfb'
+description: 'Run a command under Xvfb with a preconfigured screen size'
+
+inputs:
+ run:
+ description: 'Command to execute'
+ required: true
+
+runs:
+ using: 'composite'
+ steps:
+ - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" ${{ inputs.run }}
+ shell: bash
diff --git a/.github/actions/setup-chrome/action.yml b/.github/actions/setup-chrome/action.yml
new file mode 100644
index 00000000000..13b9f1a3f33
--- /dev/null
+++ b/.github/actions/setup-chrome/action.yml
@@ -0,0 +1,25 @@
+name: 'Setup Chrome'
+description: 'Install Chrome and set CHROME_BIN environment variable'
+
+inputs:
+ chrome-version:
+ description: 'Chrome version to install'
+ default: '136.0.7103.113'
+
+runs:
+ using: 'composite'
+ steps:
+ - uses: browser-actions/setup-chrome@4f8e94349a351df0f048634f25fec36c3c91eded # v2
+ id: setup-chrome
+ with:
+ chrome-version: ${{ inputs.chrome-version }}
+
+ - name: Set Chrome binary path
+ run: echo "CHROME_BIN=${{ steps.setup-chrome.outputs.chrome-path }}" >> $GITHUB_ENV
+ shell: bash
+
+ - name: Verify Chrome version
+ run: |
+ echo "Chrome path: $CHROME_BIN"
+ $CHROME_BIN --version
+ shell: bash
diff --git a/.github/actions/setup-image-env/action.yml b/.github/actions/setup-image-env/action.yml
new file mode 100644
index 00000000000..c319b2eff53
--- /dev/null
+++ b/.github/actions/setup-image-env/action.yml
@@ -0,0 +1,35 @@
+name: 'Setup Image Environment'
+description: 'Setup Python, uv, and install Kaleido/plotly with required fonts'
+
+inputs:
+ python-version:
+ description: 'Python version to use'
+ default: '3.12'
+ uv-version:
+ description: 'uv version to use'
+ default: '0.11.2'
+
+runs:
+ using: 'composite'
+ steps:
+ - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
+ with:
+ python-version: ${{ inputs.python-version }}
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
+ with:
+ version: ${{ inputs.uv-version }}
+
+ - name: Cache apt font packages
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ id: apt-cache
+ with:
+ path: ~/.cache/apt-fonts
+ key: apt-fonts-${{ runner.os }}-${{ hashFiles('.github/scripts/env_image.sh') }}
+
+ - name: Install Kaleido, plotly.io and required fonts
+ run: .github/scripts/env_image.sh
+ shell: bash
+ env:
+ APT_CACHE_HIT: ${{ steps.apt-cache.outputs.cache-hit }}
diff --git a/.github/actions/setup-workspace/action.yml b/.github/actions/setup-workspace/action.yml
new file mode 100644
index 00000000000..c2e39d0f41b
--- /dev/null
+++ b/.github/actions/setup-workspace/action.yml
@@ -0,0 +1,26 @@
+name: 'Setup Workspace'
+description: 'Setup Node.js, install dependencies, and download build artifacts'
+
+inputs:
+ node-version:
+ description: 'Node.js version to use'
+ default: '18'
+
+runs:
+ using: 'composite'
+ steps:
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
+ with:
+ node-version: ${{ inputs.node-version }}
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+ shell: bash
+ env:
+ NODE_OPTIONS: '--max-old-space-size=4096'
+
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
+ with:
+ name: build-output
+ path: .
diff --git a/.circleci/fonts/truetype/googleFonts/GravitasOne_400Regular.ttf b/.github/fonts/GravitasOne_400Regular.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/GravitasOne_400Regular.ttf
rename to .github/fonts/GravitasOne_400Regular.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/NotoSans-Bold.ttf b/.github/fonts/NotoSans-Bold.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/NotoSans-Bold.ttf
rename to .github/fonts/NotoSans-Bold.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/NotoSans-Italic.ttf b/.github/fonts/NotoSans-Italic.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/NotoSans-Italic.ttf
rename to .github/fonts/NotoSans-Italic.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/NotoSans-Regular.ttf b/.github/fonts/NotoSans-Regular.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/NotoSans-Regular.ttf
rename to .github/fonts/NotoSans-Regular.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/NotoSansMono-Bold.ttf b/.github/fonts/NotoSansMono-Bold.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/NotoSansMono-Bold.ttf
rename to .github/fonts/NotoSansMono-Bold.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/NotoSansMono-Regular.ttf b/.github/fonts/NotoSansMono-Regular.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/NotoSansMono-Regular.ttf
rename to .github/fonts/NotoSansMono-Regular.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/NotoSerif-Bold.ttf b/.github/fonts/NotoSerif-Bold.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/NotoSerif-Bold.ttf
rename to .github/fonts/NotoSerif-Bold.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/NotoSerif-BoldItalic.ttf b/.github/fonts/NotoSerif-BoldItalic.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/NotoSerif-BoldItalic.ttf
rename to .github/fonts/NotoSerif-BoldItalic.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/NotoSerif-Italic.ttf b/.github/fonts/NotoSerif-Italic.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/NotoSerif-Italic.ttf
rename to .github/fonts/NotoSerif-Italic.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/NotoSerif-Regular.ttf b/.github/fonts/NotoSerif-Regular.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/NotoSerif-Regular.ttf
rename to .github/fonts/NotoSerif-Regular.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/OldStandard-Bold.ttf b/.github/fonts/OldStandard-Bold.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/OldStandard-Bold.ttf
rename to .github/fonts/OldStandard-Bold.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/OldStandard-Italic.ttf b/.github/fonts/OldStandard-Italic.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/OldStandard-Italic.ttf
rename to .github/fonts/OldStandard-Italic.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/OldStandard-Regular.ttf b/.github/fonts/OldStandard-Regular.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/OldStandard-Regular.ttf
rename to .github/fonts/OldStandard-Regular.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/PT_Sans-Narrow-Web-Bold.ttf b/.github/fonts/PT_Sans-Narrow-Web-Bold.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/PT_Sans-Narrow-Web-Bold.ttf
rename to .github/fonts/PT_Sans-Narrow-Web-Bold.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/PT_Sans-Narrow-Web-Regular.ttf b/.github/fonts/PT_Sans-Narrow-Web-Regular.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/PT_Sans-Narrow-Web-Regular.ttf
rename to .github/fonts/PT_Sans-Narrow-Web-Regular.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/Raleway-Bold-Italic.ttf b/.github/fonts/Raleway-Bold-Italic.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/Raleway-Bold-Italic.ttf
rename to .github/fonts/Raleway-Bold-Italic.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/Raleway-Bold.ttf b/.github/fonts/Raleway-Bold.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/Raleway-Bold.ttf
rename to .github/fonts/Raleway-Bold.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/Raleway-Regular-Italic.ttf b/.github/fonts/Raleway-Regular-Italic.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/Raleway-Regular-Italic.ttf
rename to .github/fonts/Raleway-Regular-Italic.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/Raleway-Regular.ttf b/.github/fonts/Raleway-Regular.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/Raleway-Regular.ttf
rename to .github/fonts/Raleway-Regular.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/Roboto-Bold.ttf b/.github/fonts/Roboto-Bold.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/Roboto-Bold.ttf
rename to .github/fonts/Roboto-Bold.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/Roboto-BoldItalic.ttf b/.github/fonts/Roboto-BoldItalic.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/Roboto-BoldItalic.ttf
rename to .github/fonts/Roboto-BoldItalic.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/Roboto-Italic.ttf b/.github/fonts/Roboto-Italic.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/Roboto-Italic.ttf
rename to .github/fonts/Roboto-Italic.ttf
diff --git a/.circleci/fonts/truetype/googleFonts/Roboto-Regular.ttf b/.github/fonts/Roboto-Regular.ttf
similarity index 100%
rename from .circleci/fonts/truetype/googleFonts/Roboto-Regular.ttf
rename to .github/fonts/Roboto-Regular.ttf
diff --git a/.github/fonts/SOURCES.md b/.github/fonts/SOURCES.md
new file mode 100644
index 00000000000..95c8a881abd
--- /dev/null
+++ b/.github/fonts/SOURCES.md
@@ -0,0 +1,14 @@
+# Font Sources
+
+These fonts were downloaded from Google Fonts and related repositories for use in image baseline generation during CI. They ensure consistent text rendering across environments.
+
+They can be re-downloaded using the script at `.github/scripts/download_google_fonts.py`.
+
+- **NotoSansMono** (Regular, Bold) — [notofonts/notofonts.github.io](https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansMono/hinted/ttf/)
+- **NotoSans** (Regular, Italic, Bold) — [notofonts/notofonts.github.io](https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSans/hinted/ttf/)
+- **NotoSerif** (Regular, Italic, Bold, BoldItalic) — [notofonts/notofonts.github.io](https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSerif/hinted/ttf/)
+- **OldStandard** (Regular, Italic, Bold) — [google/fonts](https://raw.githubusercontent.com/google/fonts/refs/heads/main/ofl/oldstandardtt/)
+- **PT Sans Narrow** (Regular, Bold) — [google/fonts](https://raw.githubusercontent.com/google/fonts/refs/heads/main/ofl/ptsansnarrow/)
+- **Raleway** (Regular, Regular-Italic, Bold, Bold-Italic) — [impallari/Raleway](https://raw.githubusercontent.com/impallari/Raleway/refs/heads/master/fonts/v3.000%20Fontlab/TTF/)
+- **Roboto** (Regular, Italic, Bold, BoldItalic) — [googlefonts/roboto-2](https://raw.githubusercontent.com/googlefonts/roboto-2/refs/heads/main/src/hinted/)
+- **GravitasOne** (400Regular) — [expo/google-fonts](https://raw.githubusercontent.com/expo/google-fonts/refs/heads/main/font-packages/gravitas-one/400Regular/)
diff --git a/.github/scripts/check-un-geodata.mjs b/.github/scripts/check-un-geodata.mjs
new file mode 100644
index 00000000000..445db145461
--- /dev/null
+++ b/.github/scripts/check-un-geodata.mjs
@@ -0,0 +1,63 @@
+// ArcGIS Portal REST API — Search reference:
+// https://developers.arcgis.com/rest/users-groups-and-items/search-reference.htm
+const API_URL =
+ 'https://geoportal.un.org/arcgis/sharing/rest/search?f=json&q=(type:%22geojson%22)%20AND%20(title:%22*geodata*%22)&sortField=modified&sortOrder=desc';
+
+export default async function checkUnGeodata({ github, context, core }) {
+ // Fetch the UN Geodata API
+ const res = await fetch(API_URL);
+ if (!res.ok) throw new Error(`API request failed: ${res.status}`);
+ const data = await res.json();
+ const newest = data.results[0];
+ if (!newest) throw new Error('No results returned from API');
+
+ const marker = ``;
+
+ // Check if we already created an issue for this modified timestamp
+ const { data: issues } = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ labels: 'topojson',
+ state: 'all',
+ sort: 'created',
+ direction: 'desc',
+ per_page: 100
+ });
+
+ if (issues.some((issue) => issue.body?.includes(marker))) {
+ core.info('No new updates — issue already exists for this timestamp');
+ return;
+ }
+
+ // Build the issue body
+ const summary = data.results
+ .map((r) => {
+ const date = new Date(r.modified).toISOString().slice(0, 10);
+ return `- **${r.title}** (modified: ${date}, id: \`${r.id}\`)`;
+ })
+ .join('\n');
+
+ const body = [
+ marker,
+ '',
+ `The [UN Geoportal geodata API](${API_URL}) has new or updated artifacts.`,
+ '',
+ `### Datasets found (${data.total}):`,
+ summary,
+ '',
+ '### Next steps',
+ '- Review the updated dataset at the [UN Geoportal](https://geoportal.un.org/)',
+ '- Determine if `topojson/` sources need updating',
+ '',
+ '---',
+ `*This issue was created automatically by the [Check UN Geodata Updates](${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/workflows/check-un-geodata.yml) workflow.*`
+ ].join('\n');
+
+ await github.rest.issues.create({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: `UN Geodata update detected: ${newest.title}`,
+ body,
+ labels: ['topojson']
+ });
+}
diff --git a/.circleci/env_build.sh b/.github/scripts/env_build.sh
similarity index 100%
rename from .circleci/env_build.sh
rename to .github/scripts/env_build.sh
diff --git a/.github/scripts/env_image.sh b/.github/scripts/env_image.sh
new file mode 100755
index 00000000000..a27204a3c1e
--- /dev/null
+++ b/.github/scripts/env_image.sh
@@ -0,0 +1,36 @@
+#!/bin/sh
+set -e
+
+APT_PACKAGES="fonts-liberation2 fonts-open-sans fonts-noto-cjk fonts-noto-color-emoji fontconfig"
+APT_CACHE_DIR="${HOME}/.cache/apt-fonts"
+
+if [ "$APT_CACHE_HIT" = "true" ] && [ -d "$APT_CACHE_DIR" ]; then
+ echo "Installing font packages from cache..."
+ sudo dpkg -i "$APT_CACHE_DIR"/*.deb 2>/dev/null || sudo apt-get install -yf
+else
+ echo "Downloading and installing font packages..."
+ sudo apt-get update -q
+ sudo apt-get install -y --no-install-recommends $APT_PACKAGES
+ # Save debs for future cache
+ mkdir -p "$APT_CACHE_DIR"
+ for pkg in $APT_PACKAGES; do
+ cp /var/cache/apt/archives/${pkg}_*.deb "$APT_CACHE_DIR/" 2>/dev/null || true
+ done
+fi
+
+# Rebuild font cache
+sudo fc-cache -f
+
+# Install additional fonts (committed in .github/fonts/)
+sudo cp -r .github/fonts/ /usr/share/
+sudo fc-cache -f
+
+# Install Kaleido & Plotly
+uv pip install --system kaleido==1.2 plotly==6.6.0 --no-progress
+
+# Install numpy i.e. to convert arrays to typed arrays
+uv pip install --system numpy==2.4.3
+
+# Verify version of python and versions of installed python packages
+python --version
+uv pip freeze --system
diff --git a/.github/scripts/split_files.mjs b/.github/scripts/split_files.mjs
new file mode 100755
index 00000000000..51e9a3562a9
--- /dev/null
+++ b/.github/scripts/split_files.mjs
@@ -0,0 +1,19 @@
+#!/usr/bin/env node
+
+// Reads lines from stdin and emits only those where index % SHARD_TOTAL == SHARD_INDEX
+// Environment variables SHARD_INDEX and SHARD_TOTAL must be set.
+
+import { readFileSync } from 'fs';
+
+const lines = readFileSync('/dev/stdin', 'utf8').trim().split('\n').filter(Boolean);
+const index = parseInt(process.env.SHARD_INDEX);
+const total = parseInt(process.env.SHARD_TOTAL);
+
+if (isNaN(index) || isNaN(total) || total <= 0) {
+ console.error('SHARD_INDEX and SHARD_TOTAL environment variables must be set to valid integers');
+ process.exit(1);
+}
+
+lines.forEach((line, i) => {
+ if (i % total === index) console.log(line);
+});
diff --git a/.circleci/test.sh b/.github/scripts/test.sh
similarity index 71%
rename from .circleci/test.sh
rename to .github/scripts/test.sh
index 4fc876018b8..3497c52e98e 100755
--- a/.circleci/test.sh
+++ b/.github/scripts/test.sh
@@ -1,10 +1,10 @@
#!/bin/bash
-# override CircleCi's default run settings
set +e
set +o pipefail
-ROOT=$(dirname $0)/..
+ROOT=$(dirname $0)/../..
+SPLIT="$ROOT/.github/scripts/split_files.mjs"
EXIT_STATE=0
MAX_AUTO_RETRY=0
@@ -33,10 +33,13 @@ retry () {
fi
}
+# Ensure output directories exist (not present in fresh GHA checkout)
+mkdir -p build/test_images
+
case $1 in
no-gl-jasmine)
- SUITE=$(circleci tests glob "$ROOT/test/jasmine/tests/*" | circleci tests split)
+ SUITE=$(ls -1 $ROOT/test/jasmine/tests/* | sort | node "$SPLIT")
MAX_AUTO_RETRY=2
retry npm run test-jasmine -- $SUITE --skip-tags=gl,noCI,flaky || EXIT_STATE=$?
@@ -44,7 +47,7 @@ case $1 in
;;
webgl-jasmine)
- SHARDS=($(node $ROOT/tasks/shard_jasmine_tests.js --limit=5 --tag=gl | circleci tests split))
+ SHARDS=($(node $ROOT/tasks/shard_jasmine_tests.js --limit=5 --tag=gl | node "$SPLIT"))
for s in ${SHARDS[@]}; do
MAX_AUTO_RETRY=2
retry npm run test-jasmine -- "$s" --tags=gl --skip-tags=noCI --doNotFailOnEmptyTestSuite
@@ -54,7 +57,7 @@ case $1 in
;;
virtual-webgl-jasmine)
- SHARDS=($(node $ROOT/tasks/shard_jasmine_tests.js --limit=5 --tag=gl | circleci tests split))
+ SHARDS=($(node $ROOT/tasks/shard_jasmine_tests.js --limit=5 --tag=gl | node "$SPLIT"))
for s in ${SHARDS[@]}; do
MAX_AUTO_RETRY=2
retry ./node_modules/karma/bin/karma start test/jasmine/karma.conf.js --virtualWebgl --tags=gl --skip-tags=noCI,noVirtualWebgl --doNotFailOnEmptyTestSuite -- "$s"
@@ -64,7 +67,7 @@ case $1 in
;;
flaky-no-gl-jasmine)
- SHARDS=($(node $ROOT/tasks/shard_jasmine_tests.js --limit=1 --tag=flaky | circleci tests split))
+ SHARDS=($(node $ROOT/tasks/shard_jasmine_tests.js --limit=1 --tag=flaky | node "$SPLIT"))
for s in ${SHARDS[@]}; do
MAX_AUTO_RETRY=5
@@ -96,40 +99,46 @@ case $1 in
SUITE=$({\
find $ROOT/test/image/mocks/gl* -type f -printf "%f\n"; \
find $ROOT/test/image/mocks/map* -type f -printf "%f\n"; \
- } | sed 's/\.json$//1' | circleci tests split)
- sudo python3 test/image/make_baseline.py virtual-webgl $SUITE || EXIT_STATE=$?
+ } | sed 's/\.json$//1' | sort | node "$SPLIT")
+ python test/image/make_baseline.py virtual-webgl $SUITE || EXIT_STATE=$?
exit $EXIT_STATE
;;
make-baselines-mathjax3)
- sudo python3 test/image/make_baseline.py mathjax3 legend_mathjax_title_and_items mathjax parcats_grid_subplots table_latex_multitrace_scatter table_plain_birds table_wrapped_birds ternary-mathjax ternary-mathjax-title-place-subtitle || EXIT_STATE=$?
+ MATHJAX3_MOCKS=$(jq -r '.mathjax3 | join(" ")' test/image/compare_pixels_collections.json)
+ python test/image/make_baseline.py mathjax3 $MATHJAX3_MOCKS || EXIT_STATE=$?
exit $EXIT_STATE
;;
make-baselines-b64)
- SUITE=$(find $ROOT/test/image/mocks/ -type f -printf "%f\n" | sed 's/\.json$//1' | circleci tests split)
- sudo python3 test/image/make_baseline.py b64 $SUITE || EXIT_STATE=$?
+ SUITE=$(find $ROOT/test/image/mocks/ -type f -printf "%f\n" | sed 's/\.json$//1' | sort | node "$SPLIT")
+ python test/image/make_baseline.py b64 $SUITE || EXIT_STATE=$?
exit $EXIT_STATE
;;
make-baselines)
- SUITE=$(find $ROOT/test/image/mocks/ -type f -printf "%f\n" | sed 's/\.json$//1' | circleci tests split)
- sudo python3 test/image/make_baseline.py $SUITE || EXIT_STATE=$?
+ SUITE=$(find $ROOT/test/image/mocks/ -type f -printf "%f\n" | sed 's/\.json$//1' | sort | node "$SPLIT")
+ python test/image/make_baseline.py $SUITE || EXIT_STATE=$?
+ exit $EXIT_STATE
+ ;;
+
+ make-exports)
+ python test/image/make_exports.py || EXIT_STATE=$?
exit $EXIT_STATE
;;
test-image)
- node test/image/compare_pixels_test.js || { tar -cvf build/baselines.tar build/test_images/*.png ; exit 1 ; } || EXIT_STATE=$?
+ node test/image/compare_pixels_test.mjs || EXIT_STATE=$?
exit $EXIT_STATE
;;
test-image-mathjax3)
- node test/image/compare_pixels_test.js mathjax3 || { tar -cvf build/baselines.tar build/test_images/*.png ; exit 1 ; } || EXIT_STATE=$?
+ node test/image/compare_pixels_test.mjs mathjax3 || EXIT_STATE=$?
exit $EXIT_STATE
;;
test-image-virtual-webgl)
- node test/image/compare_pixels_test.js virtual-webgl || { tar -cvf build/baselines.tar build/test_images/*.png ; exit 1 ; } || EXIT_STATE=$?
+ node test/image/compare_pixels_test.mjs virtual-webgl || EXIT_STATE=$?
exit $EXIT_STATE
;;
diff --git a/.github/workflows/check-un-geodata.yml b/.github/workflows/check-un-geodata.yml
new file mode 100644
index 00000000000..f5f537edd70
--- /dev/null
+++ b/.github/workflows/check-un-geodata.yml
@@ -0,0 +1,25 @@
+name: Check UN Geodata Updates
+
+on:
+ schedule:
+ # First day of every month at 09:00 UTC
+ - cron: '0 9 1 * *'
+ workflow_dispatch:
+
+permissions:
+ issues: write
+
+jobs:
+ check-geodata:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ sparse-checkout: .github/scripts
+
+ - name: Check for UN Geodata updates
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ with:
+ script: |
+ const { default: checkUnGeodata } = await import('${{ github.workspace }}/.github/scripts/check-un-geodata.mjs');
+ await checkUnGeodata({ github, context, core });
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000000..79a69597525
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,755 @@
+name: CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ types:
+ - opened
+ - reopened
+ - synchronize
+ workflow_dispatch:
+
+concurrency:
+ group: ci-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+env:
+ NODE_VERSION: '18'
+
+jobs:
+ detect-changes:
+ runs-on: ubuntu-latest
+ outputs:
+ stackgl_modules: ${{ steps.check.outputs.stackgl_modules }}
+ topojson: ${{ steps.check.outputs.topojson }}
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ fetch-depth: 0
+
+ - name: Detect changed paths
+ id: check
+ run: |
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
+ BASE="${{ github.event.pull_request.base.sha }}"
+ else
+ BASE="${{ github.event.before }}"
+ fi
+ if git diff --name-only "$BASE"...HEAD -- topojson/ | grep -q .; then
+ echo "topojson=true" >> "$GITHUB_OUTPUT"
+ fi
+ if git diff --name-only "$BASE"...HEAD -- stackgl_modules/ | grep -q .; then
+ echo "stackgl_modules=true" >> "$GITHUB_OUTPUT"
+ fi
+
+ # ============================================================
+ # Root build job - all dependent jobs fan out from here
+ # ============================================================
+ install-and-cibuild:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+ env:
+ NODE_OPTIONS: '--max-old-space-size=4096'
+
+ - name: Pretest
+ run: npm run pretest
+
+ - name: CI-Build
+ run: npm run cibuild
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: build-output
+ retention-days: 3
+ path: |
+ build/
+ lib/
+ src/version.js
+
+ # ============================================================
+ # Jasmine browser tests
+ # ============================================================
+ timezone-jasmine:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+ - uses: ./.github/actions/setup-chrome
+
+ - name: Run hover_label test in UTC timezone
+ uses: ./.github/actions/run-xvfb
+ env:
+ TZ: 'UTC'
+ with:
+ run: npm run test-jasmine hover_label
+
+ - name: Run hover_label test in Europe/Berlin timezone
+ uses: ./.github/actions/run-xvfb
+ env:
+ TZ: 'Europe/Berlin'
+ with:
+ run: npm run test-jasmine hover_label
+
+ - name: Run hover_label test in Asia/Tokyo timezone
+ uses: ./.github/actions/run-xvfb
+ env:
+ TZ: 'Asia/Tokyo'
+ with:
+ run: npm run test-jasmine hover_label
+
+ - name: Run hover_label test in America/Toronto timezone
+ uses: ./.github/actions/run-xvfb
+ env:
+ TZ: 'America/Toronto'
+ with:
+ run: npm run test-jasmine hover_label
+
+ no-gl-jasmine:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ shard: [0, 1, 2, 3]
+ env:
+ TZ: 'America/Anchorage'
+ SHARD_INDEX: ${{ matrix.shard }}
+ SHARD_TOTAL: 4
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+ - uses: ./.github/actions/setup-chrome
+
+ - name: Run jasmine tests (no-gl, shard ${{ matrix.shard }})
+ uses: ./.github/actions/run-xvfb
+ with:
+ run: .github/scripts/test.sh no-gl-jasmine
+
+ webgl-jasmine:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ shard: [0, 1, 2, 3, 4, 5, 6, 7]
+ env:
+ TZ: 'America/Anchorage'
+ SHARD_INDEX: ${{ matrix.shard }}
+ SHARD_TOTAL: 8
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+ - uses: ./.github/actions/setup-chrome
+
+ - name: Run jasmine tests (webgl, shard ${{ matrix.shard }})
+ uses: ./.github/actions/run-xvfb
+ with:
+ run: .github/scripts/test.sh webgl-jasmine
+
+ virtual-webgl-jasmine:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ shard: [0, 1, 2, 3, 4, 5, 6, 7]
+ env:
+ TZ: 'America/Anchorage'
+ SHARD_INDEX: ${{ matrix.shard }}
+ SHARD_TOTAL: 8
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+ - uses: ./.github/actions/setup-chrome
+
+ - name: Run jasmine tests (virtual-webgl, shard ${{ matrix.shard }})
+ uses: ./.github/actions/run-xvfb
+ with:
+ run: .github/scripts/test.sh virtual-webgl-jasmine
+
+ webgl-jasmine-chromeLatest:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ shard: [0, 1, 2, 3, 4, 5, 6, 7]
+ env:
+ TZ: 'America/Anchorage'
+ SHARD_INDEX: ${{ matrix.shard }}
+ SHARD_TOTAL: 8
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+ - uses: ./.github/actions/setup-chrome
+ with:
+ chrome-version: '143.0.7499.192'
+
+ - name: Run jasmine tests (webgl chromeLatest, shard ${{ matrix.shard }})
+ uses: ./.github/actions/run-xvfb
+ with:
+ run: .github/scripts/test.sh webgl-jasmine
+
+ flaky-no-gl-jasmine:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ env:
+ TZ: 'America/Anchorage'
+ SHARD_INDEX: 0
+ SHARD_TOTAL: 1
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+ - uses: ./.github/actions/setup-chrome
+
+ - name: Run flaky jasmine tests
+ uses: ./.github/actions/run-xvfb
+ with:
+ run: .github/scripts/test.sh flaky-no-gl-jasmine
+
+ bundle-jasmine:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ env:
+ TZ: 'America/Anchorage'
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+ - uses: ./.github/actions/setup-chrome
+
+ - name: Run bundle jasmine tests
+ uses: ./.github/actions/run-xvfb
+ with:
+ run: .github/scripts/test.sh bundle-jasmine
+
+ mathjax-firefoxLatest:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ env:
+ TZ: 'America/Anchorage'
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+
+ - uses: browser-actions/setup-firefox@fcf821c621167805dd63a29662bd7cb5676c81a8 # v1
+
+ - name: Test MathJax on firefox-latest
+ uses: ./.github/actions/run-xvfb
+ with:
+ run: .github/scripts/test.sh mathjax-firefox
+
+ # ============================================================
+ # noCI tests
+ # ============================================================
+ noci-jasmine:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+ - uses: ./.github/actions/setup-chrome
+ with:
+ chrome-version: 'stable'
+
+ - name: Run noCI tests
+ uses: coactions/setup-xvfb@b6b4fcfb9f5a895edadc3bc76318fae0ac17c8b3 # v1
+ with:
+ run: ./tasks/noci_test.sh jasmine
+
+ # ============================================================
+ # Image baseline generation and comparison
+ # ============================================================
+ make-baselines:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
+ env:
+ SHARD_INDEX: ${{ matrix.shard }}
+ SHARD_TOTAL: 12
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+
+ - uses: ./.github/actions/setup-image-env
+
+ - name: Create all png files
+ run: .github/scripts/test.sh make-baselines
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: baselines-default-${{ matrix.shard }}
+ retention-days: 3
+ path: build/test_images/
+
+ test-baselines:
+ needs: make-baselines
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
+ with:
+ pattern: baselines-default-*
+ path: build/test_images/
+ merge-multiple: true
+
+ - name: Compare pixels
+ run: .github/scripts/test.sh test-image
+ - name: List diff folder contents
+ if: failure()
+ run: |
+ echo "=== build/test_images/ ==="
+ ls -la build/test_images/ 2>/dev/null || echo "(empty or missing)"
+ echo "=== build/test_images_diff/ ==="
+ ls -la build/test_images_diff/ 2>/dev/null || echo "(empty or missing)"
+ - name: IMAGE DIFF DETECTED
+ if: failure()
+ run: |
+ echo "::warning::Image Diff Detected: baseline images may need to be updated."
+ echo "Download the baseline images from the artifacts of this workflow run."
+ echo "Add the new images to 'test/image/baselines/' and commit them to this pull request."
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ name: Upload test-baselines image diffs
+ if: failure()
+ with:
+ name: baselines-default-diff
+ retention-days: 7
+ path: |
+ build/test_images/
+ build/test_images_diff/
+
+ make-baselines-b64:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
+ env:
+ SHARD_INDEX: ${{ matrix.shard }}
+ SHARD_TOTAL: 12
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+
+ - uses: ./.github/actions/setup-image-env
+
+ - name: Create all png files (b64)
+ run: .github/scripts/test.sh make-baselines-b64
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: baselines-b64-${{ matrix.shard }}
+ retention-days: 3
+ path: build/test_images/
+
+ test-baselines-b64:
+ needs: make-baselines-b64
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
+ with:
+ pattern: baselines-b64-*
+ path: build/test_images/
+ merge-multiple: true
+
+ - name: Compare pixels
+ run: .github/scripts/test.sh test-image
+ - name: IMAGE DIFF DETECTED
+ if: failure()
+ run: |
+ echo "::warning::Image Diff Detected: baseline images may need to be updated."
+ echo "Download the baseline images from the artifacts of this workflow run."
+ echo "Add the new images to 'test/image/baselines/' and commit them to this pull request."
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ name: Upload test-baselines-b64 image diffs
+ if: failure()
+ with:
+ name: baselines-b64-diff
+ retention-days: 7
+ path: |
+ build/test_images/
+ build/test_images_diff/
+
+ make-baselines-virtual-webgl:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ shard: [0, 1, 2, 3, 4, 5, 6, 7]
+ env:
+ SHARD_INDEX: ${{ matrix.shard }}
+ SHARD_TOTAL: 8
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+
+ - uses: ./.github/actions/setup-image-env
+
+ - name: Create png files (virtual-webgl)
+ run: .github/scripts/test.sh make-baselines-virtual-webgl
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: baselines-virtual-webgl-${{ matrix.shard }}
+ retention-days: 3
+ path: build/test_images/
+
+ test-baselines-virtual-webgl:
+ needs: make-baselines-virtual-webgl
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
+ with:
+ pattern: baselines-virtual-webgl-*
+ path: build/test_images/
+ merge-multiple: true
+
+ - name: Compare pixels
+ run: .github/scripts/test.sh test-image-virtual-webgl
+ - name: IMAGE DIFF DETECTED
+ if: failure()
+ run: |
+ echo "::warning::Image Diff Detected: baseline images may need to be updated."
+ echo "Download the baseline images from the artifacts of this workflow run."
+ echo "Add the new images to 'test/image/baselines/' and commit them to this pull request."
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ name: Upload test-baselines-virtual-webgl image diffs
+ if: failure()
+ with:
+ name: baselines-virtual-webgl-diff
+ retention-days: 7
+ path: |
+ build/test_images/
+ build/test_images_diff/
+
+ make-baselines-mathjax3:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+
+ - uses: ./.github/actions/setup-image-env
+
+ - name: Create mathjax v3 png files
+ run: .github/scripts/test.sh make-baselines-mathjax3
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: baselines-mathjax3
+ retention-days: 3
+ path: build/test_images/
+
+ test-baselines-mathjax3:
+ needs: make-baselines-mathjax3
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
+ with:
+ name: baselines-mathjax3
+ path: build/test_images/
+
+ - name: Compare pixels of mathjax v3 baselines
+ run: .github/scripts/test.sh test-image-mathjax3
+
+ - name: IMAGE DIFF DETECTED
+ if: failure()
+ run: |
+ echo "::warning::Image Diff Detected: baseline images may need to be updated."
+ echo "Download the baseline images from the artifacts of this workflow run."
+ echo "Add the new images to 'test/image/baselines/' and commit them to this pull request."
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ name: Upload test-baselines-mathjax3 image diffs
+ if: failure()
+ with:
+ name: baselines-mathjax3-diff
+ retention-days: 7
+ path: |
+ build/test_images/
+ build/test_images_diff/
+
+ # ============================================================
+ # Export generation and testing
+ # ============================================================
+ make-exports:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+
+ - uses: ./.github/actions/setup-image-env
+
+ - name: Install poppler-utils for eps export
+ run: |
+ sudo apt-get update --allow-releaseinfo-change
+ sudo apt-get install poppler-utils
+
+ - name: Create svg, jpg, jpeg, webp, pdf and eps files
+ run: .github/scripts/test.sh make-exports
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: exports
+ retention-days: 3
+ path: build/
+
+ test-exports:
+ needs: make-exports
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
+ with:
+ name: exports
+ path: build/
+
+ - name: Test export sizes
+ run: node test/image/export_test.js
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ if: failure()
+ name: Upload test-exports diffs
+ with:
+ name: exports-diff
+ retention-days: 7
+ path: build/
+
+ # ============================================================
+ # Validation jobs
+ # ============================================================
+ mock-validation:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+
+ - name: Test validation using node.js and jsdom
+ run: npm run test-plain-obj
+
+ - name: Validate mocks
+ run: npm run test-mock
+
+ source-syntax:
+ needs: install-and-cibuild
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-workspace
+
+ - name: Run syntax tests on source files
+ run: .github/scripts/test.sh source-syntax
+
+ # ============================================================
+ # Standalone jobs (no dependencies on install-and-cibuild)
+ # ============================================================
+ publish-dist:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ fetch-depth: 0
+ fetch-tags: true
+
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: 'npm'
+
+ - name: Set up build environment
+ run: .github/scripts/env_build.sh
+
+ - name: Preview CHANGELOG for next release (only on master)
+ if: github.ref == 'refs/heads/master'
+ run: npm run use-draftlogs && git --no-pager diff --color-words CHANGELOG.md || true
+
+ - name: Set draft version in package.json
+ run: |
+ node --eval "var fs = require('fs'); var inOut = './package.json'; var data = JSON.parse(fs.readFileSync(inOut)); var a = process.argv; data.version = a[a.length - 1].replace('v', ''); fs.writeFileSync(inOut, JSON.stringify(data, null, 2) + '\n');" $(git describe)
+
+ - name: View package.json diff between previous and next releases
+ run: git --no-pager diff --color-words tags/$(git describe --tags --abbrev=0) package.json || true
+
+ - name: Build dist/
+ run: npm run build
+
+ # Upload library uncompressed to allow for testing in REPLs
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ name: Upload uncompressed plotly.js built from PR, using Node 22
+ with:
+ retention-days: 30
+ archive: false
+ path: dist/plotly.js
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ name: Upload Node 18 archive of plotly.js build folder
+ with:
+ name: dist-node18
+ retention-days: 7
+ path: dist/
+
+ - name: View dist/README.md diff between previous and next releases
+ run: git --no-pager diff --color-words tags/$(git describe --tags --abbrev=0) dist/README.md || true
+
+ - name: Preview plot-schema diff (only on master)
+ if: github.ref == 'refs/heads/master'
+ run: git --no-pager diff tags/$(git describe --tags --abbrev=0) dist/plot-schema.json || true
+
+ - name: Test plot-schema.json diff
+ run: diff --unified --color dist/plot-schema.json test/plot-schema.json
+
+ publish-dist-node-v22:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ fetch-depth: 0
+ fetch-tags: true
+
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
+ with:
+ node-version: '22.14.0'
+ cache: 'npm'
+
+ - name: Set up build environment
+ run: .github/scripts/env_build.sh
+
+ - name: Preview CHANGELOG for next release (only on master)
+ if: github.ref == 'refs/heads/master'
+ run: npm run use-draftlogs && git --no-pager diff --color-words CHANGELOG.md || true
+
+ - name: Set draft version in package.json
+ run: |
+ node --eval "var fs = require('fs'); var inOut = './package.json'; var data = JSON.parse(fs.readFileSync(inOut)); var a = process.argv; data.version = a[a.length - 1].replace('v', ''); fs.writeFileSync(inOut, JSON.stringify(data, null, 2) + '\n');" $(git describe)
+
+ - name: View package.json diff between previous and next releases
+ run: git --no-pager diff --color-words tags/$(git describe --tags --abbrev=0) package.json || true
+
+ - name: Build dist/
+ run: npm run build
+
+ # This is necessary to avoid a naming collision with the upload from the Node 18 build
+ - name: Copy library for upload
+ run: cp dist/plotly.js dist/plotly.node22.js
+
+ # Upload library uncompressed to allow for testing in REPLs
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ name: Upload uncompressed plotly.js built from PR, using Node 22
+ with:
+ retention-days: 30
+ archive: false
+ path: dist/plotly.node22.js
+
+ - name: Remove copy of library
+ run: rm dist/plotly.node22.js
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ name: Upload Node 22 archive of plotly.js build folder
+ with:
+ name: dist-node22
+ retention-days: 7
+ path: dist/
+
+ - name: View dist/README.md diff between previous and next releases
+ run: git --no-pager diff --color-words tags/$(git describe --tags --abbrev=0) dist/README.md || true
+
+ - name: Preview plot-schema diff (only on master)
+ if: github.ref == 'refs/heads/master'
+ run: git --no-pager diff tags/$(git describe --tags --abbrev=0) dist/plot-schema.json || true
+
+ - name: Test plot-schema.json diff
+ run: diff --unified --color dist/plot-schema.json test/plot-schema.json
+
+ test-stackgl-bundle:
+ needs: detect-changes
+ if: >-
+ (github.event_name == 'push' && github.ref_name == github.event.repository.default_branch) ||
+ needs.detect-changes.outputs.stackgl_modules == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: 'npm'
+
+ - name: Set up build environment
+ run: cd stackgl_modules && npm ci
+
+ - name: Bundle bundle-stackgl/index.js
+ run: cd stackgl_modules && cp index.js INDEX.js && npm run bundle-stackgl
+
+ - name: Test stackgl_modules/index.js diff
+ run: diff --unified --color stackgl_modules/INDEX.js stackgl_modules/index.js
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ name: Upload archive of stackgl_modules bundle
+ if: failure()
+ with:
+ name: stackgl-bundle
+ retention-days: 7
+ path: stackgl_modules/index.js
+
+ test-topojson-build:
+ needs: detect-changes
+ if: >-
+ (github.event_name == 'push' && github.ref_name == github.event.repository.default_branch) ||
+ needs.detect-changes.outputs.topojson == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: 'npm'
+
+ - name: Set up build environment
+ run: cd topojson && npm ci
+
+ - name: Build topojson
+ run: cd topojson && mv dist dist_backup && npm run build
+
+ - name: Compare existing files with newly built files
+ run: diff -qr topojson/dist topojson/dist_backup
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ name: Upload archive of topojson files
+ if: failure()
+ with:
+ name: topojson-dist
+ retention-days: 7
+ path: topojson/dist/
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index 93caad4a454..00000000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
-# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
-
-name: No CI Test
-
-on: push
-
-jobs:
- build:
-
- runs-on: ubuntu-latest
-
- strategy:
- matrix:
- node-version: [18.x]
-
- steps:
- - uses: browser-actions/setup-chrome@v2
- id: setup-chrome
- - uses: actions/checkout@v4
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4
- with:
- node-version: ${{ matrix.node-version }}
- cache: 'npm'
- - name: Set Chrome binary path
- run: echo "CHROME_BIN=${{ steps.setup-chrome.outputs.chrome-path }}" >> $GITHUB_ENV
- - name: Verify Chrome version
- run: |
- echo "Chrome path: $CHROME_BIN"
- $CHROME_BIN --version
- - run: ls
- - run: npm run pretest
- - run: npm ci
- - run: npm run cibuild
- - name: Run noCI tests
- uses: coactions/setup-xvfb@v1
- with:
- run: ./tasks/noci_test.sh jasmine
diff --git a/.gitignore b/.gitignore
index 54c049475cc..5020b4b9c08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,7 +13,6 @@ npm-debug.log*
tags
.*
-!.circleci
!.github/
!.gitignore
!.npmignore
diff --git a/.npmignore b/.npmignore
index e2163312ed0..b340f1e4467 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,18 +1,16 @@
+# Exclude all hidden files and folders
+.*
+
+# Exclude all files in build/, except for plotcss.js and README.md
build/*
!build/plotcss.js
!build/README.md
-stackgl_modules/node_modules
-
+# Exclude these directories
devtools
-test
-draftlogs
dist/extras
-
-circle.yml
-bower.json
-
-.ackrc
-.agignore
-
-npm-debug.log
+draftlogs
+stackgl_modules/node_modules
+tasks
+test
+topojson
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46d08a00060..40c38459472 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,17 @@ To see all merged commits on the master branch that will be part of the next plo
where X.Y.Z is the semver of most recent plotly.js release.
+## [3.5.1] -- 2026-05-01
+
+### Changed
+
+- Reduce NPM bundle size by removing unneeded files [[#7746](https://github.com/plotly/plotly.js/pull/7746)]
+
+### Fixed
+
+- Fix bug where certain `axis.minallowed` values could cause axis to be reversed [[#7761](https://github.com/plotly/plotly.js/pull/7761)]
+
+
## [3.5.0] -- 2026-04-03
### Added
diff --git a/CITATION.cff b/CITATION.cff
index b910369179b..38d294398c5 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -9,7 +9,7 @@ authors:
- family-names: "Samimi"
given-names: "Mojtaba"
title: "Open source Plotly charting library"
-version: 3.5.0
+version: 3.5.1
doi: 10.5281/zenodo.13964707
-date-released: 2026-04-03
+date-released: 2026-05-01
url: "https://github.com/plotly/plotly.js"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2ef078e9739..d7cfa2bd96c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -169,10 +169,9 @@ making changes that affect the usage of regl shaders, you would need to run
npm run regl-codegen
```
-to regenerate the regl code. This opens a browser window, runs through all
-traces with 'regl' in the tags, and stores the captured code into
-[src/generated/regl-codegen](https://github.com/plotly/plotly.js/blob/master/src/generated/regl-codegen). If no updates are necessary, it would be a no-op, but
-if there are changes, you would need to commit them.
+to regenerate the regl code. This will prompt you to open a browser window. This will then run through all
+traces with 'regl' in the tags, and store the captured code into
+[src/generated/regl-codegen](https://github.com/plotly/plotly.js/blob/master/src/generated/regl-codegen). If no updates are necessary, it will be a no-op, but if there are changes, you will need to commit them.
This is needed because regl performs codegen in runtime which breaks CSP
compliance, and so for strict builds we pre-generate regl shader code here.
@@ -186,8 +185,8 @@ compliance, and so for strict builds we pre-generate regl shader code here.
## Testing
-Both jasmine and image tests are run on
-[CircleCI](https://circleci.com/gh/plotly/plotly.js) on every push to this
+Both Jasmine and image tests are run on
+[GitHub Actions](https://github.com/plotly/plotly.js/actions/workflows/ci.yml) on every push to this
repo.
### Jasmine tests
@@ -240,30 +239,31 @@ npm run test-jasmine -- --info
```
### Draft new baselines
-#### With docker:
-> If you prefer using docker each time you need to
+
+#### With Docker:
```sh
-docker run -it -v "$(pwd)":/plotly.js circleci/python:3.8.9 bash
-# then inside the docker
-cd plotly.js
-sudo bash .circleci/env_image.sh
+docker run -it -v "$(pwd)":/plotly.js python:3.12 bash
+# Then inside the container
+cd /plotly.js
+pip install uv
+bash .github/scripts/env_image.sh
```
-#### Without docker:
-> Otherwise you may need to install `python 3.8`
-Then upgrade `pip` if needed
+#### Without Docker:
+Ensure you have Python 3.12+ and [`uv`](https://docs.astral.sh/uv/) installed.
+
+To install required fonts and tools, run the [setup script](https://github.com/plotly/plotly.js/blob/master/.github/scripts/env_image.sh):
```sh
-python3 -m pip install --upgrade pip
+bash .github/scripts/env_image.sh
```
-To install required fonts and tools see this [shell script](https://github.com/plotly/plotly.js/blob/master/.circleci/env_image.sh).
+#### Scripts to generate/update new baselines with/without Docker:
-#### Scripts to generate/update new baselines with/without docker:
```sh
python3 test/image/make_baseline.py = mock_1 mock_2
```
-> Alternatively using npm & node.js (which are not available in the python docker by default)
+Alternatively, you can use Node:
```sh
npm run baseline mock_1 mock_2
@@ -287,10 +287,11 @@ If you added new mocks to test/image/mocks folder, to generate draft baselines r
```sh
python3 test/image/make_baseline.py = mockFilename1 mockFilename2
```
+
Then commit the new baselines and push.
-Please note that image pixel comparison tests run using circleci/python:3.8.9 docker container.
+Please note that image pixel comparison tests run on `ubuntu-latest` in GitHub Actions.
Therefore the final baselines may need updates.
-This could simply be done by downloading the `baselines.tar` stored in the `ARTIFACTS` tab of `test-baselines` job (if the test failed).
+To use the images generated by the CI job directly, you can download the baseline images from the "Upload image diffs" step of the `test-baselines` job in the failed workflow run.
### Using the developer console in karma to write/debug jasmine tests
@@ -390,4 +391,3 @@ Other methods used by some trace modules:
## Coding style
Check if OK with `npm run lint`
-
diff --git a/README.md b/README.md
index 112edfe6454..59b737594be 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://badge.fury.io/js/plotly.js)
-[](https://circleci.com/gh/plotly/plotly.js)
+[](https://github.com/plotly/plotly.js/actions/workflows/ci.yml)
[](https://github.com/plotly/plotly.js/blob/master/LICENSE)
[Plotly.js](https://plotly.com/javascript) is a standalone JavaScript data visualization library, and it also powers the Python and R modules named `plotly` in those respective ecosystems (referred to as [Plotly.py](https://plotly.com/python) and [Plotly.R](http://plotly.com/r)).
@@ -62,7 +62,7 @@ You may also consider using [`plotly.js-dist`](https://www.npmjs.com/package/plo
```html
@@ -23,9 +26,10 @@
-
+
+
diff --git a/devtools/test_dashboard/server.mjs b/devtools/test_dashboard/server.mjs
index f1c7ca071ed..dc16f92e332 100644
--- a/devtools/test_dashboard/server.mjs
+++ b/devtools/test_dashboard/server.mjs
@@ -2,7 +2,6 @@ import ecstatic from 'ecstatic';
import { build, context } from 'esbuild';
import http from 'http';
import minimist from 'minimist';
-import open from 'open';
import { devtoolsConfig, localDevConfig } from '../../esbuild-config.js';
import constants from '../../tasks/util/constants.js';
import { createMocksList, getMockFiles, readFiles, saveMockListToFile } from '../dashboard_utilities.mjs';
@@ -64,5 +63,6 @@ function devServer() {
indexName += '.html';
// open up browser window
- open(`http://localhost:${PORT}/devtools/test_dashboard/${indexName}${strict ? '?strict=true' : ''}`);
+ const serverUrl = `http://localhost:${PORT}/devtools/test_dashboard/${indexName}${strict ? '?strict=true' : ''}`;
+ console.log(` \x1b[1m\x1b[32m➜\x1b[0m \x1b[1mLocal:\x1b[0m \x1b[1m\x1b[36m${serverUrl}\x1b[0m\n`);
}
diff --git a/dist/README.md b/dist/README.md
index a0829959a04..0a03ba5d068 100644
--- a/dist/README.md
+++ b/dist/README.md
@@ -46,9 +46,9 @@ The main plotly.js bundles weight in at:
| 10.7 MB | 4.6 MB | 1.4 MB | 11 MB |
#### CDN links
-> https://cdn.plot.ly/plotly-3.5.0.js
+> https://cdn.plot.ly/plotly-3.5.1.js
-> https://cdn.plot.ly/plotly-3.5.0.min.js
+> https://cdn.plot.ly/plotly-3.5.1.min.js
#### npm packages
@@ -94,9 +94,9 @@ The `basic` partial bundle contains trace modules `bar`, `pie` and `scatter`.
| 2.7 MB | 1.1 MB | 364.8 kB |
#### CDN links
-> https://cdn.plot.ly/plotly-basic-3.5.0.js
+> https://cdn.plot.ly/plotly-basic-3.5.1.js
-> https://cdn.plot.ly/plotly-basic-3.5.0.min.js
+> https://cdn.plot.ly/plotly-basic-3.5.1.min.js
#### npm packages
@@ -117,9 +117,9 @@ The `cartesian` partial bundle contains trace modules `bar`, `box`, `contour`, `
| 3.4 MB | 1.4 MB | 463 kB |
#### CDN links
-> https://cdn.plot.ly/plotly-cartesian-3.5.0.js
+> https://cdn.plot.ly/plotly-cartesian-3.5.1.js
-> https://cdn.plot.ly/plotly-cartesian-3.5.0.min.js
+> https://cdn.plot.ly/plotly-cartesian-3.5.1.min.js
#### npm packages
@@ -140,9 +140,9 @@ The `geo` partial bundle contains trace modules `choropleth`, `scatter` and `sca
| 3 MB | 1.2 MB | 415.2 kB |
#### CDN links
-> https://cdn.plot.ly/plotly-geo-3.5.0.js
+> https://cdn.plot.ly/plotly-geo-3.5.1.js
-> https://cdn.plot.ly/plotly-geo-3.5.0.min.js
+> https://cdn.plot.ly/plotly-geo-3.5.1.min.js
#### npm packages
@@ -163,9 +163,9 @@ The `gl3d` partial bundle contains trace modules `cone`, `isosurface`, `mesh3d`,
| 4.2 MB | 1.6 MB | 527.4 kB |
#### CDN links
-> https://cdn.plot.ly/plotly-gl3d-3.5.0.js
+> https://cdn.plot.ly/plotly-gl3d-3.5.1.js
-> https://cdn.plot.ly/plotly-gl3d-3.5.0.min.js
+> https://cdn.plot.ly/plotly-gl3d-3.5.1.min.js
#### npm packages
@@ -186,9 +186,9 @@ The `gl2d` partial bundle contains trace modules `parcoords`, `scatter`, `scatte
| 3.8 MB | 1.5 MB | 521.2 kB |
#### CDN links
-> https://cdn.plot.ly/plotly-gl2d-3.5.0.js
+> https://cdn.plot.ly/plotly-gl2d-3.5.1.js
-> https://cdn.plot.ly/plotly-gl2d-3.5.0.min.js
+> https://cdn.plot.ly/plotly-gl2d-3.5.1.min.js
#### npm packages
@@ -209,9 +209,9 @@ The `mapbox` partial bundle contains trace modules `choroplethmapbox`, `densitym
| 4.5 MB | 1.9 MB | 580.1 kB |
#### CDN links
-> https://cdn.plot.ly/plotly-mapbox-3.5.0.js
+> https://cdn.plot.ly/plotly-mapbox-3.5.1.js
-> https://cdn.plot.ly/plotly-mapbox-3.5.0.min.js
+> https://cdn.plot.ly/plotly-mapbox-3.5.1.min.js
#### npm packages
@@ -232,9 +232,9 @@ The `finance` partial bundle contains trace modules `bar`, `candlestick`, `funne
| 2.9 MB | 1.2 MB | 398.5 kB |
#### CDN links
-> https://cdn.plot.ly/plotly-finance-3.5.0.js
+> https://cdn.plot.ly/plotly-finance-3.5.1.js
-> https://cdn.plot.ly/plotly-finance-3.5.0.min.js
+> https://cdn.plot.ly/plotly-finance-3.5.1.min.js
#### npm packages
@@ -255,9 +255,9 @@ The `strict` partial bundle contains trace modules `bar`, `barpolar`, `box`, `ca
| 11.5 MB | 4.9 MB | 1.5 MB |
#### CDN links
-> https://cdn.plot.ly/plotly-strict-3.5.0.js
+> https://cdn.plot.ly/plotly-strict-3.5.1.js
-> https://cdn.plot.ly/plotly-strict-3.5.0.min.js
+> https://cdn.plot.ly/plotly-strict-3.5.1.min.js
#### npm packages
diff --git a/dist/plotly-basic.js b/dist/plotly-basic.js
index 4659cddaac8..67d7a8efea3 100644
--- a/dist/plotly-basic.js
+++ b/dist/plotly-basic.js
@@ -1,5 +1,5 @@
/**
-* plotly.js (basic) v3.5.0
+* plotly.js (basic) v3.5.1
* Copyright 2012-2026, Plotly, Inc.
* All rights reserved.
* Licensed under the MIT license
@@ -42,7 +42,7 @@ var Plotly = (() => {
var require_version = __commonJS({
"src/version.js"(exports) {
"use strict";
- exports.version = "3.5.0";
+ exports.version = "3.5.1";
}
});
@@ -25552,20 +25552,21 @@ var Plotly = (() => {
var axrev = rng[1] < rng[0];
if (axrev) rng.reverse();
var bounds = Lib.simpleMap([minallowed, maxallowed], ax.r2l);
- if (minallowed !== void 0 && rng[0] < bounds[0]) range[axrev ? 1 : 0] = minallowed;
- if (maxallowed !== void 0 && rng[1] > bounds[1]) range[axrev ? 0 : 1] = maxallowed;
- if (range[0] === range[1]) {
- var minL = ax.l2r(minallowed);
- var maxL = ax.l2r(maxallowed);
+ if (minallowed !== void 0 && rng[0] < bounds[0]) {
+ range[axrev ? 1 : 0] = minallowed;
+ rng[0] = bounds[0];
+ }
+ if (maxallowed !== void 0 && rng[1] > bounds[1]) {
+ range[axrev ? 0 : 1] = maxallowed;
+ rng[1] = bounds[1];
+ }
+ if (rng[0] >= rng[1]) {
if (minallowed !== void 0) {
- var _max = minL + 1;
- if (maxallowed !== void 0) _max = Math.min(_max, maxL);
- range[axrev ? 1 : 0] = _max;
- }
- if (maxallowed !== void 0) {
- var _min = maxL + 1;
- if (minallowed !== void 0) _min = Math.max(_min, minL);
- range[axrev ? 0 : 1] = _min;
+ var _max = bounds[0] + 1;
+ if (maxallowed !== void 0) _max = Math.min(_max, bounds[1]);
+ range[axrev ? 0 : 1] = ax.l2r(_max);
+ } else if (maxallowed !== void 0) {
+ range[axrev ? 1 : 0] = ax.l2r(bounds[1] - 1);
}
}
};
diff --git a/dist/plotly-basic.min.js b/dist/plotly-basic.min.js
index 68d09a7651c..61576847e41 100644
--- a/dist/plotly-basic.min.js
+++ b/dist/plotly-basic.min.js
@@ -1,5 +1,5 @@
/**
-* plotly.js (basic - minified) v3.5.0
+* plotly.js (basic - minified) v3.5.1
* Copyright 2012-2026, Plotly, Inc.
* All rights reserved.
* Licensed under the MIT license
@@ -12,12 +12,12 @@
root.moduleName = factory();
}
} (typeof self !== "undefined" ? self : this, () => {
-"use strict";var Plotly=(()=>{var fI=Object.defineProperty,cI=Object.defineProperties;var vI=Object.getOwnPropertyDescriptors;var pb=Object.getOwnPropertySymbols;var hI=Object.prototype.hasOwnProperty,dI=Object.prototype.propertyIsEnumerable;var mb=(e,r,t)=>r in e?fI(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t,no=(e,r)=>{for(var t in r||(r={}))hI.call(r,t)&&mb(e,t,r[t]);if(pb)for(var t of pb(r))dI.call(r,t)&&mb(e,t,r[t]);return e},mv=(e,r)=>cI(e,vI(r));var J=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports);var yv=J(yb=>{"use strict";yb.version="3.5.0"});var bb=J((gb,gv)=>{(function(r,t,a){t[r]=t[r]||a(),typeof gv!="undefined"&&gv.exports&&(gv.exports=t[r])})("Promise",typeof window!="undefined"?window:gb,function(){"use strict";var r,t,a,n=Object.prototype.toString,i=typeof setImmediate!="undefined"?function(w){return setImmediate(w)}:setTimeout;try{Object.defineProperty({},"x",{}),r=function(w,T,b,M){return Object.defineProperty(w,T,{value:b,writable:!0,configurable:M!==!1})}}catch(_){r=function(T,b,M){return T[b]=M,T}}a=function(){var w,T,b;function M(C,k){this.fn=C,this.self=k,this.next=void 0}return{add:function(k,q){b=new M(k,q),T?T.next=b:w=b,T=b,b=void 0},drain:function(){var k=w;for(w=T=t=void 0;k;)k.fn.call(k.self),k=k.next}}}();function l(_,w){a.add(_,w),t||(t=i(a.drain))}function o(_){var w,T=typeof _;return _!=null&&(T=="object"||T=="function")&&(w=_.then),typeof w=="function"?w:!1}function s(){for(var _=0;_p?1:c>=p?0:NaN}e.descending=function(c,p){return p =p.length)return S?S.call(c,N):A?N.sort(A):N;for(var X=-1,j=N.length,ee=p[G++],he,we,te,se=new b,ue;++X p?1:c>=p?0:NaN}e.descending=function(c,p){return p =p.length)return S?S.call(c,N):A?N.sort(A):N;for(var X=-1,j=N.length,ee=p[G++],he,we,te,se=new b,ue;++X oe)E=E.L;else if(R=p-mN(E,x),R>oe){if(!E.R){A=E;break}E=E.R}else{L>-oe?(A=E.P,S=E):R>-oe?(A=E,S=E.N):A=S=E;break}var N=o5(c);if(hs.insert(A,N),!(!A&&!S)){if(A===S){ps(A),S=o5(A.site),hs.insert(N,S),N.edge=S.edge=Hu(A.site,N.site),ds(A),ds(S);return}if(!S){N.edge=Hu(A.site,N.site);return}ps(A),ps(S);var G=A.site,X=G.x,j=G.y,ee=c.x-X,he=c.y-j,we=S.site,te=we.x-X,se=we.y-j,ue=2*(ee*se-he*te),_e=ee*ee+he*he,Te=te*te+se*se,ce={x:(se*_e-he*Te)/ue+X,y:(ee*Te-te*_e)/ue+j};jc(S.edge,G,we,ce),N.edge=Hu(G,c,null,ce),S.edge=Hu(c,we,null,ce),ds(A),ds(S)}}function s5(c,p){var x=c.site,A=x.x,S=x.y,L=S-p;if(!L)return A;var R=c.P;if(!R)return-1/0;x=R.site;var E=x.x,N=x.y,G=N-p;if(!G)return E;var X=E-A,j=1/L-1/G,ee=X/G;return j?(-ee+Math.sqrt(ee*ee-2*j*(X*X/(-2*G)-N+G/2+S-L/2)))/j+A:(A+E)/2}function mN(c,p){var x=c.N;if(x)return s5(x,p);var A=c.site;return A.y===p?A.x:1/0}function u5(c){this.site=c,this.edges=[]}u5.prototype.prepare=function(){for(var c=this.edges,p=c.length,x;p--;)x=c[p].edge,(!x.b||!x.a)&&c.splice(p,1);return c.sort(f5),c.length};function yN(c){for(var p=c[0][0],x=c[1][0],A=c[0][1],S=c[1][1],L,R,E,N,G=jl,X=G.length,j,ee,he,we,te,se;X--;)if(j=G[X],!(!j||!j.prepare()))for(he=j.edges,we=he.length,ee=0;ee>>1;c(p[L],x)<0?A=L+1:S=L}return A},right:function(p,x,A,S){for(arguments.length<3&&(A=0),arguments.length<4&&(S=p.length);A>>1;c(p[L],x)>0?S=L:A=L+1}return A}}}var y=m(v);e.bisectLeft=y.left,e.bisect=e.bisectRight=y.right,e.bisector=function(c){return m(c.length===1?function(p,x){return v(c(p),x)}:c)},e.shuffle=function(c,p,x){(A=arguments.length)<3&&(x=c.length,A<2&&(p=0));for(var A=x-p,S,L;A;)L=Math.random()*A--|0,S=c[A+p],c[A+p]=c[L+p],c[L+p]=S;return c},e.permute=function(c,p){for(var x=p.length,A=new Array(x);x--;)A[x]=c[p[x]];return A},e.pairs=function(c){for(var p=0,x=c.length-1,A,S=c[0],L=new Array(x<0?0:x);p0?1:c<0?-1:0}function lt(c,p,x){return(p[0]-c[0])*(x[1]-c[1])-(p[1]-c[1])*(x[0]-c[0])}function _t(c){return c>1?0:c<-1?Ee:Math.acos(c)}function gt(c){return c>1?sr:c<-1?-sr:Math.asin(c)}function Bt(c){return((c=Math.exp(c))-1/c)/2}function zt(c){return((c=Math.exp(c))+1/c)/2}function Oa(c){return((c=Math.exp(2*c))-1)/(c+1)}function ca(c){return(c=Math.sin(c/2))*c}var aa=Math.SQRT2,Ba=2,na=4;e.interpolateZoom=function(c,p){var x=c[0],A=c[1],S=c[2],L=p[0],R=p[1],E=p[2],N=L-x,G=R-A,X=N*N+G*G,j,ee;if(X>4,A=A>>4|A,S=N&240,S=S>>4|S,L=N&15,L=L<<4|L):c.length===7&&(A=(N&16711680)>>16,S=(N&65280)>>8,L=N&255)),p(A,S,L))}function $r(c,p,x){var A=Math.min(c/=255,p/=255,x/=255),S=Math.max(c,p,x),L=S-A,R,E,N=(S+A)/2;return L?(E=N<.5?L/(S+A):L/(2-S-A),c==S?R=(p-x)/L+(p>>1;c(p[L],x)<0?A=L+1:S=L}return A},right:function(p,x,A,S){for(arguments.length<3&&(A=0),arguments.length<4&&(S=p.length);A>>1;c(p[L],x)>0?S=L:A=L+1}return A}}}var y=m(v);e.bisectLeft=y.left,e.bisect=e.bisectRight=y.right,e.bisector=function(c){return m(c.length===1?function(p,x){return v(c(p),x)}:c)},e.shuffle=function(c,p,x){(A=arguments.length)<3&&(x=c.length,A<2&&(p=0));for(var A=x-p,S,L;A;)L=Math.random()*A--|0,S=c[A+p],c[A+p]=c[L+p],c[L+p]=S;return c},e.permute=function(c,p){for(var x=p.length,A=new Array(x);x--;)A[x]=c[p[x]];return A},e.pairs=function(c){for(var p=0,x=c.length-1,A,S=c[0],L=new Array(x<0?0:x);p0?1:c<0?-1:0}function lt(c,p,x){return(p[0]-c[0])*(x[1]-c[1])-(p[1]-c[1])*(x[0]-c[0])}function _t(c){return c>1?0:c<-1?Ee:Math.acos(c)}function gt(c){return c>1?sr:c<-1?-sr:Math.asin(c)}function Bt(c){return((c=Math.exp(c))-1/c)/2}function zt(c){return((c=Math.exp(c))+1/c)/2}function Oa(c){return((c=Math.exp(2*c))-1)/(c+1)}function ca(c){return(c=Math.sin(c/2))*c}var aa=Math.SQRT2,Ba=2,na=4;e.interpolateZoom=function(c,p){var x=c[0],A=c[1],S=c[2],L=p[0],R=p[1],E=p[2],N=L-x,G=R-A,X=N*N+G*G,j,ee;if(X>4,A=A>>4|A,S=N&240,S=S>>4|S,L=N&15,L=L<<4|L):c.length===7&&(A=(N&16711680)>>16,S=(N&65280)>>8,L=N&255)),p(A,S,L))}function $r(c,p,x){var A=Math.min(c/=255,p/=255,x/=255),S=Math.max(c,p,x),L=S-A,R,E,N=(S+A)/2;return L?(E=N<.5?L/(S+A):L/(2-S-A),c==S?R=(p-x)/L+(p=X&&ue.x<=ee&&ue.y>=j&&ue.y<=he?[[X,he],[ee,he],[ee,j],[X,j]]:[];_e.point=N[te]}),G}function E(N){return N.map(function(G,X){return{x:Math.round(A(G,X)/oe)*oe,y:Math.round(S(G,X)/oe)*oe,i:X}})}return R.links=function(N){return k1(E(N)).edges.filter(function(G){return G.l&&G.r}).map(function(G){return{source:N[G.l.i],target:N[G.r.i]}})},R.triangles=function(N){var G=[];return k1(E(N)).cells.forEach(function(X,j){for(var ee=X.site,he=X.edges.sort(f5),we=-1,te=he.length,se,ue,_e=he[te-1].edge,Te=_e.l===ee?_e.r:_e.l;++we0&&(P=0);break}return P>0?k.slice(0,P)+k.slice(z+1):k}var f;function v(k,q){var D=t(k,q);if(!D)return k+"";var P=D[0],z=D[1],F=z-(f=Math.max(-8,Math.min(8,Math.floor(z/3)))*3)+1,O=P.length;return F===O?P:F>O?P+new Array(F-O+1).join("0"):F>0?P.slice(0,F)+"."+P.slice(F):"0."+new Array(1-F).join("0")+t(k,Math.max(0,q+F-1))[0]}function h(k,q){var D=t(k,q);if(!D)return k+"";var P=D[0],z=D[1];return z<0?"0."+new Array(-z).join("0")+P:P.length>z+1?P.slice(0,z+1)+"."+P.slice(z+1):P+new Array(z-P.length+2).join("0")}var d={"%":function(k,q){return(k*100).toFixed(q)},b:function(k){return Math.round(k).toString(2)},c:function(k){return k+""},d:r,e:function(k,q){return k.toExponential(q)},f:function(k,q){return k.toFixed(q)},g:function(k,q){return k.toPrecision(q)},o:function(k){return Math.round(k).toString(8)},p:function(k,q){return h(k*100,q)},r:h,s:v,X:function(k){return Math.round(k).toString(16).toUpperCase()},x:function(k){return Math.round(k).toString(16)}};function m(k){return k}var y=Array.prototype.map,g=["y","z","a","f","p","n","\xB5","m","","k","M","G","T","P","E","Z","Y"];function _(k){var q=k.grouping===void 0||k.thousands===void 0?m:n(y.call(k.grouping,Number),k.thousands+""),D=k.currency===void 0?"":k.currency[0]+"",P=k.currency===void 0?"":k.currency[1]+"",z=k.decimal===void 0?".":k.decimal+"",F=k.numerals===void 0?m:i(y.call(k.numerals,String)),O=k.percent===void 0?"%":k.percent+"",I=k.minus===void 0?"-":k.minus+"",W=k.nan===void 0?"NaN":k.nan+"";function Z(Y){Y=o(Y);var H=Y.fill,Q=Y.align,K=Y.sign,ne=Y.symbol,pe=Y.zero,ye=Y.width,Se=Y.comma,re=Y.precision,Ce=Y.trim,de=Y.type;de==="n"?(Se=!0,de="g"):d[de]||(re===void 0&&(re=12),Ce=!0,de="g"),(pe||H==="0"&&Q==="=")&&(pe=!0,H="0",Q="=");var be=ne==="$"?D:ne==="#"&&/[boxX]/.test(de)?"0"+de.toLowerCase():"",ge=ne==="$"?P:/[%p]/.test(de)?O:"",ke=d[de],B=/[defgprs%]/.test(de);re=re===void 0?6:/[gprs]/.test(de)?Math.max(1,Math.min(21,re)):Math.max(0,Math.min(20,re));function $(U){var le=be,ve=ge,me,De,Re;if(de==="c")ve=ke(U)+ve,U="";else{U=+U;var Le=U<0||1/U<0;if(U=isNaN(U)?W:ke(Math.abs(U),re),Ce&&(U=u(U)),Le&&+U==0&&K!=="+"&&(Le=!1),le=(Le?K==="("?K:I:K==="-"||K==="("?"":K)+le,ve=(de==="s"?g[8+f/3]:"")+ve+(Le&&K==="("?")":""),B){for(me=-1,De=U.length;++me
>4,g[f++]=(h&15)<<4|d>>2,g[f++]=(d&3)<<6|m&63;return y};e.decode=i,e.encode=n,Object.defineProperty(e,"__esModule",{value:!0})})});var cl=J((Pie,Cb)=>{"use strict";Cb.exports=function(r){return window&&window.process&&window.process.versions?Object.prototype.toString.call(r)==="[object Object]":Object.prototype.toString.call(r)==="[object Object]"&&Object.getPrototypeOf(r).hasOwnProperty("hasOwnProperty")}});var on=J(Xn=>{"use strict";var mI=K1().decode,yI=cl(),Q1=Array.isArray,gI=ArrayBuffer,bI=DataView;function Lb(e){return gI.isView(e)&&!(e instanceof bI)}Xn.isTypedArray=Lb;function Mv(e){return Q1(e)||Lb(e)}Xn.isArrayOrTypedArray=Mv;function xI(e){return!Mv(e[0])}Xn.isArray1D=xI;Xn.ensureArray=function(e,r){return Q1(e)||(e=[]),e.length=r,e};var sa={u1c:typeof Uint8ClampedArray=="undefined"?void 0:Uint8ClampedArray,i1:typeof Int8Array=="undefined"?void 0:Int8Array,u1:typeof Uint8Array=="undefined"?void 0:Uint8Array,i2:typeof Int16Array=="undefined"?void 0:Int16Array,u2:typeof Uint16Array=="undefined"?void 0:Uint16Array,i4:typeof Int32Array=="undefined"?void 0:Int32Array,u4:typeof Uint32Array=="undefined"?void 0:Uint32Array,f4:typeof Float32Array=="undefined"?void 0:Float32Array,f8:typeof Float64Array=="undefined"?void 0:Float64Array};sa.uint8c=sa.u1c;sa.uint8=sa.u1;sa.int8=sa.i1;sa.uint16=sa.u2;sa.int16=sa.i2;sa.uint32=sa.u4;sa.int32=sa.i4;sa.float32=sa.f4;sa.float64=sa.f8;function $1(e){return e.constructor===ArrayBuffer}Xn.isArrayBuffer=$1;Xn.decodeTypedArraySpec=function(e){var r=[],t=_I(e),a=t.dtype,n=sa[a];if(!n)throw new Error('Error in dtype: "'+a+'"');var i=n.BYTES_PER_ELEMENT,l=t.bdata;$1(l)||(l=mI(l));var o=t.shape===void 0?[l.byteLength/i]:(""+t.shape).split(",");o.reverse();var s=o.length,u,f,v=+o[0],h=i*v,d=0;if(s===1)r=new n(l);else if(s===2)for(u=+o[1],f=0;f{"use strict";var Db=Or(),ep=on().isArrayOrTypedArray;zb.exports=function(r,t){if(Db(t))t=String(t);else if(typeof t!="string"||t.slice(-4)==="[-1]")throw"bad property string";var a=t.split("."),n,i,l,o;for(o=0;o
/g),h=0;h
/i;An.BR_TAG_ALL=/
/gi;var o7=/(^|[\s"'])style\s*=\s*("([^"]*);?"|'([^']*);?')/i,s7=/(^|[\s"'])href\s*=\s*("([^"]*)"|'([^']*)')/i,u7=/(^|[\s"'])target\s*=\s*("([^"\s]*)"|'([^'\s]*)')/i,qY=/(^|[\s"'])popup\s*=\s*("([\w=,]*)"|'([\w=,]*)')/i;function _o(e,r){if(!e)return null;var t=e.match(r),a=t&&(t[3]||t[4]);return a&&f0(a)}var DY=/(^|;)\s*color:/;An.plainText=function(e,r){r=r||{};for(var t=r.len!==void 0&&r.len!==-1?r.len:1/0,a=r.allowedTags!==void 0?r.allowedTags:["br"],n="...",i=n.length,l=e.split(Xp),o=[],s="",u=0,f=0;f=k&&b
=re.min&&(ne
=D:I<=D;I=Ye.tickIncrement(I,V,f,n)){if(b&&W++,C.rangebreaks&&!f){if(I
=h)break}if(g.length>d||I===O)break;O=I;var Y={value:I};b?(z&&I!==(I|0)&&(Y.simpleLabel=!0),i>1&&W%i&&(Y.skipLabel=!0),g.push(Y)):(Y.minor=!0,_.push(Y))}}if(!_||_.length<2)s=!1;else{var H=(_[1].value-_[0].value)*(o?-1:1);gG(H,r.tickformat)||(s=!1)}if(!s)w=g;else{var Q=g.concat(_);l&&g.length&&(Q=Q.slice(1)),Q=Q.sort(function(Fe,Oe){return Fe.value-Oe.value}).filter(function(Fe,Oe,Ge){return Oe===0||Fe.value!==Ge[Oe-1].value});var K=Q.map(function(Fe,Oe){return Fe.minor===void 0&&!Fe.skipLabel?Oe:null}).filter(function(Fe){return Fe!==null});K.forEach(function(Fe){s.map(function(Oe){var Ge=Fe+Oe;Ge>=0&&Ge