diff --git a/.github/workflows/bokeh-ci.yml b/.github/workflows/bokeh-ci.yml index b54644ac229..2ab01c49881 100644 --- a/.github/workflows/bokeh-ci.yml +++ b/.github/workflows/bokeh-ci.yml @@ -179,36 +179,6 @@ jobs: name: examples-report path: examples-report - integration-tests: - if: ${{ false }} # disable for now - needs: build - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Prepare Environment - uses: ./.github/workflows/composite/test-setup - with: - test-env: '3.11' - source-tree: 'delete' - - - name: List installed software - run: | - conda info - conda list - echo "node $(node --version)" - echo "npm $(npm --version)" - - - name: Run tests - run: pytest -v --cov=bokeh --cov-report=xml --tb=short --driver chrome --color=yes tests/integration - - - name: Upload code coverage - uses: codecov/codecov-action@v4 - with: - flags: integration - verbose: true - unit-test: needs: build runs-on: ${{ matrix.os }} @@ -351,27 +321,3 @@ jobs: - name: Run tests run: bash scripts/ci/run_downstream_tests.sh - - docker_from_wheel: - if: ${{ false }} # temporarily disable - needs: build - runs-on: ubuntu-latest - env: - IMAGE_TAG: bokeh/bokeh-dev:branch-3.1 - - steps: - - uses: actions/checkout@v4 - - - name: Download wheel package - id: download - uses: actions/download-artifact@v4 - with: - name: wheel-package - path: dist/ - - - name: Start Docker container, install Bokeh from wheel and run Python tests. - env: - BOKEH_DOCKER_FROM_WHEEL: 1 - BOKEH_DOCKER_INTERACTIVE: 0 - run: | - scripts/docker/docker_run.sh $IMAGE_TAG diff --git a/.github/workflows/bokeh-docker-build.yml b/.github/workflows/bokeh-docker-build.yml deleted file mode 100644 index 70f3502fb18..00000000000 --- a/.github/workflows/bokeh-docker-build.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Bokeh-Docker-Build - -on: - workflow_dispatch: - inputs: - push_or_save_image: - type: choice - description: What to do with image? - options: - - "Push to Docker Hub" - - "Save as artifact" - required: true - default: "Push to Docker Hub" - -jobs: - docker-build: - runs-on: ubuntu-latest - steps: - - name: Set date environment variable - run: | - echo "iso_date=$(date -u --iso-8601)" >> $GITHUB_ENV - echo "branch_name=$(echo ${{ github.ref_name }} | tr / -)" >> $GITHUB_ENV - - - name: Checkout source - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to Docker Hub - uses: docker/login-action@v2 - with: - username: bokehservice - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Build image - uses: docker/build-push-action@v5 - with: - context: scripts/docker - load: true - tags: | - bokeh/bokeh-dev:latest - bokeh/bokeh-dev:${{ env.iso_date }} - bokeh/bokeh-dev:${{ env.branch_name }} - - - name: Push image to Docker Hub - if: ${{ inputs.push_or_save_image }} == "Push to Docker Hub" - uses: docker/build-push-action@v5 - with: - context: scripts/docker - push: true - tags: | - bokeh/bokeh-dev:latest - bokeh/bokeh-dev:${{ env.iso_date }} - bokeh/bokeh-dev:${{ env.branch_name }} - - - name: Save image to tar file - if: ${{ inputs.push_to_docker_hub }} == "Save as artifact" - run: | - docker save -o bokeh-dev.tar bokeh/bokeh-dev - - - name: Upload artifact - if: ${{ inputs.push_to_docker_hub }} == "Save as artifact" - uses: actions/upload-artifact@v4 - with: - name: artifact - path: bokeh-dev.tar diff --git a/.github/workflows/bokeh-docker-test.yml b/.github/workflows/bokeh-docker-test.yml deleted file mode 100644 index 820c6da92fb..00000000000 --- a/.github/workflows/bokeh-docker-test.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Bokeh-Docker-Test - -on: - push: - branches: - - main - - branch-* - pull_request: - -jobs: - docker-test: - if: ${{ false }} # disable for now - - runs-on: ubuntu-latest - env: - IMAGE_TAG: bokeh/bokeh-dev:branch-3.1 - - steps: - - name: Checkout the repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 # full history to get proper build version - - - name: Start Docker container, build Bokeh and run tests. - env: - BOKEH_DOCKER_BUILD: 1 - BOKEH_DOCKER_TEST: 1 - BOKEH_DOCKER_INTERACTIVE: 0 - run: | - scripts/docker/docker_run.sh $IMAGE_TAG - - - name: Collect results - shell: bash - run: | - SRC="bokehjs/test/baselines/linux" - DST="bokeh-report-docker/${SRC}" - mkdir -p ${DST} - if [[ -e ${SRC}/report.json ]]; - then - CHANGED=$(git status --short ${SRC}/\*.blf ${SRC}/\*.png | cut -c4-) - cp ${SRC}/report.json ${CHANGED} ${DST} - fi - - - name: Upload report - uses: actions/upload-artifact@v4 - with: - name: bokeh-report-docker - path: bokeh-report-docker diff --git a/.gitignore b/.gitignore index f8ec46cfa9a..f8caea7774a 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ .sw[nop] *.tmp .vscode +.idea # compressed / binary files *.bz2 diff --git a/README.md b/README.md index 65d504c74c1..86d751bc35b 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ - + Bokeh license (BSD 3-clause) diff --git a/bokehjs/make/package.json b/bokehjs/make/package.json index a0637fd9676..85f7774a626 100644 --- a/bokehjs/make/package.json +++ b/bokehjs/make/package.json @@ -1,6 +1,6 @@ { "name": "@bokeh/make", - "version": "3.5.0-dev.3", + "version": "3.6.0-dev.1", "private": true, "description": "Internal package for bokehjs' build system", "license": "BSD-3-Clause", @@ -13,18 +13,18 @@ ], "devDependencies": { "@types/eslint": "^8.56.7", - "@types/node": "^20.12.4", + "@types/node": "^20.14.7", "@types/which": "^3.0.3", "@types/yargs": "^17.0.32", - "acorn": "^8.11.3", + "acorn": "^8.12.1", "chalk": "^4.1.2", "del": "^6.1.1", - "eslint": "^8.57.0", - "semver": "^7.6.0", + "eslint": "^8.56.0", + "semver": "^7.6.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript-eslint": "^7.5.0", - "typescript": "~5.4.3", + "typescript-eslint": "^7.16.0", + "typescript": "~5.5.3", "which": "^3.0.1", "yargs": "^17.7.2" } diff --git a/bokehjs/package-lock.json b/bokehjs/package-lock.json index 0bf93630f4c..98ab105fe38 100644 --- a/bokehjs/package-lock.json +++ b/bokehjs/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bokeh/bokehjs", - "version": "3.5.0-dev.3", + "version": "3.6.0-dev.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bokeh/bokehjs", - "version": "3.5.0-dev.3", + "version": "3.6.0-dev.1", "license": "BSD-3-Clause", "workspaces": [ "./make", @@ -31,7 +31,7 @@ "regl": "^2.1.0", "sprintf-js": "^1.1.3", "timezone": "^1.0.23", - "tslib": "^2.6.2", + "tslib": "^2.6.3", "underscore.template": "^0.1.7" }, "engines": { @@ -41,25 +41,25 @@ }, "make": { "name": "@bokeh/make", - "version": "3.5.0-dev.3", + "version": "3.6.0-dev.1", "license": "BSD-3-Clause", "workspaces": [ "./src/compiler" ], "devDependencies": { "@types/eslint": "^8.56.7", - "@types/node": "^20.12.4", + "@types/node": "^20.14.7", "@types/which": "^3.0.3", "@types/yargs": "^17.0.32", - "acorn": "^8.11.3", + "acorn": "^8.12.1", "chalk": "^4.1.2", "del": "^6.1.1", - "eslint": "^8.57.0", - "semver": "^7.6.0", + "eslint": "^8.56.0", + "semver": "^7.6.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "~5.4.3", - "typescript-eslint": "^7.5.0", + "typescript": "~5.5.3", + "typescript-eslint": "^7.16.0", "which": "^3.0.1", "yargs": "^17.7.2" } @@ -408,6 +408,7 @@ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } @@ -417,6 +418,7 @@ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" } @@ -445,7 +447,8 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true + "dev": true, + "license": "(Unlicense OR Apache-2.0)" }, "node_modules/@tsconfig/node10": { "version": "1.0.11", @@ -609,10 +612,11 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.4.tgz", - "integrity": "sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==", + "version": "20.14.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.7.tgz", + "integrity": "sha512-uTr2m2IbJJucF3KUxgnGOZvYbN0QgkGyWxG6973HCpMYFy2KfcgYuIwkJQMQkt1VbBMlvWRbpshFTLxnxCZjKQ==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } @@ -649,12 +653,6 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true - }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", @@ -749,22 +747,21 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.5.0.tgz", - "integrity": "sha512-HpqNTH8Du34nLxbKgVMGljZMG0rJd2O9ecvr2QLYp+7512ty1j42KnsFwspPXg1Vh8an9YImf6CokUBltisZFQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", + "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.5.0", - "@typescript-eslint/type-utils": "7.5.0", - "@typescript-eslint/utils": "7.5.0", - "@typescript-eslint/visitor-keys": "7.5.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/type-utils": "7.16.0", + "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -784,15 +781,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz", - "integrity": "sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", + "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.5.0", - "@typescript-eslint/types": "7.5.0", - "@typescript-eslint/typescript-estree": "7.5.0", - "@typescript-eslint/visitor-keys": "7.5.0", + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", "debug": "^4.3.4" }, "engines": { @@ -812,13 +810,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.5.0.tgz", - "integrity": "sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", + "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.5.0", - "@typescript-eslint/visitor-keys": "7.5.0" + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -829,15 +828,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz", - "integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", + "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.5.0", - "@typescript-eslint/utils": "7.5.0", + "@typescript-eslint/typescript-estree": "7.16.0", + "@typescript-eslint/utils": "7.16.0", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -856,10 +856,11 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.5.0.tgz", - "integrity": "sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", + "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || >=20.0.0" }, @@ -869,19 +870,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.5.0.tgz", - "integrity": "sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", + "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.5.0", - "@typescript-eslint/visitor-keys": "7.5.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -901,15 +903,17 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -921,18 +925,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz", - "integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", + "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.5.0", - "@typescript-eslint/types": "7.5.0", - "@typescript-eslint/typescript-estree": "7.5.0", - "semver": "^7.5.4" + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -946,13 +948,14 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz", - "integrity": "sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", + "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.5.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "7.16.0", + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -988,10 +991,11 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1158,12 +1162,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1239,10 +1244,11 @@ } }, "node_modules/chrome-remote-interface": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.33.0.tgz", - "integrity": "sha512-tv/SgeBfShXk43fwFpQ9wnS7mOCPzETnzDXTNxCb6TqKOiOeIfbrJz+2NAp8GmzwizpKa058wnU1Te7apONaYg==", + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.33.2.tgz", + "integrity": "sha512-wvm9cOeBTrb218EC+6DteGt92iXr2iY0+XJP30f15JVDhqvWvJEVACh9GvUm8b9Yd8bxQivaLSb8k7mgrbyomQ==", "dev": true, + "license": "MIT", "dependencies": { "commander": "2.11.x", "ws": "^7.2.0" @@ -1258,10 +1264,11 @@ "dev": true }, "node_modules/chrome-remote-interface/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.3.0" }, @@ -1553,10 +1560,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1282316", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1282316.tgz", - "integrity": "sha512-i7eIqWdVxeXBY/M+v83yRkOV1sTHnr3XYiC0YNBivLIE6hBfE2H0c2o8VC5ynT44yjy+Ei0kLrBQFK/RUKaAHQ==", - "dev": true + "version": "0.0.1325906", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1325906.tgz", + "integrity": "sha512-TdtfCa2r5embtfj5TS26lPZd631x4YrEFBgx1dwF3vJm5K9Fw2/2XhiyFkQNn3FJSyq+b/UtEHqP2kwAFqxcJA==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/diff": { "version": "5.2.0", @@ -1957,10 +1965,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2428,6 +2437,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -2512,7 +2522,8 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", @@ -2595,18 +2606,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -2815,10 +2814,11 @@ } }, "node_modules/nise": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", - "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0", "@sinonjs/fake-timers": "^11.2.2", @@ -2828,10 +2828,11 @@ } }, "node_modules/nise/node_modules/path-to-regexp": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", - "dev": true + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true, + "license": "MIT" }, "node_modules/nouislider": { "version": "15.7.1", @@ -3291,13 +3292,11 @@ "optional": true }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3428,17 +3427,18 @@ } }, "node_modules/sinon": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", - "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0", + "@sinonjs/commons": "^3.0.1", "@sinonjs/fake-timers": "^11.2.2", "@sinonjs/samsam": "^8.0.0", - "diff": "^5.1.0", - "nise": "^5.1.5", - "supports-color": "^7.2.0" + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" }, "funding": { "type": "opencollective", @@ -3571,10 +3571,11 @@ } }, "node_modules/terser": { - "version": "5.30.3", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz", - "integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==", + "version": "5.31.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.2.tgz", + "integrity": "sha512-LGyRZVFm/QElZHy/CPr/O4eNZOZIzsrQ92y4v9UJe/pFJjypje2yI3C2FmPtvUEnhadlSbmG2nXtdcjHOjCfxw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -3610,6 +3611,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -3631,6 +3633,7 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -3705,9 +3708,10 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -3756,10 +3760,11 @@ } }, "node_modules/typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3769,14 +3774,15 @@ } }, "node_modules/typescript-eslint": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.5.0.tgz", - "integrity": "sha512-eKhF39LRi2xYvvXh3h3S+mCxC01dZTIZBlka25o39i81VeQG+OZyfC4i2GEDspNclMRdXkg9uGhmvWMhjph2XQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.16.0.tgz", + "integrity": "sha512-kaVRivQjOzuoCXU6+hLnjo3/baxyzWVO5GrnExkFzETRYJKVHYkrJglOu2OCm8Hi9RPDWX1PTNNTpU5KRV0+RA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "7.5.0", - "@typescript-eslint/parser": "7.5.0", - "@typescript-eslint/utils": "7.5.0" + "@typescript-eslint/eslint-plugin": "7.16.0", + "@typescript-eslint/parser": "7.16.0", + "@typescript-eslint/utils": "7.16.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -3896,10 +3902,11 @@ "dev": true }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -3933,12 +3940,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -3989,33 +3990,33 @@ }, "src/compiler": { "name": "@bokeh/compiler", - "version": "3.5.0-dev.3", + "version": "3.6.0-dev.1", "license": "BSD-3-Clause", "devDependencies": { "@types/combine-source-map": "^0.8.4", "@types/convert-source-map": "^2.0.3", "@types/css": "^0.0.37", "@types/less": "^3.0.6", - "@types/node": "^20.12.4", + "@types/node": "^20.14.7", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "combine-source-map": "^0.8.0", "convert-source-map": "^2.0.0", "css": "^3.0.0", "less": "^4.2.0", - "terser": "^5.30.3", - "typescript": "~5.4.3", + "terser": "^5.31.2", + "typescript": "~5.5.3", "yargs": "^17.7.2" } }, "src/lib": { "name": "@bokeh/lib", - "version": "3.5.0-dev.3", + "version": "3.6.0-dev.1", "license": "BSD-3-Clause" }, "src/server": { "name": "@bokeh/server", - "version": "3.5.0-dev.3", + "version": "3.6.0-dev.1", "license": "BSD-3-Clause", "devDependencies": { "@types/node": "^20.12.4", @@ -4024,13 +4025,13 @@ "chalk": "^4.1.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "ws": "^8.16.0", + "ws": "^8.18.0", "yargs": "^17.7.2" } }, "test": { "name": "@bokeh/test", - "version": "3.5.0-dev.3", + "version": "3.6.0-dev.1", "license": "BSD-3-Clause", "workspaces": [ "./src/lib" @@ -4039,23 +4040,23 @@ "@types/cli-progress": "^3.11.5", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", - "@types/node": "^20.12.4", + "@types/node": "^20.14.7", "@types/nunjucks": "^3.2.6", "@types/pngjs": "^6.0.4", "@types/sinon": "^17.0.2", "@types/source-map-support": "^0.5.10", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", - "chrome-remote-interface": "^0.33.0", + "chrome-remote-interface": "^0.33.2", "cli-progress": "^3.12.0", "cors": "^2.8.5", - "devtools-protocol": "^0.0.1282316", + "devtools-protocol": "^0.0.1325906", "express": "^4.19.2", "json5": "^2.2.3", "nunjucks": "^3.2.4", "path-browserify": "^1.0.1", "pngjs": "^7.0.0", - "sinon": "^17.0.1", + "sinon": "^18.0.0", "source-map-support": "^0.5.21", "ts-node": "^10.9.2", "yargs": "^17.7.2" diff --git a/bokehjs/package.json b/bokehjs/package.json index ff2e39249be..7fa9513eb02 100644 --- a/bokehjs/package.json +++ b/bokehjs/package.json @@ -1,6 +1,6 @@ { "name": "@bokeh/bokehjs", - "version": "3.5.0-dev.3", + "version": "3.6.0-dev.1", "description": "Interactive, novel data visualization", "keywords": [ "bokeh", @@ -38,7 +38,7 @@ "dev-build": "node make dev-build" }, "main": "build/js/lib/bokeh.js", - "types": "build/js/types/bokeh.d.ts", + "types": "build/js/lib/bokeh.d.ts", "dependencies": { "@bokeh/numbro": "^1.6.2", "@bokeh/slickgrid": "~2.4.4103", @@ -55,7 +55,7 @@ "regl": "^2.1.0", "sprintf-js": "^1.1.3", "timezone": "^1.0.23", - "tslib": "^2.6.2", + "tslib": "^2.6.3", "underscore.template": "^0.1.7" } } diff --git a/bokehjs/src/compiler/linker.ts b/bokehjs/src/compiler/linker.ts index 6b8336194d7..98e51aeaeb5 100644 --- a/bokehjs/src/compiler/linker.ts +++ b/bokehjs/src/compiler/linker.ts @@ -270,8 +270,8 @@ export class Linker { this.entries = opts.entries.map((path) => resolve(path)) this.bases = (opts.bases ?? []).map((path) => resolve(path)) this.excludes = new Set((opts.excludes ?? []).map((path) => resolve(path))) - this.external_modules = new Set((opts.externals ?? []).filter((s): s is string => typeof s === "string")) - this.external_regex = (opts.externals ?? []).filter((s): s is RegExp => s instanceof RegExp) + this.external_modules = new Set((opts.externals ?? []).filter((s) => typeof s === "string")) + this.external_regex = (opts.externals ?? []).filter((s) => s instanceof RegExp) this.excluded = opts.excluded ?? (() => false) this.builtins = opts.builtins ?? false diff --git a/bokehjs/src/compiler/package.json b/bokehjs/src/compiler/package.json index 0af6fce6f5a..e814f0275db 100644 --- a/bokehjs/src/compiler/package.json +++ b/bokehjs/src/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@bokeh/compiler", - "version": "3.5.0-dev.3", + "version": "3.6.0-dev.1", "private": true, "description": "Internal package for bokehjs' extensions compiler", "license": "BSD-3-Clause", @@ -13,15 +13,15 @@ "@types/convert-source-map": "^2.0.3", "@types/css": "^0.0.37", "@types/less": "^3.0.6", - "@types/node": "^20.12.4", + "@types/node": "^20.14.7", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "combine-source-map": "^0.8.0", "convert-source-map": "^2.0.0", "css": "^3.0.0", "less": "^4.2.0", - "terser": "^5.30.3", - "typescript": "~5.4.3", + "terser": "^5.31.2", + "typescript": "~5.5.3", "yargs": "^17.7.2" } } diff --git a/bokehjs/src/less/base.less b/bokehjs/src/less/base.less index 6e9f48cbbec..50e2544119c 100644 --- a/bokehjs/src/less/base.less +++ b/bokehjs/src/less/base.less @@ -11,7 +11,7 @@ --padding-vertical: 6px; --padding-horizontal: 12px; - --bokeh-top-level: 1000; // used for z-index of menus, dropdowns, etc. + --bokeh-top-level: 10000; // used for z-index of menus, dropdowns, etc. } :host { diff --git a/bokehjs/src/less/canvas.less b/bokehjs/src/less/canvas.less index 7df02b0b005..c2421408d60 100644 --- a/bokehjs/src/less/canvas.less +++ b/bokehjs/src/less/canvas.less @@ -6,8 +6,10 @@ left: 0; width: 100%; height: 100%; + overflow: hidden; } .bk-events { touch-action: none; + overflow: visible; } diff --git a/bokehjs/src/less/icons.less b/bokehjs/src/less/icons.less index e52642577d2..73a85d98d98 100644 --- a/bokehjs/src/less/icons.less +++ b/bokehjs/src/less/icons.less @@ -18,6 +18,10 @@ .tool-icon(--bokeh-icon-replace-mode, "ReplaceMode"); } +.bk-tool-icon-toggle-mode { + .tool-icon(--bokeh-icon-toggle-mode, "ToggleMode"); +} + .bk-tool-icon-append-mode { .tool-icon(--bokeh-icon-append-mode, "AppendMode"); } diff --git a/bokehjs/src/less/renderer.less b/bokehjs/src/less/renderer.less deleted file mode 100644 index 9ca4f805dd0..00000000000 --- a/bokehjs/src/less/renderer.less +++ /dev/null @@ -1,3 +0,0 @@ -:host { - position: absolute; -} diff --git a/bokehjs/src/lib/api/figure.ts b/bokehjs/src/lib/api/figure.ts index 4a0a04c960f..4bcc3e93220 100644 --- a/bokehjs/src/lib/api/figure.ts +++ b/bokehjs/src/lib/api/figure.ts @@ -1,15 +1,16 @@ import type {HasProps} from "../core/has_props" import type {Attrs} from "../core/types" import type {Value, Field, Vector} from "../core/vectorization" +import {isVectorized} from "../core/vectorization" import type {Property} from "../core/properties" import {VectorSpec, UnitsSpec} from "../core/properties" import type {Class} from "../core/class" import {extend} from "../core/class" import type {Location} from "../core/enums" import {is_equal, Comparator} from "../core/util/eq" -import {includes, uniq} from "../core/util/array" +import {includes, uniq, zip} from "../core/util/array" import {clone, keys, entries, is_empty, dict} from "../core/util/object" -import {isNumber, isString, isArray, isArrayOf} from "../core/util/types" +import {isNumber, isString, isArray, isArrayOf, isPlainObject} from "../core/util/types" import {enumerate} from "core/util/iterator" import * as nd from "core/util/ndarray" @@ -35,6 +36,7 @@ import { Range, Range1d, Tool, + ToolProxy, } from "./models" import {Legend} from "../models/annotations/legend" @@ -43,7 +45,7 @@ import type {ToolAliases} from "../models/tools/tool" import {Figure as BaseFigure} from "../models/plots/figure" import {GestureTool} from "../models/tools/gestures/gesture_tool" -import type {TypedGlyphRenderer, NamesOf, AuxGlyph} from "./glyph_api" +import type {NamesOf, AuxGlyph} from "./glyph_api" import {GlyphAPI} from "./glyph_api" export type ToolName = keyof ToolAliases @@ -149,9 +151,9 @@ export class SubFigure extends GlyphAPI { super() } - _glyph(cls: Class, positional: NamesOf, args: unknown[], overrides?: object): TypedGlyphRenderer { + _glyph(cls: Class, method: string, positional: NamesOf, args: unknown[], overrides?: object): GlyphRenderer { const {coordinates} = this - return this.parent._glyph(cls, positional, args, {coordinates, ...overrides}) + return this.parent._glyph(cls, method, positional, args, {coordinates, ...overrides}) } } @@ -159,13 +161,13 @@ export interface Figure extends GlyphAPI {} export class Figure extends BaseFigure { get xaxes(): Axis[] { - return [...this.below, ...this.above].filter((r): r is Axis => r instanceof Axis) + return [...this.below, ...this.above].filter((r) => r instanceof Axis) } get yaxes(): Axis[] { - return [...this.left, ...this.right].filter((r): r is Axis => r instanceof Axis) + return [...this.left, ...this.right].filter((r) => r instanceof Axis) } get axes(): Axis[] { - return [...this.below, ...this.above, ...this.left, ...this.right].filter((r): r is Axis => r instanceof Axis) + return [...this.below, ...this.above, ...this.left, ...this.right].filter((r) => r instanceof Axis) } get xaxis(): Proxied { @@ -179,13 +181,13 @@ export class Figure extends BaseFigure { } get xgrids(): Grid[] { - return this.center.filter((r): r is Grid => r instanceof Grid && r.dimension == 0) + return this.center.filter((r) => r instanceof Grid).filter((grid) => grid.dimension == 0) } get ygrids(): Grid[] { - return this.center.filter((r): r is Grid => r instanceof Grid && r.dimension == 1) + return this.center.filter((r) => r instanceof Grid).filter((grid) => grid.dimension == 1) } get grids(): Grid[] { - return this.center.filter((r): r is Grid => r instanceof Grid) + return this.center.filter((r) => r instanceof Grid) } get xgrid(): Proxied { @@ -199,7 +201,7 @@ export class Figure extends BaseFigure { } get legend(): Legend { - const legends = this.panels.filter((r): r is Legend => r instanceof Legend) + const legends = this.panels.filter((r) => r instanceof Legend) if (legends.length == 0) { const legend = new Legend() @@ -302,7 +304,7 @@ export class Figure extends BaseFigure { if (isString(active_drag) && active_drag != "auto") { const tool = tool_map.get(active_drag) - if (tool != null) { + if (tool instanceof GestureTool || tool instanceof ToolProxy) { this.toolbar.active_drag = tool } } else if (active_drag !== undefined) { @@ -320,7 +322,7 @@ export class Figure extends BaseFigure { if (isString(active_scroll) && active_scroll != "auto") { const tool = tool_map.get(active_scroll) - if (tool != null) { + if (tool instanceof GestureTool || tool instanceof ToolProxy) { this.toolbar.active_scroll = tool } } else if (active_scroll !== undefined) { @@ -329,7 +331,7 @@ export class Figure extends BaseFigure { if (isString(active_tap) && active_tap != "auto") { const tool = tool_map.get(active_tap) - if (tool != null) { + if (tool instanceof GestureTool || tool instanceof ToolProxy) { this.toolbar.active_tap = tool } } else if (active_tap !== undefined) { @@ -338,7 +340,7 @@ export class Figure extends BaseFigure { if (isString(active_multi) && active_multi != "auto") { const tool = tool_map.get(active_multi) - if (tool instanceof GestureTool) { + if (tool instanceof GestureTool || tool instanceof ToolProxy) { this.toolbar.active_multi = tool } } else if (active_multi !== undefined) { @@ -479,22 +481,41 @@ export class Figure extends BaseFigure { return unresolved_attrs } - _glyph(cls: Class, positional: NamesOf, args: unknown[], overrides: object = {}): TypedGlyphRenderer { + _signature(method: string, positional: string[]): string { + return `the method signature is ${method}(${positional.join(", ")}, args?)` + } + + _glyph(cls: Class, method: string, positional: NamesOf, args: unknown[], overrides: object = {}): GlyphRenderer { let attrs: Attrs & Partial - if (args.length == 0) { + + const n_args = args.length + const n_pos = positional.length + + if (n_args == n_pos || n_args == n_pos + 1) { attrs = {} - } else if (args.length == 1) { - attrs = {...args[0] as Attrs} - } else { - if (args.length == positional.length) { - attrs = {} - } else { - attrs = {...args[args.length - 1] as Attrs} + + for (const [[param, arg], i] of enumerate(zip(positional, args))) { + if (isPlainObject(arg) && !isVectorized(arg)) { + throw new Error(`invalid value for '${param}' parameter at position ${i}; ${this._signature(method, positional)}`) + } else { + attrs[param] = arg + } } - for (const [param, i] of enumerate(positional)) { - attrs[param as string] = args[i] + if (n_args == n_pos + 1) { + const opts = args[n_args - 1] + if (!isPlainObject(opts) || isVectorized(opts)) { + throw new Error(`expected optional arguments; ${this._signature(method, positional)}`) + } else { + attrs = {...attrs, ...args[args.length - 1] as Attrs} + } } + } else if (n_args == 0) { + attrs = {} + } else if (n_args == 1) { + attrs = {...args[0] as Attrs} + } else { + throw new Error(`wrong number of arguments; ${this._signature(method, positional)}`) } attrs = {...attrs, ...overrides} @@ -600,7 +621,7 @@ export class Figure extends BaseFigure { } this.add_renderers(glyph_renderer) - return glyph_renderer as TypedGlyphRenderer + return glyph_renderer as GlyphRenderer } static _get_range(range?: Range | [number, number] | ArrayLike): Range { diff --git a/bokehjs/src/lib/api/glyph_api.ts b/bokehjs/src/lib/api/glyph_api.ts index 4ab037395a6..5b15968e00a 100644 --- a/bokehjs/src/lib/api/glyph_api.ts +++ b/bokehjs/src/lib/api/glyph_api.ts @@ -9,24 +9,50 @@ import type * as nd from "core/util/ndarray" import type {Glyph, GlyphRenderer, ColumnarDataSource, CDSView, CoordinateMapping} from "./models" import { - AnnularWedge, Annulus, Arc, Bezier, Block, Circle, Ellipse, HArea, HAreaStep, HBar, HSpan, - HStrip, HexTile, Image, ImageRGBA, ImageStack, ImageURL, Line, MultiLine, MultiPolygons, - Patch, Patches, Quad, Quadratic, Ray, Rect, Scatter, Segment, Spline, Step, MathMLGlyph, - TeXGlyph, Text, VArea, VAreaStep, VBar, VSpan, VStrip, Wedge, + AnnularWedge, + Annulus, + Arc, + Bezier, + Block, + Circle, + Ellipse, + HArea, + HAreaStep, + HBar, + HSpan, + HStrip, + HexTile, + Image, + ImageRGBA, + ImageStack, + ImageURL, + Line, + MathMLGlyph as MathML, + MultiLine, + MultiPolygons, + Patch, + Patches, + Quad, + Quadratic, + Ray, + Rect, + Scatter, + Segment, + Spline, + Step, + TeXGlyph as TeX, + Text, + VArea, + VAreaStep, + VBar, + VSpan, + VStrip, + Wedge, } from "../models/glyphs" import type {Marker} from "../models/glyphs/marker" -export type NamesOf = (keyof T["properties"])[] - -export type TypedGlyphRenderer = GlyphRenderer & { - data_source: ColumnarDataSource - glyph: G - hover_glyph: G | null - nonselection_glyph: G | "auto" | null - selection_glyph: G | "auto" | null - muted_glyph: G | "auto" | null -} +export type NamesOf = (Extract)[] export type ColorNDArray = nd.Uint32Array1d | nd.Uint8Array1d | nd.Uint8Array2d | nd.FloatArray2d | nd.ObjectNDArray export type VectorArg = T | Arrayable | Vector @@ -138,7 +164,7 @@ export type ImageStackArgs = GlyphArgs export type ImageURLArgs = GlyphArgs export type LineArgs = GlyphArgs & AuxLine export type MarkerArgs = GlyphArgs & AuxLine & AuxFill & AuxHatch -export type MathMLGlyphArgs = GlyphArgs & AuxText +export type MathMLArgs = GlyphArgs & AuxText export type MultiLineArgs = GlyphArgs & AuxLine export type MultiPolygonsArgs = GlyphArgs & AuxLine & AuxFill & AuxHatch export type PatchArgs = GlyphArgs & AuxLine & AuxFill & AuxHatch @@ -151,8 +177,8 @@ export type ScatterArgs = GlyphArgs & AuxLine & AuxFi export type SegmentArgs = GlyphArgs & AuxLine export type SplineArgs = GlyphArgs & AuxLine export type StepArgs = GlyphArgs & AuxLine -export type TeXGlyphArgs = GlyphArgs & AuxText -export type TextArgs = GlyphArgs & AuxText +export type TeXArgs = GlyphArgs & AuxText +export type TextArgs = GlyphArgs & AuxText export type VAreaArgs = GlyphArgs & AuxFill & AuxHatch export type VAreaStepArgs = GlyphArgs & AuxFill & AuxHatch export type VBarArgs = GlyphArgs & AuxLine & AuxFill & AuxHatch @@ -161,9 +187,10 @@ export type VStripArgs = GlyphArgs & AuxLine & AuxFi export type WedgeArgs = GlyphArgs & AuxLine & AuxFill & AuxHatch export abstract class GlyphAPI { - abstract _glyph(cls: Class, positional: NamesOf, args: unknown[], overrides?: object): TypedGlyphRenderer + abstract _glyph(cls: Class, method: string, positional: NamesOf, args: unknown[], overrides?: object): GlyphRenderer - annular_wedge(args: Partial): TypedGlyphRenderer + annular_wedge(): GlyphRenderer + annular_wedge(args: Partial): GlyphRenderer annular_wedge( x: AnnularWedgeArgs["x"], y: AnnularWedgeArgs["y"], @@ -171,35 +198,38 @@ export abstract class GlyphAPI { outer_radius: AnnularWedgeArgs["outer_radius"], start_angle: AnnularWedgeArgs["start_angle"], end_angle: AnnularWedgeArgs["end_angle"], - args?: Partial): TypedGlyphRenderer - annular_wedge(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(AnnularWedge, ["x", "y", "inner_radius", "outer_radius", "start_angle", "end_angle"], args) + args?: Partial): GlyphRenderer + annular_wedge(...args: unknown[]): GlyphRenderer { + return this._glyph(AnnularWedge, "annular_wedge", ["x", "y", "inner_radius", "outer_radius", "start_angle", "end_angle"], args) } - annulus(args: Partial): TypedGlyphRenderer + annulus(): GlyphRenderer + annulus(args: Partial): GlyphRenderer annulus( x: AnnulusArgs["x"], y: AnnulusArgs["y"], inner_radius: AnnulusArgs["inner_radius"], outer_radius: AnnulusArgs["outer_radius"], - args?: Partial): TypedGlyphRenderer - annulus(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Annulus, ["x", "y", "inner_radius", "outer_radius"], args) + args?: Partial): GlyphRenderer + annulus(...args: unknown[]): GlyphRenderer { + return this._glyph(Annulus, "annulus", ["x", "y", "inner_radius", "outer_radius"], args) } - arc(args: Partial): TypedGlyphRenderer + arc(): GlyphRenderer + arc(args: Partial): GlyphRenderer arc( x: ArcArgs["x"], y: ArcArgs["y"], radius: ArcArgs["radius"], start_angle: ArcArgs["start_angle"], end_angle: ArcArgs["end_angle"], - args?: Partial): TypedGlyphRenderer - arc(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Arc, ["x", "y", "radius", "start_angle", "end_angle"], args) + args?: Partial): GlyphRenderer + arc(...args: unknown[]): GlyphRenderer { + return this._glyph(Arc, "arc", ["x", "y", "radius", "start_angle", "end_angle"], args) } - bezier(args: Partial): TypedGlyphRenderer + bezier(): GlyphRenderer + bezier(args: Partial): GlyphRenderer bezier( x0: BezierArgs["x0"], y0: BezierArgs["y0"], @@ -209,216 +239,237 @@ export abstract class GlyphAPI { cy0: BezierArgs["cy0"], cx1: BezierArgs["cx1"], cy1: BezierArgs["cy1"], - args?: Partial): TypedGlyphRenderer - bezier(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Bezier, ["x0", "y0", "x1", "y1", "cx0", "cy0", "cx1", "cy1"], args) + args?: Partial): GlyphRenderer + bezier(...args: unknown[]): GlyphRenderer { + return this._glyph(Bezier, "bezier", ["x0", "y0", "x1", "y1", "cx0", "cy0", "cx1", "cy1"], args) } - block(args: Partial): TypedGlyphRenderer + block(): GlyphRenderer + block(args: Partial): GlyphRenderer block( x: BlockArgs["x"], y: BlockArgs["y"], width: BlockArgs["width"], height: BlockArgs["height"], - args?: Partial): TypedGlyphRenderer - block(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Block, ["x", "y", "width", "height"], args) + args?: Partial): GlyphRenderer + block(...args: unknown[]): GlyphRenderer { + return this._glyph(Block, "block", ["x", "y", "width", "height"], args) } - circle(args: Partial): TypedGlyphRenderer + circle(): GlyphRenderer + circle(args: Partial): GlyphRenderer circle( x: CircleArgs["x"], y: CircleArgs["y"], radius: CircleArgs["radius"], - args?: Partial): TypedGlyphRenderer - circle(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Circle, ["x", "y", "radius"], args) + args?: Partial): GlyphRenderer + circle(...args: unknown[]): GlyphRenderer { + return this._glyph(Circle, "circle", ["x", "y", "radius"], args) } - ellipse(args: Partial): TypedGlyphRenderer + ellipse(): GlyphRenderer + ellipse(args: Partial): GlyphRenderer ellipse( x: EllipseArgs["x"], y: EllipseArgs["y"], width: EllipseArgs["width"], height: EllipseArgs["height"], - args?: Partial): TypedGlyphRenderer - ellipse(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Ellipse, ["x", "y", "width", "height"], args) + args?: Partial): GlyphRenderer + ellipse(...args: unknown[]): GlyphRenderer { + return this._glyph(Ellipse, "ellipse", ["x", "y", "width", "height"], args) } - harea(args: Partial): TypedGlyphRenderer + harea(): GlyphRenderer + harea(args: Partial): GlyphRenderer harea( x1: HAreaArgs["x1"], x2: HAreaArgs["x2"], y: HAreaArgs["y"], - args?: Partial): TypedGlyphRenderer - harea(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(HArea, ["x1", "x2", "y"], args) + args?: Partial): GlyphRenderer + harea(...args: unknown[]): GlyphRenderer { + return this._glyph(HArea, "harea", ["x1", "x2", "y"], args) } - harea_step(args: Partial): TypedGlyphRenderer + harea_step(): GlyphRenderer + harea_step(args: Partial): GlyphRenderer harea_step( x1: HAreaStepArgs["x1"], x2: HAreaStepArgs["x2"], y: HAreaStepArgs["y"], step_mode: HAreaStepArgs["step_mode"], - args?: Partial): TypedGlyphRenderer - harea_step(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(HAreaStep, ["x1", "x2", "y", "step_mode"], args) + args?: Partial): GlyphRenderer + harea_step(...args: unknown[]): GlyphRenderer { + return this._glyph(HAreaStep, "harea_step", ["x1", "x2", "y", "step_mode"], args) } - hbar(args: Partial): TypedGlyphRenderer + hbar(): GlyphRenderer + hbar(args: Partial): GlyphRenderer hbar( y: HBarArgs["y"], height: HBarArgs["height"], right: HBarArgs["right"], left: HBarArgs["left"], - args?: Partial): TypedGlyphRenderer - hbar(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(HBar, ["y", "height", "right", "left"], args) + args?: Partial): GlyphRenderer + hbar(...args: unknown[]): GlyphRenderer { + return this._glyph(HBar, "hbar", ["y", "height", "right", "left"], args) } - hspan(args: Partial): TypedGlyphRenderer + hspan(): GlyphRenderer + hspan(args: Partial): GlyphRenderer hspan( y: HSpanArgs["y"], - args?: Partial): TypedGlyphRenderer - hspan(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(HSpan, ["y"], args) + args?: Partial): GlyphRenderer + hspan(...args: unknown[]): GlyphRenderer { + return this._glyph(HSpan, "hspan", ["y"], args) } - hstrip(args: Partial): TypedGlyphRenderer + hstrip(): GlyphRenderer + hstrip(args: Partial): GlyphRenderer hstrip( y0: HStripArgs["y0"], y1: HStripArgs["y1"], - args?: Partial): TypedGlyphRenderer - hstrip(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(HStrip, ["y0", "y1"], args) + args?: Partial): GlyphRenderer + hstrip(...args: unknown[]): GlyphRenderer { + return this._glyph(HStrip, "hstrip", ["y0", "y1"], args) } - hex_tile(args: Partial): TypedGlyphRenderer + hex_tile(): GlyphRenderer + hex_tile(args: Partial): GlyphRenderer hex_tile( q: HexTileArgs["q"], r: HexTileArgs["r"], - args?: Partial): TypedGlyphRenderer - hex_tile(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(HexTile, ["q", "r"], args) + args?: Partial): GlyphRenderer + hex_tile(...args: unknown[]): GlyphRenderer { + return this._glyph(HexTile, "hex_tile", ["q", "r"], args) } - image(args: Partial): TypedGlyphRenderer + image(): GlyphRenderer + image(args: Partial): GlyphRenderer image( image: ImageArgs["image"], x: ImageArgs["x"], y: ImageArgs["y"], dw: ImageArgs["dw"], dh: ImageArgs["dh"], - args?: Partial): TypedGlyphRenderer - image(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Image, ["color_mapper", "image", "x", "y", "dw", "dh"], args) + args?: Partial): GlyphRenderer + image(...args: unknown[]): GlyphRenderer { + return this._glyph(Image, "image", ["color_mapper", "image", "x", "y", "dw", "dh"], args) } - image_stack(args: Partial): TypedGlyphRenderer + image_stack(): GlyphRenderer + image_stack(args: Partial): GlyphRenderer image_stack( image: ImageStackArgs["image"], x: ImageStackArgs["x"], y: ImageStackArgs["y"], dw: ImageStackArgs["dw"], dh: ImageStackArgs["dh"], - args?: Partial): TypedGlyphRenderer - image_stack(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(ImageStack, ["color_mapper", "image", "x", "y", "dw", "dh"], args) + args?: Partial): GlyphRenderer + image_stack(...args: unknown[]): GlyphRenderer { + return this._glyph(ImageStack, "image_stack", ["color_mapper", "image", "x", "y", "dw", "dh"], args) } - image_rgba(args: Partial): TypedGlyphRenderer + image_rgba(): GlyphRenderer + image_rgba(args: Partial): GlyphRenderer image_rgba( image: ImageRGBAArgs["image"], x: ImageRGBAArgs["x"], y: ImageRGBAArgs["y"], dw: ImageRGBAArgs["dw"], dh: ImageRGBAArgs["dh"], - args?: Partial): TypedGlyphRenderer - image_rgba(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(ImageRGBA, ["image", "x", "y", "dw", "dh"], args) + args?: Partial): GlyphRenderer + image_rgba(...args: unknown[]): GlyphRenderer { + return this._glyph(ImageRGBA, "image_rgba", ["image", "x", "y", "dw", "dh"], args) } - image_url(args: Partial): TypedGlyphRenderer + image_url(): GlyphRenderer + image_url(args: Partial): GlyphRenderer image_url( url: ImageURLArgs["url"], x: ImageURLArgs["x"], y: ImageURLArgs["y"], w: ImageURLArgs["w"], h: ImageURLArgs["h"], - args?: Partial): TypedGlyphRenderer - image_url(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(ImageURL, ["url", "x", "y", "w", "h"], args) + args?: Partial): GlyphRenderer + image_url(...args: unknown[]): GlyphRenderer { + return this._glyph(ImageURL, "image_url", ["url", "x", "y", "w", "h"], args) } - line(args: Partial): TypedGlyphRenderer + line(): GlyphRenderer + line(args: Partial): GlyphRenderer line( x: LineArgs["x"], y: LineArgs["y"], - args?: Partial): TypedGlyphRenderer - line(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Line, ["x", "y"], args) + args?: Partial): GlyphRenderer + line(...args: unknown[]): GlyphRenderer { + return this._glyph(Line, "line", ["x", "y"], args) } - mathml(args: Partial): TypedGlyphRenderer + mathml(): GlyphRenderer + mathml(args: Partial): GlyphRenderer mathml( - x: MathMLGlyphArgs["x"], - y: MathMLGlyphArgs["y"], - text: MathMLGlyphArgs["text"], - args?: Partial): TypedGlyphRenderer - mathml(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(MathMLGlyph, ["x", "y", "text"], args) + x: MathMLArgs["x"], + y: MathMLArgs["y"], + text: MathMLArgs["text"], + args?: Partial): GlyphRenderer + mathml(...args: unknown[]): GlyphRenderer { + return this._glyph(MathML, "mathml", ["x", "y", "text"], args) } - multi_line(args: Partial): TypedGlyphRenderer + multi_line(): GlyphRenderer + multi_line(args: Partial): GlyphRenderer multi_line( xs: MultiLineArgs["xs"], ys: MultiLineArgs["ys"], - args?: Partial): TypedGlyphRenderer - multi_line(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(MultiLine, ["xs", "ys"], args) + args?: Partial): GlyphRenderer + multi_line(...args: unknown[]): GlyphRenderer { + return this._glyph(MultiLine, "multi_line", ["xs", "ys"], args) } - multi_polygons(args: Partial): TypedGlyphRenderer + multi_polygons(): GlyphRenderer + multi_polygons(args: Partial): GlyphRenderer multi_polygons( xs: MultiPolygonsArgs["xs"], ys: MultiPolygonsArgs["ys"], - args?: Partial): TypedGlyphRenderer - multi_polygons(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(MultiPolygons, ["xs", "ys"], args) + args?: Partial): GlyphRenderer + multi_polygons(...args: unknown[]): GlyphRenderer { + return this._glyph(MultiPolygons, "multi_polygons", ["xs", "ys"], args) } - patch(args: Partial): TypedGlyphRenderer + patch(): GlyphRenderer + patch(args: Partial): GlyphRenderer patch( x: PatchArgs["x"], y: PatchArgs["y"], - args?: Partial): TypedGlyphRenderer - patch(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Patch, ["x", "y"], args) + args?: Partial): GlyphRenderer + patch(...args: unknown[]): GlyphRenderer { + return this._glyph(Patch, "patch", ["x", "y"], args) } - patches(args: Partial): TypedGlyphRenderer + patches(): GlyphRenderer + patches(args: Partial): GlyphRenderer patches( xs: PatchesArgs["xs"], ys: PatchesArgs["ys"], - args?: Partial): TypedGlyphRenderer - patches(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Patches, ["xs", "ys"], args) + args?: Partial): GlyphRenderer + patches(...args: unknown[]): GlyphRenderer { + return this._glyph(Patches, "patches", ["xs", "ys"], args) } - quad(args: Partial): TypedGlyphRenderer + quad(): GlyphRenderer + quad(args: Partial): GlyphRenderer quad( left: QuadArgs["left"], right: QuadArgs["right"], bottom: QuadArgs["bottom"], top: QuadArgs["top"], - args?: Partial): TypedGlyphRenderer - quad(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Quad, ["left", "right", "bottom", "top"], args) + args?: Partial): GlyphRenderer + quad(...args: unknown[]): GlyphRenderer { + return this._glyph(Quad, "quad", ["left", "right", "bottom", "top"], args) } - quadratic(args: Partial): TypedGlyphRenderer + quadratic(): GlyphRenderer + quadratic(args: Partial): GlyphRenderer quadratic( x0: QuadraticArgs["x0"], y0: QuadraticArgs["y0"], @@ -426,312 +477,353 @@ export abstract class GlyphAPI { y1: QuadraticArgs["y1"], cx: QuadraticArgs["cx"], cy: QuadraticArgs["cy"], - args?: Partial): TypedGlyphRenderer - quadratic(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Quadratic, ["x0", "y0", "x1", "y1", "cx", "cy"], args) + args?: Partial): GlyphRenderer + quadratic(...args: unknown[]): GlyphRenderer { + return this._glyph(Quadratic, "quadratic", ["x0", "y0", "x1", "y1", "cx", "cy"], args) } - ray(args: Partial): TypedGlyphRenderer + ray(): GlyphRenderer + ray(args: Partial): GlyphRenderer ray( x: RayArgs["x"], y: RayArgs["y"], length: RayArgs["length"], - args?: Partial): TypedGlyphRenderer - ray(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Ray, ["x", "y", "length"], args) + args?: Partial): GlyphRenderer + ray(...args: unknown[]): GlyphRenderer { + return this._glyph(Ray, "ray", ["x", "y", "length"], args) } - rect(args: Partial): TypedGlyphRenderer + rect(): GlyphRenderer + rect(args: Partial): GlyphRenderer rect( x: RectArgs["x"], y: RectArgs["y"], width: RectArgs["width"], height: RectArgs["height"], - args?: Partial): TypedGlyphRenderer - rect(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Rect, ["x", "y", "width", "height"], args) + args?: Partial): GlyphRenderer + rect(...args: unknown[]): GlyphRenderer { + return this._glyph(Rect, "rect", ["x", "y", "width", "height"], args) } - segment(args: Partial): TypedGlyphRenderer + segment(): GlyphRenderer + segment(args: Partial): GlyphRenderer segment( x0: SegmentArgs["x0"], y0: SegmentArgs["y0"], x1: SegmentArgs["x1"], y1: SegmentArgs["y1"], - args?: Partial): TypedGlyphRenderer - segment(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Segment, ["x0", "y0", "x1", "y1"], args) + args?: Partial): GlyphRenderer + segment(...args: unknown[]): GlyphRenderer { + return this._glyph(Segment, "segment", ["x0", "y0", "x1", "y1"], args) } - spline(args: Partial): TypedGlyphRenderer + spline(): GlyphRenderer + spline(args: Partial): GlyphRenderer spline( x: SplineArgs["x"], y: SplineArgs["y"], - args?: Partial): TypedGlyphRenderer - spline(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Spline, ["x", "y"], args) + args?: Partial): GlyphRenderer + spline(...args: unknown[]): GlyphRenderer { + return this._glyph(Spline, "spline", ["x", "y"], args) } - step(args: Partial): TypedGlyphRenderer + step(): GlyphRenderer + step(args: Partial): GlyphRenderer step( x: StepArgs["x"], y: StepArgs["y"], mode: StepArgs["mode"], - args?: Partial): TypedGlyphRenderer - step(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Step, ["x", "y", "mode"], args) + args?: Partial): GlyphRenderer + step(...args: unknown[]): GlyphRenderer { + return this._glyph(Step, "step", ["x", "y", "mode"], args) } - tex(args: Partial): TypedGlyphRenderer + tex(): GlyphRenderer + tex(args: Partial): GlyphRenderer tex( - x: TeXGlyphArgs["x"], - y: TeXGlyphArgs["y"], - text: TeXGlyphArgs["text"], - args?: Partial): TypedGlyphRenderer - tex(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(TeXGlyph, ["x", "y", "text"], args) + x: TeXArgs["x"], + y: TeXArgs["y"], + text: TeXArgs["text"], + args?: Partial): GlyphRenderer + tex(...args: unknown[]): GlyphRenderer { + return this._glyph(TeX, "tex", ["x", "y", "text"], args) } - text(args: Partial): TypedGlyphRenderer + text(): GlyphRenderer + text(args: Partial): GlyphRenderer text( x: TextArgs["x"], y: TextArgs["y"], text: TextArgs["text"], - args?: Partial): TypedGlyphRenderer - text(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Text, ["x", "y", "text"], args) + args?: Partial): GlyphRenderer + text(...args: unknown[]): GlyphRenderer { + return this._glyph(Text, "text", ["x", "y", "text"], args) } - varea(args: Partial): TypedGlyphRenderer + varea(): GlyphRenderer + varea(args: Partial): GlyphRenderer varea( x: VAreaArgs["x"], y1: VAreaArgs["y1"], y2: VAreaArgs["y2"], - args?: Partial): TypedGlyphRenderer - varea(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(VArea, ["x", "y1", "y2"], args) + args?: Partial): GlyphRenderer + varea(...args: unknown[]): GlyphRenderer { + return this._glyph(VArea, "varea", ["x", "y1", "y2"], args) } - varea_step(args: Partial): TypedGlyphRenderer + varea_step(): GlyphRenderer + varea_step(args: Partial): GlyphRenderer varea_step( x: VAreaStepArgs["x"], y1: VAreaStepArgs["y1"], y2: VAreaStepArgs["y2"], step_mode: VAreaStepArgs["step_mode"], - args?: Partial): TypedGlyphRenderer - varea_step(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(VAreaStep, ["x", "y1", "y2", "step_mode"], args) + args?: Partial): GlyphRenderer + varea_step(...args: unknown[]): GlyphRenderer { + return this._glyph(VAreaStep, "varea_step", ["x", "y1", "y2", "step_mode"], args) } - vbar(args: Partial): TypedGlyphRenderer + vbar(): GlyphRenderer + vbar(args: Partial): GlyphRenderer vbar( x: VBarArgs["x"], width: VBarArgs["width"], top: VBarArgs["top"], bottom: VBarArgs["bottom"], - args?: Partial): TypedGlyphRenderer - vbar(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(VBar, ["x", "width", "top", "bottom"], args) + args?: Partial): GlyphRenderer + vbar(...args: unknown[]): GlyphRenderer { + return this._glyph(VBar, "vbar", ["x", "width", "top", "bottom"], args) } - vspan(args: Partial): TypedGlyphRenderer + vspan(): GlyphRenderer + vspan(args: Partial): GlyphRenderer vspan( x: VSpanArgs["x"], - args?: Partial): TypedGlyphRenderer - vspan(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(VSpan, ["x"], args) + args?: Partial): GlyphRenderer + vspan(...args: unknown[]): GlyphRenderer { + return this._glyph(VSpan, "vspan", ["x"], args) } - vstrip(args: Partial): TypedGlyphRenderer + vstrip(): GlyphRenderer + vstrip(args: Partial): GlyphRenderer vstrip( x0: VStripArgs["x0"], x1: VStripArgs["x1"], - args?: Partial): TypedGlyphRenderer - vstrip(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(VStrip, ["x0", "x1"], args) + args?: Partial): GlyphRenderer + vstrip(...args: unknown[]): GlyphRenderer { + return this._glyph(VStrip, "vstrip", ["x0", "x1"], args) } - wedge(args: Partial): TypedGlyphRenderer + wedge(): GlyphRenderer + wedge(args: Partial): GlyphRenderer wedge( x: WedgeArgs["x"], y: WedgeArgs["y"], radius: WedgeArgs["radius"], start_angle: WedgeArgs["start_angle"], end_angle: WedgeArgs["end_angle"], - args?: Partial): TypedGlyphRenderer - wedge(...args: unknown[]): TypedGlyphRenderer { - return this._glyph(Wedge, ["x", "y", "radius", "start_angle", "end_angle"], args) + args?: Partial): GlyphRenderer + wedge(...args: unknown[]): GlyphRenderer { + return this._glyph(Wedge, "wedge", ["x", "y", "radius", "start_angle", "end_angle"], args) } - private _scatter(args: unknown[], marker?: MarkerType): TypedGlyphRenderer { - return this._glyph(Scatter, ["x", "y"], args, marker != null ? {marker} : undefined) + private _scatter(args: unknown[], marker?: MarkerType): GlyphRenderer { + return this._glyph(Scatter, marker ?? "scatter", ["x", "y"], args, marker != null ? {marker} : undefined) } - scatter(args: Partial): TypedGlyphRenderer - scatter(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - scatter(...args: unknown[]): TypedGlyphRenderer { + scatter(): GlyphRenderer + scatter(args: Partial): GlyphRenderer + scatter(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + scatter(...args: unknown[]): GlyphRenderer { return this._scatter(args) } - /** @deprecated */ asterisk(args: Partial): TypedGlyphRenderer - /** @deprecated */ asterisk(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ asterisk(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ asterisk(): GlyphRenderer + /** @deprecated */ asterisk(args: Partial): GlyphRenderer + /** @deprecated */ asterisk(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ asterisk(...args: unknown[]): GlyphRenderer { return this._scatter(args, "asterisk") } - /** @deprecated */ circle_cross(args: Partial): TypedGlyphRenderer - /** @deprecated */ circle_cross(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ circle_cross(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ circle_cross(): GlyphRenderer + /** @deprecated */ circle_cross(args: Partial): GlyphRenderer + /** @deprecated */ circle_cross(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ circle_cross(...args: unknown[]): GlyphRenderer { return this._scatter(args, "circle_cross") } - /** @deprecated */ circle_dot(args: Partial): TypedGlyphRenderer - /** @deprecated */ circle_dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ circle_dot(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ circle_dot(): GlyphRenderer + /** @deprecated */ circle_dot(args: Partial): GlyphRenderer + /** @deprecated */ circle_dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ circle_dot(...args: unknown[]): GlyphRenderer { return this._scatter(args, "circle_dot") } - /** @deprecated */ circle_x(args: Partial): TypedGlyphRenderer - /** @deprecated */ circle_x(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ circle_x(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ circle_x(): GlyphRenderer + /** @deprecated */ circle_x(args: Partial): GlyphRenderer + /** @deprecated */ circle_x(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ circle_x(...args: unknown[]): GlyphRenderer { return this._scatter(args, "circle_x") } - /** @deprecated */ circle_y(args: Partial): TypedGlyphRenderer - /** @deprecated */ circle_y(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ circle_y(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ circle_y(): GlyphRenderer + /** @deprecated */ circle_y(args: Partial): GlyphRenderer + /** @deprecated */ circle_y(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ circle_y(...args: unknown[]): GlyphRenderer { return this._scatter(args, "circle_y") } - /** @deprecated */ cross(args: Partial): TypedGlyphRenderer - /** @deprecated */ cross(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ cross(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ cross(): GlyphRenderer + /** @deprecated */ cross(args: Partial): GlyphRenderer + /** @deprecated */ cross(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ cross(...args: unknown[]): GlyphRenderer { return this._scatter(args, "cross") } - /** @deprecated */ dash(args: Partial): TypedGlyphRenderer - /** @deprecated */ dash(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ dash(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ dash(): GlyphRenderer + /** @deprecated */ dash(args: Partial): GlyphRenderer + /** @deprecated */ dash(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ dash(...args: unknown[]): GlyphRenderer { return this._scatter(args, "dash") } - /** @deprecated */ diamond(args: Partial): TypedGlyphRenderer - /** @deprecated */ diamond(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ diamond(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ diamond(): GlyphRenderer + /** @deprecated */ diamond(args: Partial): GlyphRenderer + /** @deprecated */ diamond(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ diamond(...args: unknown[]): GlyphRenderer { return this._scatter(args, "diamond") } - /** @deprecated */ diamond_cross(args: Partial): TypedGlyphRenderer - /** @deprecated */ diamond_cross(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ diamond_cross(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ diamond_cross(): GlyphRenderer + /** @deprecated */ diamond_cross(args: Partial): GlyphRenderer + /** @deprecated */ diamond_cross(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ diamond_cross(...args: unknown[]): GlyphRenderer { return this._scatter(args, "diamond_cross") } - /** @deprecated */ diamond_dot(args: Partial): TypedGlyphRenderer - /** @deprecated */ diamond_dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ diamond_dot(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ diamond_dot(): GlyphRenderer + /** @deprecated */ diamond_dot(args: Partial): GlyphRenderer + /** @deprecated */ diamond_dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ diamond_dot(...args: unknown[]): GlyphRenderer { return this._scatter(args, "diamond_dot") } - /** @deprecated */ dot(args: Partial): TypedGlyphRenderer - /** @deprecated */ dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ dot(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ dot(): GlyphRenderer + /** @deprecated */ dot(args: Partial): GlyphRenderer + /** @deprecated */ dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ dot(...args: unknown[]): GlyphRenderer { return this._scatter(args, "dot") } - /** @deprecated */ hex(args: Partial): TypedGlyphRenderer - /** @deprecated */ hex(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ hex(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ hex(): GlyphRenderer + /** @deprecated */ hex(args: Partial): GlyphRenderer + /** @deprecated */ hex(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ hex(...args: unknown[]): GlyphRenderer { return this._scatter(args, "hex") } - /** @deprecated */ hex_dot(args: Partial): TypedGlyphRenderer - /** @deprecated */ hex_dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ hex_dot(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ hex_dot(): GlyphRenderer + /** @deprecated */ hex_dot(args: Partial): GlyphRenderer + /** @deprecated */ hex_dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ hex_dot(...args: unknown[]): GlyphRenderer { return this._scatter(args, "hex_dot") } - /** @deprecated */ inverted_triangle(args: Partial): TypedGlyphRenderer - /** @deprecated */ inverted_triangle(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ inverted_triangle(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ inverted_triangle(): GlyphRenderer + /** @deprecated */ inverted_triangle(args: Partial): GlyphRenderer + /** @deprecated */ inverted_triangle(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ inverted_triangle(...args: unknown[]): GlyphRenderer { return this._scatter(args, "inverted_triangle") } - /** @deprecated */ plus(args: Partial): TypedGlyphRenderer - /** @deprecated */ plus(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ plus(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ plus(): GlyphRenderer + /** @deprecated */ plus(args: Partial): GlyphRenderer + /** @deprecated */ plus(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ plus(...args: unknown[]): GlyphRenderer { return this._scatter(args, "plus") } - /** @deprecated */ square(args: Partial): TypedGlyphRenderer - /** @deprecated */ square(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ square(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ square(): GlyphRenderer + /** @deprecated */ square(args: Partial): GlyphRenderer + /** @deprecated */ square(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ square(...args: unknown[]): GlyphRenderer { return this._scatter(args, "square") } - /** @deprecated */ square_cross(args: Partial): TypedGlyphRenderer - /** @deprecated */ square_cross(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ square_cross(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ square_cross(): GlyphRenderer + /** @deprecated */ square_cross(args: Partial): GlyphRenderer + /** @deprecated */ square_cross(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ square_cross(...args: unknown[]): GlyphRenderer { return this._scatter(args, "square_cross") } - /** @deprecated */ square_dot(args: Partial): TypedGlyphRenderer - /** @deprecated */ square_dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ square_dot(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ square_dot(): GlyphRenderer + /** @deprecated */ square_dot(args: Partial): GlyphRenderer + /** @deprecated */ square_dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ square_dot(...args: unknown[]): GlyphRenderer { return this._scatter(args, "square_dot") } - /** @deprecated */ square_pin(args: Partial): TypedGlyphRenderer - /** @deprecated */ square_pin(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ square_pin(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ square_pin(): GlyphRenderer + /** @deprecated */ square_pin(args: Partial): GlyphRenderer + /** @deprecated */ square_pin(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ square_pin(...args: unknown[]): GlyphRenderer { return this._scatter(args, "square_pin") } - /** @deprecated */ square_x(args: Partial): TypedGlyphRenderer - /** @deprecated */ square_x(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ square_x(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ square_x(): GlyphRenderer + /** @deprecated */ square_x(args: Partial): GlyphRenderer + /** @deprecated */ square_x(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ square_x(...args: unknown[]): GlyphRenderer { return this._scatter(args, "square_x") } - /** @deprecated */ star(args: Partial): TypedGlyphRenderer - /** @deprecated */ star(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ star(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ star(): GlyphRenderer + /** @deprecated */ star(args: Partial): GlyphRenderer + /** @deprecated */ star(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ star(...args: unknown[]): GlyphRenderer { return this._scatter(args, "star") } - /** @deprecated */ star_dot(args: Partial): TypedGlyphRenderer - /** @deprecated */ star_dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ star_dot(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ star_dot(): GlyphRenderer + /** @deprecated */ star_dot(args: Partial): GlyphRenderer + /** @deprecated */ star_dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ star_dot(...args: unknown[]): GlyphRenderer { return this._scatter(args, "star_dot") } - /** @deprecated */ triangle(args: Partial): TypedGlyphRenderer - /** @deprecated */ triangle(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ triangle(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ triangle(): GlyphRenderer + /** @deprecated */ triangle(args: Partial): GlyphRenderer + /** @deprecated */ triangle(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ triangle(...args: unknown[]): GlyphRenderer { return this._scatter(args, "triangle") } - /** @deprecated */ triangle_dot(args: Partial): TypedGlyphRenderer - /** @deprecated */ triangle_dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ triangle_dot(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ triangle_dot(): GlyphRenderer + /** @deprecated */ triangle_dot(args: Partial): GlyphRenderer + /** @deprecated */ triangle_dot(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ triangle_dot(...args: unknown[]): GlyphRenderer { return this._scatter(args, "triangle_dot") } - /** @deprecated */ triangle_pin(args: Partial): TypedGlyphRenderer - /** @deprecated */ triangle_pin(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ triangle_pin(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ triangle_pin(): GlyphRenderer + /** @deprecated */ triangle_pin(args: Partial): GlyphRenderer + /** @deprecated */ triangle_pin(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ triangle_pin(...args: unknown[]): GlyphRenderer { return this._scatter(args, "triangle_pin") } - /** @deprecated */ x(args: Partial): TypedGlyphRenderer - /** @deprecated */ x(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ x(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ x(): GlyphRenderer + /** @deprecated */ x(args: Partial): GlyphRenderer + /** @deprecated */ x(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ x(...args: unknown[]): GlyphRenderer { return this._scatter(args, "x") } - /** @deprecated */ y(args: Partial): TypedGlyphRenderer - /** @deprecated */ y(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): TypedGlyphRenderer - /** @deprecated */ y(...args: unknown[]): TypedGlyphRenderer { + /** @deprecated */ y(): GlyphRenderer + /** @deprecated */ y(args: Partial): GlyphRenderer + /** @deprecated */ y(x: MarkerArgs["x"], y: MarkerArgs["y"], args?: Partial): GlyphRenderer + /** @deprecated */ y(...args: unknown[]): GlyphRenderer { return this._scatter(args, "y") } } diff --git a/bokehjs/src/lib/core/bokeh_events.ts b/bokehjs/src/lib/core/bokeh_events.ts index 06225abff10..3b30083d328 100644 --- a/bokehjs/src/lib/core/bokeh_events.ts +++ b/bokehjs/src/lib/core/bokeh_events.ts @@ -1,12 +1,30 @@ +import {Model} from "../model" import type {HasProps} from "./has_props" import type {Attrs} from "./types" +import {isPlainObject} from "./util/types" +import {assert} from "./util/assert" import type {GeometryData} from "./geometry" import type {Class} from "./class" import type {KeyModifiers} from "./ui_gestures" import type {Serializable, Serializer} from "./serialization" import {serialize} from "./serialization" +import {Deserializer} from "./serialization/deserializer" import type {Equatable, Comparator} from "./util/eq" import {equals} from "./util/eq" +import type {Legend} from "../models/annotations/legend" +import type {LegendItem} from "../models/annotations/legend_item" +import type {ClearInput} from "../models/widgets/input_widget" + +Deserializer.register("event", (rep: BokehEventRep, deserializer: Deserializer): BokehEvent => { + const cls = deserializable_events.get(rep.name) + if (cls !== undefined && cls.from_values != null) { + const values = deserializer.decode(rep.values) + assert(isPlainObject(values)) + return cls.from_values(values) + } else { + deserializer.error(`deserialization of '${rep.name}' event is not supported`) + } +}) export type BokehEventType = DocumentEventType | @@ -21,6 +39,7 @@ export type ConnectionEventType = export type ModelEventType = "button_click" | + "legend_item_click" | "menu_item_click" | "value_submit" | UIEventType @@ -52,10 +71,16 @@ export type PointEventType = "rotatestart" | "rotateend" +/** + * Events known to bokeh by name, for type-safety of Model.on_event(event_name, (EventType) => void). + * Other events, including user defined events, can be referred to by event's class object. + */ export type BokehEventMap = { document_ready: DocumentReady + clear_input: ClearInput connection_lost: ConnectionLost button_click: ButtonClick + legend_item_click: LegendItemClick menu_item_click: MenuItemClick value_submit: ValueSubmit lodstart: LODStart @@ -94,6 +119,22 @@ function event(event_name: string) { } } +const deserializable_events: Map = new Map() + +/** + * Marks and registers a class as a one way (server -> client) event. + */ +export function server_event(event_name: string) { + return (cls: Class) => { + if (deserializable_events.has(event_name)) { + throw new Error(`'${event_name}' event is already registered`) + } + deserializable_events.set(event_name, cls) + cls.prototype.event_name = event_name + cls.prototype.publish = false + } +} + export abstract class BokehEvent implements Serializable, Equatable { declare event_name: string declare publish: boolean @@ -110,6 +151,8 @@ export abstract class BokehEvent implements Serializable, Equatable { protected abstract get event_values(): Attrs + static from_values?(values: Attrs): BokehEvent + static { this.prototype.publish = true } @@ -123,6 +166,32 @@ export abstract class ModelEvent extends BokehEvent { } } +export abstract class UserEvent extends ModelEvent { + constructor(readonly values: Attrs) { + super() + } + + protected override get event_values(): Attrs { + return {...super.event_values, ...this.values} + } + + static override from_values(values: Attrs): UserEvent { + const origin = (() => { + if ("model" in values) { + const {model} = values + assert(model === null || model instanceof Model) + delete values.model + return model + } else { + return null + } + })() + const event = new (this as any)(values) + event.origin = origin + return event + } +} + export abstract class DocumentEvent extends BokehEvent {} @event("document_ready") @@ -151,6 +220,19 @@ export class ConnectionLost extends ConnectionEvent { @event("button_click") export class ButtonClick extends ModelEvent {} +@event("legend_item_click") +export class LegendItemClick extends ModelEvent { + + constructor(readonly model: Legend, readonly item: LegendItem) { + super() + } + + protected override get event_values(): Attrs { + const {item} = this + return {...super.event_values, item} + } +} + @event("menu_item_click") export class MenuItemClick extends ModelEvent { diff --git a/bokehjs/src/lib/core/build_views.ts b/bokehjs/src/lib/core/build_views.ts index 2d4455cfc07..a1e86f46ad0 100644 --- a/bokehjs/src/lib/core/build_views.ts +++ b/bokehjs/src/lib/core/build_views.ts @@ -70,3 +70,21 @@ export function remove_views(view_storage: ViewStorage): void { view_storage.delete(model) } } + +export function traverse_views(views: View[], fn: (view: View) => void): void { + const visited = new Set() + const queue: View[] = [...views] + + while (true) { + const view = queue.shift() + if (view === undefined) { + break + } + if (visited.has(view)) { + continue + } + visited.add(view) + queue.push(...view.children()) + fn(view) + } +} diff --git a/bokehjs/src/lib/core/css.ts b/bokehjs/src/lib/core/css.ts index 239b4ade81c..ff9e0626dad 100644 --- a/bokehjs/src/lib/core/css.ts +++ b/bokehjs/src/lib/core/css.ts @@ -139,7 +139,7 @@ type CSSStylesCamel = { counterSet?: string | null cursor?: string | null direction?: string | null - display?: Display | null + display?: string /*Display*/ | null dominantBaseline?: string | null emptyCells?: string | null fill?: string | null @@ -148,7 +148,7 @@ type CSSStylesCamel = { filter?: string | null flex?: string | null flexBasis?: string | null - flexDirection?: FlexDirection | null + flexDirection?: string /*FlexDirection*/ | null flexFlow?: string | null flexGrow?: string | null flexShrink?: string | null @@ -296,7 +296,7 @@ type CSSStylesCamel = { placeItems?: string | null placeSelf?: string | null pointerEvents?: string | null - position?: Position | null + position?: string /*Position*/ | null quotes?: string | null resize?: string | null right?: string | null @@ -398,775 +398,775 @@ type CSSStylesCamel = { } type CSSStylesDashed = { - "accent-color"?: string | null - "align-content"?: string | null - "align-items"?: string | null - "align-self"?: string | null - "alignment-baseline"?: string | null - "all"?: string | null - "animation"?: string | null - "animation-delay"?: string | null - "animation-direction"?: string | null - "animation-duration"?: string | null - "animation-fill-mode"?: string | null - "animation-iteration-count"?: string | null - "animation-name"?: string | null - "animation-play-state"?: string | null - "animation-timing-function"?: string | null - "appearance"?: string | null - "aspect-ratio"?: string | null - "backface-visibility"?: string | null - "background"?: string | null - "background-attachment"?: string | null - "background-blend-mode"?: string | null - "background-clip"?: string | null - "background-color"?: string | null - "background-image"?: string | null - "background-origin"?: string | null - "background-position"?: string | null - "background-position-x"?: string | null - "background-position-y"?: string | null - "background-repeat"?: string | null - "background-size"?: string | null - "baseline-shift"?: string | null - "block-size"?: string | null - "border"?: string | null - "border-block"?: string | null - "border-block-color"?: string | null - "border-block-end"?: string | null - "border-block-end-color"?: string | null - "border-block-end-style"?: string | null - "border-block-end-width"?: string | null - "border-block-start"?: string | null - "border-block-start-color"?: string | null - "border-block-start-style"?: string | null - "border-block-start-width"?: string | null - "border-block-style"?: string | null - "border-block-width"?: string | null - "border-bottom"?: string | null - "border-bottom-color"?: string | null - "border-bottom-left-radius"?: string | null - "border-bottom-right-radius"?: string | null - "border-bottom-style"?: string | null - "border-bottom-width"?: string | null - "border-collapse"?: string | null - "border-color"?: string | null - "border-end-end-radius"?: string | null - "border-end-start-radius"?: string | null - "border-image"?: string | null - "border-image-outset"?: string | null - "border-image-repeat"?: string | null - "border-image-slice"?: string | null - "border-image-source"?: string | null - "border-image-width"?: string | null - "border-inline"?: string | null - "border-inline-color"?: string | null - "border-inline-end"?: string | null - "border-inline-end-color"?: string | null - "border-inline-end-style"?: string | null - "border-inline-end-width"?: string | null - "border-inline-start"?: string | null - "border-inline-start-color"?: string | null - "border-inline-start-style"?: string | null - "border-inline-start-width"?: string | null - "border-inline-style"?: string | null - "border-inline-width"?: string | null - "border-left"?: string | null - "border-left-color"?: string | null - "border-left-style"?: string | null - "border-left-width"?: string | null - "border-radius"?: string | null - "border-right"?: string | null - "border-right-color"?: string | null - "border-right-style"?: string | null - "border-right-width"?: string | null - "border-spacing"?: string | null - "border-start-end-radius"?: string | null - "border-start-start-radius"?: string | null - "border-style"?: string | null - "border-top"?: string | null - "border-top-color"?: string | null - "border-top-left-radius"?: string | null - "border-top-right-radius"?: string | null - "border-top-style"?: string | null - "border-top-width"?: string | null - "border-width"?: string | null - "bottom"?: string | null - "box-shadow"?: string | null - "box-sizing"?: string | null - "break-after"?: string | null - "break-before"?: string | null - "break-inside"?: string | null - "caption-side"?: string | null - "caret-color"?: string | null - "clear"?: string | null + "accent-color"?: CSSStylesCamel["accentColor"] + "align-content"?: CSSStylesCamel["alignContent"] + "align-items"?: CSSStylesCamel["alignItems"] + "align-self"?: CSSStylesCamel["alignSelf"] + "alignment-baseline"?: CSSStylesCamel["alignmentBaseline"] + "all"?: CSSStylesCamel["all"] + "animation"?: CSSStylesCamel["animation"] + "animation-delay"?: CSSStylesCamel["animationDelay"] + "animation-direction"?: CSSStylesCamel["animationDirection"] + "animation-duration"?: CSSStylesCamel["animationDuration"] + "animation-fill-mode"?: CSSStylesCamel["animationFillMode"] + "animation-iteration-count"?: CSSStylesCamel["animationIterationCount"] + "animation-name"?: CSSStylesCamel["animationName"] + "animation-play-state"?: CSSStylesCamel["animationPlayState"] + "animation-timing-function"?: CSSStylesCamel["animationTimingFunction"] + "appearance"?: CSSStylesCamel["appearance"] + "aspect-ratio"?: CSSStylesCamel["aspectRatio"] + "backface-visibility"?: CSSStylesCamel["backfaceVisibility"] + "background"?: CSSStylesCamel["background"] + "background-attachment"?: CSSStylesCamel["backgroundAttachment"] + "background-blend-mode"?: CSSStylesCamel["backgroundBlendMode"] + "background-clip"?: CSSStylesCamel["backgroundClip"] + "background-color"?: CSSStylesCamel["backgroundColor"] + "background-image"?: CSSStylesCamel["backgroundImage"] + "background-origin"?: CSSStylesCamel["backgroundOrigin"] + "background-position"?: CSSStylesCamel["backgroundPosition"] + "background-position-x"?: CSSStylesCamel["backgroundPositionX"] + "background-position-y"?: CSSStylesCamel["backgroundPositionY"] + "background-repeat"?: CSSStylesCamel["backgroundRepeat"] + "background-size"?: CSSStylesCamel["backgroundSize"] + "baseline-shift"?: CSSStylesCamel["baselineShift"] + "block-size"?: CSSStylesCamel["blockSize"] + "border"?: CSSStylesCamel["border"] + "border-block"?: CSSStylesCamel["borderBlock"] + "border-block-color"?: CSSStylesCamel["borderBlockColor"] + "border-block-end"?: CSSStylesCamel["borderBlockEnd"] + "border-block-end-color"?: CSSStylesCamel["borderBlockEndColor"] + "border-block-end-style"?: CSSStylesCamel["borderBlockEndStyle"] + "border-block-end-width"?: CSSStylesCamel["borderBlockEndWidth"] + "border-block-start"?: CSSStylesCamel["borderBlockStart"] + "border-block-start-color"?: CSSStylesCamel["borderBlockStartColor"] + "border-block-start-style"?: CSSStylesCamel["borderBlockStartStyle"] + "border-block-start-width"?: CSSStylesCamel["borderBlockStartWidth"] + "border-block-style"?: CSSStylesCamel["borderBlockStyle"] + "border-block-width"?: CSSStylesCamel["borderBlockWidth"] + "border-bottom"?: CSSStylesCamel["borderBottom"] + "border-bottom-color"?: CSSStylesCamel["borderBottomColor"] + "border-bottom-left-radius"?: CSSStylesCamel["borderBottomLeftRadius"] + "border-bottom-right-radius"?: CSSStylesCamel["borderBottomRightRadius"] + "border-bottom-style"?: CSSStylesCamel["borderBottomStyle"] + "border-bottom-width"?: CSSStylesCamel["borderBottomWidth"] + "border-collapse"?: CSSStylesCamel["borderCollapse"] + "border-color"?: CSSStylesCamel["borderColor"] + "border-end-end-radius"?: CSSStylesCamel["borderEndEndRadius"] + "border-end-start-radius"?: CSSStylesCamel["borderEndStartRadius"] + "border-image"?: CSSStylesCamel["borderImage"] + "border-image-outset"?: CSSStylesCamel["borderImageOutset"] + "border-image-repeat"?: CSSStylesCamel["borderImageRepeat"] + "border-image-slice"?: CSSStylesCamel["borderImageSlice"] + "border-image-source"?: CSSStylesCamel["borderImageSource"] + "border-image-width"?: CSSStylesCamel["borderImageWidth"] + "border-inline"?: CSSStylesCamel["borderInline"] + "border-inline-color"?: CSSStylesCamel["borderInlineColor"] + "border-inline-end"?: CSSStylesCamel["borderInlineEnd"] + "border-inline-end-color"?: CSSStylesCamel["borderInlineEndColor"] + "border-inline-end-style"?: CSSStylesCamel["borderInlineEndStyle"] + "border-inline-end-width"?: CSSStylesCamel["borderInlineEndWidth"] + "border-inline-start"?: CSSStylesCamel["borderInlineStart"] + "border-inline-start-color"?: CSSStylesCamel["borderInlineStartColor"] + "border-inline-start-style"?: CSSStylesCamel["borderInlineStartStyle"] + "border-inline-start-width"?: CSSStylesCamel["borderInlineStartWidth"] + "border-inline-style"?: CSSStylesCamel["borderInlineStyle"] + "border-inline-width"?: CSSStylesCamel["borderInlineWidth"] + "border-left"?: CSSStylesCamel["borderLeft"] + "border-left-color"?: CSSStylesCamel["borderLeftColor"] + "border-left-style"?: CSSStylesCamel["borderLeftStyle"] + "border-left-width"?: CSSStylesCamel["borderLeftWidth"] + "border-radius"?: CSSStylesCamel["borderRadius"] + "border-right"?: CSSStylesCamel["borderRight"] + "border-right-color"?: CSSStylesCamel["borderRightColor"] + "border-right-style"?: CSSStylesCamel["borderRightStyle"] + "border-right-width"?: CSSStylesCamel["borderRightWidth"] + "border-spacing"?: CSSStylesCamel["borderSpacing"] + "border-start-end-radius"?: CSSStylesCamel["borderStartEndRadius"] + "border-start-start-radius"?: CSSStylesCamel["borderStartStartRadius"] + "border-style"?: CSSStylesCamel["borderStyle"] + "border-top"?: CSSStylesCamel["borderTop"] + "border-top-color"?: CSSStylesCamel["borderTopColor"] + "border-top-left-radius"?: CSSStylesCamel["borderTopLeftRadius"] + "border-top-right-radius"?: CSSStylesCamel["borderTopRightRadius"] + "border-top-style"?: CSSStylesCamel["borderTopStyle"] + "border-top-width"?: CSSStylesCamel["borderTopWidth"] + "border-width"?: CSSStylesCamel["borderWidth"] + "bottom"?: CSSStylesCamel["bottom"] + "box-shadow"?: CSSStylesCamel["boxShadow"] + "box-sizing"?: CSSStylesCamel["boxSizing"] + "break-after"?: CSSStylesCamel["breakAfter"] + "break-before"?: CSSStylesCamel["breakBefore"] + "break-inside"?: CSSStylesCamel["breakInside"] + "caption-side"?: CSSStylesCamel["captionSide"] + "caret-color"?: CSSStylesCamel["caretColor"] + "clear"?: CSSStylesCamel["clear"] /** @deprecated */ - "clip"?: string | null - "clip-path"?: string | null - "clip-rule"?: string | null - "color"?: string | null - "color-interpolation"?: string | null - "color-interpolation-filters"?: string | null - "color-scheme"?: string | null - "column-count"?: string | null - "column-fill"?: string | null - "column-gap"?: string | null - "column-rule"?: string | null - "column-rule-color"?: string | null - "column-rule-style"?: string | null - "column-rule-width"?: string | null - "column-span"?: string | null - "column-width"?: string | null - "columns"?: string | null - "contain"?: string | null - "content"?: string | null - "counter-increment"?: string | null - "counter-reset"?: string | null - "counter-set"?: string | null - "cursor"?: string | null - "direction"?: string | null - "display"?: Display | null - "dominant-baseline"?: string | null - "empty-cells"?: string | null - "fill"?: string | null - "fill-opacity"?: string | null - "fill-rule"?: string | null - "filter"?: string | null - "flex"?: string | null - "flex-basis"?: string | null - "flex-direction"?: FlexDirection | null - "flex-flow"?: string | null - "flex-grow"?: string | null - "flex-shrink"?: string | null - "flex-wrap"?: string | null - "float"?: string | null - "flood-color"?: string | null - "flood-opacity"?: string | null - "font"?: string | null - "font-family"?: string | null - "font-feature-settings"?: string | null - "font-kerning"?: string | null - "font-optical-sizing"?: string | null - "font-size"?: string | null - "font-size-adjust"?: string | null - "font-stretch"?: string | null - "font-style"?: string | null - "font-synthesis"?: string | null - "font-variant"?: string | null + "clip"?: CSSStylesCamel["clip"] + "clip-path"?: CSSStylesCamel["clipPath"] + "clip-rule"?: CSSStylesCamel["clipRule"] + "color"?: CSSStylesCamel["color"] + "color-interpolation"?: CSSStylesCamel["colorInterpolation"] + "color-interpolation-filters"?: CSSStylesCamel["colorInterpolationFilters"] + "color-scheme"?: CSSStylesCamel["colorScheme"] + "column-count"?: CSSStylesCamel["columnCount"] + "column-fill"?: CSSStylesCamel["columnFill"] + "column-gap"?: CSSStylesCamel["columnGap"] + "column-rule"?: CSSStylesCamel["columnRule"] + "column-rule-color"?: CSSStylesCamel["columnRuleColor"] + "column-rule-style"?: CSSStylesCamel["columnRuleStyle"] + "column-rule-width"?: CSSStylesCamel["columnRuleWidth"] + "column-span"?: CSSStylesCamel["columnSpan"] + "column-width"?: CSSStylesCamel["columnWidth"] + "columns"?: CSSStylesCamel["columns"] + "contain"?: CSSStylesCamel["contain"] + "content"?: CSSStylesCamel["content"] + "counter-increment"?: CSSStylesCamel["counterIncrement"] + "counter-reset"?: CSSStylesCamel["counterReset"] + "counter-set"?: CSSStylesCamel["counterSet"] + "cursor"?: CSSStylesCamel["cursor"] + "direction"?: CSSStylesCamel["direction"] + "display"?: CSSStylesCamel["display"] + "dominant-baseline"?: CSSStylesCamel["dominantBaseline"] + "empty-cells"?: CSSStylesCamel["emptyCells"] + "fill"?: CSSStylesCamel["fill"] + "fill-opacity"?: CSSStylesCamel["fillOpacity"] + "fill-rule"?: CSSStylesCamel["fillRule"] + "filter"?: CSSStylesCamel["filter"] + "flex"?: CSSStylesCamel["flex"] + "flex-basis"?: CSSStylesCamel["flexBasis"] + "flex-direction"?: CSSStylesCamel["flexDirection"] + "flex-flow"?: CSSStylesCamel["flexFlow"] + "flex-grow"?: CSSStylesCamel["flexGrow"] + "flex-shrink"?: CSSStylesCamel["flexShrink"] + "flex-wrap"?: CSSStylesCamel["flexWrap"] + "float"?: CSSStylesCamel["float"] + "flood-color"?: CSSStylesCamel["floodColor"] + "flood-opacity"?: CSSStylesCamel["floodOpacity"] + "font"?: CSSStylesCamel["font"] + "font-family"?: CSSStylesCamel["fontFamily"] + "font-feature-settings"?: CSSStylesCamel["fontFeatureSettings"] + "font-kerning"?: CSSStylesCamel["fontKerning"] + "font-optical-sizing"?: CSSStylesCamel["fontOpticalSizing"] + "font-size"?: CSSStylesCamel["fontSize"] + "font-size-adjust"?: CSSStylesCamel["fontSizeAdjust"] + "font-stretch"?: CSSStylesCamel["fontStretch"] + "font-style"?: CSSStylesCamel["fontStyle"] + "font-synthesis"?: CSSStylesCamel["fontSynthesis"] + "font-variant"?: CSSStylesCamel["fontVariant"] /** @deprecated */ - "font-variant-alternates"?: string | null - "font-variant-caps"?: string | null - "font-variant-east-asian"?: string | null - "font-variant-ligatures"?: string | null - "font-variant-numeric"?: string | null - "font-variant-position"?: string | null - "font-variation-settings"?: string | null - "font-weight"?: string | null - "gap"?: string | null - "grid"?: string | null - "grid-area"?: string | null - "grid-auto-columns"?: string | null - "grid-auto-flow"?: string | null - "grid-auto-rows"?: string | null - "grid-column"?: string | null - "grid-column-end"?: string | null + "font-variant-alternates"?: CSSStylesCamel["fontVariantAlternates"] + "font-variant-caps"?: CSSStylesCamel["fontVariantCaps"] + "font-variant-east-asian"?: CSSStylesCamel["fontVariantEastAsian"] + "font-variant-ligatures"?: CSSStylesCamel["fontVariantLigatures"] + "font-variant-numeric"?: CSSStylesCamel["fontVariantNumeric"] + "font-variant-position"?: CSSStylesCamel["fontVariantPosition"] + "font-variation-settings"?: CSSStylesCamel["fontVariationSettings"] + "font-weight"?: CSSStylesCamel["fontWeight"] + "gap"?: CSSStylesCamel["gap"] + "grid"?: CSSStylesCamel["grid"] + "grid-area"?: CSSStylesCamel["gridArea"] + "grid-auto-columns"?: CSSStylesCamel["gridAutoColumns"] + "grid-auto-flow"?: CSSStylesCamel["gridAutoFlow"] + "grid-auto-rows"?: CSSStylesCamel["gridAutoRows"] + "grid-column"?: CSSStylesCamel["gridColumn"] + "grid-column-end"?: CSSStylesCamel["gridColumnEnd"] /** @deprecated This is a legacy alias of `column-gap`. */ - "grid-column-gap"?: string | null - "grid-column-start"?: string | null + "grid-column-gap"?: CSSStylesCamel["gridColumnGap"] + "grid-column-start"?: CSSStylesCamel["gridColumnStart"] /** @deprecated This is a legacy alias of `gap`. */ - "grid-gap"?: string | null - "grid-row"?: string | null - "grid-row-end"?: string | null + "grid-gap"?: CSSStylesCamel["gridGap"] + "grid-row"?: CSSStylesCamel["gridRow"] + "grid-row-end"?: CSSStylesCamel["gridRowEnd"] /** @deprecated This is a legacy alias of `row-gap`. */ - "grid-row-gap"?: string | null - "grid-row-start"?: string | null - "grid-template"?: string | null - "grid-template-areas"?: string | null - "grid-template-columns"?: string | null - "grid-template-rows"?: string | null - "height"?: string | null - "hyphens"?: string | null + "grid-row-gap"?: CSSStylesCamel["gridRowGap"] + "grid-row-start"?: CSSStylesCamel["gridRowStart"] + "grid-template"?: CSSStylesCamel["gridTemplate"] + "grid-template-areas"?: CSSStylesCamel["gridTemplateAreas"] + "grid-template-columns"?: CSSStylesCamel["gridTemplateColumns"] + "grid-template-rows"?: CSSStylesCamel["gridTemplateRows"] + "height"?: CSSStylesCamel["height"] + "hyphens"?: CSSStylesCamel["hyphens"] /** @deprecated */ - "image-orientation"?: string | null - "image-rendering"?: string | null - "inline-size"?: string | null - "inset"?: string | null - "inset-block"?: string | null - "inset-block-end"?: string | null - "inset-block-start"?: string | null - "inset-inline"?: string | null - "inset-inline-end"?: string | null - "inset-inline-start"?: string | null - "isolation"?: string | null - "justify-content"?: string | null - "justify-items"?: string | null - "justify-self"?: string | null - "left"?: string | null - "letter-spacing"?: string | null - "lighting-color"?: string | null - "line-break"?: string | null - "line-height"?: string | null - "list-style"?: string | null - "list-style-image"?: string | null - "list-style-position"?: string | null - "list-style-type"?: string | null - "margin"?: string | null - "margin-block"?: string | null - "margin-block-end"?: string | null - "margin-block-start"?: string | null - "margin-bottom"?: string | null - "margin-inline"?: string | null - "margin-inline-end"?: string | null - "margin-inline-start"?: string | null - "margin-left"?: string | null - "margin-right"?: string | null - "margin-top"?: string | null - "marker"?: string | null - "marker-end"?: string | null - "marker-mid"?: string | null - "marker-start"?: string | null - "mask"?: string | null - "mask-type"?: string | null - "max-block-size"?: string | null - "max-height"?: string | null - "max-inline-size"?: string | null - "max-width"?: string | null - "min-block-size"?: string | null - "min-height"?: string | null - "min-inline-size"?: string | null - "min-width"?: string | null - "mix-blend-mode"?: string | null - "object-fit"?: string | null - "object-position"?: string | null - "offset"?: string | null - "offset-anchor"?: string | null - "offset-distance"?: string | null - "offset-path"?: string | null - "offset-rotate"?: string | null - "opacity"?: string | null - "order"?: string | null - "orphans"?: string | null - "outline"?: string | null - "outline-color"?: string | null - "outline-offset"?: string | null - "outline-style"?: string | null - "outline-width"?: string | null - "overflow"?: string | null - "overflow-anchor"?: string | null - "overflow-wrap"?: string | null - "overflow-x"?: string | null - "overflow-y"?: string | null - "overscroll-behavior"?: string | null - "overscroll-behavior-block"?: string | null - "overscroll-behavior-inline"?: string | null - "overscroll-behavior-x"?: string | null - "overscroll-behavior-y"?: string | null - "padding"?: string | null - "padding-block"?: string | null - "padding-block-end"?: string | null - "padding-block-start"?: string | null - "padding-bottom"?: string | null - "padding-inline"?: string | null - "padding-inline-end"?: string | null - "padding-inline-start"?: string | null - "padding-left"?: string | null - "padding-right"?: string | null - "padding-top"?: string | null - "page-break-after"?: string | null - "page-break-before"?: string | null - "page-break-inside"?: string | null - "paint-order"?: string | null - "perspective"?: string | null - "perspective-origin"?: string | null - "place-content"?: string | null - "place-items"?: string | null - "place-self"?: string | null - "pointer-events"?: string | null - "position"?: Position | null - "quotes"?: string | null - "resize"?: string | null - "right"?: string | null - "rotate"?: string | null - "row-gap"?: string | null - "ruby-position"?: string | null - "scale"?: string | null - "scroll-behavior"?: string | null - "scroll-margin"?: string | null - "scroll-margin-block"?: string | null - "scroll-margin-block-end"?: string | null - "scroll-margin-block-start"?: string | null - "scroll-margin-bottom"?: string | null - "scroll-margin-inline"?: string | null - "scroll-margin-inline-end"?: string | null - "scroll-margin-inline-start"?: string | null - "scroll-margin-left"?: string | null - "scroll-margin-right"?: string | null - "scroll-margin-top"?: string | null - "scroll-padding"?: string | null - "scroll-padding-block"?: string | null - "scroll-padding-block-end"?: string | null - "scroll-padding-block-start"?: string | null - "scroll-padding-bottom"?: string | null - "scroll-padding-inline"?: string | null - "scroll-padding-inline-end"?: string | null - "scroll-padding-inline-start"?: string | null - "scroll-padding-left"?: string | null - "scroll-padding-right"?: string | null - "scroll-padding-top"?: string | null - "scroll-snap-align"?: string | null - "scroll-snap-stop"?: string | null - "scroll-snap-type"?: string | null - "scrollbar-gutter"?: string | null - "shape-image-threshold"?: string | null - "shape-margin"?: string | null - "shape-outside"?: string | null - "shape-rendering"?: string | null - "stop-color"?: string | null - "stop-opacity"?: string | null - "stroke"?: string | null - "stroke-dasharray"?: string | null - "stroke-dashoffset"?: string | null - "stroke-linecap"?: string | null - "stroke-linejoin"?: string | null - "stroke-miterlimit"?: string | null - "stroke-opacity"?: string | null - "stroke-width"?: string | null - "tab-size"?: string | null - "table-layout"?: string | null - "text-align"?: string | null - "text-align-last"?: string | null - "text-anchor"?: string | null - "text-combine-upright"?: string | null - "text-decoration"?: string | null - "text-decoration-color"?: string | null - "text-decoration-line"?: string | null - "text-decoration-skip-ink"?: string | null - "text-decoration-style"?: string | null - "text-decoration-thickness"?: string | null - "text-emphasis"?: string | null - "text-emphasis-color"?: string | null - "text-emphasis-position"?: string | null - "text-emphasis-style"?: string | null - "text-indent"?: string | null - "text-orientation"?: string | null - "text-overflow"?: string | null - "text-rendering"?: string | null - "text-shadow"?: string | null - "text-transform"?: string | null - "text-underline-offset"?: string | null - "text-underline-position"?: string | null - "top"?: string | null - "touch-action"?: string | null - "transform"?: string | null - "transform-box"?: string | null - "transform-origin"?: string | null - "transform-style"?: string | null - "transition"?: string | null - "transition-delay"?: string | null - "transition-duration"?: string | null - "transition-property"?: string | null - "transition-timing-function"?: string | null - "translate"?: string | null - "unicode-bidi"?: string | null - "user-select"?: string | null - "vertical-align"?: string | null - "visibility"?: string | null - "white-space"?: string | null - "widows"?: string | null - "width"?: string | null - "will-change"?: string | null - "word-break"?: string | null - "word-spacing"?: string | null + "image-orientation"?: CSSStylesCamel["imageOrientation"] + "image-rendering"?: CSSStylesCamel["imageRendering"] + "inline-size"?: CSSStylesCamel["inlineSize"] + "inset"?: CSSStylesCamel["inset"] + "inset-block"?: CSSStylesCamel["insetBlock"] + "inset-block-end"?: CSSStylesCamel["insetBlockEnd"] + "inset-block-start"?: CSSStylesCamel["insetBlockStart"] + "inset-inline"?: CSSStylesCamel["insetInline"] + "inset-inline-end"?: CSSStylesCamel["insetInlineEnd"] + "inset-inline-start"?: CSSStylesCamel["insetInlineStart"] + "isolation"?: CSSStylesCamel["isolation"] + "justify-content"?: CSSStylesCamel["justifyContent"] + "justify-items"?: CSSStylesCamel["justifyItems"] + "justify-self"?: CSSStylesCamel["justifySelf"] + "left"?: CSSStylesCamel["left"] + "letter-spacing"?: CSSStylesCamel["letterSpacing"] + "lighting-color"?: CSSStylesCamel["lightingColor"] + "line-break"?: CSSStylesCamel["lineBreak"] + "line-height"?: CSSStylesCamel["lineHeight"] + "list-style"?: CSSStylesCamel["listStyle"] + "list-style-image"?: CSSStylesCamel["listStyleImage"] + "list-style-position"?: CSSStylesCamel["listStylePosition"] + "list-style-type"?: CSSStylesCamel["listStyleType"] + "margin"?: CSSStylesCamel["margin"] + "margin-block"?: CSSStylesCamel["marginBlock"] + "margin-block-end"?: CSSStylesCamel["marginBlockEnd"] + "margin-block-start"?: CSSStylesCamel["marginBlockStart"] + "margin-bottom"?: CSSStylesCamel["marginBottom"] + "margin-inline"?: CSSStylesCamel["marginInline"] + "margin-inline-end"?: CSSStylesCamel["marginInlineEnd"] + "margin-inline-start"?: CSSStylesCamel["marginInlineStart"] + "margin-left"?: CSSStylesCamel["marginLeft"] + "margin-right"?: CSSStylesCamel["marginRight"] + "margin-top"?: CSSStylesCamel["marginTop"] + "marker"?: CSSStylesCamel["marker"] + "marker-end"?: CSSStylesCamel["markerEnd"] + "marker-mid"?: CSSStylesCamel["markerMid"] + "marker-start"?: CSSStylesCamel["markerStart"] + "mask"?: CSSStylesCamel["mask"] + "mask-type"?: CSSStylesCamel["maskType"] + "max-block-size"?: CSSStylesCamel["maxBlockSize"] + "max-height"?: CSSStylesCamel["maxHeight"] + "max-inline-size"?: CSSStylesCamel["maxInlineSize"] + "max-width"?: CSSStylesCamel["maxWidth"] + "min-block-size"?: CSSStylesCamel["minBlockSize"] + "min-height"?: CSSStylesCamel["minHeight"] + "min-inline-size"?: CSSStylesCamel["minInlineSize"] + "min-width"?: CSSStylesCamel["minWidth"] + "mix-blend-mode"?: CSSStylesCamel["mixBlendMode"] + "object-fit"?: CSSStylesCamel["objectFit"] + "object-position"?: CSSStylesCamel["objectPosition"] + "offset"?: CSSStylesCamel["offset"] + "offset-anchor"?: CSSStylesCamel["offsetAnchor"] + "offset-distance"?: CSSStylesCamel["offsetDistance"] + "offset-path"?: CSSStylesCamel["offsetPath"] + "offset-rotate"?: CSSStylesCamel["offsetRotate"] + "opacity"?: CSSStylesCamel["opacity"] + "order"?: CSSStylesCamel["order"] + "orphans"?: CSSStylesCamel["orphans"] + "outline"?: CSSStylesCamel["outline"] + "outline-color"?: CSSStylesCamel["outlineColor"] + "outline-offset"?: CSSStylesCamel["outlineOffset"] + "outline-style"?: CSSStylesCamel["outlineStyle"] + "outline-width"?: CSSStylesCamel["outlineWidth"] + "overflow"?: CSSStylesCamel["overflow"] + "overflow-anchor"?: CSSStylesCamel["overflowAnchor"] + "overflow-wrap"?: CSSStylesCamel["overflowWrap"] + "overflow-x"?: CSSStylesCamel["overflowX"] + "overflow-y"?: CSSStylesCamel["overflowY"] + "overscroll-behavior"?: CSSStylesCamel["overscrollBehavior"] + "overscroll-behavior-block"?: CSSStylesCamel["overscrollBehaviorBlock"] + "overscroll-behavior-inline"?: CSSStylesCamel["overscrollBehaviorInline"] + "overscroll-behavior-x"?: CSSStylesCamel["overscrollBehaviorX"] + "overscroll-behavior-y"?: CSSStylesCamel["overscrollBehaviorY"] + "padding"?: CSSStylesCamel["padding"] + "padding-block"?: CSSStylesCamel["paddingBlock"] + "padding-block-end"?: CSSStylesCamel["paddingBlockEnd"] + "padding-block-start"?: CSSStylesCamel["paddingBlockStart"] + "padding-bottom"?: CSSStylesCamel["paddingBottom"] + "padding-inline"?: CSSStylesCamel["paddingInline"] + "padding-inline-end"?: CSSStylesCamel["paddingInlineEnd"] + "padding-inline-start"?: CSSStylesCamel["paddingInlineStart"] + "padding-left"?: CSSStylesCamel["paddingLeft"] + "padding-right"?: CSSStylesCamel["paddingRight"] + "padding-top"?: CSSStylesCamel["paddingTop"] + "page-break-after"?: CSSStylesCamel["pageBreakAfter"] + "page-break-before"?: CSSStylesCamel["pageBreakBefore"] + "page-break-inside"?: CSSStylesCamel["pageBreakInside"] + "paint-order"?: CSSStylesCamel["paintOrder"] + "perspective"?: CSSStylesCamel["perspective"] + "perspective-origin"?: CSSStylesCamel["perspectiveOrigin"] + "place-content"?: CSSStylesCamel["placeContent"] + "place-items"?: CSSStylesCamel["placeItems"] + "place-self"?: CSSStylesCamel["placeSelf"] + "pointer-events"?: CSSStylesCamel["pointerEvents"] + "position"?: CSSStylesCamel["position"] + "quotes"?: CSSStylesCamel["quotes"] + "resize"?: CSSStylesCamel["resize"] + "right"?: CSSStylesCamel["right"] + "rotate"?: CSSStylesCamel["rotate"] + "row-gap"?: CSSStylesCamel["rowGap"] + "ruby-position"?: CSSStylesCamel["rubyPosition"] + "scale"?: CSSStylesCamel["scale"] + "scroll-behavior"?: CSSStylesCamel["scrollBehavior"] + "scroll-margin"?: CSSStylesCamel["scrollMargin"] + "scroll-margin-block"?: CSSStylesCamel["scrollMarginBlock"] + "scroll-margin-block-end"?: CSSStylesCamel["scrollMarginBlockEnd"] + "scroll-margin-block-start"?: CSSStylesCamel["scrollMarginBlockStart"] + "scroll-margin-bottom"?: CSSStylesCamel["scrollMarginBottom"] + "scroll-margin-inline"?: CSSStylesCamel["scrollMarginInline"] + "scroll-margin-inline-end"?: CSSStylesCamel["scrollMarginInlineEnd"] + "scroll-margin-inline-start"?: CSSStylesCamel["scrollMarginInlineStart"] + "scroll-margin-left"?: CSSStylesCamel["scrollMarginLeft"] + "scroll-margin-right"?: CSSStylesCamel["scrollMarginRight"] + "scroll-margin-top"?: CSSStylesCamel["scrollMarginTop"] + "scroll-padding"?: CSSStylesCamel["scrollPadding"] + "scroll-padding-block"?: CSSStylesCamel["scrollPaddingBlock"] + "scroll-padding-block-end"?: CSSStylesCamel["scrollPaddingBlockEnd"] + "scroll-padding-block-start"?: CSSStylesCamel["scrollPaddingBlockStart"] + "scroll-padding-bottom"?: CSSStylesCamel["scrollPaddingBottom"] + "scroll-padding-inline"?: CSSStylesCamel["scrollPaddingInline"] + "scroll-padding-inline-end"?: CSSStylesCamel["scrollPaddingInlineEnd"] + "scroll-padding-inline-start"?: CSSStylesCamel["scrollPaddingInlineStart"] + "scroll-padding-left"?: CSSStylesCamel["scrollPaddingLeft"] + "scroll-padding-right"?: CSSStylesCamel["scrollPaddingRight"] + "scroll-padding-top"?: CSSStylesCamel["scrollPaddingTop"] + "scroll-snap-align"?: CSSStylesCamel["scrollSnapAlign"] + "scroll-snap-stop"?: CSSStylesCamel["scrollSnapStop"] + "scroll-snap-type"?: CSSStylesCamel["scrollSnapType"] + "scrollbar-gutter"?: CSSStylesCamel["scrollbarGutter"] + "shape-image-threshold"?: CSSStylesCamel["shapeImageThreshold"] + "shape-margin"?: CSSStylesCamel["shapeMargin"] + "shape-outside"?: CSSStylesCamel["shapeOutside"] + "shape-rendering"?: CSSStylesCamel["shapeRendering"] + "stop-color"?: CSSStylesCamel["stopColor"] + "stop-opacity"?: CSSStylesCamel["stopOpacity"] + "stroke"?: CSSStylesCamel["stroke"] + "stroke-dasharray"?: CSSStylesCamel["strokeDasharray"] + "stroke-dashoffset"?: CSSStylesCamel["strokeDashoffset"] + "stroke-linecap"?: CSSStylesCamel["strokeLinecap"] + "stroke-linejoin"?: CSSStylesCamel["strokeLinejoin"] + "stroke-miterlimit"?: CSSStylesCamel["strokeMiterlimit"] + "stroke-opacity"?: CSSStylesCamel["strokeOpacity"] + "stroke-width"?: CSSStylesCamel["strokeWidth"] + "tab-size"?: CSSStylesCamel["tabSize"] + "table-layout"?: CSSStylesCamel["tableLayout"] + "text-align"?: CSSStylesCamel["textAlign"] + "text-align-last"?: CSSStylesCamel["textAlignLast"] + "text-anchor"?: CSSStylesCamel["textAnchor"] + "text-combine-upright"?: CSSStylesCamel["textCombineUpright"] + "text-decoration"?: CSSStylesCamel["textDecoration"] + "text-decoration-color"?: CSSStylesCamel["textDecorationColor"] + "text-decoration-line"?: CSSStylesCamel["textDecorationLine"] + "text-decoration-skip-ink"?: CSSStylesCamel["textDecorationSkipInk"] + "text-decoration-style"?: CSSStylesCamel["textDecorationStyle"] + "text-decoration-thickness"?: CSSStylesCamel["textDecorationThickness"] + "text-emphasis"?: CSSStylesCamel["textEmphasis"] + "text-emphasis-color"?: CSSStylesCamel["textEmphasisColor"] + "text-emphasis-position"?: CSSStylesCamel["textEmphasisPosition"] + "text-emphasis-style"?: CSSStylesCamel["textEmphasisStyle"] + "text-indent"?: CSSStylesCamel["textIndent"] + "text-orientation"?: CSSStylesCamel["textOrientation"] + "text-overflow"?: CSSStylesCamel["textOverflow"] + "text-rendering"?: CSSStylesCamel["textRendering"] + "text-shadow"?: CSSStylesCamel["textShadow"] + "text-transform"?: CSSStylesCamel["textTransform"] + "text-underline-offset"?: CSSStylesCamel["textUnderlineOffset"] + "text-underline-position"?: CSSStylesCamel["textUnderlinePosition"] + "top"?: CSSStylesCamel["top"] + "touch-action"?: CSSStylesCamel["touchAction"] + "transform"?: CSSStylesCamel["transform"] + "transform-box"?: CSSStylesCamel["transformBox"] + "transform-origin"?: CSSStylesCamel["transformOrigin"] + "transform-style"?: CSSStylesCamel["transformStyle"] + "transition"?: CSSStylesCamel["transition"] + "transition-delay"?: CSSStylesCamel["transitionDelay"] + "transition-duration"?: CSSStylesCamel["transitionDuration"] + "transition-property"?: CSSStylesCamel["transitionProperty"] + "transition-timing-function"?: CSSStylesCamel["transitionTimingFunction"] + "translate"?: CSSStylesCamel["translate"] + "unicode-bidi"?: CSSStylesCamel["unicodeBidi"] + "user-select"?: CSSStylesCamel["userSelect"] + "vertical-align"?: CSSStylesCamel["verticalAlign"] + "visibility"?: CSSStylesCamel["visibility"] + "white-space"?: CSSStylesCamel["whiteSpace"] + "widows"?: CSSStylesCamel["widows"] + "width"?: CSSStylesCamel["width"] + "will-change"?: CSSStylesCamel["willChange"] + "word-break"?: CSSStylesCamel["wordBreak"] + "word-spacing"?: CSSStylesCamel["wordSpacing"] /** @deprecated */ - "word-wrap"?: string | null - "writing-mode"?: string | null - "z-index"?: string | null + "word-wrap"?: CSSStylesCamel["wordWrap"] + "writing-mode"?: CSSStylesCamel["writingMode"] + "z-index"?: CSSStylesCamel["zIndex"] } type CSSStylesSnake = { - accent_color?: string | null - align_content?: string | null - align_items?: string | null - align_self?: string | null - alignment_baseline?: string | null - all?: string | null - animation?: string | null - animation_delay?: string | null - animation_direction?: string | null - animation_duration?: string | null - animation_fill_mode?: string | null - animation_iteration_count?: string | null - animation_name?: string | null - animation_play_state?: string | null - animation_timing_function?: string | null - appearance?: string | null - aspect_ratio?: string | null - backface_visibility?: string | null - background?: string | null - background_attachment?: string | null - background_blend_mode?: string | null - background_clip?: string | null - background_color?: string | null - background_image?: string | null - background_origin?: string | null - background_position?: string | null - background_position_x?: string | null - background_position_y?: string | null - background_repeat?: string | null - background_size?: string | null - baseline_shift?: string | null - block_size?: string | null - border?: string | null - border_block?: string | null - border_block_color?: string | null - border_block_end?: string | null - border_block_end_color?: string | null - border_block_end_style?: string | null - border_block_end_width?: string | null - border_block_start?: string | null - border_block_start_color?: string | null - border_block_start_style?: string | null - border_block_start_width?: string | null - border_block_style?: string | null - border_block_width?: string | null - border_bottom?: string | null - border_bottom_color?: string | null - border_bottom_left_radius?: string | null - border_bottom_right_radius?: string | null - border_bottom_style?: string | null - border_bottom_width?: string | null - border_collapse?: string | null - border_color?: string | null - border_end_end_radius?: string | null - border_end_start_radius?: string | null - border_image?: string | null - border_image_outset?: string | null - border_image_repeat?: string | null - border_image_slice?: string | null - border_image_source?: string | null - border_image_width?: string | null - border_inline?: string | null - border_inline_color?: string | null - border_inline_end?: string | null - border_inline_end_color?: string | null - border_inline_end_style?: string | null - border_inline_end_width?: string | null - border_inline_start?: string | null - border_inline_start_color?: string | null - border_inline_start_style?: string | null - border_inline_start_width?: string | null - border_inline_style?: string | null - border_inline_width?: string | null - border_left?: string | null - border_left_color?: string | null - border_left_style?: string | null - border_left_width?: string | null - border_radius?: string | null - border_right?: string | null - border_right_color?: string | null - border_right_style?: string | null - border_right_width?: string | null - border_spacing?: string | null - border_start_end_radius?: string | null - border_start_start_radius?: string | null - border_style?: string | null - border_top?: string | null - border_top_color?: string | null - border_top_left_radius?: string | null - border_top_right_radius?: string | null - border_top_style?: string | null - border_top_width?: string | null - border_width?: string | null - bottom?: string | null - box_shadow?: string | null - box_sizing?: string | null - break_after?: string | null - break_before?: string | null - break_inside?: string | null - caption_side?: string | null - caret_color?: string | null - clear?: string | null + accent_color?: CSSStylesCamel["accentColor"] + align_content?: CSSStylesCamel["alignContent"] + align_items?: CSSStylesCamel["alignItems"] + align_self?: CSSStylesCamel["alignSelf"] + alignment_baseline?: CSSStylesCamel["alignmentBaseline"] + all?: CSSStylesCamel["all"] + animation?: CSSStylesCamel["animation"] + animation_delay?: CSSStylesCamel["animationDelay"] + animation_direction?: CSSStylesCamel["animationDirection"] + animation_duration?: CSSStylesCamel["animationDuration"] + animation_fill_mode?: CSSStylesCamel["animationFillMode"] + animation_iteration_count?: CSSStylesCamel["animationIterationCount"] + animation_name?: CSSStylesCamel["animationName"] + animation_play_state?: CSSStylesCamel["animationPlayState"] + animation_timing_function?: CSSStylesCamel["animationTimingFunction"] + appearance?: CSSStylesCamel["appearance"] + aspect_ratio?: CSSStylesCamel["aspectRatio"] + backface_visibility?: CSSStylesCamel["backfaceVisibility"] + background?: CSSStylesCamel["background"] + background_attachment?: CSSStylesCamel["backgroundAttachment"] + background_blend_mode?: CSSStylesCamel["backgroundBlendMode"] + background_clip?: CSSStylesCamel["backgroundClip"] + background_color?: CSSStylesCamel["backgroundColor"] + background_image?: CSSStylesCamel["backgroundImage"] + background_origin?: CSSStylesCamel["backgroundOrigin"] + background_position?: CSSStylesCamel["backgroundPosition"] + background_position_x?: CSSStylesCamel["backgroundPositionX"] + background_position_y?: CSSStylesCamel["backgroundPositionY"] + background_repeat?: CSSStylesCamel["backgroundRepeat"] + background_size?: CSSStylesCamel["backgroundSize"] + baseline_shift?: CSSStylesCamel["baselineShift"] + block_size?: CSSStylesCamel["blockSize"] + border?: CSSStylesCamel["border"] + border_block?: CSSStylesCamel["borderBlock"] + border_block_color?: CSSStylesCamel["borderBlockColor"] + border_block_end?: CSSStylesCamel["borderBlockEnd"] + border_block_end_color?: CSSStylesCamel["borderBlockEndColor"] + border_block_end_style?: CSSStylesCamel["borderBlockEndStyle"] + border_block_end_width?: CSSStylesCamel["borderBlockEndWidth"] + border_block_start?: CSSStylesCamel["borderBlockStart"] + border_block_start_color?: CSSStylesCamel["borderBlockStartColor"] + border_block_start_style?: CSSStylesCamel["borderBlockStartStyle"] + border_block_start_width?: CSSStylesCamel["borderBlockStartWidth"] + border_block_style?: CSSStylesCamel["borderBlockStyle"] + border_block_width?: CSSStylesCamel["borderBlockWidth"] + border_bottom?: CSSStylesCamel["borderBottom"] + border_bottom_color?: CSSStylesCamel["borderBottomColor"] + border_bottom_left_radius?: CSSStylesCamel["borderBottomLeftRadius"] + border_bottom_right_radius?: CSSStylesCamel["borderBottomRightRadius"] + border_bottom_style?: CSSStylesCamel["borderBottomStyle"] + border_bottom_width?: CSSStylesCamel["borderBottomWidth"] + border_collapse?: CSSStylesCamel["borderCollapse"] + border_color?: CSSStylesCamel["borderColor"] + border_end_end_radius?: CSSStylesCamel["borderEndEndRadius"] + border_end_start_radius?: CSSStylesCamel["borderEndStartRadius"] + border_image?: CSSStylesCamel["borderImage"] + border_image_outset?: CSSStylesCamel["borderImageOutset"] + border_image_repeat?: CSSStylesCamel["borderImageRepeat"] + border_image_slice?: CSSStylesCamel["borderImageSlice"] + border_image_source?: CSSStylesCamel["borderImageSource"] + border_image_width?: CSSStylesCamel["borderImageWidth"] + border_inline?: CSSStylesCamel["borderInline"] + border_inline_color?: CSSStylesCamel["borderInlineColor"] + border_inline_end?: CSSStylesCamel["borderInlineEnd"] + border_inline_end_color?: CSSStylesCamel["borderInlineEndColor"] + border_inline_end_style?: CSSStylesCamel["borderInlineEndStyle"] + border_inline_end_width?: CSSStylesCamel["borderInlineEndWidth"] + border_inline_start?: CSSStylesCamel["borderInlineStart"] + border_inline_start_color?: CSSStylesCamel["borderInlineStartColor"] + border_inline_start_style?: CSSStylesCamel["borderInlineStartStyle"] + border_inline_start_width?: CSSStylesCamel["borderInlineStartWidth"] + border_inline_style?: CSSStylesCamel["borderInlineStyle"] + border_inline_width?: CSSStylesCamel["borderInlineWidth"] + border_left?: CSSStylesCamel["borderLeft"] + border_left_color?: CSSStylesCamel["borderLeftColor"] + border_left_style?: CSSStylesCamel["borderLeftStyle"] + border_left_width?: CSSStylesCamel["borderLeftWidth"] + border_radius?: CSSStylesCamel["borderRadius"] + border_right?: CSSStylesCamel["borderRight"] + border_right_color?: CSSStylesCamel["borderRightColor"] + border_right_style?: CSSStylesCamel["borderRightStyle"] + border_right_width?: CSSStylesCamel["borderRightWidth"] + border_spacing?: CSSStylesCamel["borderSpacing"] + border_start_end_radius?: CSSStylesCamel["borderStartEndRadius"] + border_start_start_radius?: CSSStylesCamel["borderStartStartRadius"] + border_style?: CSSStylesCamel["borderStyle"] + border_top?: CSSStylesCamel["borderTop"] + border_top_color?: CSSStylesCamel["borderTopColor"] + border_top_left_radius?: CSSStylesCamel["borderTopLeftRadius"] + border_top_right_radius?: CSSStylesCamel["borderTopRightRadius"] + border_top_style?: CSSStylesCamel["borderTopStyle"] + border_top_width?: CSSStylesCamel["borderTopWidth"] + border_width?: CSSStylesCamel["borderWidth"] + bottom?: CSSStylesCamel["bottom"] + box_shadow?: CSSStylesCamel["boxShadow"] + box_sizing?: CSSStylesCamel["boxSizing"] + break_after?: CSSStylesCamel["breakAfter"] + break_before?: CSSStylesCamel["breakBefore"] + break_inside?: CSSStylesCamel["breakInside"] + caption_side?: CSSStylesCamel["captionSide"] + caret_color?: CSSStylesCamel["caretColor"] + clear?: CSSStylesCamel["clear"] /** @deprecated */ - clip?: string | null - clip_path?: string | null - clip_rule?: string | null - color?: string | null - color_interpolation?: string | null - color_interpolation_filters?: string | null - color_scheme?: string | null - column_count?: string | null - column_fill?: string | null - column_gap?: string | null - column_rule?: string | null - column_rule_color?: string | null - column_rule_style?: string | null - column_rule_width?: string | null - column_span?: string | null - column_width?: string | null - columns?: string | null - contain?: string | null - content?: string | null - counter_increment?: string | null - counter_reset?: string | null - counter_set?: string | null - cursor?: string | null - direction?: string | null - display?: Display | null - dominant_baseline?: string | null - empty_cells?: string | null - fill?: string | null - fill_opacity?: string | null - fill_rule?: string | null - filter?: string | null - flex?: string | null - flex_basis?: string | null - flex_direction?: FlexDirection | null - flex_flow?: string | null - flex_grow?: string | null - flex_shrink?: string | null - flex_wrap?: string | null - float?: string | null - flood_color?: string | null - flood_opacity?: string | null - font?: string | null - font_family?: string | null - font_feature_settings?: string | null - font_kerning?: string | null - font_optical_sizing?: string | null - font_size?: string | null - font_size_adjust?: string | null - font_stretch?: string | null - font_style?: string | null - font_synthesis?: string | null - font_variant?: string | null + clip?: CSSStylesCamel["clip"] + clip_path?: CSSStylesCamel["clipPath"] + clip_rule?: CSSStylesCamel["clipRule"] + color?: CSSStylesCamel["color"] + color_interpolation?: CSSStylesCamel["colorInterpolation"] + color_interpolation_filters?: CSSStylesCamel["colorInterpolationFilters"] + color_scheme?: CSSStylesCamel["colorScheme"] + column_count?: CSSStylesCamel["columnCount"] + column_fill?: CSSStylesCamel["columnFill"] + column_gap?: CSSStylesCamel["columnGap"] + column_rule?: CSSStylesCamel["columnRule"] + column_rule_color?: CSSStylesCamel["columnRuleColor"] + column_rule_style?: CSSStylesCamel["columnRuleStyle"] + column_rule_width?: CSSStylesCamel["columnRuleWidth"] + column_span?: CSSStylesCamel["columnSpan"] + column_width?: CSSStylesCamel["columnWidth"] + columns?: CSSStylesCamel["columns"] + contain?: CSSStylesCamel["contain"] + content?: CSSStylesCamel["content"] + counter_increment?: CSSStylesCamel["counterIncrement"] + counter_reset?: CSSStylesCamel["counterReset"] + counter_set?: CSSStylesCamel["counterSet"] + cursor?: CSSStylesCamel["cursor"] + direction?: CSSStylesCamel["direction"] + display?: CSSStylesCamel["display"] + dominant_baseline?: CSSStylesCamel["dominantBaseline"] + empty_cells?: CSSStylesCamel["emptyCells"] + fill?: CSSStylesCamel["fill"] + fill_opacity?: CSSStylesCamel["fillOpacity"] + fill_rule?: CSSStylesCamel["fillRule"] + filter?: CSSStylesCamel["filter"] + flex?: CSSStylesCamel["flex"] + flex_basis?: CSSStylesCamel["flexBasis"] + flex_direction?: CSSStylesCamel["flexDirection"] + flex_flow?: CSSStylesCamel["flexFlow"] + flex_grow?: CSSStylesCamel["flexGrow"] + flex_shrink?: CSSStylesCamel["flexShrink"] + flex_wrap?: CSSStylesCamel["flexWrap"] + float?: CSSStylesCamel["float"] + flood_color?: CSSStylesCamel["floodColor"] + flood_opacity?: CSSStylesCamel["floodOpacity"] + font?: CSSStylesCamel["font"] + font_family?: CSSStylesCamel["fontFamily"] + font_feature_settings?: CSSStylesCamel["fontFeatureSettings"] + font_kerning?: CSSStylesCamel["fontKerning"] + font_optical_sizing?: CSSStylesCamel["fontOpticalSizing"] + font_size?: CSSStylesCamel["fontSize"] + font_size_adjust?: CSSStylesCamel["fontSizeAdjust"] + font_stretch?: CSSStylesCamel["fontStretch"] + font_style?: CSSStylesCamel["fontStyle"] + font_synthesis?: CSSStylesCamel["fontSynthesis"] + font_variant?: CSSStylesCamel["fontVariant"] /** @deprecated */ - font_variant_alternates?: string | null - font_variant_caps?: string | null - font_variant_east_asian?: string | null - font_variant_ligatures?: string | null - font_variant_numeric?: string | null - font_variant_position?: string | null - font_variation_settings?: string | null - font_weight?: string | null - gap?: string | null - grid?: string | null - grid_area?: string | null - grid_auto_columns?: string | null - grid_auto_flow?: string | null - grid_auto_rows?: string | null - grid_column?: string | null - grid_column_end?: string | null + font_variant_alternates?: CSSStylesCamel["fontVariantAlternates"] + font_variant_caps?: CSSStylesCamel["fontVariantCaps"] + font_variant_east_asian?: CSSStylesCamel["fontVariantEastAsian"] + font_variant_ligatures?: CSSStylesCamel["fontVariantLigatures"] + font_variant_numeric?: CSSStylesCamel["fontVariantNumeric"] + font_variant_position?: CSSStylesCamel["fontVariantPosition"] + font_variation_settings?: CSSStylesCamel["fontVariationSettings"] + font_weight?: CSSStylesCamel["fontWeight"] + gap?: CSSStylesCamel["gap"] + grid?: CSSStylesCamel["grid"] + grid_area?: CSSStylesCamel["gridArea"] + grid_auto_columns?: CSSStylesCamel["gridAutoColumns"] + grid_auto_flow?: CSSStylesCamel["gridAutoFlow"] + grid_auto_rows?: CSSStylesCamel["gridAutoRows"] + grid_column?: CSSStylesCamel["gridColumn"] + grid_column_end?: CSSStylesCamel["gridColumnEnd"] /** @deprecated This is a legacy alias of `column_gap`. */ - grid_column_gap?: string | null - grid_column_start?: string | null + grid_column_gap?: CSSStylesCamel["gridColumnGap"] + grid_column_start?: CSSStylesCamel["gridColumnStart"] /** @deprecated This is a legacy alias of `gap`. */ - grid_gap?: string | null - grid_row?: string | null - grid_row_end?: string | null + grid_gap?: CSSStylesCamel["gridGap"] + grid_row?: CSSStylesCamel["gridRow"] + grid_row_end?: CSSStylesCamel["gridRowEnd"] /** @deprecated This is a legacy alias of `row_gap`. */ - grid_row_gap?: string | null - grid_row_start?: string | null - grid_template?: string | null - grid_template_areas?: string | null - grid_template_columns?: string | null - grid_template_rows?: string | null - height?: string | null - hyphens?: string | null + grid_row_gap?: CSSStylesCamel["gridRowGap"] + grid_row_start?: CSSStylesCamel["gridRowStart"] + grid_template?: CSSStylesCamel["gridTemplate"] + grid_template_areas?: CSSStylesCamel["gridTemplateAreas"] + grid_template_columns?: CSSStylesCamel["gridTemplateColumns"] + grid_template_rows?: CSSStylesCamel["gridTemplateRows"] + height?: CSSStylesCamel["height"] + hyphens?: CSSStylesCamel["hyphens"] /** @deprecated */ - image_orientation?: string | null - image_rendering?: string | null - inline_size?: string | null - inset?: string | null - inset_block?: string | null - inset_block_end?: string | null - inset_block_start?: string | null - inset_inline?: string | null - inset_inline_end?: string | null - inset_inline_start?: string | null - isolation?: string | null - justify_content?: string | null - justify_items?: string | null - justify_self?: string | null - left?: string | null - letter_spacing?: string | null - lighting_color?: string | null - line_break?: string | null - line_height?: string | null - list_style?: string | null - list_style_image?: string | null - list_style_position?: string | null - list_style_type?: string | null - margin?: string | null - margin_block?: string | null - margin_block_end?: string | null - margin_block_start?: string | null - margin_bottom?: string | null - margin_inline?: string | null - margin_inline_end?: string | null - margin_inline_start?: string | null - margin_left?: string | null - margin_right?: string | null - margin_top?: string | null - marker?: string | null - marker_end?: string | null - marker_mid?: string | null - marker_start?: string | null - mask?: string | null - mask_type?: string | null - max_block_size?: string | null - max_height?: string | null - max_inline_size?: string | null - max_width?: string | null - min_block_size?: string | null - min_height?: string | null - min_inline_size?: string | null - min_width?: string | null - mix_blend_mode?: string | null - object_fit?: string | null - object_position?: string | null - offset?: string | null - offset_anchor?: string | null - offset_distance?: string | null - offset_path?: string | null - offset_rotate?: string | null - opacity?: string | null - order?: string | null - orphans?: string | null - outline?: string | null - outline_color?: string | null - outline_offset?: string | null - outline_style?: string | null - outline_width?: string | null - overflow?: string | null - overflow_anchor?: string | null - overflow_wrap?: string | null - overflow_x?: string | null - overflow_y?: string | null - overscroll_behavior?: string | null - overscroll_behavior_block?: string | null - overscroll_behavior_inline?: string | null - overscroll_behavior_x?: string | null - overscroll_behavior_y?: string | null - padding?: string | null - padding_block?: string | null - padding_block_end?: string | null - padding_block_start?: string | null - padding_bottom?: string | null - padding_inline?: string | null - padding_inline_end?: string | null - padding_inline_start?: string | null - padding_left?: string | null - padding_right?: string | null - padding_top?: string | null - page_break_after?: string | null - page_break_before?: string | null - page_break_inside?: string | null - paint_order?: string | null - perspective?: string | null - perspective_origin?: string | null - place_content?: string | null - place_items?: string | null - place_self?: string | null - pointer_events?: string | null - position?: Position | null - quotes?: string | null - resize?: string | null - right?: string | null - rotate?: string | null - row_gap?: string | null - ruby_position?: string | null - scale?: string | null - scroll_behavior?: string | null - scroll_margin?: string | null - scroll_margin_block?: string | null - scroll_margin_block_end?: string | null - scroll_margin_block_start?: string | null - scroll_margin_bottom?: string | null - scroll_margin_inline?: string | null - scroll_margin_inline_end?: string | null - scroll_margin_inline_start?: string | null - scroll_margin_left?: string | null - scroll_margin_right?: string | null - scroll_margin_top?: string | null - scroll_padding?: string | null - scroll_padding_block?: string | null - scroll_padding_block_end?: string | null - scroll_padding_block_start?: string | null - scroll_padding_bottom?: string | null - scroll_padding_inline?: string | null - scroll_padding_inline_end?: string | null - scroll_padding_inline_start?: string | null - scroll_padding_left?: string | null - scroll_padding_right?: string | null - scroll_padding_top?: string | null - scroll_snap_align?: string | null - scroll_snap_stop?: string | null - scroll_snap_type?: string | null - scrollbar_gutter?: string | null - shape_image_threshold?: string | null - shape_margin?: string | null - shape_outside?: string | null - shape_rendering?: string | null - stop_color?: string | null - stop_opacity?: string | null - stroke?: string | null - stroke_dasharray?: string | null - stroke_dashoffset?: string | null - stroke_linecap?: string | null - stroke_linejoin?: string | null - stroke_miterlimit?: string | null - stroke_opacity?: string | null - stroke_width?: string | null - tab_size?: string | null - table_layout?: string | null - text_align?: string | null - text_align_last?: string | null - text_anchor?: string | null - text_combine_upright?: string | null - text_decoration?: string | null - text_decoration_color?: string | null - text_decoration_line?: string | null - text_decoration_skip_ink?: string | null - text_decoration_style?: string | null - text_decoration_thickness?: string | null - text_emphasis?: string | null - text_emphasis_color?: string | null - text_emphasis_position?: string | null - text_emphasis_style?: string | null - text_indent?: string | null - text_orientation?: string | null - text_overflow?: string | null - text_rendering?: string | null - text_shadow?: string | null - text_transform?: string | null - text_underline_offset?: string | null - text_underline_position?: string | null - top?: string | null - touch_action?: string | null - transform?: string | null - transform_box?: string | null - transform_origin?: string | null - transform_style?: string | null - transition?: string | null - transition_delay?: string | null - transition_duration?: string | null - transition_property?: string | null - transition_timing_function?: string | null - translate?: string | null - unicode_bidi?: string | null - user_select?: string | null - vertical_align?: string | null - visibility?: string | null - white_space?: string | null - widows?: string | null - width?: string | null - will_change?: string | null - word_break?: string | null - word_spacing?: string | null + image_orientation?: CSSStylesCamel["imageOrientation"] + image_rendering?: CSSStylesCamel["imageRendering"] + inline_size?: CSSStylesCamel["inlineSize"] + inset?: CSSStylesCamel["inset"] + inset_block?: CSSStylesCamel["insetBlock"] + inset_block_end?: CSSStylesCamel["insetBlockEnd"] + inset_block_start?: CSSStylesCamel["insetBlockStart"] + inset_inline?: CSSStylesCamel["insetInline"] + inset_inline_end?: CSSStylesCamel["insetInlineEnd"] + inset_inline_start?: CSSStylesCamel["insetInlineStart"] + isolation?: CSSStylesCamel["isolation"] + justify_content?: CSSStylesCamel["justifyContent"] + justify_items?: CSSStylesCamel["justifyItems"] + justify_self?: CSSStylesCamel["justifySelf"] + left?: CSSStylesCamel["left"] + letter_spacing?: CSSStylesCamel["letterSpacing"] + lighting_color?: CSSStylesCamel["lightingColor"] + line_break?: CSSStylesCamel["lineBreak"] + line_height?: CSSStylesCamel["lineHeight"] + list_style?: CSSStylesCamel["listStyle"] + list_style_image?: CSSStylesCamel["listStyleImage"] + list_style_position?: CSSStylesCamel["listStylePosition"] + list_style_type?: CSSStylesCamel["listStyleType"] + margin?: CSSStylesCamel["margin"] + margin_block?: CSSStylesCamel["marginBlock"] + margin_block_end?: CSSStylesCamel["marginBlockEnd"] + margin_block_start?: CSSStylesCamel["marginBlockStart"] + margin_bottom?: CSSStylesCamel["marginBottom"] + margin_inline?: CSSStylesCamel["marginInline"] + margin_inline_end?: CSSStylesCamel["marginInlineEnd"] + margin_inline_start?: CSSStylesCamel["marginInlineStart"] + margin_left?: CSSStylesCamel["marginLeft"] + margin_right?: CSSStylesCamel["marginRight"] + margin_top?: CSSStylesCamel["marginTop"] + marker?: CSSStylesCamel["marker"] + marker_end?: CSSStylesCamel["markerEnd"] + marker_mid?: CSSStylesCamel["markerMid"] + marker_start?: CSSStylesCamel["markerStart"] + mask?: CSSStylesCamel["mask"] + mask_type?: CSSStylesCamel["maskType"] + max_block_size?: CSSStylesCamel["maxBlockSize"] + max_height?: CSSStylesCamel["maxHeight"] + max_inline_size?: CSSStylesCamel["maxInlineSize"] + max_width?: CSSStylesCamel["maxWidth"] + min_block_size?: CSSStylesCamel["minBlockSize"] + min_height?: CSSStylesCamel["minHeight"] + min_inline_size?: CSSStylesCamel["minInlineSize"] + min_width?: CSSStylesCamel["minWidth"] + mix_blend_mode?: CSSStylesCamel["mixBlendMode"] + object_fit?: CSSStylesCamel["objectFit"] + object_position?: CSSStylesCamel["objectPosition"] + offset?: CSSStylesCamel["offset"] + offset_anchor?: CSSStylesCamel["offsetAnchor"] + offset_distance?: CSSStylesCamel["offsetDistance"] + offset_path?: CSSStylesCamel["offsetPath"] + offset_rotate?: CSSStylesCamel["offsetRotate"] + opacity?: CSSStylesCamel["opacity"] + order?: CSSStylesCamel["order"] + orphans?: CSSStylesCamel["orphans"] + outline?: CSSStylesCamel["outline"] + outline_color?: CSSStylesCamel["outlineColor"] + outline_offset?: CSSStylesCamel["outlineOffset"] + outline_style?: CSSStylesCamel["outlineStyle"] + outline_width?: CSSStylesCamel["outlineWidth"] + overflow?: CSSStylesCamel["overflow"] + overflow_anchor?: CSSStylesCamel["overflowAnchor"] + overflow_wrap?: CSSStylesCamel["overflowWrap"] + overflow_x?: CSSStylesCamel["overflowX"] + overflow_y?: CSSStylesCamel["overflowY"] + overscroll_behavior?: CSSStylesCamel["overscrollBehavior"] + overscroll_behavior_block?: CSSStylesCamel["overscrollBehaviorBlock"] + overscroll_behavior_inline?: CSSStylesCamel["overscrollBehaviorInline"] + overscroll_behavior_x?: CSSStylesCamel["overscrollBehaviorX"] + overscroll_behavior_y?: CSSStylesCamel["overscrollBehaviorY"] + padding?: CSSStylesCamel["padding"] + padding_block?: CSSStylesCamel["paddingBlock"] + padding_block_end?: CSSStylesCamel["paddingBlockEnd"] + padding_block_start?: CSSStylesCamel["paddingBlockStart"] + padding_bottom?: CSSStylesCamel["paddingBottom"] + padding_inline?: CSSStylesCamel["paddingInline"] + padding_inline_end?: CSSStylesCamel["paddingInlineEnd"] + padding_inline_start?: CSSStylesCamel["paddingInlineStart"] + padding_left?: CSSStylesCamel["paddingLeft"] + padding_right?: CSSStylesCamel["paddingRight"] + padding_top?: CSSStylesCamel["paddingTop"] + page_break_after?: CSSStylesCamel["pageBreakAfter"] + page_break_before?: CSSStylesCamel["pageBreakBefore"] + page_break_inside?: CSSStylesCamel["pageBreakInside"] + paint_order?: CSSStylesCamel["paintOrder"] + perspective?: CSSStylesCamel["perspective"] + perspective_origin?: CSSStylesCamel["perspectiveOrigin"] + place_content?: CSSStylesCamel["placeContent"] + place_items?: CSSStylesCamel["placeItems"] + place_self?: CSSStylesCamel["placeSelf"] + pointer_events?: CSSStylesCamel["pointerEvents"] + position?: CSSStylesCamel["position"] + quotes?: CSSStylesCamel["quotes"] + resize?: CSSStylesCamel["resize"] + right?: CSSStylesCamel["right"] + rotate?: CSSStylesCamel["rotate"] + row_gap?: CSSStylesCamel["rowGap"] + ruby_position?: CSSStylesCamel["rubyPosition"] + scale?: CSSStylesCamel["scale"] + scroll_behavior?: CSSStylesCamel["scrollBehavior"] + scroll_margin?: CSSStylesCamel["scrollMargin"] + scroll_margin_block?: CSSStylesCamel["scrollMarginBlock"] + scroll_margin_block_end?: CSSStylesCamel["scrollMarginBlockEnd"] + scroll_margin_block_start?: CSSStylesCamel["scrollMarginBlockStart"] + scroll_margin_bottom?: CSSStylesCamel["scrollMarginBottom"] + scroll_margin_inline?: CSSStylesCamel["scrollMarginInline"] + scroll_margin_inline_end?: CSSStylesCamel["scrollMarginInlineEnd"] + scroll_margin_inline_start?: CSSStylesCamel["scrollMarginInlineStart"] + scroll_margin_left?: CSSStylesCamel["scrollMarginLeft"] + scroll_margin_right?: CSSStylesCamel["scrollMarginRight"] + scroll_margin_top?: CSSStylesCamel["scrollMarginTop"] + scroll_padding?: CSSStylesCamel["scrollPadding"] + scroll_padding_block?: CSSStylesCamel["scrollPaddingBlock"] + scroll_padding_block_end?: CSSStylesCamel["scrollPaddingBlockEnd"] + scroll_padding_block_start?: CSSStylesCamel["scrollPaddingBlockStart"] + scroll_padding_bottom?: CSSStylesCamel["scrollPaddingBottom"] + scroll_padding_inline?: CSSStylesCamel["scrollPaddingInline"] + scroll_padding_inline_end?: CSSStylesCamel["scrollPaddingInlineEnd"] + scroll_padding_inline_start?: CSSStylesCamel["scrollPaddingInlineStart"] + scroll_padding_left?: CSSStylesCamel["scrollPaddingLeft"] + scroll_padding_right?: CSSStylesCamel["scrollPaddingRight"] + scroll_padding_top?: CSSStylesCamel["scrollPaddingTop"] + scroll_snap_align?: CSSStylesCamel["scrollSnapAlign"] + scroll_snap_stop?: CSSStylesCamel["scrollSnapStop"] + scroll_snap_type?: CSSStylesCamel["scrollSnapType"] + scrollbar_gutter?: CSSStylesCamel["scrollbarGutter"] + shape_image_threshold?: CSSStylesCamel["shapeImageThreshold"] + shape_margin?: CSSStylesCamel["shapeMargin"] + shape_outside?: CSSStylesCamel["shapeOutside"] + shape_rendering?: CSSStylesCamel["shapeRendering"] + stop_color?: CSSStylesCamel["stopColor"] + stop_opacity?: CSSStylesCamel["stopOpacity"] + stroke?: CSSStylesCamel["stroke"] + stroke_dasharray?: CSSStylesCamel["strokeDasharray"] + stroke_dashoffset?: CSSStylesCamel["strokeDashoffset"] + stroke_linecap?: CSSStylesCamel["strokeLinecap"] + stroke_linejoin?: CSSStylesCamel["strokeLinejoin"] + stroke_miterlimit?: CSSStylesCamel["strokeMiterlimit"] + stroke_opacity?: CSSStylesCamel["strokeOpacity"] + stroke_width?: CSSStylesCamel["strokeWidth"] + tab_size?: CSSStylesCamel["tabSize"] + table_layout?: CSSStylesCamel["tableLayout"] + text_align?: CSSStylesCamel["textAlign"] + text_align_last?: CSSStylesCamel["textAlignLast"] + text_anchor?: CSSStylesCamel["textAnchor"] + text_combine_upright?: CSSStylesCamel["textCombineUpright"] + text_decoration?: CSSStylesCamel["textDecoration"] + text_decoration_color?: CSSStylesCamel["textDecorationColor"] + text_decoration_line?: CSSStylesCamel["textDecorationLine"] + text_decoration_skip_ink?: CSSStylesCamel["textDecorationSkipInk"] + text_decoration_style?: CSSStylesCamel["textDecorationStyle"] + text_decoration_thickness?: CSSStylesCamel["textDecorationThickness"] + text_emphasis?: CSSStylesCamel["textEmphasis"] + text_emphasis_color?: CSSStylesCamel["textEmphasisColor"] + text_emphasis_position?: CSSStylesCamel["textEmphasisPosition"] + text_emphasis_style?: CSSStylesCamel["textEmphasisStyle"] + text_indent?: CSSStylesCamel["textIndent"] + text_orientation?: CSSStylesCamel["textOrientation"] + text_overflow?: CSSStylesCamel["textOverflow"] + text_rendering?: CSSStylesCamel["textRendering"] + text_shadow?: CSSStylesCamel["textShadow"] + text_transform?: CSSStylesCamel["textTransform"] + text_underline_offset?: CSSStylesCamel["textUnderlineOffset"] + text_underline_position?: CSSStylesCamel["textUnderlinePosition"] + top?: CSSStylesCamel["top"] + touch_action?: CSSStylesCamel["touchAction"] + transform?: CSSStylesCamel["transform"] + transform_box?: CSSStylesCamel["transformBox"] + transform_origin?: CSSStylesCamel["transformOrigin"] + transform_style?: CSSStylesCamel["transformStyle"] + transition?: CSSStylesCamel["transition"] + transition_delay?: CSSStylesCamel["transitionDelay"] + transition_duration?: CSSStylesCamel["transitionDuration"] + transition_property?: CSSStylesCamel["transitionProperty"] + transition_timing_function?: CSSStylesCamel["transitionTimingFunction"] + translate?: CSSStylesCamel["translate"] + unicode_bidi?: CSSStylesCamel["unicodeBidi"] + user_select?: CSSStylesCamel["userSelect"] + vertical_align?: CSSStylesCamel["verticalAlign"] + visibility?: CSSStylesCamel["visibility"] + white_space?: CSSStylesCamel["whiteSpace"] + widows?: CSSStylesCamel["widows"] + width?: CSSStylesCamel["width"] + will_change?: CSSStylesCamel["willChange"] + word_break?: CSSStylesCamel["wordBreak"] + word_spacing?: CSSStylesCamel["wordSpacing"] /** @deprecated */ - word_wrap?: string | null - writing_mode?: string | null - z_index?: string | null + word_wrap?: CSSStylesCamel["wordWrap"] + writing_mode?: CSSStylesCamel["writingMode"] + z_index?: CSSStylesCamel["zIndex"] } export type CSSVariables = {[key in `--${string}`]?: string | null} diff --git a/bokehjs/src/lib/core/dom.ts b/bokehjs/src/lib/core/dom.ts index 72be11b9e26..d0a0d974e62 100644 --- a/bokehjs/src/lib/core/dom.ts +++ b/bokehjs/src/lib/core/dom.ts @@ -1,69 +1,102 @@ import {isBoolean, isNumber, isString, isArray, isPlainObject} from "./util/types" import {entries} from "./util/object" import {BBox} from "./util/bbox" -import type {Size, Box, Extents} from "./types" +import type {Size, Box, Extents, PlainObject} from "./types" import type {CSSStyles, CSSStyleSheetDecl} from "./css" -import {compose_stylesheet} from "./css" +import {compose_stylesheet, apply_styles} from "./css" +import {logger} from "./logging" + +export type Optional = {[P in keyof T]?: T[P] | null | undefined} + +export type HTMLElementName = keyof HTMLElementTagNameMap + +export type CSSClass = string + +export type ElementOurAttrs = { + class?: CSSClass | (CSSClass | null | undefined)[] + style?: CSSStyles | string + data?: PlainObject +} + +export type ElementCommonAttrs = { + id: Element["id"] + title: HTMLElement["title"] + tabIndex: HTMLOrSVGElement["tabIndex"] +} + +export type HTMLAttrs<_T extends HTMLElementName, ElementSpecificAttrs> = ElementOurAttrs & Optional & Optional -export type HTMLAttrs = {[name: string]: unknown} export type HTMLItem = string | Node | NodeList | HTMLCollection | null | undefined export type HTMLChild = HTMLItem | HTMLItem[] -const _createElement = (tag: T) => { - return (attrs: HTMLAttrs | HTMLChild = {}, ...children: HTMLChild[]): HTMLElementTagNameMap[T] => { +const _element = (tag: T) => { + return (attrs: HTMLAttrs | HTMLChild = {}, ...children: HTMLChild[]): HTMLElementTagNameMap[T] => { const element = document.createElement(tag) if (!isPlainObject(attrs)) { children = [attrs, ...children] attrs = {} + } else { + attrs = {...attrs} } - for (let [attr, value] of entries(attrs)) { - if (value == null || isBoolean(value) && !value) { - continue - } - - if (attr === "class") { - if (isString(value)) { - value = value.split(/\s+/) + if (attrs.class != null) { + const classes = (() => { + if (isString(attrs.class)) { + return attrs.class.split(/\s+/) + } else { + return attrs.class } + })() - if (isArray(value)) { - for (const cls of value as (string | null | undefined)[]) { - if (cls != null) { - element.classList.add(cls) - } - } - continue + for (const cls of classes) { + if (cls != null) { + element.classList.add(cls) } } - if (attr === "style" && isPlainObject(value)) { - for (const [prop, data] of entries(value)) { - (element.style as any)[prop] = data - } - continue + delete attrs.class + } + + if (attrs.style != null) { + if (isString(attrs.style)) { + element.setAttribute("style", attrs.style) + } else { + apply_styles(element.style, attrs.style) } + delete attrs.style + } - if (attr === "data" && isPlainObject(value)) { - for (const [key, data] of entries(value)) { - element.dataset[key] = data as string | undefined // XXX: attrs needs a better type + if (attrs.data != null) { + for (const [key, data] of entries(attrs.data)) { + if (data != null) { + element.dataset[key] = data } - continue } + delete attrs.data + } - element.setAttribute(attr, value as string) + for (const [attr, value] of entries(attrs)) { + if (value == null) { + continue + } else if (isBoolean(value)) { + element.toggleAttribute(attr, value) + } else if (isNumber(value)) { + element.setAttribute(attr, `${value}`) + } else if (isString(value)) { + element.setAttribute(attr, value) + } else { + logger.warn(`unable to set attribute: ${attr} = ${value}`) + } } function append(child: HTMLItem) { if (isString(child)) { - element.appendChild(document.createTextNode(child)) + element.append(document.createTextNode(child)) } else if (child instanceof Node) { - element.appendChild(child) + element.append(child) } else if (child instanceof NodeList || child instanceof HTMLCollection) { - for (const el of child) { - element.appendChild(el) - } + element.append(...child) } else if (child != null && child !== false) { throw new Error(`expected a DOM element, string, false or null, got ${JSON.stringify(child)}`) } @@ -83,30 +116,270 @@ const _createElement = (tag: T) => { } } -export function createElement( - tag: T, attrs: HTMLAttrs | null, ...children: HTMLChild[]): HTMLElementTagNameMap[T] { - return _createElement(tag)(attrs, ...children) -} - -export const - div = _createElement("div"), - span = _createElement("span"), - canvas = _createElement("canvas"), - link = _createElement("link"), - style = _createElement("style"), - a = _createElement("a"), - p = _createElement("p"), - i = _createElement("i"), - pre = _createElement("pre"), - button = _createElement("button"), - label = _createElement("label"), - legend = _createElement("legend"), - fieldset = _createElement("fieldset"), - input = _createElement("input"), - select = _createElement("select"), - option = _createElement("option"), - optgroup = _createElement("optgroup"), - textarea = _createElement("textarea") +export function create_element( + tag: T, attrs: HTMLAttrs | null, ...children: HTMLChild[]): HTMLElementTagNameMap[T] { + return _element(tag)(attrs, ...children) +} + +export type AAttrs = { + href: HTMLAnchorElement["href"] + target: HTMLAnchorElement["target"] +} +export type AbbrAttrs = {} +export type AddressAttrs = {} +export type AreaAttrs = {} +export type ArticleAttrs = {} +export type AsideAttrs = {} +export type AudioAttrs = {} +export type BAttrs = {} +export type BaseAttrs = {} +export type BdiAttrs = {} +export type BdoAttrs = {} +export type BlockQuoteAttrs = {} +export type BodyAttrs = {} +export type BrAttrs = {} +export type ButtonAttrs = { + type: "button" + disabled: HTMLButtonElement["disabled"] +} +export type CanvasAttrs = { + width: HTMLCanvasElement["width"] + height: HTMLCanvasElement["height"] +} +export type CaptionAttrs = {} +export type CiteAttrs = {} +export type CodeAttrs = {} +export type ColAttrs = {} +export type ColGroupAttrs = {} +export type DataAttrs = {} +export type DataListAttrs = {} +export type DdAttrs = {} +export type DelAttrs = {} +export type DetailsAttrs = {} +export type DfnAttrs = {} +export type DialogAttrs = {} +export type DivAttrs = {} +export type DlAttrs = {} +export type DtAttrs = {} +export type EmAttrs = {} +export type EmbedAttrs = {} +export type FieldSetAttrs = {} +export type FigCaptionAttrs = {} +export type FigureAttrs = {} +export type FooterAttrs = {} +export type FormAttrs = {} +export type H1Attrs = {} +export type H2Attrs = {} +export type H3Attrs = {} +export type H4Attrs = {} +export type H5Attrs = {} +export type H6Attrs = {} +export type HeadAttrs = {} +export type HeaderAttrs = {} +export type HGroupAttrs = {} +export type HrAttrs = {} +export type HtmlAttrs = {} +export type IAttrs = {} +export type IFrameAttrs = {} +export type ImgAttrs = {} +export type InputAttrs = { + type: "text" | "checkbox" | "radio" | "file" | "color" + name: HTMLInputElement["name"] + multiple: HTMLInputElement["multiple"] + disabled: HTMLInputElement["disabled"] + checked: HTMLInputElement["checked"] + placeholder: HTMLInputElement["placeholder"] + accept: HTMLInputElement["accept"] + value: HTMLInputElement["value"] + readonly: HTMLInputElement["readOnly"] + webkitdirectory: HTMLInputElement["webkitdirectory"] +} +export type InsAttrs = {} +export type KbdAttrs = {} +export type LabelAttrs = { + for: HTMLLabelElement["htmlFor"] +} +export type LegendAttrs = {} +export type LiAttrs = {} +export type LinkAttrs = { + rel: HTMLLinkElement["rel"] + href: HTMLLinkElement["href"] + disabled: HTMLLinkElement["disabled"] +} +export type MainAttrs = {} +export type MapAttrs = {} +export type MarkAttrs = {} +export type MenuAttrs = {} +export type MetaAttrs = {} +export type MeterAttrs = {} +export type NavAttrs = {} +export type NoScriptAttrs = {} +export type ObjectAttrs = {} +export type OlAttrs = {} +export type OptGroupAttrs = { + disabled: HTMLOptGroupElement["disabled"] + label: HTMLOptGroupElement["label"] +} +export type OptionAttrs = { + disabled: HTMLOptionElement["disabled"] + value: HTMLOptionElement["value"] +} +export type OutputAttrs = {} +export type PAttrs = {} +export type PictureAttrs = {} +export type PreAttrs = {} +export type ProgressAttrs = {} +export type QAttrs = {} +export type RpAttrs = {} +export type RtAttrs = {} +export type RubyAttrs = {} +export type SAttrs = {} +export type SAmpAttrs = {} +export type ScriptAttrs = {} +export type SearchAttrs = {} +export type SectionAttrs = {} +export type SelectAttrs = { + name: HTMLSelectElement["name"] + disabled: HTMLSelectElement["disabled"] + multiple: HTMLSelectElement["multiple"] +} +export type SlotAttrs = {} +export type SmallAttrs = {} +export type SourceAttrs = {} +export type SpanAttrs = {} +export type StrongAttrs = {} +export type StyleAttrs = {} +export type SubAttrs = {} +export type SummaryAttrs = {} +export type SupAttrs = {} +export type TableAttrs = {} +export type TBodyAttrs = {} +export type TdAttrs = {} +export type TemplateAttrs = {} +export type TextAreaAttrs = {} +export type TFootAttrs = {} +export type ThAttrs = {} +export type THeadAttrs = {} +export type TimeAttrs = {} +export type TitleAttrs = {} +export type TrAttrs = {} +export type TrackAttrs = {} +export type UAttrs = {} +export type UlAttrs = {} +export type VideoAttrs = {} +export type WbrAttrs = {} + +export const a = _element<"a", AAttrs>("a") +export const abbr = _element<"abbr", AbbrAttrs>("abbr") +export const address = _element<"address", AddressAttrs>("address") +export const area = _element<"area", AreaAttrs>("area") +export const article = _element<"article", ArticleAttrs>("article") +export const aside = _element<"aside", AsideAttrs>("aside") +export const audio = _element<"audio", AudioAttrs>("audio") +export const b = _element<"b", BAttrs>("b") +export const base = _element<"base", BaseAttrs>("base") +export const bdi = _element<"bdi", BdiAttrs>("bdi") +export const bdo = _element<"bdo", BdoAttrs>("bdo") +export const blockquote = _element<"blockquote", BlockQuoteAttrs>("blockquote") +export const body = _element<"body", BodyAttrs>("body") +export const br = _element<"br", BrAttrs>("br") +export const button = _element<"button", ButtonAttrs>("button") +export const canvas = _element<"canvas", CanvasAttrs>("canvas") +export const caption = _element<"caption", CaptionAttrs>("caption") +export const cite = _element<"cite", CiteAttrs>("cite") +export const code = _element<"code", CodeAttrs>("code") +export const col = _element<"col", ColAttrs>("col") +export const colgroup = _element<"colgroup", ColGroupAttrs>("colgroup") +export const data = _element<"data", DataAttrs>("data") +export const datalist = _element<"datalist", DataListAttrs>("datalist") +export const dd = _element<"dd", DdAttrs>("dd") +export const del = _element<"del", DelAttrs>("del") +export const details = _element<"details", DetailsAttrs>("details") +export const dfn = _element<"dfn", DfnAttrs>("dfn") +export const dialog = _element<"dialog", DialogAttrs>("dialog") +export const div = _element<"div", DivAttrs>("div") +export const dl = _element<"dl", DlAttrs>("dl") +export const dt = _element<"dt", DtAttrs>("dt") +export const em = _element<"em", EmAttrs>("em") +export const embed = _element<"embed", EmbedAttrs>("embed") +export const fieldset = _element<"fieldset", FieldSetAttrs>("fieldset") +export const figcaption = _element<"figcaption", FigCaptionAttrs>("figcaption") +export const figure = _element<"figure", FigureAttrs>("figure") +export const footer = _element<"footer", FooterAttrs>("footer") +export const form = _element<"form", FormAttrs>("form") +export const h1 = _element<"h1", H1Attrs>("h1") +export const h2 = _element<"h2", H2Attrs>("h2") +export const h3 = _element<"h3", H3Attrs>("h3") +export const h4 = _element<"h4", H4Attrs>("h4") +export const h5 = _element<"h5", H5Attrs>("h5") +export const h6 = _element<"h6", H6Attrs>("h6") +export const head = _element<"head", HeadAttrs>("head") +export const header = _element<"header", HeaderAttrs>("header") +export const hgroup = _element<"hgroup", HGroupAttrs>("hgroup") +export const hr = _element<"hr", HrAttrs>("hr") +export const html = _element<"html", HtmlAttrs>("html") +export const i = _element<"i", IAttrs>("i") +export const iframe = _element<"iframe", IFrameAttrs>("iframe") +export const img = _element<"img", ImgAttrs>("img") +export const input = _element<"input", InputAttrs>("input") +export const ins = _element<"ins", InsAttrs>("ins") +export const kbd = _element<"kbd", KbdAttrs>("kbd") +export const label = _element<"label", LabelAttrs>("label") +export const legend = _element<"legend", LegendAttrs>("legend") +export const li = _element<"li", LiAttrs>("li") +export const link = _element<"link", LinkAttrs>("link") +export const main = _element<"main", MainAttrs>("main") +export const map = _element<"map", MapAttrs>("map") +export const mark = _element<"mark", MarkAttrs>("mark") +export const menu = _element<"menu", MenuAttrs>("menu") +export const meta = _element<"meta", MetaAttrs>("meta") +export const meter = _element<"meter", MeterAttrs>("meter") +export const nav = _element<"nav", NavAttrs>("nav") +export const noscript = _element<"noscript", NoScriptAttrs>("noscript") +export const object = _element<"object", ObjectAttrs>("object") +export const ol = _element<"ol", OlAttrs>("ol") +export const optgroup = _element<"optgroup", OptGroupAttrs>("optgroup") +export const option = _element<"option", OptionAttrs>("option") +export const output = _element<"output", OutputAttrs>("output") +export const p = _element<"p", PAttrs>("p") +export const picture = _element<"picture", PictureAttrs>("picture") +export const pre = _element<"pre", PreAttrs>("pre") +export const progress = _element<"progress", ProgressAttrs>("progress") +export const q = _element<"q", QAttrs>("q") +export const rp = _element<"rp", RpAttrs>("rp") +export const rt = _element<"rt", RtAttrs>("rt") +export const ruby = _element<"ruby", RubyAttrs>("ruby") +export const s = _element<"s", SAttrs>("s") +export const samp = _element<"samp", SAmpAttrs>("samp") +export const script = _element<"script", ScriptAttrs>("script") +export const search = _element<"search", SearchAttrs>("search") +export const section = _element<"section", SectionAttrs>("section") +export const select = _element<"select", SelectAttrs>("select") +export const slot = _element<"slot", SlotAttrs>("slot") +export const small = _element<"small", SmallAttrs>("small") +export const source = _element<"source", SourceAttrs>("source") +export const span = _element<"span", SpanAttrs>("span") +export const strong = _element<"strong", StrongAttrs>("strong") +export const style = _element<"style", StyleAttrs>("style") +export const sub = _element<"sub", SubAttrs>("sub") +export const summary = _element<"summary", SummaryAttrs>("summary") +export const sup = _element<"sup", SupAttrs>("sup") +export const table = _element<"table", TableAttrs>("table") +export const tbody = _element<"tbody", TBodyAttrs>("tbody") +export const td = _element<"td", TdAttrs>("td") +export const template = _element<"template", TemplateAttrs>("template") +export const textarea = _element<"textarea", TextAreaAttrs>("textarea") +export const tfoot = _element<"tfoot", TFootAttrs>("tfoot") +export const th = _element<"th", ThAttrs>("th") +export const thead = _element<"thead", THeadAttrs>("thead") +export const time = _element<"time", TimeAttrs>("time") +export const title = _element<"title", TitleAttrs>("title") +export const tr = _element<"tr", TrAttrs>("tr") +export const track = _element<"track", TrackAttrs>("track") +export const u = _element<"u", UAttrs>("u") +export const ul = _element<"ul", UlAttrs>("ul") +export const video = _element<"video", VideoAttrs>("video") +export const wbr = _element<"wbr", WbrAttrs>("wbr") export type SVGAttrs = {[key: string]: string | false | null | undefined} @@ -431,7 +704,7 @@ export abstract class StyleSheet { } export class InlineStyleSheet extends StyleSheet { - protected override readonly el = style({type: "text/css"}) + protected override readonly el = style() constructor(css?: string | CSSStyleSheetDecl) { super() diff --git a/bokehjs/src/lib/core/dom_view.ts b/bokehjs/src/lib/core/dom_view.ts index d4814851192..e9fa27f75fe 100644 --- a/bokehjs/src/lib/core/dom_view.ts +++ b/bokehjs/src/lib/core/dom_view.ts @@ -1,7 +1,7 @@ import {View} from "./view" import type {SerializableState} from "./view" import type {StyleSheet, StyleSheetLike} from "./dom" -import {createElement, empty, InlineStyleSheet, ClassList} from "./dom" +import {create_element, empty, InlineStyleSheet, ClassList} from "./dom" import {isString} from "./util/types" import {assert} from "./util/assert" import type {BBox} from "./util/bbox" @@ -73,7 +73,7 @@ export abstract class DOMView extends View { } protected _create_element(): this["el"] { - return createElement(this.constructor.tag_name, {class: this.css_classes()}) + return create_element(this.constructor.tag_name, {}) } reposition(_displayed?: boolean): void {} @@ -89,6 +89,16 @@ export abstract class DOMView extends View { this.r_after_render() this.notify_finished() } + + /** + * Define where to render this element or let the parent decide. + * + * This is useful when creating "floating" components or adding + * components to canvas' layers. + */ + rendering_target(): HTMLElement | null { + return null + } } export abstract class DOMElementView extends DOMView { diff --git a/bokehjs/src/lib/core/enums.ts b/bokehjs/src/lib/core/enums.ts index f1525269de7..5b8860b150f 100644 --- a/bokehjs/src/lib/core/enums.ts +++ b/bokehjs/src/lib/core/enums.ts @@ -70,6 +70,9 @@ export const HatchPatternType = Enum( ) export type HatchPatternType = typeof HatchPatternType["__type__"] +export const BuiltinFormatter = Enum("raw", "basic", "numeral", "printf", "datetime") +export type BuiltinFormatter = typeof BuiltinFormatter["__type__"] + export const HTTPMethod = Enum("POST", "GET") export type HTTPMethod = typeof HTTPMethod["__type__"] @@ -127,6 +130,9 @@ export type MutedPolicy = typeof MutedPolicy["__type__"] export const Orientation = Enum("vertical", "horizontal") export type Orientation = typeof Orientation["__type__"] +export const OutlineShapeName = Enum("none", "box", "rectangle", "square", "circle", "ellipse", "trapezoid", "parallelogram", "diamond", "triangle") +export type OutlineShapeName = typeof OutlineShapeName["__type__"] + export const OutputBackend = Enum("canvas", "svg", "webgl") export type OutputBackend = typeof OutputBackend["__type__"] @@ -157,7 +163,10 @@ export type RoundingFunction = typeof RoundingFunction["__type__"] export const ScrollbarPolicy = Enum("auto", "visible", "hidden") export type ScrollbarPolicy = typeof ScrollbarPolicy["__type__"] -export const SelectionMode = Enum("replace", "append", "intersect", "subtract", "xor") +export const RegionSelectionMode = Enum("replace", "append", "intersect", "subtract", "xor") +export type RegionSelectionMode = typeof RegionSelectionMode["__type__"] + +export const SelectionMode = Enum(...RegionSelectionMode, "toggle") export type SelectionMode = typeof SelectionMode["__type__"] export const Side = Enum("above", "below", "left", "right") diff --git a/bokehjs/src/lib/core/has_props.ts b/bokehjs/src/lib/core/has_props.ts index e394dc00077..282d91a695a 100644 --- a/bokehjs/src/lib/core/has_props.ts +++ b/bokehjs/src/lib/core/has_props.ts @@ -13,14 +13,13 @@ import {assert} from "./util/assert" import {unique_id} from "./util/string" import {keys, values, entries, extend, is_empty, dict} from "./util/object" import {isObject, isIterable, isPlainObject, isArray, isFunction, isPrimitive} from "./util/types" -import {is_equal} from "./util/eq" import type {Serializable, Serializer, ObjectRefRep, AnyVal} from "./serialization" import {serialize} from "./serialization" import type {Document} from "../document/document" import type {DocumentEvent} from "../document/events" import {DocumentEventBatch, ModelChangedEvent, ColumnsPatchedEvent, ColumnsStreamedEvent} from "../document/events" import type {Equatable, Comparator} from "./util/eq" -import {equals} from "./util/eq" +import {equals, is_equal} from "./util/eq" import type {Printable, Printer} from "./util/pretty" import {pretty} from "./util/pretty" import type {Cloneable} from "./util/cloneable" @@ -402,9 +401,13 @@ export abstract class HasProps extends Signalable() implements Equatable, Printa } // Create a new model with exact attribute values to this one, but new identity. - clone(): this { + clone(attrs?: Partial): this { const cloner = new Cloner() - return cloner.clone(this) + const that = cloner.clone(this) + if (attrs != null) { + that.setv(attrs) + } + return that } private _watchers: WeakMap = new WeakMap() diff --git a/bokehjs/src/lib/core/kinds.ts b/bokehjs/src/lib/core/kinds.ts index 989b7991891..c587662fa2c 100644 --- a/bokehjs/src/lib/core/kinds.ts +++ b/bokehjs/src/lib/core/kinds.ts @@ -161,6 +161,27 @@ export namespace Kinds { } } + export class And extends Kind { + readonly types: [Kind, Kind] + + constructor(type0: Kind, type1: Kind) { + super() + this.types = [type0, type1] + } + + valid(value: unknown): value is T0 & T1 { + return this.types.some((type) => type.valid(value)) // TODO not sure if this is correct, probably not + } + + override toString(): string { + return `And(${this.types.map((type) => type.toString()).join(", ")})` + } + + may_have_refs(): boolean { + return this.types.some((type) => type.may_have_refs()) + } + } + export class Tuple extends Kind { constructor(readonly types: TupleKind) { super() @@ -329,6 +350,16 @@ export namespace Kinds { } } + export class NonEmptyList extends List { + override valid(value: unknown): value is ItemType[] { + return super.valid(value) && value.length != 0 + } + + override toString(): string { + return `NonEmptyList(${this.item_type.toString()})` + } + } + export class Null extends Primitive { valid(value: unknown): value is null { return value === null @@ -633,12 +664,14 @@ export const Null = new Kinds.Null() export const Nullable = (base_type: Kind) => new Kinds.Nullable(base_type) export const Opt = (base_type: Kind) => new Kinds.Opt(base_type) export const Or = (...types: Kinds.TupleKind) => new Kinds.Or(types) +export const And = (type0: Kind, type1: Kind) => new Kinds.And(type0, type1) export const Tuple = (...types: Kinds.TupleKind) => new Kinds.Tuple(types) export const Struct = (struct_type: Kinds.ObjectKind) => new Kinds.Struct(struct_type) export const PartialStruct = (struct_type: Kinds.ObjectKind) => new Kinds.PartialStruct(struct_type) export const Iterable = (item_type: Kind) => new Kinds.Iterable(item_type) export const Arrayable = (item_type: Kind) => new Kinds.Arrayable(item_type) export const List = (item_type: Kind) => new Kinds.List(item_type) +export const NonEmptyList = (item_type: Kind) => new Kinds.NonEmptyList(item_type) export const Dict = (item_type: Kind) => new Kinds.Dict(item_type) export const Mapping = (key_type: Kind, item_type: Kind) => new Kinds.Mapping(key_type, item_type) export const Set = (item_type: Kind) => new Kinds.Set(item_type) diff --git a/bokehjs/src/lib/core/types.ts b/bokehjs/src/lib/core/types.ts index b87ff05cc22..d9d6479ac6d 100644 --- a/bokehjs/src/lib/core/types.ts +++ b/bokehjs/src/lib/core/types.ts @@ -1,3 +1,5 @@ +import type {Anchor} from "./enums" + export const GeneratorFunction: GeneratorFunctionConstructor = Object.getPrototypeOf(function* () {}).constructor export const AsyncGeneratorFunction: AsyncGeneratorFunctionConstructor = Object.getPrototypeOf(async function* () {}).constructor @@ -90,6 +92,7 @@ export type Box = { y: number width: number height: number + origin?: Anchor } export type Rect = { diff --git a/bokehjs/src/lib/core/ui_events.ts b/bokehjs/src/lib/core/ui_events.ts index b4d550ba460..1770790b642 100644 --- a/bokehjs/src/lib/core/ui_events.ts +++ b/bokehjs/src/lib/core/ui_events.ts @@ -201,13 +201,14 @@ export class UIEventBus { } } - hit_test_renderers(plot_view: PlotView, sx: number, sy: number): RendererView | null { - for (const view of reversed(plot_view.computed_renderer_views)) { + hit_test_renderers(plot_view: PlotView, sx: number, sy: number): RendererView[] { + const collected = [] + for (const view of reversed(plot_view.all_renderer_views)) { if (view.interactive_hit?.(sx, sy) ?? false) { - return view + collected.push(view) } } - return null + return collected } set_cursor(cursor?: string | null): void { @@ -333,7 +334,7 @@ export class UIEventBus { private _current_pan_view: (RendererView & Pannable) | null = null private _current_pinch_view: (RendererView & Pinchable) | null = null private _current_rotate_view: (RendererView & Rotatable) | null = null - private _current_move_view: (RendererView & Moveable) | null = null + private _current_move_views: (RendererView & Moveable)[] = [] __trigger(plot_view: PlotView, signal: UISignal, e: E, srcEvent: Event): void { const gestures = plot_view.model.toolbar.gestures @@ -341,13 +342,17 @@ export class UIEventBus { const event_type = signal.name const base_type = event_type.split(":")[0] as BaseType - const view = this.hit_test_renderers(plot_view, e.sx, e.sy) + const views = this.hit_test_renderers(plot_view, e.sx, e.sy) if (base_type == "pan") { + const event = e as PanEvent if (this._current_pan_view == null) { - if (view != null) { - if (event_type == "pan:start" && is_Pannable(view)) { - if (view.on_pan_start(e as PanEvent)) { + if (event_type == "pan:start") { + for (const view of views) { + if (!is_Pannable(view)) { + continue + } + if (view.on_pan_start(event)) { this._current_pan_view = view srcEvent.preventDefault() return @@ -356,19 +361,23 @@ export class UIEventBus { } } else { if (event_type == "pan") { - this._current_pan_view.on_pan(e as PanEvent) + this._current_pan_view.on_pan(event) } else if (event_type == "pan:end") { - this._current_pan_view.on_pan_end(e as PanEvent) + this._current_pan_view.on_pan_end(event) this._current_pan_view = null } srcEvent.preventDefault() return } } else if (base_type == "pinch") { + const event = e as PinchEvent if (this._current_pinch_view == null) { - if (view != null) { - if (event_type == "pinch:start" && is_Pinchable(view)) { - if (view.on_pinch_start(e as PinchEvent)) { + if (event_type == "pinch:start") { + for (const view of views) { + if (!is_Pinchable(view)) { + continue + } + if (view.on_pinch_start(event)) { this._current_pinch_view = view srcEvent.preventDefault() return @@ -377,19 +386,23 @@ export class UIEventBus { } } else { if (event_type == "pinch") { - this._current_pinch_view.on_pinch(e as PinchEvent) + this._current_pinch_view.on_pinch(event) } else if (event_type == "pinch:end") { - this._current_pinch_view.on_pinch_end(e as PinchEvent) + this._current_pinch_view.on_pinch_end(event) this._current_pinch_view = null } srcEvent.preventDefault() return } } else if (base_type == "rotate") { + const event = e as RotateEvent if (this._current_rotate_view == null) { - if (view != null) { - if (event_type == "rotate:start" && is_Rotatable(view)) { - if (view.on_rotate_start(e as RotateEvent)) { + if (event_type == "rotate:start") { + for (const view of views) { + if (!is_Rotatable(view)) { + continue + } + if (view.on_rotate_start(event)) { this._current_rotate_view = view srcEvent.preventDefault() return @@ -398,25 +411,40 @@ export class UIEventBus { } } else { if (event_type == "rotate") { - this._current_rotate_view.on_rotate(e as RotateEvent) + this._current_rotate_view.on_rotate(event) } else if (event_type == "rotate:end") { - this._current_rotate_view.on_rotate_end(e as RotateEvent) + this._current_rotate_view.on_rotate_end(event) this._current_rotate_view = null } srcEvent.preventDefault() return } } else if (base_type == "move") { - if (this._current_move_view == view) { - this._current_move_view?.on_move(e as MoveEvent) - } else { - this._current_move_view?.on_leave(e as MoveEvent) - this._current_move_view = null + const event = e as MoveEvent + const new_views = new Set(views) + + const current_views = new Set(this._current_move_views) + this._current_move_views = [] - if (view != null && is_Moveable(view)) { + for (const view of current_views) { + if (!new_views.has(view)) { + current_views.delete(view) + view.on_leave(event) + } + } + + for (const view of views) { + if (!is_Moveable(view)) { + continue + } + + if (!current_views.has(view)) { if (view.on_enter(e as MoveEvent)) { - this._current_move_view = view + this._current_move_views.push(view) } + } else { + this._current_move_views.push(view) + view.on_move(event) } } } @@ -430,6 +458,8 @@ export class UIEventBus { } } + const top_view = views.at(0) + switch (base_type) { case "move": { const active_gesture = gestures.move.active @@ -444,8 +474,8 @@ export class UIEventBus { this._current_pan_view ?? this._current_pinch_view ?? this._current_rotate_view ?? - this._current_move_view ?? - view ?? + this._current_move_views.at(0) ?? + top_view ?? get_tool_view(active_gesture) if (current_view != null) { @@ -464,7 +494,7 @@ export class UIEventBus { })() this.set_cursor(cursor) - if (view != null && !view.model.propagate_hover && !is_empty(active_inspectors)) { + if (top_view != null && !top_view.model.propagate_hover && !is_empty(active_inspectors)) { // override event_type to cause inspectors to clear overlays signal = this.move_exit as any // XXX } @@ -478,7 +508,7 @@ export class UIEventBus { return // don't trigger bokeh events } - view?.on_hit?.(e.sx, e.sy) + top_view?.on_hit?.(e.sx, e.sy) if (this.hit_test_frame(plot_view, e.sx, e.sy)) { const active_gesture = gestures.tap.active diff --git a/bokehjs/src/lib/core/util/array.ts b/bokehjs/src/lib/core/util/array.ts index 2f800f3efda..7f45ce53659 100644 --- a/bokehjs/src/lib/core/util/array.ts +++ b/bokehjs/src/lib/core/util/array.ts @@ -15,7 +15,7 @@ export { is_empty, includes, contains, sort_by, } from "./arrayable" -const slice = Array.prototype.slice +const {slice} = Array.prototype export function head(array: T[]): T { if (array.length != 0) { diff --git a/bokehjs/src/lib/core/util/arrayable.ts b/bokehjs/src/lib/core/util/arrayable.ts index 979f4fe11eb..cb16962c911 100644 --- a/bokehjs/src/lib/core/util/arrayable.ts +++ b/bokehjs/src/lib/core/util/arrayable.ts @@ -27,6 +27,9 @@ export function is_sorted(array: Arrayable): boolean { return true } +export function copy(array: T[]): T[] +export function copy(array: Arrayable): Arrayable + export function copy(array: Arrayable): Arrayable { if (Array.isArray(array)) { return array.slice() @@ -35,7 +38,20 @@ export function copy(array: Arrayable): Arrayable { } } +export function splice(array: T[], start: number, k?: number, ...items: T[]): T[] +export function splice(array: Arrayable, start: number, k?: number, ...items: T[]): Arrayable + export function splice(array: Arrayable, start: number, k?: number, ...items: T[]): Arrayable { + if (Array.isArray(array)) { + const result = copy(array) + if (k === undefined) { + result.splice(start) + } else { + result.splice(start, k, ...items) + } + return result + } + const len = array.length if (start < 0) { @@ -74,18 +90,30 @@ export function splice(array: Arrayable, start: number, k?: number, ...ite return result } +export function head(array: T[], n: number): T[] +export function head(array: Arrayable, n: number): Arrayable + export function head(array: Arrayable, n: number): Arrayable { return splice(array, n, array.length - n) } +export function insert(array: T[], item: T, i: number): T[] +export function insert(array: Arrayable, item: T, i: number): Arrayable + export function insert(array: Arrayable, item: T, i: number): Arrayable { return splice(array, i, 0, item) } +export function append(array: T[], item: T): T[] +export function append(array: Arrayable, item: T): Arrayable + export function append(array: Arrayable, item: T): Arrayable { return splice(array, array.length, 0, item) } +export function prepend(array: T[], item: T): T[] +export function prepend(array: Arrayable, item: T): Arrayable + export function prepend(array: Arrayable, item: T): Arrayable { return splice(array, 0, 0, item) } diff --git a/bokehjs/src/lib/core/util/bbox.ts b/bokehjs/src/lib/core/util/bbox.ts index 19eefb15b6d..904ddfce07f 100644 --- a/bokehjs/src/lib/core/util/bbox.ts +++ b/bokehjs/src/lib/core/util/bbox.ts @@ -1,5 +1,6 @@ import type {Arrayable, Rect, Box, Interval, Size} from "../types" import {ScreenArray} from "../types" +import type {VAlign, HAlign} from "../enums" import type {Equatable, Comparator} from "./eq" import {equals} from "./eq" import type * as affine from "./affine" @@ -143,14 +144,45 @@ export class BBox implements Rect, Equatable { this.y1 = y1 } } else if ("x" in box) { - const {x, y, width, height} = box + const {x, y, width, height, origin="top_left"} = box if (!(width >= 0 && height >= 0)) { throw new Error(`invalid bbox {x: ${x}, y: ${y}, width: ${width}, height: ${height}}`) } - this.x0 = x - this.y0 = y - this.x1 = x + width - this.y1 = y + height + const base_origin = (() => { + switch (origin) { + case "left": return "center_left" + case "right": return "center_right" + case "top": return "top_center" + case "bottom": return "bottom_center" + case "center": return "center_center" + default: return origin + } + })() + const [y_align, x_align] = base_origin.split("_", 2) as [VAlign, HAlign] + const y_coeff = (() => { + switch (y_align) { + case "top": return 0.0 + case "center": return 0.5 + case "bottom": return 1.0 + } + })() + const x_coeff = (() => { + switch (x_align) { + case "left": return 0.0 + case "center": return 0.5 + case "right": return 1.0 + } + })() + const d_width = x_coeff*width + const d_height = y_coeff*height + const x0 = x - d_width + const y0 = y - d_height + const x1 = x0 + width + const y1 = y0 + height + this.x0 = x0 + this.y0 = y0 + this.x1 = x1 + this.y1 = y1 } else { let left: number, right: number let top: number, bottom: number @@ -342,32 +374,39 @@ export class BBox implements Rect, Equatable { return this.width/this.height } - get hcenter(): number { + get x_center(): number { return (this.left + this.right)/2 } - get vcenter(): number { + get y_center(): number { return (this.top + this.bottom)/2 } + get hcenter(): number { + return this.x_center + } + get vcenter(): number { + return this.y_center + } + get area(): number { return this.width*this.height } resolve(symbol: string): XY | number { switch (symbol) { - case "top_left": return {x: this.left, y: this.top} - case "top_center": return {x: this.hcenter, y: this.top} - case "top_right": return {x: this.right, y: this.top} + case "top_left": return this.top_left + case "top_center": return this.top_center + case "top_right": return this.top_right - case "center_left": return {x: this.left, y: this.vcenter} - case "center_center": return {x: this.hcenter, y: this.vcenter} - case "center_right": return {x: this.right, y: this.vcenter} + case "center_left": return this.center_left + case "center_center": return this.center_center + case "center_right": return this.center_right - case "bottom_left": return {x: this.left, y: this.bottom} - case "bottom_center": return {x: this.hcenter, y: this.bottom} - case "bottom_right": return {x: this.right, y: this.bottom} + case "bottom_left": return this.bottom_left + case "bottom_center": return this.bottom_center + case "bottom_right": return this.bottom_right - case "center": return {x: this.hcenter, y: this.vcenter} + case "center": return this.center case "top": return this.top case "left": return this.left @@ -381,6 +420,40 @@ export class BBox implements Rect, Equatable { } } + get top_left(): XY { + return {x: this.left, y: this.top} + } + get top_center(): XY { + return {x: this.hcenter, y: this.top} + } + get top_right(): XY { + return {x: this.right, y: this.top} + } + + get center_left(): XY { + return {x: this.left, y: this.vcenter} + } + get center_center(): XY { + return {x: this.hcenter, y: this.vcenter} + } + get center_right(): XY { + return {x: this.right, y: this.vcenter} + } + + get bottom_left(): XY { + return {x: this.left, y: this.bottom} + } + get bottom_center(): XY { + return {x: this.hcenter, y: this.bottom} + } + get bottom_right(): XY { + return {x: this.right, y: this.bottom} + } + + get center(): XY { + return {x: this.hcenter, y: this.vcenter} + } + round(): BBox { return new BBox({ x0: round(this.x0), diff --git a/bokehjs/src/lib/core/util/iterator.ts b/bokehjs/src/lib/core/util/iterator.ts index 609a7087cc6..d25f9075800 100644 --- a/bokehjs/src/lib/core/util/iterator.ts +++ b/bokehjs/src/lib/core/util/iterator.ts @@ -118,6 +118,15 @@ export function* flat_map(iterable: Iterable, fn: (item: T, i: number) } } +export function* filter(iterable: Iterable, fn: (item: T, i: number) => boolean): Iterable { + let i = 0 + for (const item of iterable) { + if (fn(item, i++)) { + yield item + } + } +} + export function every(iterable: Iterable, predicate: (item: T) => boolean): boolean { for (const item of iterable) { if (!predicate(item)) { diff --git a/bokehjs/src/lib/core/util/math.ts b/bokehjs/src/lib/core/util/math.ts index 39cd100ab83..6f07147d8c1 100644 --- a/bokehjs/src/lib/core/util/math.ts +++ b/bokehjs/src/lib/core/util/math.ts @@ -2,7 +2,8 @@ import type {AngleUnits, Direction} from "../enums" import {isObject} from "./types" import {assert} from "./assert" -const {PI, abs, sign} = Math +const {PI, abs, sign, sqrt} = Math +export {PI, abs, sqrt} export function angle_norm(angle: number): number { if (angle == 0) { diff --git a/bokehjs/src/lib/core/util/refs.ts b/bokehjs/src/lib/core/util/refs.ts index cb1f7f1351f..fb666a23869 100644 --- a/bokehjs/src/lib/core/util/refs.ts +++ b/bokehjs/src/lib/core/util/refs.ts @@ -21,7 +21,7 @@ export interface HasRefs { readonly [has_refs]: boolean } -export function _is_HasRefs(v: object): v is HasRefs { +function _is_HasRefs(v: object): v is HasRefs { return has_refs in v } @@ -34,7 +34,7 @@ export function may_have_refs(obj: object): boolean { return obj[has_refs] } const type = obj.constructor - if (_is_HasRefs(type)) { + if (is_HasRefs(type)) { return type[has_refs] } return true diff --git a/bokehjs/src/lib/core/util/templating.ts b/bokehjs/src/lib/core/util/templating.ts index f16c3b92d7a..55e3821cf82 100644 --- a/bokehjs/src/lib/core/util/templating.ts +++ b/bokehjs/src/lib/core/util/templating.ts @@ -5,25 +5,26 @@ import type {CustomJSHover} from "models/tools/inspectors/customjs_hover" import {sprintf as sprintf_js} from "sprintf-js" import tz from "timezone" import type {Dict} from "../types" -import {Enum} from "../kinds" +import type {BuiltinFormatter} from "../enums" import {logger} from "../logging" import {dict} from "./object" import {is_NDArray} from "./ndarray" import {isArray, isNumber, isString, isTypedArray} from "./types" -export const FormatterType = Enum("numeral", "printf", "datetime") -export type FormatterType = typeof FormatterType["__type__"] +const {abs} = Math -export type FormatterSpec = CustomJSHover | FormatterType +export type FormatterSpec = CustomJSHover | BuiltinFormatter export type Formatters = Dict export type FormatterFunc = (value: unknown, format: string, special_vars: Vars) => string export type Index = number | ImageIndex export type Vars = {[key: string]: unknown} -export const DEFAULT_FORMATTERS = { - numeral: (value: unknown, format: string, _special_vars: Vars) => Numbro.format(value, format), - datetime: (value: unknown, format: string, _special_vars: Vars) => tz(value, format), - printf: (value: unknown, format: string, _special_vars: Vars) => sprintf(format, value), +export const DEFAULT_FORMATTERS: {[key in BuiltinFormatter]: FormatterFunc} = { + raw: (value: unknown, _format: string, _special_vars: Vars) => `${value}`, + basic: (value: unknown, format: string, special_vars: Vars) => basic_formatter(value, format, special_vars), + numeral: (value: unknown, format: string, _special_vars: Vars) => Numbro.format(value, format), + datetime: (value: unknown, format: string, _special_vars: Vars) => tz(value, format), + printf: (value: unknown, format: string, _special_vars: Vars) => sprintf(format, value), } export function sprintf(format: string, ...args: unknown[]): string { @@ -33,13 +34,12 @@ export function sprintf(format: string, ...args: unknown[]): string { export function basic_formatter(value: unknown, _format: string, _special_vars: Vars): string { if (isNumber(value)) { const format = (() => { - switch (false) { - case Math.floor(value) != value: - return "%d" - case !(Math.abs(value) > 0.1) || !(Math.abs(value) < 1000): - return "%0.3f" - default: - return "%0.3e" + if (Number.isInteger(value)) { + return "%d" + } else if (0.1 < abs(value) && abs(value) < 1000) { + return "%0.3f" + } else { + return "%0.3e" } })() @@ -52,7 +52,7 @@ export function basic_formatter(value: unknown, _format: string, _special_vars: export function get_formatter(spec: string, format?: string, formatters?: Formatters): FormatterFunc { // no format, use default built in formatter if (format == null) { - return basic_formatter + return DEFAULT_FORMATTERS.basic } // format spec in the formatters dict, use that @@ -77,7 +77,7 @@ export function get_formatter(spec: string, format?: string, formatters?: Format return DEFAULT_FORMATTERS.numeral } -const MISSING = "???" +export const MISSING = "???" function _get_special_value(name: string, special_vars: Vars) { if (name in special_vars) { diff --git a/bokehjs/src/lib/core/util/text.ts b/bokehjs/src/lib/core/util/text.ts index 4fd07f68e80..efb6aebb519 100644 --- a/bokehjs/src/lib/core/util/text.ts +++ b/bokehjs/src/lib/core/util/text.ts @@ -1,4 +1,5 @@ import {assert} from "./assert" +import {canvas} from "../dom" export type BoxMetrics = { width: number @@ -16,8 +17,9 @@ export type FontMetrics = { } const _offscreen_context = (() => { - const canvas = new OffscreenCanvas(0, 0) - const ctx = canvas.getContext("2d") + // Support Firefox ESR, etc., see https://github.com/bokeh/bokeh/issues/14006. + const canvas_el = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(0, 0) : canvas({width: 0, height: 0}) + const ctx = canvas_el.getContext("2d") assert(ctx != null, "can't obtain 2d context") return ctx })() @@ -30,8 +32,9 @@ function _font_metrics(font: string): FontMetrics { const x_metrics = ctx.measureText("x") const metrics = ctx.measureText("Ã…Åšg|") - const ascent = metrics.fontBoundingBoxAscent - const descent = metrics.fontBoundingBoxDescent + // Support Firefox ESR, etc., see https://github.com/bokeh/bokeh/issues/13969. + const ascent = typeof metrics.fontBoundingBoxAscent !== "undefined" ? metrics.fontBoundingBoxAscent : metrics.actualBoundingBoxAscent + const descent = typeof metrics.fontBoundingBoxDescent !== "undefined" ? metrics.fontBoundingBoxDescent : metrics.actualBoundingBoxDescent return { height: ascent + descent, diff --git a/bokehjs/src/lib/core/util/types.ts b/bokehjs/src/lib/core/util/types.ts index 5ad206bcf9d..5e8b96b5250 100644 --- a/bokehjs/src/lib/core/util/types.ts +++ b/bokehjs/src/lib/core/util/types.ts @@ -20,16 +20,6 @@ export function is_nullish(obj: unknown): obj is null | undefined { return obj == null } -export function isNull(obj: unknown): obj is null | undefined { - return obj == null -} - -export function isNotNull(obj: T | null | undefined): obj is T { - return obj != null -} - -export const non_null = isNotNull - export function isBoolean(obj: unknown): obj is boolean { return obj === true || obj === false || toString.call(obj) === "[object Boolean]" } diff --git a/bokehjs/src/lib/core/vectorization.ts b/bokehjs/src/lib/core/vectorization.ts index fd8f11248f3..39fc65450f5 100644 --- a/bokehjs/src/lib/core/vectorization.ts +++ b/bokehjs/src/lib/core/vectorization.ts @@ -1,4 +1,5 @@ import {isPlainObject} from "./util/types" +import {size} from "./util/object" import type {Arrayable} from "./types" import type {HasProps} from "./has_props" import type {Signal0} from "./signaling" @@ -46,14 +47,35 @@ export type Transformed = { transform?: Transform } +function is_of_type(obj: unknown, field: string): boolean { + if (!isPlainObject(obj)) { + return false + } + if (!(field in obj)) { + return false + } + let n = size(obj) - 1 + if ("transform" in obj) { + n -= 1 + } + if ("units" in obj) { + n -= 1 + } + return n == 0 +} + export function isValue(obj: unknown): obj is Value { - return isPlainObject(obj) && "value" in obj + return is_of_type(obj, "value") } export function isField(obj: unknown): obj is Field { - return isPlainObject(obj) && "field" in obj + return is_of_type(obj, "field") } export function isExpr(obj: unknown): obj is Expr { - return isPlainObject(obj) && "expr" in obj + return is_of_type(obj, "expr") +} + +export function isVectorized(obj: unknown): obj is Vector { + return isValue(obj) || isField(obj) || isExpr(obj) } diff --git a/bokehjs/src/lib/core/view.ts b/bokehjs/src/lib/core/view.ts index c5d532d7a44..66d641a561f 100644 --- a/bokehjs/src/lib/core/view.ts +++ b/bokehjs/src/lib/core/view.ts @@ -9,7 +9,9 @@ import type {NodeTarget} from "../models/coordinates/node" import {Node} from "../models/coordinates/node" import {XY as XY_} from "../models/coordinates/xy" import {Indexed} from "../models/coordinates/indexed" -import {ViewManager} from "./view_manager" +import {ViewManager, ViewQuery} from "./view_manager" +import type {Equatable, Comparator} from "./util/eq" +import {equals} from "./util/eq" export type ViewOf = T["__view_type__"] @@ -29,7 +31,7 @@ export namespace View { export type IterViews = Generator -export class View implements ISignalable { +export class View implements ISignalable, Equatable { readonly removed = new Signal0(this, "removed") readonly model: HasProps @@ -39,6 +41,8 @@ export class View implements ISignalable { readonly owner: ViewManager + readonly views: ViewQuery = new ViewQuery(this) + protected _ready: Promise = Promise.resolve(undefined) get ready(): Promise { return this._ready @@ -102,6 +106,10 @@ export class View implements ISignalable { return `${this.model.type}View(${this.model.id})` } + [equals](that: this, _cmp: Comparator): boolean { + return Object.is(this, that) + } + public *children(): IterViews {} protected _has_finished: boolean = false diff --git a/bokehjs/src/lib/core/view_manager.ts b/bokehjs/src/lib/core/view_manager.ts index 94e406abc28..8e098b1e9a3 100644 --- a/bokehjs/src/lib/core/view_manager.ts +++ b/bokehjs/src/lib/core/view_manager.ts @@ -3,79 +3,11 @@ import type {View, ViewOf, IterViews} from "./view" import type {Options} from "core/build_views" import {build_view} from "./build_views" -export class ViewManager { - protected readonly _roots: Set - - constructor(roots: Iterable = [], protected global?: ViewManager) { - this._roots = new Set(roots) - } - - toString(): string { - const views = [...this._roots].map((view) => `${view}`).join(", ") - return `ViewManager(${views})` - } - - async build_view(model: T, parent: Options>["parent"] = null): Promise> { - const view = await build_view(model, {owner: this, parent}) - if (parent == null) { - this.add(view) - } - return view - } - - get(model: T): ViewOf | null { - for (const view of this._roots) { - if (view.model == model) { - return view - } - } - return null - } - - get_by_id(id: string): ViewOf | null { - for (const view of this._roots) { - if (view.model.id == id) { - return view - } - } - return null - } - - add(view: View): void { - this._roots.add(view) - this.global?.add(view) - } +abstract class AbstractViewQuery { - delete(view: View): void { - this._roots.delete(view) - this.global?.delete(view) - } + abstract [Symbol.iterator](): IterViews - remove(view: View): void { - this.delete(view) - } - - clear(): void { - for (const view of this) { - view.remove() - } - } - - /* TODO (TS 5.2) - [Symbol.dispose](): void { - this.clear() - } - */ - - get roots(): View[] { - return [...this._roots] - } - - *[Symbol.iterator](): IterViews { - yield* this._roots - } - - *views(): IterViews { + *all_views(): IterViews { yield* this.query(() => true) } @@ -98,8 +30,8 @@ export class ViewManager { } } - for (const root of this._roots) { - yield* descend(root) + for (const view of this) { + yield* descend(view) } } @@ -158,3 +90,91 @@ export class ViewManager { return [...this.find_by_id(id)] } } + +export class ViewQuery extends AbstractViewQuery { + constructor(public view: View) { + super() + } + + *[Symbol.iterator](): IterViews { + yield this.view + } + + override toString(): string { + return `ViewQuery(${this.view})` + } +} + +export class ViewManager extends AbstractViewQuery { + protected readonly _roots: Set + + constructor(roots: Iterable = [], protected global?: ViewManager) { + super() + this._roots = new Set(roots) + } + + override toString(): string { + const views = [...this._roots].map((view) => `${view}`).join(", ") + return `ViewManager(${views})` + } + + async build_view(model: T, parent: Options>["parent"] = null): Promise> { + const view = await build_view(model, {owner: this, parent}) + if (parent == null) { + this.add(view) + } + return view + } + + get(model: T): ViewOf | null { + for (const view of this._roots) { + if (view.model == model) { + return view + } + } + return null + } + + get_by_id(id: string): ViewOf | null { + for (const view of this._roots) { + if (view.model.id == id) { + return view + } + } + return null + } + + add(view: View): void { + this._roots.add(view) + this.global?.add(view) + } + + delete(view: View): void { + this._roots.delete(view) + this.global?.delete(view) + } + + remove(view: View): void { + this.delete(view) + } + + clear(): void { + for (const view of this) { + view.remove() + } + } + + /* TODO (TS 5.2) + [Symbol.dispose](): void { + this.clear() + } + */ + + get roots(): View[] { + return [...this._roots] + } + + *[Symbol.iterator](): IterViews { + yield* this._roots + } +} diff --git a/bokehjs/src/lib/document/document.ts b/bokehjs/src/lib/document/document.ts index 6c95e68ab78..baf34d1e68c 100644 --- a/bokehjs/src/lib/document/document.ts +++ b/bokehjs/src/lib/document/document.ts @@ -20,10 +20,12 @@ import {entries, dict} from "core/util/object" import * as sets from "core/util/set" import type {CallbackLike} from "core/util/callbacks" import {execute} from "core/util/callbacks" +import {assert} from "core/util/assert" import {Model} from "model" import type {ModelDef} from "./defs" import {decode_def} from "./defs" -import type {BokehEvent, BokehEventType, BokehEventMap, ModelEvent} from "core/bokeh_events" +import type {BokehEvent, BokehEventType, BokehEventMap} from "core/bokeh_events" +import {ModelEvent} from "core/bokeh_events" import {DocumentReady, LODStart, LODEnd} from "core/bokeh_events" import type {DocumentEvent, DocumentChangedEvent, Decoded, DocumentChanged} from "./events" import {DocumentEventBatch, RootRemovedEvent, TitleChangedEvent, MessageSentEvent, RootAddedEvent} from "./events" @@ -120,6 +122,10 @@ export class Document implements Equatable { if (options.roots != null) { this._add_roots(...options.roots) } + this.on_message("bokeh_event", (event) => { + assert(event instanceof ModelEvent) + this.event_manager.trigger(event) + }) } [equals](that: this, _cmp: Comparator): boolean { diff --git a/bokehjs/src/lib/embed/server.ts b/bokehjs/src/lib/embed/server.ts index 4316750ba78..b3ddfcb04b0 100644 --- a/bokehjs/src/lib/embed/server.ts +++ b/bokehjs/src/lib/embed/server.ts @@ -8,9 +8,10 @@ import type {EmbedTarget} from "./dom" // @internal export function _get_ws_url(app_path: string | undefined, absolute_url: string | undefined): string { - let protocol = "ws:" - if (window.location.protocol == "https:") { - protocol = "wss:" + // if in an `srcdoc` iframe, try to get the absolute URL + // from the `data-absolute-url` attribute if not passed explicitly + if (absolute_url === undefined && _is_frame_HTMLElement(frameElement) && frameElement.dataset.absoluteUrl !== undefined) { + absolute_url = frameElement.dataset.absoluteUrl } let loc: HTMLAnchorElement | Location @@ -21,6 +22,7 @@ export function _get_ws_url(app_path: string | undefined, absolute_url: string | loc = window.location } + const protocol = loc.protocol == "https:" ? "wss:" : "ws:" if (app_path != null) { if (app_path == "/") { app_path = "" @@ -32,6 +34,21 @@ export function _get_ws_url(app_path: string | undefined, absolute_url: string | return `${protocol}//${loc.host}${app_path}/ws` } +function _is_frame_HTMLElement(frame: Element | null): frame is HTMLIFrameElement { + // `frameElement` is a delicate construct; it allows the document inside the frame to access + // some (but not all) properties of the parent element in which the frame document is embedded. + // Because it lives in a different DOM context than the frame's `window`, we cannot just use + // `frameElement instanceof HTMLIFrameElement`; we could use `window.parent.HTMLIFrameElement` + // but this can be blocked by CORS policy and throw an exception. + if (frame === null) { + return false + } + if (frame.tagName.toUpperCase() === "IFRAME") { + return true + } + return false +} + type WebSocketURL = string type SessionID = string diff --git a/bokehjs/src/lib/models/annotations/area_visuals.ts b/bokehjs/src/lib/models/annotations/area_visuals.ts new file mode 100644 index 00000000000..59c5f142fff --- /dev/null +++ b/bokehjs/src/lib/models/annotations/area_visuals.ts @@ -0,0 +1,48 @@ +import {Model} from "../../model" +import * as mixins from "core/property_mixins" +import type * as visuals from "core/visuals" +import type * as p from "core/properties" + +export namespace AreaVisuals { + export type Attrs = p.AttrsOf + + export type Props = Model.Props & {} & Mixins + + export type Mixins = + mixins.Line & mixins.Fill & mixins.Hatch & + mixins.HoverLine & mixins.HoverFill & mixins.HoverHatch + + export type Visuals = { + line: visuals.Line + fill: visuals.Fill + hatch: visuals.Hatch + hover_line: visuals.Line + hover_fill: visuals.Fill + hover_hatch: visuals.Hatch + } +} + +export interface AreaVisuals extends AreaVisuals.Attrs {} + +export class AreaVisuals extends Model { + declare properties: AreaVisuals.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + override clone(attrs?: Partial): this { + return super.clone(attrs) + } + + static { + this.mixins([ + mixins.Line, + mixins.Fill, + mixins.Hatch, + ["hover_", mixins.Line], + ["hover_", mixins.Fill], + ["hover_", mixins.Hatch], + ]) + } +} diff --git a/bokehjs/src/lib/models/annotations/box_annotation.ts b/bokehjs/src/lib/models/annotations/box_annotation.ts index 461f54c465b..3f665fd8981 100644 --- a/bokehjs/src/lib/models/annotations/box_annotation.ts +++ b/bokehjs/src/lib/models/annotations/box_annotation.ts @@ -1,7 +1,10 @@ import {Annotation, AnnotationView} from "./annotation" +import {Model} from "../../model" +import {AreaVisuals} from "./area_visuals" import type {Scale} from "../scales/scale" import type {AutoRanged} from "../ranges/data_range1d" import {auto_ranged} from "../ranges/data_range1d" +import type {ViewOf, BuildResult} from "core/build_views" import * as mixins from "core/property_mixins" import type * as visuals from "core/visuals" import {CoordinateUnits} from "core/enums" @@ -14,17 +17,89 @@ import {Signal} from "core/signaling" import type {Rect} from "core/types" import {clamp} from "core/util/math" import {assert} from "core/util/assert" +import {values} from "core/util/object" import {BorderRadius} from "../common/kinds" import * as Box from "../common/box_kinds" import {round_rect} from "../common/painting" import * as resolve from "../common/resolve" import {Node} from "../coordinates/node" import {Coordinate} from "../coordinates/coordinate" +import type {Renderer} from "../renderers/renderer" export const EDGE_TOLERANCE = 2.5 const {abs} = Math +export namespace BoxInteractionHandles { + export type Attrs = p.AttrsOf + + export type Props = Model.Props & { + all: p.Property // move, resize + + move: p.Property + resize: p.Property // sides, corners + + sides: p.Property // left, right, top, bottom + corners: p.Property // top_left, top_right, bottom_left, bottom_right + + left: p.Property + right: p.Property + top: p.Property + bottom: p.Property + + top_left: p.Property + top_right: p.Property + bottom_left: p.Property + bottom_right: p.Property + } +} + +export interface BoxInteractionHandles extends BoxInteractionHandles.Attrs {} + +export class BoxInteractionHandles extends Model { + declare properties: BoxInteractionHandles.Props + declare __view_type__: BoxAnnotationView + + constructor(attrs?: Partial) { + super(attrs) + } + + static { + this.define(({Ref, Nullable}) => ({ + all: [ Ref(AreaVisuals) ], + + move: [ Nullable(Ref(AreaVisuals)), null ], + resize: [ Nullable(Ref(AreaVisuals)), null ], + + sides: [ Nullable(Ref(AreaVisuals)), null ], + corners: [ Nullable(Ref(AreaVisuals)), null ], + + left: [ Nullable(Ref(AreaVisuals)), null ], + right: [ Nullable(Ref(AreaVisuals)), null ], + top: [ Nullable(Ref(AreaVisuals)), null ], + bottom: [ Nullable(Ref(AreaVisuals)), null ], + + top_left: [ Nullable(Ref(AreaVisuals)), null ], + top_right: [ Nullable(Ref(AreaVisuals)), null ], + bottom_left: [ Nullable(Ref(AreaVisuals)), null ], + bottom_right: [ Nullable(Ref(AreaVisuals)), null ], + })) + } +} + +const DEFAULT_HANDLES = () => { + return new BoxInteractionHandles({ + all: new AreaVisuals({ + fill_color: "white", + fill_alpha: 1.0, + line_color: "black", + line_alpha: 1.0, + hover_fill_color: "lightgray", + hover_fill_alpha: 1.0, + }), + }) +} + export class BoxAnnotationView extends AnnotationView implements Pannable, Pinchable, Moveable, AutoRanged { declare model: BoxAnnotation declare visuals: BoxAnnotation.Visuals @@ -34,11 +109,120 @@ export class BoxAnnotationView extends AnnotationView implements Pannable, Pinch return this._bbox } + protected _handles: {[key in Box.HitTarget]: BoxAnnotation | null} + protected _handles_views: {[key in Box.HitTarget]?: ViewOf} = {} + + override initialize(): void { + super.initialize() + this._update_handles() + } + + protected _update_handles(): void { + const {editable, use_handles, handles} = this.model + if (editable && use_handles) { + const {movable, resizable} = this + + const common: Partial = { + visible: true, + resizable: "none", + left_units: "canvas", + right_units: "canvas", + top_units: "canvas", + bottom_units: "canvas", + level: this.model.level, + } + + function attrs_of(source: AreaVisuals) { + return { + ...mixins.attrs_of(source, "", mixins.Line, true), + ...mixins.attrs_of(source, "", mixins.Fill, true), + ...mixins.attrs_of(source, "", mixins.Hatch, true), + ...mixins.attrs_of(source, "hover_", mixins.Line, true), + ...mixins.attrs_of(source, "hover_", mixins.Fill, true), + ...mixins.attrs_of(source, "hover_", mixins.Hatch, true), + } + } + + const h = handles + const attrs = { + area: attrs_of(h.move ?? h.all), + left: attrs_of(h.left ?? h.sides ?? h.resize ?? h.all), + right: attrs_of(h.right ?? h.sides ?? h.resize ?? h.all), + top: attrs_of(h.top ?? h.sides ?? h.resize ?? h.all), + bottom: attrs_of(h.bottom ?? h.sides ?? h.resize ?? h.all), + top_left: attrs_of(h.top_left ?? h.corners ?? h.resize ?? h.all), + top_right: attrs_of(h.top_right ?? h.corners ?? h.resize ?? h.all), + bottom_left: attrs_of(h.bottom_left ?? h.corners ?? h.resize ?? h.all), + bottom_right: attrs_of(h.bottom_right ?? h.corners ?? h.resize ?? h.all), + } + + const { + tl_cursor, tr_cursor, bl_cursor, br_cursor, + ew_cursor, ns_cursor, + } = this.model + + this._handles = { + area: movable ? new BoxAnnotation({...common, ...attrs.area, movable: this.model.movable}) : null, + left: resizable.left ? new BoxAnnotation({...common, ...attrs.left, in_cursor: ew_cursor}) : null, + right: resizable.right ? new BoxAnnotation({...common, ...attrs.right, in_cursor: ew_cursor}) : null, + top: resizable.top ? new BoxAnnotation({...common, ...attrs.top, in_cursor: ns_cursor}) : null, + bottom: resizable.bottom ? new BoxAnnotation({...common, ...attrs.bottom, in_cursor: ns_cursor}) : null, + top_left: resizable.top_left ? new BoxAnnotation({...common, ...attrs.top_left, in_cursor: tl_cursor}) : null, + top_right: resizable.top_right ? new BoxAnnotation({...common, ...attrs.top_right, in_cursor: tr_cursor}) : null, + bottom_left: resizable.bottom_left ? new BoxAnnotation({...common, ...attrs.bottom_left, in_cursor: bl_cursor}) : null, + bottom_right: resizable.bottom_right ? new BoxAnnotation({...common, ...attrs.bottom_right, in_cursor: br_cursor}) : null, + } + } else { + this._handles = { + area: null, + left: null, + right: null, + top: null, + bottom: null, + top_left: null, + top_right: null, + bottom_left: null, + bottom_right: null, + } + } + } + + override get computed_renderers(): Renderer[] { + return [...super.computed_renderers, ...values(this._handles).filter((handle) => handle != null)] + } + override connect_signals(): void { super.connect_signals() + const {editable, use_handles, handles, resizable, movable} = this.model.properties + this.on_change([editable, use_handles, handles, resizable, movable], async () => { + this._update_handles() + await this._update_renderers() + }) this.connect(this.model.change, () => this.request_paint()) } + protected override async _build_renderers(): Promise> { + const build_result = await super._build_renderers() + + const get = (handle: Renderer | null) => { + return handle != null ? this._renderer_views.get(handle) as ViewOf | undefined : undefined + } + + this._handles_views = { + area: get(this._handles.area), + left: get(this._handles.left), + right: get(this._handles.right), + top: get(this._handles.top), + bottom: get(this._handles.bottom), + top_left: get(this._handles.top_left), + top_right: get(this._handles.top_right), + bottom_left: get(this._handles.bottom_left), + bottom_right: get(this._handles.bottom_right), + } + + return build_result + } + readonly [auto_ranged] = true bounds(): Rect { @@ -116,19 +300,40 @@ export class BoxAnnotationView extends AnnotationView implements Pannable, Pinch override compute_geometry(): void { super.compute_geometry() - const compute = (dim: "x" | "y", value: number | Coordinate, mapper: CoordinateMapper): number => { - return value instanceof Coordinate ? this.resolve_as_scalar(value, dim) : mapper.compute(value) - } + const bbox = (() => { + const compute = (dim: "x" | "y", value: number | Coordinate, mapper: CoordinateMapper): number => { + return value instanceof Coordinate ? this.resolve_as_scalar(value, dim) : mapper.compute(value) + } - const {left, right, top, bottom} = this.model - const {mappers} = this + const {left, right, top, bottom} = this.model + const {mappers} = this - this._bbox = BBox.from_lrtb({ - left: compute("x", left, mappers.left), - right: compute("x", right, mappers.right), - top: compute("y", top, mappers.top), - bottom: compute("y", bottom, mappers.bottom), - }) + return BBox.from_lrtb({ + left: compute("x", left, mappers.left), + right: compute("x", right, mappers.right), + top: compute("y", top, mappers.top), + bottom: compute("y", bottom, mappers.bottom), + }) + })() + this._bbox = bbox + + const width = 10 + const height = 10 + + function update(renderer: BoxAnnotation | null, bbox: BBox): void { + const {left, right, top, bottom} = bbox + renderer?.setv({left, right, top, bottom}, {silent: true}) + } + + update(this._handles.area, new BBox({...bbox.center, width, height, origin: "center"})) + update(this._handles.left, new BBox({...bbox.center_left, width, height, origin: "center"})) + update(this._handles.right, new BBox({...bbox.center_right, width, height, origin: "center"})) + update(this._handles.top, new BBox({...bbox.top_center, width, height, origin: "center"})) + update(this._handles.bottom, new BBox({...bbox.bottom_center, width, height, origin: "center"})) + update(this._handles.top_left, new BBox({...bbox.top_left, width, height, origin: "center"})) + update(this._handles.top_right, new BBox({...bbox.top_right, width, height, origin: "center"})) + update(this._handles.bottom_left, new BBox({...bbox.bottom_left, width, height, origin: "center"})) + update(this._handles.bottom_right, new BBox({...bbox.bottom_right, width, height, origin: "center"})) } protected _paint(): void { @@ -177,7 +382,7 @@ export class BoxAnnotationView extends AnnotationView implements Pannable, Pinch } override interactive_hit(sx: number, sy: number): boolean { - if (!this.model.visible || !this.model.editable) { + if (!this.model.visible) { return false } const bbox = this.interactive_bbox() @@ -193,51 +398,94 @@ export class BoxAnnotationView extends AnnotationView implements Pannable, Pinch const dt = abs(top - sy) const db = abs(bottom - sy) - const hits_left = dl < tolerance && dl < dr - const hits_right = dr < tolerance && dr < dl - const hits_top = dt < tolerance && dt < db - const hits_bottom = db < tolerance && db < dt + const hits = { + left: dl < tolerance && dl < dr, + right: dr < tolerance && dr < dl, + top: dt < tolerance && dt < db, + bottom: db < tolerance && db < dt, + } - if (hits_top && hits_left) { + const hittable = this._hittable() + + const hits_handle = (hit_target: Box.HitTarget, condition: boolean): boolean => { + if (!hittable[hit_target]) { + return false + } + const handle = this._handles_views[hit_target] + if (handle != null) { + return handle.bbox.contains(sx, sy) + } else { + return condition + } + } + + if (hits_handle("top_left", hits.top && hits.left)) { return "top_left" } - if (hits_top && hits_right) { + if (hits_handle("top_right", hits.top && hits.right)) { return "top_right" } - if (hits_bottom && hits_left) { + if (hits_handle("bottom_left", hits.bottom && hits.left)) { return "bottom_left" } - if (hits_bottom && hits_right) { + if (hits_handle("bottom_right", hits.bottom && hits.right)) { return "bottom_right" } - if (hits_left) { + if (hits_handle("left", hits.left)) { return "left" } - if (hits_right) { + if (hits_handle("right", hits.right)) { return "right" } - if (hits_top) { + if (hits_handle("top", hits.top)) { return "top" } - if (hits_bottom) { + if (hits_handle("bottom", hits.bottom)) { return "bottom" } - if (this.bbox.contains(sx, sy)) { + if (hits_handle("area", this.bbox.contains(sx, sy))) { return "area" } return null } - get resizable(): LRTB { + get resizable(): LRTB & Corners { const {resizable} = this.model + const left = resizable == "left" || resizable == "x" || resizable == "all" + const right = resizable == "right" || resizable == "x" || resizable == "all" + const top = resizable == "top" || resizable == "y" || resizable == "all" + const bottom = resizable == "bottom" || resizable == "y" || resizable == "all" + return { + left, + right, + top, + bottom, + top_left: top && left, + top_right: top && right, + bottom_left: bottom && left, + bottom_right: bottom && right, + } + } + + get movable(): boolean { + return this.model.movable != "none" + } + + private _hittable(): {[key in Box.HitTarget]: boolean} { + const {left, right, top, bottom} = this.resizable return { - left: resizable == "left" || resizable == "x" || resizable == "all", - right: resizable == "right" || resizable == "x" || resizable == "all", - top: resizable == "top" || resizable == "y" || resizable == "all", - bottom: resizable == "bottom" || resizable == "y" || resizable == "all", + top_left: top && left, + top_right: top && right, + bottom_left: bottom && left, + bottom_right: bottom && right, + left, + right, + top, + bottom, + area: this.movable, } } @@ -252,7 +500,7 @@ export class BoxAnnotationView extends AnnotationView implements Pannable, Pinch case "right": return right case "top": return top case "bottom": return bottom - case "area": return this.model.movable != "none" + case "area": return this.movable } } @@ -519,21 +767,32 @@ export class BoxAnnotationView extends AnnotationView implements Pannable, Pinch if (target == null || !this._can_hit(target)) { return null } + + const { + tl_cursor, tr_cursor, bl_cursor, br_cursor, + ew_cursor, ns_cursor, + in_cursor, + } = this.model + switch (target) { - case "top_left": return this.model.tl_cursor - case "top_right": return this.model.tr_cursor - case "bottom_left": return this.model.bl_cursor - case "bottom_right": return this.model.br_cursor - case "left": - case "right": return this.model.ew_cursor - case "top": - case "bottom": return this.model.ns_cursor + case "top_left": return this._handles.top_left == null ? tl_cursor : null + case "top_right": return this._handles.top_right == null ? tr_cursor : null + case "bottom_left": return this._handles.bottom_left == null ? bl_cursor : null + case "bottom_right": return this._handles.bottom_right == null ? br_cursor : null + case "left": return this._handles.left == null ? ew_cursor : null + case "right": return this._handles.right == null ? ew_cursor : null + case "top": return this._handles.top == null ? ns_cursor : null + case "bottom": return this._handles.bottom == null ? ns_cursor : null case "area": { - switch (this.model.movable) { - case "both": return this.model.in_cursor - case "x": return this.model.ew_cursor - case "y": return this.model.ns_cursor - case "none": return null + if (this._handles.area == null) { + switch (this.model.movable) { + case "both": return in_cursor + case "x": return ew_cursor + case "y": return ns_cursor + case "none": return null + } + } else { + return null } } } @@ -571,6 +830,9 @@ export namespace BoxAnnotation { movable: p.Property symmetric: p.Property + use_handles: p.Property + handles: p.Property + inverted: p.Property tl_cursor: p.Property @@ -606,6 +868,10 @@ export class BoxAnnotation extends Annotation { super(attrs) } + override clone(attrs?: Partial): this { + return super.clone(attrs) + } + static { this.prototype.default_view = BoxAnnotationView @@ -646,6 +912,9 @@ export class BoxAnnotation extends Annotation { movable: [ Box.Movable, "both" ], symmetric: [ Bool, false ], + use_handles: [ Bool, false ], + handles: [ Ref(BoxInteractionHandles), DEFAULT_HANDLES ], + inverted: [ Bool, false ], })) diff --git a/bokehjs/src/lib/models/annotations/html/text_annotation.ts b/bokehjs/src/lib/models/annotations/html/text_annotation.ts index f7e77edd101..2ee2f20baf2 100644 --- a/bokehjs/src/lib/models/annotations/html/text_annotation.ts +++ b/bokehjs/src/lib/models/annotations/html/text_annotation.ts @@ -70,6 +70,7 @@ export abstract class TextAnnotationView extends AnnotationView { this.position.replace(` :host { + position: absolute; left: ${sx}px; top: ${sy}px; } diff --git a/bokehjs/src/lib/models/annotations/index.ts b/bokehjs/src/lib/models/annotations/index.ts index 1ecfb34db37..f7383be3e2b 100644 --- a/bokehjs/src/lib/models/annotations/index.ts +++ b/bokehjs/src/lib/models/annotations/index.ts @@ -1,3 +1,4 @@ +export {AreaVisuals} from "./area_visuals" export {Annotation} from "./annotation" export {Arrow} from "./arrow" export {ArrowHead} from "./arrow_head" @@ -7,7 +8,7 @@ export {TeeHead} from "./arrow_head" export {VeeHead} from "./arrow_head" export {BaseColorBar} from "./base_color_bar" export {Band} from "./band" -export {BoxAnnotation} from "./box_annotation" +export {BoxAnnotation, BoxInteractionHandles} from "./box_annotation" export {ColorBar} from "./color_bar" export {ContourColorBar} from "./contour_color_bar" export {Label} from "./label" diff --git a/bokehjs/src/lib/models/annotations/legend.ts b/bokehjs/src/lib/models/annotations/legend.ts index a8f2aa7489a..b0bcd7e9a14 100644 --- a/bokehjs/src/lib/models/annotations/legend.ts +++ b/bokehjs/src/lib/models/annotations/legend.ts @@ -10,11 +10,13 @@ import type {Size} from "core/layout" import {SideLayout, SidePanel} from "core/layout/side_panel" import {BBox} from "core/util/bbox" import {every, some} from "core/util/array" +import {dict} from "core/util/object" import {enumerate} from "core/util/iterator" import {isString} from "core/util/types" import type {Context2d} from "core/util/canvas" import {TextBox} from "core/graphics" import {Column, Row, Grid, ContentLayoutable, Sizeable, TextLayout} from "core/layout" +import {LegendItemClick} from "core/bokeh_events" const {max, ceil} = Math @@ -294,7 +296,7 @@ export class LegendView extends AnnotationView { } override cursor(sx: number, sy: number): string | null { - if (this.model.click_policy == "none") { + if (this.model.click_policy == "none" && !dict(this.model.js_event_callbacks).has("legend_item_click")) { // this doesn't cover server callbacks return null } if (this._hit_test(sx, sy) != null) { @@ -314,8 +316,9 @@ export class LegendView extends AnnotationView { const target = this._hit_test(sx, sy) if (target != null) { - const {renderers} = target.entry.item - for (const renderer of renderers) { + const {item} = target.entry + this.model.trigger_event(new LegendItemClick(this.model, item)) + for (const renderer of item.renderers) { fn(renderer) } return true @@ -423,7 +426,7 @@ export class LegendView extends AnnotationView { const y1 = y0 + glyph_height for (const renderer of item.renderers) { - const view = this.plot_view.renderer_view(renderer) + const view = this.plot_view.views.find_one(renderer) view?.draw_legend(ctx, x0, x1, y0, y1, field, label, item.index) } diff --git a/bokehjs/src/lib/models/canvas/cartesian_frame.ts b/bokehjs/src/lib/models/canvas/cartesian_frame.ts index 456df898659..c8facb8c4a8 100644 --- a/bokehjs/src/lib/models/canvas/cartesian_frame.ts +++ b/bokehjs/src/lib/models/canvas/cartesian_frame.ts @@ -16,6 +16,8 @@ import {assert} from "core/util/assert" import {isNumber} from "core/util/types" import type {Dict} from "core/types" import type * as p from "core/properties" +import {InlineStyleSheet} from "core/dom" +import type {StyleSheetLike} from "core/dom" type Ranges = Dict type Scales = Dict @@ -145,6 +147,7 @@ export class CartesianFrameView extends StyledElementView { set_geometry(bbox: BBox): void { this._bbox = bbox this._update_scales() + this._update_position() } get x_range(): Range { @@ -207,6 +210,41 @@ export class CartesianFrameView extends StyledElementView { return {x: x + offset, y: y + offset} } } + + readonly position = new InlineStyleSheet() + + override stylesheets(): StyleSheetLike[] { + return [...super.stylesheets(), this.position] + } + + override rendering_target(): HTMLElement { + return this.parent.canvas_view.underlays_el + } + + /** + * Updates the position of the associated DOM element. + */ + protected _update_position(): void { + const {bbox, position} = this + if (bbox.is_valid) { + position.replace(` + :host { + position: absolute; + left: ${bbox.left}px; + top: ${bbox.top}px; + width: ${bbox.width}px; + height: ${bbox.height}px; + } + `) + } else { + position.replace(` + :host { + display: none; + } + `) + } + } + } export namespace CartesianFrame { diff --git a/bokehjs/src/lib/models/comparisons/comparison.ts b/bokehjs/src/lib/models/comparisons/comparison.ts new file mode 100644 index 00000000000..ccf98e35b86 --- /dev/null +++ b/bokehjs/src/lib/models/comparisons/comparison.ts @@ -0,0 +1,20 @@ +import {Model} from "../../model" +import type * as p from "core/properties" + +export namespace Comparison { + export type Attrs = p.AttrsOf + + export type Props = Model.Props +} + +export interface Comparison extends Comparison.Attrs {} + +export abstract class Comparison extends Model { + declare properties: Comparison.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + abstract compute(x: unknown, y: unknown): -1 | 0 | 1 +} diff --git a/bokehjs/src/lib/models/comparisons/customjs_compare.ts b/bokehjs/src/lib/models/comparisons/customjs_compare.ts new file mode 100644 index 00000000000..02a94013853 --- /dev/null +++ b/bokehjs/src/lib/models/comparisons/customjs_compare.ts @@ -0,0 +1,49 @@ +import {Comparison} from "./comparison" +import type * as p from "core/properties" +import type {Dict} from "core/types" +import {keys, values} from "core/util/object" +import {use_strict} from "core/util/string" + +export namespace CustomJSCompare { + export type Attrs = p.AttrsOf + + export type Props = Comparison.Props & { + args: p.Property> + code: p.Property + } +} + +export interface CustomJSCompare extends CustomJSCompare.Attrs {} + +export class CustomJSCompare extends Comparison { + declare properties: CustomJSCompare.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + static { + this.define(({Unknown, Str, Dict}) => ({ + args: [ Dict(Unknown), {} ], + code: [ Str, "" ], + })) + } + + get names(): string[] { + return keys(this.args) + } + + get values(): unknown[] { + return values(this.args) + } + + private _make_func(): Function { + const code = use_strict(this.code) + return new Function("x", "y", ...this.names, code) + } + + compute(x: unknown, y: unknown): 0 | 1 | -1 { + const func = this._make_func() + return func(x, y, this.values) + } +} diff --git a/bokehjs/src/lib/models/comparisons/index.ts b/bokehjs/src/lib/models/comparisons/index.ts new file mode 100644 index 00000000000..3d10cb478f3 --- /dev/null +++ b/bokehjs/src/lib/models/comparisons/index.ts @@ -0,0 +1,3 @@ +export {Comparison} from "./comparison" +export {CustomJSCompare} from "./customjs_compare" +export {NanCompare} from "./nan_compare" diff --git a/bokehjs/src/lib/models/comparisons/nan_compare.ts b/bokehjs/src/lib/models/comparisons/nan_compare.ts new file mode 100644 index 00000000000..e8985699576 --- /dev/null +++ b/bokehjs/src/lib/models/comparisons/nan_compare.ts @@ -0,0 +1,40 @@ +import {Comparison} from "./comparison" +import {isNumber} from "core/util/types" +import type * as p from "core/properties" + +export namespace NanCompare { + export type Attrs = p.AttrsOf + + export type Props = Comparison.Props & { + ascending_first: p.Property + } +} + +export interface NanCompare extends NanCompare.Attrs {} + +export class NanCompare extends Comparison { + declare properties: NanCompare.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + static { + this.define(({Bool}) => ({ + ascending_first: [ Bool, false ], + })) + } + + compute(x: unknown, y: unknown): 0 | 1 | -1 { + if (isNumber(x) && isNaN(x)) { + return this.ascending_first ? -1 : 1 + } + if (isNumber(y) && isNaN(y)) { + return this.ascending_first ? 1 : -1 + } + if (isNumber(x) && isNumber(y)) { + return x==y ? 0 : x < y ? -1 : 1 + } + return 0 + } +} diff --git a/bokehjs/src/lib/models/dom/color_ref.ts b/bokehjs/src/lib/models/dom/color_ref.ts index ac72982fbd2..2ba404f2fec 100644 --- a/bokehjs/src/lib/models/dom/color_ref.ts +++ b/bokehjs/src/lib/models/dom/color_ref.ts @@ -1,8 +1,10 @@ import {ValueRef, ValueRefView} from "./value_ref" +import type {Formatters} from "./placeholder" import type {ColumnarDataSource} from "../sources/columnar_data_source" -import type {Index as DataIndex} from "core/util/templating" +import type {Index} from "core/util/templating" import {_get_column_value} from "core/util/templating" import {span} from "core/dom" +import type {PlainObject} from "core/types" import type * as p from "core/properties" import * as styles from "styles/tooltips.css" @@ -22,9 +24,9 @@ export class ColorRefView extends ValueRefView { this.el.appendChild(this.swatch_el) } - override update(source: ColumnarDataSource, i: DataIndex | null, _vars: object/*, formatters?: Formatters*/): void { + override update(source: ColumnarDataSource, i: Index | null, _vars: PlainObject, _formatters?: Formatters): void { const value = _get_column_value(this.model.field, source, i) - const text = value == null ? "???" : `${value}` //.toString() + const text = value == null ? "???" : `${value}` this.el.textContent = text } } diff --git a/bokehjs/src/lib/models/dom/html.ts b/bokehjs/src/lib/models/dom/html.ts index 75475456aff..8d767d63478 100644 --- a/bokehjs/src/lib/models/dom/html.ts +++ b/bokehjs/src/lib/models/dom/html.ts @@ -1,20 +1,20 @@ -import {DOMNode, DOMNodeView} from "./dom_node" +import {DOMElement, DOMElementView} from "./dom_element" import {UIElement} from "../ui/ui_element" import type {ViewStorage, IterViews} from "core/build_views" import {build_views, remove_views} from "core/build_views" -import {empty, span} from "core/dom" +import {span} from "core/dom" import {assert} from "core/util/assert" import {isString, isArray} from "core/util/types" import type * as p from "core/properties" import {Str, Ref, Or} from "core/kinds" -const HTMLRef = Or(Ref(DOMNode), Ref(UIElement)) +const HTMLRef = Or(Ref(DOMElement), Ref(UIElement)) type HTMLRef = typeof HTMLRef["__type__"] const HTMLMarkup = Str type RawHTML = typeof HTMLMarkup["__type__"] -export class HTMLView extends DOMNodeView { +export class HTMLView extends DOMElementView { declare model: HTML declare el: HTMLElement @@ -43,8 +43,8 @@ export class HTMLView extends DOMNodeView { super.remove() } - render(): void { - empty(this.el) + override render(): void { + super.render() const html = (() => { const {html} = this.model @@ -113,7 +113,7 @@ export class HTMLView extends DOMNodeView { export namespace HTML { export type Attrs = p.AttrsOf - export type Props = DOMNode.Props & { + export type Props = DOMElement.Props & { html: p.Property refs: p.Property } @@ -121,7 +121,7 @@ export namespace HTML { export interface HTML extends HTML.Attrs {} -export class HTML extends DOMNode { +export class HTML extends DOMElement { declare properties: HTML.Props declare __view_type__: HTMLView diff --git a/bokehjs/src/lib/models/dom/index_.ts b/bokehjs/src/lib/models/dom/index_.ts index 9f8b416350c..3abdd9ea0ae 100644 --- a/bokehjs/src/lib/models/dom/index_.ts +++ b/bokehjs/src/lib/models/dom/index_.ts @@ -1,13 +1,15 @@ import {Placeholder, PlaceholderView} from "./placeholder" +import type {Formatters} from "./placeholder" import type {ColumnarDataSource} from "../sources/columnar_data_source" import type {Index as DataIndex} from "core/util/templating" +import type {PlainObject} from "core/types" import type * as p from "core/properties" export class IndexView extends PlaceholderView { declare model: Index - update(_source: ColumnarDataSource, i: DataIndex | null, _vars: object/*, formatters?: Formatters*/): void { - this.el.textContent = i == null ? "(null)" : i.toString() + update(_source: ColumnarDataSource, i: DataIndex | null, _vars: PlainObject, _formatters?: Formatters): void { + this.el.textContent = i == null ? "(null)" : `${i}` } } diff --git a/bokehjs/src/lib/models/dom/placeholder.ts b/bokehjs/src/lib/models/dom/placeholder.ts index ff115ff5e4e..483cd7c9179 100644 --- a/bokehjs/src/lib/models/dom/placeholder.ts +++ b/bokehjs/src/lib/models/dom/placeholder.ts @@ -1,27 +1,34 @@ -import {DOMNode, DOMNodeView} from "./dom_node" +import {DOMElement, DOMElementView} from "./dom_element" +import {CustomJS} from "../callbacks/customjs" +import {CustomJSHover} from "../tools/inspectors/customjs_hover" import type {ColumnarDataSource} from "../sources/columnar_data_source" import type {Index as DataIndex} from "core/util/templating" import type * as p from "core/properties" +import type {Dict, PlainObject} from "core/types" +import {BuiltinFormatter} from "core/enums" +import {Or, Ref} from "core/kinds" -export abstract class PlaceholderView extends DOMNodeView { +export const Formatter = Or(BuiltinFormatter, Ref(CustomJS), Ref(CustomJSHover)) +export type Formatter = typeof Formatter["__type__"] + +export type Formatters = Dict + +export abstract class PlaceholderView extends DOMElementView { declare model: Placeholder - static override tag_name = "span" as const - override render(): void { - // XXX: no implementation? - } + static override tag_name = "span" as const - abstract update(source: ColumnarDataSource, i: DataIndex | null, vars: object/*, formatters?: Formatters*/): void + abstract update(source: ColumnarDataSource, i: DataIndex | null, vars: PlainObject, formatters?: Formatters): void } export namespace Placeholder { export type Attrs = p.AttrsOf - export type Props = DOMNode.Props + export type Props = DOMElement.Props } export interface Placeholder extends Placeholder.Attrs {} -export abstract class Placeholder extends DOMNode { +export abstract class Placeholder extends DOMElement { declare properties: Placeholder.Props declare __view_type__: PlaceholderView diff --git a/bokehjs/src/lib/models/dom/template.ts b/bokehjs/src/lib/models/dom/template.ts index cdaf838a43e..fc3c5cc6c0a 100644 --- a/bokehjs/src/lib/models/dom/template.ts +++ b/bokehjs/src/lib/models/dom/template.ts @@ -1,15 +1,16 @@ import {DOMElement, DOMElementView} from "./dom_element" import {Action} from "./action" import {PlaceholderView} from "./placeholder" +import type {Formatters} from "./placeholder" import type {ColumnarDataSource} from "../sources/columnar_data_source" -import type {Index as DataIndex} from "core/util/templating" +import type {Index} from "core/util/templating" import type {ViewStorage, IterViews} from "core/build_views" -import {build_views, remove_views} from "core/build_views" +import {build_views, remove_views, traverse_views} from "core/build_views" +import type {PlainObject} from "core/types" import type * as p from "core/properties" export class TemplateView extends DOMElementView { declare model: Template - static override tag_name = "div" as const readonly action_views: ViewStorage = new Map() @@ -28,19 +29,12 @@ export class TemplateView extends DOMElementView { super.remove() } - update(source: ColumnarDataSource, i: DataIndex | null, vars: object = {}/*, formatters?: Formatters*/): void { - function descend(obj: DOMElementView): void { - for (const child of obj.child_views.values()) { - if (child instanceof PlaceholderView) { - child.update(source, i, vars) - } else if (child instanceof DOMElementView) { - descend(child) - } + update(source: ColumnarDataSource, i: Index | null, vars: PlainObject, formatters?: Formatters): void { + traverse_views([this], (view) => { + if (view instanceof PlaceholderView) { + view.update(source, i, vars, formatters) } - } - - descend(this) - + }) for (const action of this.action_views.values()) { action.update(source, i, vars) } diff --git a/bokehjs/src/lib/models/dom/value_of.ts b/bokehjs/src/lib/models/dom/value_of.ts index 22d1be43f1f..989fe791966 100644 --- a/bokehjs/src/lib/models/dom/value_of.ts +++ b/bokehjs/src/lib/models/dom/value_of.ts @@ -1,12 +1,10 @@ -import {DOMNode, DOMNodeView} from "./dom_node" +import {DOMElement, DOMElementView} from "./dom_element" import {HasProps} from "core/has_props" -import {empty} from "core/dom" import {to_string} from "core/util/pretty" import type * as p from "core/properties" -export class ValueOfView extends DOMNodeView { +export class ValueOfView extends DOMElementView { declare model: ValueOf - declare el: HTMLElement override connect_signals(): void { super.connect_signals() @@ -17,8 +15,8 @@ export class ValueOfView extends DOMNodeView { } } - render(): void { - empty(this.el) + override render(): void { + super.render() this.el.style.display = "contents" const text = (() => { @@ -37,7 +35,7 @@ export class ValueOfView extends DOMNodeView { export namespace ValueOf { export type Attrs = p.AttrsOf - export type Props = DOMNode.Props & { + export type Props = DOMElement.Props & { obj: p.Property attr: p.Property } @@ -45,7 +43,7 @@ export namespace ValueOf { export interface ValueOf extends ValueOf.Attrs {} -export class ValueOf extends DOMNode { +export class ValueOf extends DOMElement { declare properties: ValueOf.Props declare __view_type__: ValueOfView diff --git a/bokehjs/src/lib/models/dom/value_ref.ts b/bokehjs/src/lib/models/dom/value_ref.ts index 52a2d673e00..ac3d0440e0f 100644 --- a/bokehjs/src/lib/models/dom/value_ref.ts +++ b/bokehjs/src/lib/models/dom/value_ref.ts @@ -1,16 +1,53 @@ -import {Placeholder, PlaceholderView} from "./placeholder" +import {Placeholder, PlaceholderView, Formatter} from "./placeholder" +import type {Formatters} from "./placeholder" +import {CustomJS} from "../callbacks/customjs" +import {CustomJSHover} from "../tools/inspectors/customjs_hover" import type {ColumnarDataSource} from "../sources/columnar_data_source" -import type {Index as DataIndex} from "core/util/templating" -import {_get_column_value} from "core/util/templating" +import type {Index} from "core/util/templating" +import {_get_column_value, MISSING, DEFAULT_FORMATTERS} from "core/util/templating" +import {execute} from "core/util/callbacks" +import {isArray} from "core/util/types" import type * as p from "core/properties" +import type {PlainObject} from "core/types" export class ValueRefView extends PlaceholderView { declare model: ValueRef - update(source: ColumnarDataSource, i: DataIndex | null, _vars: object/*, formatters?: Formatters*/): void { - const value = _get_column_value(this.model.field, source, i) - const text = value == null ? "???" : `${value}` //.toString() - this.el.textContent = text + update(source: ColumnarDataSource, i: Index | null, vars: PlainObject, _formatters?: Formatters): void { + const {field, format, formatter} = this.model + const value = _get_column_value(field, source, i) + + const render = (output: unknown) => { + if (output == null) { + this.el.textContent = MISSING + } else if (output instanceof Node) { + this.el.replaceChildren(output) + } else if (isArray(output)) { + this.el.replaceChildren(...output.map((item) => item instanceof Node ? item : `${item}`)) + } else { + this.el.textContent = `${output}` + } + } + + if (formatter instanceof CustomJS) { + void (async () => { + const output = await execute(formatter, this.model, {value, format, vars}) + render(output) + })() + } else { + const output = (() => { + if (format == null) { + return DEFAULT_FORMATTERS.basic(value, "", vars) + } else { + if (formatter instanceof CustomJSHover) { + return formatter.format(value, format, vars) + } else { + return DEFAULT_FORMATTERS[formatter](value, format, vars) + } + } + })() + render(output) + } } } @@ -18,6 +55,8 @@ export namespace ValueRef { export type Attrs = p.AttrsOf export type Props = Placeholder.Props & { field: p.Property + format: p.Property + formatter: p.Property } } @@ -33,8 +72,10 @@ export class ValueRef extends Placeholder { static { this.prototype.default_view = ValueRefView - this.define(({Str}) => ({ + this.define(({Str, Nullable}) => ({ field: [ Str ], + format: [ Nullable(Str), null ], + formatter: [ Formatter, "raw" ], })) } } diff --git a/bokehjs/src/lib/models/glyphs/defs.ts b/bokehjs/src/lib/models/glyphs/defs.ts index 653b3973bdf..00908be666e 100644 --- a/bokehjs/src/lib/models/glyphs/defs.ts +++ b/bokehjs/src/lib/models/glyphs/defs.ts @@ -2,6 +2,8 @@ import type {MarkerType} from "core/enums" import type {LineVector, FillVector, HatchVector} from "core/visuals" import type {Context2d} from "core/util/canvas" +export type VectorVisuals = {line: LineVector, fill: FillVector, hatch: HatchVector} + const SQ3 = Math.sqrt(3) const SQ5 = Math.sqrt(5) const c36 = (SQ5+1)/4 @@ -91,8 +93,6 @@ function _one_tri(ctx: Context2d, r: number): void { ctx.closePath() } -type VectorVisuals = {line: LineVector, fill: FillVector, hatch: HatchVector} - function asterisk(ctx: Context2d, i: number, r: number, visuals: VectorVisuals): void { _one_cross(ctx, r) _one_x(ctx, r) @@ -342,7 +342,7 @@ function y(ctx: Context2d, i: number, r: number, visuals: VectorVisuals): void { export type RenderOne = (ctx: Context2d, i: number, r: number, visuals: VectorVisuals) => void -export const marker_funcs: {[key in MarkerType]: RenderOne} = { +export const marker_funcs = { asterisk, circle, circle_cross, @@ -371,4 +371,4 @@ export const marker_funcs: {[key in MarkerType]: RenderOne} = { dash, x, y, -} +} satisfies {[key in MarkerType]: RenderOne} diff --git a/bokehjs/src/lib/models/glyphs/glyph.ts b/bokehjs/src/lib/models/glyphs/glyph.ts index eea9f708d7b..9a0d9103edb 100644 --- a/bokehjs/src/lib/models/glyphs/glyph.ts +++ b/bokehjs/src/lib/models/glyphs/glyph.ts @@ -317,7 +317,11 @@ export abstract class GlyphView extends DOMComponentView { }) } - protected _can_inherit_from(prop: p.Property, base: this): boolean { + protected _can_inherit_from(prop: p.Property, base: this | null): boolean { + if (base == null) { + return false + } + const base_prop = base.model.property(prop.attr) const value = prop.get_value() diff --git a/bokehjs/src/lib/models/glyphs/image.ts b/bokehjs/src/lib/models/glyphs/image.ts index 8400913272d..e374e68b271 100644 --- a/bokehjs/src/lib/models/glyphs/image.ts +++ b/bokehjs/src/lib/models/glyphs/image.ts @@ -36,6 +36,11 @@ export class ImageView extends ImageBaseView { } } + protected override get _can_inherit_image_data(): boolean { + return super._can_inherit_image_data && + this._can_inherit_from(this.model.properties.color_mapper, this.base) + } + protected _flat_img_to_buf8(img: NDArrayType): Uint8ClampedArray { const cmap = this.model.color_mapper.rgba_mapper return cmap.v_compute(img) diff --git a/bokehjs/src/lib/models/glyphs/image_base.ts b/bokehjs/src/lib/models/glyphs/image_base.ts index 33a99d8a60a..02797a9f420 100644 --- a/bokehjs/src/lib/models/glyphs/image_base.ts +++ b/bokehjs/src/lib/models/glyphs/image_base.ts @@ -25,9 +25,6 @@ export abstract class ImageBaseView extends XYGlyphView { declare model: ImageBase declare visuals: ImageBase.Visuals - protected _width: Uint32Array - protected _height: Uint32Array - override connect_signals(): void { super.connect_signals() this.connect(this.model.properties.global_alpha.change, () => this.renderer.request_paint()) @@ -107,31 +104,41 @@ export abstract class ImageBaseView extends XYGlyphView { protected abstract _flat_img_to_buf8(img: NDArrayType): Uint8ClampedArray + protected get _can_inherit_image_data(): boolean { + return this.inherited_image + } + protected override _set_data(indices: number[] | null): void { const n = this.data_size - if (this.image_data == null || this.image_data.length != n) { - this.image_data = new Array(n).fill(null) - this._width = new Uint32Array(n) - this._height = new Uint32Array(n) - } + if (!this._can_inherit_image_data) { + if (typeof this.image_data === "undefined" || this.image_data.length != n) { + this._define_attr("image_data", new Array(n).fill(null)) + this._define_attr("image_width", new Uint32Array(n)) + this._define_attr("image_height", new Uint32Array(n)) + } - const {image_dimension} = this + const {image_dimension} = this - for (let i = 0; i < n; i++) { - if (indices != null && !indices.includes(i)) { - continue - } + for (let i = 0; i < n; i++) { + if (indices != null && !indices.includes(i)) { + continue + } - const img = this.image.get(i) - assert(img.dimension == image_dimension, `expected a ${image_dimension}D array, not ${img.dimension}D`) + const img = this.image.get(i) + assert(img.dimension == image_dimension, `expected a ${image_dimension}D array, not ${img.dimension}D`) - const [width, height] = img.shape - this._height[i] = width - this._width[i] = height + const [height, width] = img.shape + this.image_width[i] = width + this.image_height[i] = height - const buf8 = this._flat_img_to_buf8(img) - this._set_image_data_from_buffer(i, buf8) + const buf8 = this._flat_img_to_buf8(img) + this._set_image_data_from_buffer(i, buf8) + } + } else { + this._inherit_attr("image_data") + this._inherit_attr("image_width") + this._inherit_attr("image_height") } } @@ -164,13 +171,13 @@ export abstract class ImageBaseView extends XYGlyphView { protected _get_or_create_canvas(i: number): HTMLCanvasElement { assert(this.image_data != null) const image_data_i = this.image_data[i] - if (image_data_i != null && image_data_i.width == this._width[i] - && image_data_i.height == this._height[i]) { + if (image_data_i != null && image_data_i.width == this.image_width[i] + && image_data_i.height == this.image_height[i]) { return image_data_i } else { const canvas = document.createElement("canvas") - canvas.width = this._width[i] - canvas.height = this._height[i] + canvas.width = this.image_width[i] + canvas.height = this.image_height[i] return canvas } } @@ -179,7 +186,7 @@ export abstract class ImageBaseView extends XYGlyphView { assert(this.image_data != null) const canvas = this._get_or_create_canvas(i) const ctx = canvas.getContext("2d")! - const image_data = ctx.getImageData(0, 0, this._width[i], this._height[i]) + const image_data = ctx.getImageData(0, 0, this.image_width[i], this.image_height[i]) image_data.data.set(buf8) ctx.putImageData(image_data, 0, 0) this.image_data[i] = canvas @@ -213,8 +220,8 @@ export abstract class ImageBaseView extends XYGlyphView { protected _image_index(index: number, x: number, y: number): ImageIndex { const [l, r, t, b] = this._lrtb(index) - const width = this._width[index] - const height = this._height[index] + const width = this.image_width[index] + const height = this.image_height[index] const dx = (r - l) / width const dy = (t - b) / height const i = Math.floor((x - l) / dx) @@ -260,7 +267,14 @@ export namespace ImageBase { export type Visuals = XYGlyph.Visuals & {image: visuals.ImageVector} export type Data = p.GlyphDataOf & { - image_data: Arrayable | null + image_data: Arrayable | undefined + inherited_image_data: boolean + + image_width: Uint32Array + inherited_image_width: boolean + + image_height: Uint32Array + inherited_image_height: boolean } } diff --git a/bokehjs/src/lib/models/glyphs/image_stack.ts b/bokehjs/src/lib/models/glyphs/image_stack.ts index 53c8fd069c5..a2290d78b2b 100644 --- a/bokehjs/src/lib/models/glyphs/image_stack.ts +++ b/bokehjs/src/lib/models/glyphs/image_stack.ts @@ -39,6 +39,11 @@ export class ImageStackView extends ImageBaseView { } } + protected override get _can_inherit_image_data(): boolean { + return super._can_inherit_image_data && + this._can_inherit_from(this.model.properties.color_mapper, this.base) + } + protected _flat_img_to_buf8(img: NDArrayType): Uint8ClampedArray { const cmap = this.model.color_mapper.rgba_mapper return cmap.v_compute(img) diff --git a/bokehjs/src/lib/models/glyphs/math_text_glyph.ts b/bokehjs/src/lib/models/glyphs/math_text_glyph.ts index 213f16acc4e..c9b0b22b928 100644 --- a/bokehjs/src/lib/models/glyphs/math_text_glyph.ts +++ b/bokehjs/src/lib/models/glyphs/math_text_glyph.ts @@ -1,10 +1,10 @@ import {Text, TextView} from "./text" import type {BaseText} from "../text/base_text" import {MathTextView} from "../text/math_text" +import type {GraphicsBox} from "core/graphics" import type * as p from "core/properties" import type {ViewStorage, IterViews} from "core/build_views" import {build_views, remove_views} from "core/build_views" -import {non_null} from "core/util/types" import {enumerate} from "core/util/iterator" export interface MathTextGlyphView extends MathTextGlyph.Data {} @@ -41,16 +41,14 @@ export abstract class MathTextGlyphView extends TextView { protected abstract _build_label(text: string): BaseText - protected override async _build_labels(): Promise { - const {text} = this.base ?? this - + protected override async _build_labels(text: p.Uniform): Promise<(GraphicsBox | null)[]> { const labels = Array.from(text, (text_i) => { return text_i == null ? null : this._build_label(text_i) }) - await build_views(this._label_views, labels.filter(non_null), {parent: this.renderer}) + await build_views(this._label_views, labels.filter((v) => v != null), {parent: this.renderer}) - this.labels = labels.map((label_i) => { + return labels.map((label_i) => { return label_i == null ? null : this._label_views.get(label_i)!.graphics() }) } diff --git a/bokehjs/src/lib/models/glyphs/multi_polygons.ts b/bokehjs/src/lib/models/glyphs/multi_polygons.ts index 9638d837476..fb9686dfefb 100644 --- a/bokehjs/src/lib/models/glyphs/multi_polygons.ts +++ b/bokehjs/src/lib/models/glyphs/multi_polygons.ts @@ -105,10 +105,10 @@ export class MultiPolygonsView extends GlyphView { } protected override _mask_data(): Indices { - const {x_range, y_range} = this.renderer.plot_view.frame + const {x_source, y_source} = this.renderer.coordinates return this.index.indices({ - x0: x_range.min, x1: x_range.max, - y0: y_range.min, y1: y_range.max, + x0: x_source.min, x1: x_source.max, + y0: y_source.min, y1: y_source.max, }) } diff --git a/bokehjs/src/lib/models/glyphs/patches.ts b/bokehjs/src/lib/models/glyphs/patches.ts index af4824f30c7..22dc53ced3d 100644 --- a/bokehjs/src/lib/models/glyphs/patches.ts +++ b/bokehjs/src/lib/models/glyphs/patches.ts @@ -35,10 +35,10 @@ export class PatchesView extends GlyphView { } protected override _mask_data(): Indices { - const {x_range, y_range} = this.renderer.plot_view.frame + const {x_source, y_source} = this.renderer.coordinates return this.index.indices({ - x0: x_range.min, x1: x_range.max, - y0: y_range.min, y1: y_range.max, + x0: x_source.min, x1: x_source.max, + y0: y_source.min, y1: y_source.max, }) } diff --git a/bokehjs/src/lib/models/glyphs/tex_glyph.ts b/bokehjs/src/lib/models/glyphs/tex_glyph.ts index bec23bc61fa..04fcf84213a 100644 --- a/bokehjs/src/lib/models/glyphs/tex_glyph.ts +++ b/bokehjs/src/lib/models/glyphs/tex_glyph.ts @@ -6,8 +6,8 @@ import type {Dict} from "core/types" import {Enum, Or, Auto} from "core/kinds" import {parse_delimited_string} from "../text/utils" -const DisplayMode = Or(Enum("inline", "block"), Auto) -type DisplayMode = typeof DisplayMode["__type__"] +export const DisplayMode = Or(Enum("inline", "block"), Auto) +export type DisplayMode = typeof DisplayMode["__type__"] export interface TeXGlyphView extends TeXGlyph.Data {} diff --git a/bokehjs/src/lib/models/glyphs/text.ts b/bokehjs/src/lib/models/glyphs/text.ts index 1e7765c995c..d14faf2a633 100644 --- a/bokehjs/src/lib/models/glyphs/text.ts +++ b/bokehjs/src/lib/models/glyphs/text.ts @@ -17,8 +17,12 @@ import type {TextAnchor} from "../common/kinds" import {BorderRadius, Padding} from "../common/kinds" import * as resolve from "../common/resolve" import {round_rect} from "../common/painting" +import type {VectorVisuals} from "./defs" +import {sqrt, PI} from "core/util/math" +import type {OutlineShapeName} from "core/enums" class TextAnchorSpec extends p.DataSpec {} +class OutlineShapeSpec extends p.DataSpec {} export interface TextView extends Text.Data {} @@ -26,9 +30,8 @@ export class TextView extends XYGlyphView { declare model: Text declare visuals: Text.Visuals - protected async _build_labels(): Promise { - const {text} = this.base ?? this - this.labels = Array.from(text, (value) => { + protected async _build_labels(text: p.Uniform): Promise<(GraphicsBox | null)[]> { + return Array.from(text, (value) => { if (value == null) { return null } else { @@ -39,7 +42,11 @@ export class TextView extends XYGlyphView { } override async _set_lazy_data(): Promise { - await this._build_labels() + if (this.inherited_text) { + this._inherit_attr("labels") + } else { + this._define_attr("labels", await this._build_labels(this.text)) + } } override after_visuals(): void { @@ -92,7 +99,7 @@ export class TextView extends XYGlyphView { } protected _paint(ctx: Context2d, indices: number[], data?: Partial): void { - const {sx, sy, x_offset, y_offset, angle} = {...this, ...data} + const {sx, sy, x_offset, y_offset, angle, outline_shape} = {...this, ...data} const {text, background_fill, background_hatch, border_line} = this.visuals const {anchor_: anchor, border_radius, padding} = this const {labels, swidth, sheight} = this @@ -102,6 +109,7 @@ export class TextView extends XYGlyphView { const sy_i = sy[i] + y_offset.get(i) const angle_i = angle.get(i) const label_i = labels[i] + const shape_i = outline_shape.get(i) if (!isFinite(sx_i + sy_i + angle_i) || label_i == null) { continue @@ -118,18 +126,20 @@ export class TextView extends XYGlyphView { ctx.rotate(angle_i) ctx.translate(-dx_i, -dy_i) - if (background_fill.v_doit(i) || background_hatch.v_doit(i) || border_line.v_doit(i)) { - ctx.beginPath() + if (shape_i != "none" && (background_fill.v_doit(i) || background_hatch.v_doit(i) || border_line.v_doit(i))) { const bbox = new BBox({x: 0, y: 0, width: swidth_i, height: sheight_i}) - round_rect(ctx, bbox, border_radius) - background_fill.apply(ctx, i) - background_hatch.apply(ctx, i) - border_line.apply(ctx, i) + const visuals = { + fill: background_fill, + hatch: background_hatch, + line: border_line, + } + this._paint_shape(ctx, i, shape_i, bbox, visuals, border_radius) } if (text.v_doit(i)) { const {left, top} = padding ctx.translate(left, top) + label_i.visuals = text.values(i) label_i.paint(ctx) ctx.translate(-left, -top) } @@ -140,6 +150,100 @@ export class TextView extends XYGlyphView { } } + protected _paint_shape(ctx: Context2d, i: number, shape: OutlineShapeName, bbox: BBox, visuals: VectorVisuals, border_radius: Corners): void { + ctx.beginPath() + switch (shape) { + case "none": { + break + } + case "box": + case "rectangle": { + round_rect(ctx, bbox, border_radius) + break + } + case "square": { + const square = (() => { + const {x, y, width, height} = bbox + if (width > height) { + const dy = (width - height)/2 + return new BBox({x, y: y - dy, width, height: width}) + } else { + const dx = (height - width)/2 + return new BBox({x: x - dx, y, width: height, height}) + } + })() + round_rect(ctx, square, border_radius) + break + } + case "circle": { + const cx = bbox.x_center + const cy = bbox.y_center + const radius = sqrt(bbox.width**2 + bbox.height**2)/2 + ctx.arc(cx, cy, radius, 0, 2*PI, false) + break + } + case "ellipse": { + const cx = bbox.x_center + const cy = bbox.y_center + const rx = bbox.width/2 + const ry = bbox.height/2 + const n = 1.5 + const x_0 = rx + const y_0 = ry + const a = sqrt(x_0**2 + x_0**(2/n)*y_0**(2 - 2/n)) + const b = sqrt(y_0**2 + y_0**(2/n)*x_0**(2 - 2/n)) + ctx.ellipse(cx, cy, a, b, 0, 0, 2*PI) + break + } + case "trapezoid": { + const {left, right, top, bottom, width} = bbox + const ext = 0.2*width + ctx.moveTo(left, top) + ctx.lineTo(right, top) + ctx.lineTo(right + ext, bottom) + ctx.lineTo(left - ext, bottom) + ctx.closePath() + break + } + case "parallelogram": { + const {left, right, top, bottom, width} = bbox + const ext = 0.2*width + ctx.moveTo(left, top) + ctx.lineTo(right + ext, top) + ctx.lineTo(right, bottom) + ctx.lineTo(left - ext, bottom) + ctx.closePath() + break + } + case "diamond": { + const {x_center, y_center, width, height} = bbox + ctx.moveTo(x_center, y_center - height) + ctx.lineTo(width + width/2, y_center) + ctx.lineTo(x_center, y_center + height) + ctx.lineTo(-width/2, y_center) + ctx.closePath() + break + } + case "triangle": { + const w = bbox.width + const h = bbox.height + const l = sqrt(3)/2*w + const H = h + l + ctx.translate(w/2, -l) + ctx.moveTo(0, 0) + ctx.lineTo(H/2, H) + ctx.lineTo(-H/2, H) + ctx.closePath() + ctx.translate(-w/2, l) + break + } + } + + visuals.fill.apply(ctx, i) + visuals.hatch.apply(ctx, i) + visuals.line.apply(ctx, i) + } + protected override _hit_point(geometry: PointGeometry): Selection { const hit_xy = {x: geometry.sx, y: geometry.sy} @@ -244,6 +348,7 @@ export namespace Text { anchor: TextAnchorSpec padding: p.Property border_radius: p.Property + outline_shape: OutlineShapeSpec } & Mixins export type Mixins = @@ -260,7 +365,7 @@ export namespace Text { } export type Data = p.GlyphDataOf & { - labels: (GraphicsBox | null)[] + readonly labels: (GraphicsBox | null)[] swidth: Float32Array sheight: Float32Array @@ -299,6 +404,7 @@ export class Text extends XYGlyph { anchor: [ TextAnchorSpec, {value: "auto"} ], padding: [ Padding, 0 ], border_radius: [ BorderRadius, 0 ], + outline_shape: [ OutlineShapeSpec, "box" ], })) this.override({ diff --git a/bokehjs/src/lib/models/glyphs/webgl/base_line.ts b/bokehjs/src/lib/models/glyphs/webgl/base_line.ts index fc2bef195db..270118bb6ea 100644 --- a/bokehjs/src/lib/models/glyphs/webgl/base_line.ts +++ b/bokehjs/src/lib/models/glyphs/webgl/base_line.ts @@ -29,7 +29,7 @@ export abstract class BaseLineGL extends BaseGLGlyph { // visual properties that are only used if line is dashed. protected _length_so_far?: Float32Buffer // Depends on both data and visuals. - protected _dash_tex: Array = [] + protected _dash_tex: (Texture2D | null)[] = [] protected _dash_tex_info?: Float32Buffer protected _dash_scale?: Float32Buffer protected _dash_offset?: Float32Buffer @@ -63,7 +63,7 @@ export abstract class BaseLineGL extends BaseGLGlyph { const dashed_props: LineDashGlyphProps = { ...solid_props, length_so_far: main_gl_glyph._length_so_far!, - dash_tex: this._dash_tex[line_offset]!, + dash_tex: this._dash_tex[line_offset], dash_tex_info: this._dash_tex_info!, dash_scale: this._dash_scale!, dash_offset: this._dash_offset!, diff --git a/bokehjs/src/lib/models/glyphs/webgl/image.ts b/bokehjs/src/lib/models/glyphs/webgl/image.ts index 6ef12c3d3f2..19580773b00 100644 --- a/bokehjs/src/lib/models/glyphs/webgl/image.ts +++ b/bokehjs/src/lib/models/glyphs/webgl/image.ts @@ -9,8 +9,8 @@ import {assert} from "core/util/assert" export class ImageGL extends BaseGLGlyph { // data properties - protected _tex: Array = [] - protected _bounds: Array = [] + protected _tex: (Texture2D | null)[] = [] + protected _bounds: (Float32Buffer | null)[] = [] // image_changed is separate from data_changed as it can occur through changed colormapping. protected _image_changed: boolean = false @@ -54,8 +54,8 @@ export class ImageGL extends BaseGLGlyph { scissor: this.regl_wrapper.scissor, viewport: this.regl_wrapper.viewport, canvas_size: [transform.width, transform.height], - bounds: main_gl_glyph._bounds[i]!, - tex: main_gl_glyph._tex[i]!, + bounds: main_gl_glyph._bounds[i], + tex: main_gl_glyph._tex[i], global_alpha: global_alpha.get(i), } this.regl_wrapper.image()(props) diff --git a/bokehjs/src/lib/models/index.ts b/bokehjs/src/lib/models/index.ts index 3876a25e286..3347f720e5e 100644 --- a/bokehjs/src/lib/models/index.ts +++ b/bokehjs/src/lib/models/index.ts @@ -2,6 +2,7 @@ export * from "./annotations" export * from "./axes" export * from "./callbacks" export * from "./canvas" +export * from "./comparisons" export * from "./coordinates" export * from "./expressions" export * from "./filters" @@ -12,6 +13,7 @@ export * from "./graphs" export * from "./grids" export * from "./layouts" export * from "./mappers" +export * from "./misc" export * from "./text" export * from "./transforms" export * from "./plots" diff --git a/bokehjs/src/lib/models/layouts/layout_dom.ts b/bokehjs/src/lib/models/layouts/layout_dom.ts index 254db2bfbd5..ecb9b982dac 100644 --- a/bokehjs/src/lib/models/layouts/layout_dom.ts +++ b/bokehjs/src/lib/models/layouts/layout_dom.ts @@ -5,7 +5,7 @@ import {Signal} from "core/signaling" import {Align, Dimensions, FlowMode, SizingMode} from "core/enums" import {px} from "core/dom" import type {Display, CSSStyles} from "core/css" -import {isNumber, isArray, isNotNull} from "core/util/types" +import {isNumber, isArray} from "core/util/types" import type * as p from "core/properties" import type {ViewStorage, IterViews} from "core/build_views" @@ -114,11 +114,11 @@ export abstract class LayoutDOMView extends PaneView { // TODO In case of a race condition somewhere between layout, resize and children updates, // child_models and _child_views may be temporarily inconsistent, resulting in undefined // values. Eventually this shouldn't happen and undefined should be treated as a bug. - return this.child_models.map((child) => this._child_views.get(child)).filter(isNotNull) + return this.child_models.map((child) => this._child_views.get(child)).filter((view) => view != null) } get layoutable_views(): LayoutDOMView[] { - return this.child_views.filter((c): c is LayoutDOMView => c instanceof LayoutDOMView) + return this.child_views.filter((c) => c instanceof LayoutDOMView) } async build_child_views(): Promise { // TODO BuildResult @@ -139,7 +139,8 @@ export abstract class LayoutDOMView extends PaneView { super.render() for (const child_view of this.child_views) { - child_view.render_to(this.shadow_el) + const target = child_view.rendering_target() ?? this.shadow_el + child_view.render_to(target) } } @@ -159,10 +160,11 @@ export abstract class LayoutDOMView extends PaneView { for (const child_view of this.child_views) { const is_new = created_children.has(child_view) + const target = child_view.rendering_target() ?? this.shadow_el if (is_new) { - child_view.render_to(this.shadow_el) + child_view.render_to(target) } else { - this.shadow_el.append(child_view.el) + target.append(child_view.el) } } this.r_after_render() diff --git a/bokehjs/src/lib/models/layouts/tabs.ts b/bokehjs/src/lib/models/layouts/tabs.ts index 282d7f80157..e02a5d5b24d 100644 --- a/bokehjs/src/lib/models/layouts/tabs.ts +++ b/bokehjs/src/lib/models/layouts/tabs.ts @@ -39,7 +39,7 @@ export class TabsView extends LayoutDOMView { override async lazy_initialize(): Promise { await super.lazy_initialize() const {tabs} = this.model - const tooltips = tabs.map((tab) => tab.tooltip).filter((tt): tt is Tooltip => tt != null) + const tooltips = tabs.map((tab) => tab.tooltip).filter((tt) => tt != null) await build_views(this.tooltip_views, tooltips, {parent: this}) } diff --git a/bokehjs/src/lib/models/misc/group_by.ts b/bokehjs/src/lib/models/misc/group_by.ts new file mode 100644 index 00000000000..252f7116d01 --- /dev/null +++ b/bokehjs/src/lib/models/misc/group_by.ts @@ -0,0 +1,91 @@ +import {Model} from "../../model" +import type * as p from "core/properties" +import {List, Ref} from "core/kinds" + +export namespace GroupBy { + export type Attrs = p.AttrsOf + export type Props = Model.Props +} + +export interface GroupBy extends GroupBy.Attrs {} + +export abstract class GroupBy extends Model { + declare properties: GroupBy.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + abstract query_groups(models: Iterable, pool: Iterable): Iterable +} + +export namespace GroupByModels { + export type Attrs = p.AttrsOf + export type Props = GroupBy.Props & { + groups: p.Property + } +} + +export interface GroupByModels extends GroupByModels.Attrs {} + +export class GroupByModels extends GroupBy { + declare properties: GroupByModels.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + static { + this.define({ + groups: [ List(List(Ref(Model))) ], + }) + } + + *query_groups(models: Iterable, _pool: Iterable): Iterable { + for (const model of models) { + for (const group of this.groups) { + if (group.includes(model)) { + yield group + } + } + } + } +} + +export namespace GroupByName { + export type Attrs = p.AttrsOf + export type Props = GroupBy.Props +} + +export interface GroupByName extends GroupByName.Attrs {} + +export class GroupByName extends GroupBy { + declare properties: GroupByName.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + *query_groups(models: Model[], pool: Model[]): Iterable { + const groups = new Map>() + for (const model of pool) { + const {name} = model + if (name != null) { + let group = groups.get(name) + if (group === undefined) { + group = new Set() + groups.set(name, group) + } + group.add(model) + } + } + + for (const model of models) { + for (const group of groups.values()) { + if (model.name != null && group.has(model)) { + yield [...group] + } + } + } + } +} diff --git a/bokehjs/src/lib/models/misc/index.ts b/bokehjs/src/lib/models/misc/index.ts new file mode 100644 index 00000000000..cc001327ab2 --- /dev/null +++ b/bokehjs/src/lib/models/misc/index.ts @@ -0,0 +1 @@ +export {GroupByModels, GroupByName} from "./group_by" diff --git a/bokehjs/src/lib/models/plots/gmap_plot_canvas.ts b/bokehjs/src/lib/models/plots/gmap_plot_canvas.ts index 75cd1c39250..ae4a2ab4ab6 100644 --- a/bokehjs/src/lib/models/plots/gmap_plot_canvas.ts +++ b/bokehjs/src/lib/models/plots/gmap_plot_canvas.ts @@ -101,7 +101,7 @@ export class GMapPlotView extends PlotView { super.remove() } - override update_range(range_info: GMapRangeInfo | null, options?: RangeOptions): void { + override update_range(range_info: GMapRangeInfo | null, options?: Partial): void { // RESET ------------------------- if (range_info == null) { this.map.setCenter({lat: this.initial_lat, lng: this.initial_lng}) diff --git a/bokehjs/src/lib/models/plots/grid_plot.ts b/bokehjs/src/lib/models/plots/grid_plot.ts index 2add4aa5b44..8fdb8021983 100644 --- a/bokehjs/src/lib/models/plots/grid_plot.ts +++ b/bokehjs/src/lib/models/plots/grid_plot.ts @@ -79,7 +79,7 @@ export class GridPlotView extends LayoutDOMView { private readonly _tool_views: ViewStorage = new Map() async build_tool_views(): Promise { - const tools = this.model.toolbar.tools.filter((tool): tool is ActionTool => tool instanceof ActionTool) + const tools = this.model.toolbar.tools.filter((tool) => tool instanceof ActionTool) await build_views(this._tool_views, tools, {parent: this}) } diff --git a/bokehjs/src/lib/models/plots/plot.ts b/bokehjs/src/lib/models/plots/plot.ts index 370fcaa6bd7..ed01170360a 100644 --- a/bokehjs/src/lib/models/plots/plot.ts +++ b/bokehjs/src/lib/models/plots/plot.ts @@ -228,15 +228,15 @@ export class Plot extends LayoutDOM { } get data_renderers(): DataRenderer[] { - return this.renderers.filter((r): r is DataRenderer => r instanceof DataRenderer) + return this.renderers.filter((r) => r instanceof DataRenderer) } add_renderers(...renderers: Renderer[]): void { this.renderers = [...this.renderers, ...renderers] } - add_glyph(glyph: Glyph, source: ColumnarDataSource = new ColumnDataSource(), - attrs: Partial = {}): GlyphRenderer { + add_glyph(glyph: BaseGlyph, source: ColumnarDataSource = new ColumnDataSource(), + attrs: Partial> = {}): GlyphRenderer { const renderer = new GlyphRenderer({...attrs, data_source: source, glyph}) this.add_renderers(renderer) return renderer diff --git a/bokehjs/src/lib/models/plots/plot_canvas.ts b/bokehjs/src/lib/models/plots/plot_canvas.ts index ae79f029c97..425352846af 100644 --- a/bokehjs/src/lib/models/plots/plot_canvas.ts +++ b/bokehjs/src/lib/models/plots/plot_canvas.ts @@ -4,6 +4,7 @@ import type {CanvasView, FrameBox} from "../canvas/canvas" import {Canvas} from "../canvas/canvas" import type {Renderer} from "../renderers/renderer" import {RendererView} from "../renderers/renderer" +import {CompositeRendererView} from "../renderers/composite_renderer" import type {DataRenderer} from "../renderers/data_renderer" import type {Range} from "../ranges/range" import type {Tool} from "../tools/tool" @@ -36,7 +37,7 @@ import type {Side, RenderLevel} from "core/enums" import type {View} from "core/view" import {Signal0} from "core/signaling" import {throttle} from "core/util/throttle" -import {isBoolean, isArray, isString, isNotNull} from "core/util/types" +import {isBoolean, isArray, isString} from "core/util/types" import {copy, reversed} from "core/util/array" import {flat_map} from "core/util/iterator" import type {Context2d} from "core/util/canvas" @@ -98,8 +99,8 @@ export class PlotView extends LayoutDOMView implements Paintable { protected _attribution: Panel protected _notifications: Panel - get toolbar_panel(): ToolbarPanelView | undefined { - return this._toolbar != null ? this.renderer_view(this._toolbar) : undefined + get toolbar_panel(): ToolbarPanelView | null { + return this._toolbar != null ? this.views.find_one(this._toolbar) : null } protected _outer_bbox: BBox = new BBox() @@ -128,20 +129,21 @@ export class PlotView extends LayoutDOMView implements Paintable { computed_renderers: Renderer[] = [] get computed_renderer_views(): RendererView[] { - return this.computed_renderers.map((r) => this.renderer_views.get(r)).filter(isNotNull) // TODO race condition again + return this + .computed_renderers + .map((r) => this.renderer_views.get(r)) + .filter((rv) => rv != null) // TODO race condition again } - renderer_view(renderer: T): T["__view_type__"] | undefined { - const view = this.renderer_views.get(renderer) - if (view == null) { - for (const [, renderer_view] of this.renderer_views) { - const view = renderer_view.renderer_view(renderer) - if (view != null) { - return view - } + get all_renderer_views(): RendererView[] { + const collected: RendererView[] = [] + for (const rv of this.computed_renderer_views) { + collected.push(rv) + if (rv instanceof CompositeRendererView) { + collected.push(...rv.computed_renderer_views) } } - return view + return collected } get auto_ranged_renderers(): (RendererView & AutoRanged)[] { @@ -216,7 +218,7 @@ export class PlotView extends LayoutDOMView implements Paintable { if (item instanceof RendererView) { return item } else { - return this.renderer_view(item)! + return this.views.get_one(item) } })() this._invalidated_painters.add(view) @@ -463,7 +465,7 @@ export class PlotView extends LayoutDOMView implements Paintable { } const set_layout = (side: Side, model: Annotation | Axis): Layoutable | undefined => { - const view = this.renderer_view(model)! + const view = this.views.get_one(model) view.panel = new SidePanel(side) view.update_layout?.() return view.layout @@ -485,7 +487,7 @@ export class PlotView extends LayoutDOMView implements Paintable { item.set_sizing({...item.sizing, [dim]: "min"}) } return item - }).filter((item): item is Layoutable => item != null) + }).filter((item) => item != null) let layout: Row | Column if (horizontal) { @@ -542,15 +544,13 @@ export class PlotView extends LayoutDOMView implements Paintable { inner_right_panel.absolute = true center_panel.children = - this.model.center.filter((obj): obj is Annotation => { + this.model.center.filter((obj) => { return obj instanceof Annotation }).map((model) => { - const view = this.renderer_view(model)! + const view = this.views.get_one(model) view.update_layout?.() return view.layout - }).filter((layout): layout is Layoutable => { - return layout != null - }) + }).filter((layout) => layout != null) const {frame_width, frame_height} = this.model @@ -642,7 +642,7 @@ export class PlotView extends LayoutDOMView implements Paintable { return views } - update_range(range_info: RangeInfo, options?: RangeOptions): void { + update_range(range_info: RangeInfo, options?: Partial): void { this.pause() this._range_manager.update(range_info, options) this.unpause() @@ -744,7 +744,7 @@ export class PlotView extends LayoutDOMView implements Paintable { const attribution = [ ...this.model.attribution, ...this.computed_renderer_views.map((rv) => rv.attribution), - ].filter(isNotNull) + ].filter((rv) => rv != null) const elements = attribution.map((attrib) => isString(attrib) ? new Div({children: [attrib]}) : attrib) this._attribution.elements = elements // TODO this._attribution.title = contents_el.textContent!.replace(/\s*\n\s*/g, " ") @@ -975,8 +975,9 @@ export class PlotView extends LayoutDOMView implements Paintable { override render(): void { super.render() - for (const rv of this.computed_renderer_views) { - rv.render_to(rv.rendering_target()) + for (const renderer_view of this.computed_renderer_views) { + const target = renderer_view.rendering_target() + renderer_view.render_to(target) } } @@ -1225,7 +1226,7 @@ export class PlotView extends LayoutDOMView implements Paintable { override resolve_indexed(coord: Indexed): XY { const {index: i, renderer} = coord - const rv = this.renderer_view(renderer) + const rv = this.views.find_one(renderer) if (rv != null && rv.has_finished()) { const [sx, sy] = rv.glyph.scenterxy(i, NaN, NaN) if (this.frame.bbox.contains(sx, sy)) { diff --git a/bokehjs/src/lib/models/plots/range_manager.ts b/bokehjs/src/lib/models/plots/range_manager.ts index fbe8ac6bc24..145a1d88fb0 100644 --- a/bokehjs/src/lib/models/plots/range_manager.ts +++ b/bokehjs/src/lib/models/plots/range_manager.ts @@ -15,9 +15,9 @@ export type RangeInfo = { } export type RangeOptions = { - panning?: boolean - scrolling?: boolean - maintain_focus?: boolean + panning: boolean + scrolling: boolean + maintain_focus: boolean } export class RangeManager { @@ -29,7 +29,11 @@ export class RangeManager { invalidate_dataranges: boolean = true - update(range_info: RangeInfo, options: RangeOptions = {}): void { + update(range_info: RangeInfo, options: Partial = {}): void { + const panning = options.panning ?? false + const scrolling = options.scrolling ?? false + const maintain_focus = options.maintain_focus ?? false + const range_state: RangeState = new Map() for (const [range, interval] of range_info.xrs) { range_state.set(range, interval) @@ -37,10 +41,11 @@ export class RangeManager { for (const [range, interval] of range_info.yrs) { range_state.set(range, interval) } - if (options.scrolling ?? false) { + + if (scrolling && maintain_focus) { this._update_ranges_together(range_state) // apply interval bounds while keeping aspect } - this._update_ranges_individually(range_state, options) + this._update_ranges_individually(range_state, {panning, scrolling, maintain_focus}) } ranges(): {x_ranges: Range[], y_ranges: Range[]} { @@ -216,16 +221,14 @@ export class RangeManager { } } - protected _update_ranges_individually(range_state: RangeState, options: RangeOptions = {}): void { - const panning = options.panning ?? false - const scrolling = options.scrolling ?? false - const maintain_focus = options.maintain_focus ?? false + protected _update_ranges_individually(range_state: RangeState, options: RangeOptions): void { + const {panning, scrolling, maintain_focus} = options let hit_bound = false for (const [rng, range_info] of range_state) { // Limit range interval first. Note that for scroll events, // the interval has already been limited for all ranges simultaneously - if (!scrolling) { + if (!scrolling || maintain_focus) { const weight = this._get_weight_to_constrain_interval(rng, range_info) if (weight < 1) { range_info.start = weight*range_info.start + (1 - weight)*rng.start diff --git a/bokehjs/src/lib/models/renderers/composite_renderer.ts b/bokehjs/src/lib/models/renderers/composite_renderer.ts index 2985b710565..f41dd160565 100644 --- a/bokehjs/src/lib/models/renderers/composite_renderer.ts +++ b/bokehjs/src/lib/models/renderers/composite_renderer.ts @@ -35,12 +35,28 @@ export abstract class CompositeRendererView extends RendererView { await this._build_elements() } + protected readonly _computed_renderers: Renderer[] = [] + get computed_renderers(): Renderer[] { + return [...this.model.renderers, ...this._computed_renderers] + } + get computed_renderer_views(): ViewOf[] { + return this.computed_renderers.map((item) => this._renderer_views.get(item)).filter((rv) => rv != null) + } + protected async _build_renderers(): Promise> { - return await build_views(this._renderer_views, this.model.renderers, {parent: this.plot_view}) + return await build_views(this._renderer_views, this.computed_renderers, {parent: this.plot_view}) + } + + protected readonly _computed_elements: ElementLike[] = [] + get computed_elements(): ElementLike[] { + return [...this.model.elements, ...this._computed_elements] + } + get computed_element_views(): ViewOf[] { + return this.computed_elements.map((item) => this._element_views.get(item)).filter((ev) => ev != null) } protected async _build_elements(): Promise> { - return await build_views(this._element_views, this.model.elements, {parent: this.plot_view}) + return await build_views(this._element_views, this.computed_elements, {parent: this.plot_view}) } protected async _update_renderers(): Promise { @@ -61,10 +77,11 @@ export abstract class CompositeRendererView extends RendererView { for (const element_view of this.element_views) { const is_new = created_elements.has(element_view) + const target = element_view.rendering_target() ?? this.shadow_el if (is_new) { - element_view.render_to(this.plot_view.shadow_el) + element_view.render_to(target) } else { - this.plot_view.shadow_el.append(element_view.el) + target.append(element_view.el) } } this.r_after_render() @@ -92,13 +109,20 @@ export abstract class CompositeRendererView extends RendererView { override paint(): void { if (!this._has_rendered_elements) { for (const element_view of this.element_views) { - element_view.render_to(this.plot_view.shadow_el) + const target = element_view.rendering_target() ?? this.shadow_el + element_view.render_to(target) } this._has_rendered_elements = true } super.paint() + if (this.displayed && this.is_renderable) { + for (const renderer of this.computed_renderer_views) { + renderer.paint() + } + } + const {displayed} = this for (const element_view of this.element_views) { element_view.reposition(displayed) diff --git a/bokehjs/src/lib/models/renderers/contour_renderer.ts b/bokehjs/src/lib/models/renderers/contour_renderer.ts index 08ba37d8cab..43e68df49db 100644 --- a/bokehjs/src/lib/models/renderers/contour_renderer.ts +++ b/bokehjs/src/lib/models/renderers/contour_renderer.ts @@ -1,12 +1,13 @@ import {DataRenderer, DataRendererView} from "./data_renderer" import type {GlyphRendererView} from "./glyph_renderer" import {GlyphRenderer} from "./glyph_renderer" -import type {Renderer} from "./renderer" import type {GlyphView} from "../glyphs/glyph" import type * as p from "core/properties" import type {IterViews} from "core/build_views" import {build_view} from "core/build_views" import type {SelectionManager} from "core/selection_manager" +import type {Geometry} from "core/geometry" +import type {HitTestResult} from "core/hittest" export class ContourRendererView extends DataRendererView { declare model: ContourRenderer @@ -47,16 +48,8 @@ export class ContourRendererView extends DataRendererView { this.line_view.paint() } - override renderer_view(renderer: T): T["__view_type__"] | undefined { - if (renderer instanceof GlyphRenderer) { - if (renderer == this.fill_view.model) { - return this.fill_view - } - if (renderer == this.line_view.model) { - return this.line_view - } - } - return super.renderer_view(renderer) + hit_test(geometry: Geometry): HitTestResult { + return this.fill_view.hit_test(geometry) } } diff --git a/bokehjs/src/lib/models/renderers/data_renderer.ts b/bokehjs/src/lib/models/renderers/data_renderer.ts index 095758e34b2..6be77ec546d 100644 --- a/bokehjs/src/lib/models/renderers/data_renderer.ts +++ b/bokehjs/src/lib/models/renderers/data_renderer.ts @@ -4,6 +4,8 @@ import type {Scale} from "../scales/scale" import type {AutoRanged} from "../ranges/data_range1d" import {auto_ranged} from "../ranges/data_range1d" import type {SelectionManager} from "core/selection_manager" +import type {Geometry} from "core/geometry" +import type {HitTestResult} from "core/hittest" import type * as p from "core/properties" import type {Rect} from "core/types" @@ -30,6 +32,8 @@ export abstract class DataRendererView extends RendererView implements AutoRange log_bounds(): Rect { return this.glyph_view.log_bounds() } + + abstract hit_test(geometry: Geometry): HitTestResult } export namespace DataRenderer { diff --git a/bokehjs/src/lib/models/renderers/glyph_renderer.ts b/bokehjs/src/lib/models/renderers/glyph_renderer.ts index 9b4f52e6670..77b40fc112b 100644 --- a/bokehjs/src/lib/models/renderers/glyph_renderer.ts +++ b/bokehjs/src/lib/models/renderers/glyph_renderer.ts @@ -527,34 +527,58 @@ export class GlyphRendererView extends DataRendererView { } export namespace GlyphRenderer { - export type Attrs = p.AttrsOf - - export type Props = DataRenderer.Props & { + export type Attrs< + BaseGlyph, + HoverGlyph = BaseGlyph, + NonSelectionGlyph = BaseGlyph, + SelectionGlyph = BaseGlyph, + MutedGlyph = BaseGlyph, + > = p.AttrsOf> + + export type Props< + BaseGlyph, + HoverGlyph = BaseGlyph, + NonSelectionGlyph = BaseGlyph, + SelectionGlyph = BaseGlyph, + MutedGlyph = BaseGlyph, + > = DataRenderer.Props & { data_source: p.Property view: p.Property - glyph: p.Property - hover_glyph: p.Property - nonselection_glyph: p.Property - selection_glyph: p.Property - muted_glyph: p.Property + glyph: p.Property + hover_glyph: p.Property + nonselection_glyph: p.Property + selection_glyph: p.Property + muted_glyph: p.Property muted: p.Property } } -export interface GlyphRenderer extends GlyphRenderer.Attrs {} - -export class GlyphRenderer extends DataRenderer { - declare properties: GlyphRenderer.Props +export interface GlyphRenderer< + BaseGlyph extends Glyph = Glyph, + HoverGlyph extends Glyph = BaseGlyph, + NonSelectionGlyph extends Glyph = BaseGlyph, + SelectionGlyph extends Glyph = BaseGlyph, + MutedGlyph extends Glyph = BaseGlyph, +> extends GlyphRenderer.Attrs {} + +export class GlyphRenderer< + BaseGlyph extends Glyph = Glyph, + HoverGlyph extends Glyph = BaseGlyph, + NonSelectionGlyph extends Glyph = BaseGlyph, + SelectionGlyph extends Glyph = BaseGlyph, + MutedGlyph extends Glyph = BaseGlyph, +> extends DataRenderer { + declare properties: GlyphRenderer.Props declare __view_type__: GlyphRendererView - constructor(attrs?: Partial) { + constructor(attrs?: Partial>) { super(attrs) } static { this.prototype.default_view = GlyphRendererView - this.define(({Bool, Auto, Or, Ref, Null, Nullable}) => ({ + this.define>(({Bool, Auto, Or, Ref, Null, Nullable}) => ({ data_source: [ Ref(ColumnarDataSource) ], view: [ Ref(CDSView), () => new CDSView() ], glyph: [ Ref(Glyph) ], diff --git a/bokehjs/src/lib/models/renderers/graph_renderer.ts b/bokehjs/src/lib/models/renderers/graph_renderer.ts index 2eae8b1c459..9b1427a69e7 100644 --- a/bokehjs/src/lib/models/renderers/graph_renderer.ts +++ b/bokehjs/src/lib/models/renderers/graph_renderer.ts @@ -1,7 +1,6 @@ import {DataRenderer, DataRendererView} from "./data_renderer" import type {GlyphRendererView} from "./glyph_renderer" import {GlyphRenderer} from "./glyph_renderer" -import type {Renderer} from "./renderer" import type {GlyphView} from "../glyphs/glyph" import {LayoutProvider} from "../graphs/layout_provider" import {GraphHitTestPolicy, NodesOnly} from "../graphs/graph_hit_test_policy" @@ -9,11 +8,15 @@ import type * as p from "core/properties" import type {IterViews} from "core/build_views" import {build_view} from "core/build_views" import {logger} from "core/logging" +import type {Geometry} from "core/geometry" +import type {HitTestResult} from "core/hittest" import type {SelectionManager} from "core/selection_manager" import {XYGlyph} from "../glyphs/xy_glyph" import {MultiLine} from "../glyphs/multi_line" import {Patches} from "../glyphs/patches" +type XsYsGlyph = MultiLine | Patches + export class GraphRendererView extends DataRendererView { declare model: GraphRenderer @@ -127,16 +130,8 @@ export class GraphRendererView extends DataRendererView { return this.edge_view.has_webgl || this.node_view.has_webgl } - override renderer_view(renderer: T): T["__view_type__"] | undefined { - if (renderer instanceof GlyphRenderer) { - if (renderer == this.edge_view.model) { - return this.edge_view - } - if (renderer == this.node_view.model) { - return this.node_view - } - } - return super.renderer_view(renderer) + hit_test(geometry: Geometry): HitTestResult { + return this.model.inspection_policy.hit_test(geometry, this) } } @@ -145,8 +140,8 @@ export namespace GraphRenderer { export type Props = DataRenderer.Props & { layout_provider: p.Property - node_renderer: p.Property - edge_renderer: p.Property + node_renderer: p.Property> + edge_renderer: p.Property> selection_policy: p.Property inspection_policy: p.Property } @@ -167,8 +162,8 @@ export class GraphRenderer extends DataRenderer { this.define(({Ref}) => ({ layout_provider: [ Ref(LayoutProvider) ], - node_renderer: [ Ref(GlyphRenderer) ], - edge_renderer: [ Ref(GlyphRenderer) ], + node_renderer: [ Ref(GlyphRenderer) ], + edge_renderer: [ Ref(GlyphRenderer) ], selection_policy: [ Ref(GraphHitTestPolicy), () => new NodesOnly() ], inspection_policy: [ Ref(GraphHitTestPolicy), () => new NodesOnly() ], })) diff --git a/bokehjs/src/lib/models/renderers/renderer.ts b/bokehjs/src/lib/models/renderers/renderer.ts index 18b8c6ffaf5..7a83064c050 100644 --- a/bokehjs/src/lib/models/renderers/renderer.ts +++ b/bokehjs/src/lib/models/renderers/renderer.ts @@ -17,7 +17,6 @@ import type {HTML} from "../dom/html" import {RendererGroup} from "./renderer_group" import {InlineStyleSheet} from "core/dom" import type {StyleSheetLike} from "core/dom" -import renderer_css from "styles/renderer.css" export abstract class RendererView extends StyledElementView implements visuals.Paintable { declare model: Renderer @@ -27,10 +26,7 @@ export abstract class RendererView extends StyledElementView implements visuals. readonly position = new InlineStyleSheet() - /** - * Define where to render this element, usually a canvas layer. - */ - rendering_target(): HTMLElement { + override rendering_target(): HTMLElement { return this.plot_view.canvas_view.underlays_el } @@ -55,7 +51,7 @@ export abstract class RendererView extends StyledElementView implements visuals. } override stylesheets(): StyleSheetLike[] { - return [...super.stylesheets(), renderer_css, this.position] + return [...super.stylesheets(), this.position] } override initialize(): void { @@ -202,18 +198,19 @@ export abstract class RendererView extends StyledElementView implements visuals. * Updates the position of the associated DOM element. */ update_position(): void { - const {bbox} = this + const {bbox, position} = this if (bbox != null && bbox.is_valid) { - this.position.replace(` + position.replace(` :host { - left: ${bbox.left}px; - top: ${bbox.top}px; - width: ${bbox.width}px; - height: ${bbox.height}px; + position: absolute; + left: ${bbox.left}px; + top: ${bbox.top}px; + width: ${bbox.width}px; + height: ${bbox.height}px; } `) } else { - this.position.replace(` + position.replace(` :host { display: none; } diff --git a/bokehjs/src/lib/models/selections/selection.ts b/bokehjs/src/lib/models/selections/selection.ts index ee90c56d444..3401d5cb32b 100644 --- a/bokehjs/src/lib/models/selections/selection.ts +++ b/bokehjs/src/lib/models/selections/selection.ts @@ -75,6 +75,10 @@ export class Selection extends Model { this.update_through_replacement(selection) break } + case "toggle": { + this.update_through_toggle(selection) + break + } case "append": { this.update_through_union(selection) break @@ -172,6 +176,17 @@ export class Selection extends Model { this.selected_glyphs = other.selected_glyphs } + update_through_toggle(other: Selection): void { + // note the order of arguments when comparing with update_through_subtraction() + this.indices = difference(other.indices, this.indices) + // TODO: think through and fix any logic below + this.selected_glyphs = union(other.selected_glyphs, this.selected_glyphs) + this.line_indices = union(other.line_indices, this.line_indices) + this.image_indices = this._union_image_indices(this.image_indices, other.image_indices) // TODO + this.view = other.view + this.multiline_indices = merge(other.multiline_indices, this.multiline_indices) + } + update_through_union(other: Selection): void { this.indices = union(this.indices, other.indices) this.selected_glyphs = union(other.selected_glyphs, this.selected_glyphs) diff --git a/bokehjs/src/lib/models/sources/ajax_data_source.ts b/bokehjs/src/lib/models/sources/ajax_data_source.ts index 71012f8df02..87ba94434a9 100644 --- a/bokehjs/src/lib/models/sources/ajax_data_source.ts +++ b/bokehjs/src/lib/models/sources/ajax_data_source.ts @@ -37,8 +37,10 @@ export class AjaxDataSource extends WebDataSource { })) } - protected interval: number | null = null - protected initialized: boolean = false + // TODO don't use initializers until https://github.com/bokeh/bokeh/issues/13732 is fixed + protected interval?: number + protected initialized?: boolean + protected last_fetch_time?: Date override destroy(): void { if (this.interval != null) { @@ -48,7 +50,7 @@ export class AjaxDataSource extends WebDataSource { } setup(): void { - if (!this.initialized) { + if (this.initialized !== true) { this.initialized = true this.get_data(this.mode) if (this.polling_interval != null) { @@ -58,13 +60,16 @@ export class AjaxDataSource extends WebDataSource { } } - get_data(mode: UpdateMode, max_size: number | null = null, _if_modified: boolean = false): void { + get_data(mode: UpdateMode, max_size: number | null = null, if_modified: boolean = false): void { const xhr = this.prepare_request() - // TODO: if_modified xhr.addEventListener("load", () => this.do_load(xhr, mode, max_size ?? undefined)) xhr.addEventListener("error", () => this.do_error(xhr)) + if (if_modified && this.last_fetch_time != null) { + xhr.setRequestHeader("If-Modified-Since", this.last_fetch_time.toUTCString()) + } + xhr.send() } @@ -84,6 +89,7 @@ export class AjaxDataSource extends WebDataSource { async do_load(xhr: XMLHttpRequest, mode: UpdateMode, max_size?: number): Promise { if (xhr.status == 200) { const raw_data = JSON.parse(xhr.responseText) + this.last_fetch_time = new Date() await this.load_data(raw_data, mode, max_size) } } diff --git a/bokehjs/src/lib/models/tickers/composite_ticker.ts b/bokehjs/src/lib/models/tickers/composite_ticker.ts index 6f3f59539d1..f5dadf00224 100644 --- a/bokehjs/src/lib/models/tickers/composite_ticker.ts +++ b/bokehjs/src/lib/models/tickers/composite_ticker.ts @@ -24,8 +24,8 @@ export class CompositeTicker extends ContinuousTicker { } static { - this.define(({List, Ref}) => ({ - tickers: [ List(Ref(ContinuousTicker)), [] ], + this.define(({NonEmptyList, Ref}) => ({ + tickers: [ NonEmptyList(Ref(ContinuousTicker)) ], })) } @@ -52,6 +52,10 @@ export class CompositeTicker extends ContinuousTicker { get_best_ticker(data_low: number, data_high: number, desired_n_ticks: number): ContinuousTicker { const data_range = data_high - data_low + if (data_range == 0) { + return this.tickers[0] + } + const ideal_interval = this.get_ideal_interval(data_low, data_high, desired_n_ticks) const ticker_ndxs = [ sorted_index(this.min_intervals, ideal_interval) - 1, diff --git a/bokehjs/src/lib/models/tickers/customjs_ticker.ts b/bokehjs/src/lib/models/tickers/customjs_ticker.ts new file mode 100644 index 00000000000..f17f4be44ab --- /dev/null +++ b/bokehjs/src/lib/models/tickers/customjs_ticker.ts @@ -0,0 +1,93 @@ +import type {FactorTickSpec} from "./categorical_ticker" +import type {TickSpec} from "./ticker" +import {Ticker} from "./ticker" +import {FactorRange} from "../ranges/factor_range" +import type {Range} from "../ranges/range" +import type * as p from "core/properties" +import type {Dict} from "core/types" +import {keys, values} from "core/util/object" +import {use_strict} from "core/util/string" + +type MajorCBData = { + start: number + end: number + range: Range + cross_loc: number +} + +type MinorCBData = MajorCBData & { + major_ticks: any[] +} + +export namespace CustomJSTicker { + export type Attrs = p.AttrsOf + + export type Props = Ticker.Props & { + args: p.Property> + major_code: p.Property + minor_code: p.Property + } +} + +export interface CustomJSTicker extends CustomJSTicker.Attrs {} + +export class CustomJSTicker extends Ticker { + declare properties: CustomJSTicker.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + static { + this.define(({Unknown, Str, Dict}) => ({ + args: [ Dict(Unknown), {} ], + major_code: [ Str, "" ], + minor_code: [ Str, "" ], + })) + } + + get names(): string[] { + return keys(this.args) + } + + get values(): unknown[] { + return values(this.args) + } + + get_ticks(start: number, end: number, range: Range, cross_loc: number): TickSpec | FactorTickSpec { + const major_cb_data = {start, end, range, cross_loc} + const major_ticks = this.major_ticks(major_cb_data) + + // CustomJSTicker for categorical axes only support a single level of major ticks + if (range instanceof FactorRange) { + return {major: major_ticks, minor: [], tops: [], mids: []} + } + + const minor_cb_data = {major_ticks, ...major_cb_data} + const minor_ticks = this.minor_ticks(minor_cb_data) + + return { + major: major_ticks, + minor: minor_ticks, + } + } + + protected major_ticks(cb_data: MajorCBData): any[] { + if (this.major_code == "") { + return [] + } + const code = use_strict(this.major_code) + const func = new Function("cb_data", ...this.names, code) + return func(cb_data, ...this.values) + } + + protected minor_ticks(cb_data: MinorCBData): any[] { + if (this.minor_code == "") { + return [] + } + const code = use_strict(this.minor_code) + const func = new Function("cb_data", ...this.names, code) + return func(cb_data, ...this.values) + } + +} diff --git a/bokehjs/src/lib/models/tickers/index.ts b/bokehjs/src/lib/models/tickers/index.ts index df91178bd08..6d61022f20a 100644 --- a/bokehjs/src/lib/models/tickers/index.ts +++ b/bokehjs/src/lib/models/tickers/index.ts @@ -3,6 +3,7 @@ export {BasicTicker} from "./basic_ticker" export {CategoricalTicker} from "./categorical_ticker" export {CompositeTicker} from "./composite_ticker" export {ContinuousTicker} from "./continuous_ticker" +export {CustomJSTicker} from "./customjs_ticker" export {DatetimeTicker} from "./datetime_ticker" export {DaysTicker} from "./days_ticker" export {FixedTicker} from "./fixed_ticker" diff --git a/bokehjs/src/lib/models/tools/actions/zoom_base_tool.ts b/bokehjs/src/lib/models/tools/actions/zoom_base_tool.ts index a2c97d27927..ed749c1abe9 100644 --- a/bokehjs/src/lib/models/tools/actions/zoom_base_tool.ts +++ b/bokehjs/src/lib/models/tools/actions/zoom_base_tool.ts @@ -4,7 +4,6 @@ import type {Scale} from "../../scales/scale" import {CompositeScale} from "../../scales/composite_scale" import {Dimensions} from "core/enums" import {scale_range} from "core/util/zoom" -import {assert} from "core/util/assert" import type * as p from "core/properties" import {logger} from "core/logging" @@ -59,8 +58,7 @@ export abstract class ZoomBaseToolView extends PlotActionToolView { continue } - const rv = this.plot_view.renderer_view(renderer) - assert(rv != null) + const rv = this.plot_view.views.get_one(renderer) const process = (scale: Scale, dim: "x" | "y") => { const {level} = this.model diff --git a/bokehjs/src/lib/models/tools/edit/box_edit_tool.ts b/bokehjs/src/lib/models/tools/edit/box_edit_tool.ts index 15bf68bdba5..f7cfe0fd4a1 100644 --- a/bokehjs/src/lib/models/tools/edit/box_edit_tool.ts +++ b/bokehjs/src/lib/models/tools/edit/box_edit_tool.ts @@ -56,7 +56,7 @@ export class BoxEditToolView extends EditToolView { _set_extent([sx0, sx1]: [number, number], [sy0, sy1]: [number, number], append: boolean, emit: boolean = false): void { const renderer = this._recent_renderers[0] ?? this.model.renderers[0] - const renderer_view = this.plot_view.renderer_view(renderer) + const renderer_view = this.plot_view.views.find_one(renderer) if (renderer_view == null) { return } diff --git a/bokehjs/src/lib/models/tools/edit/common.ts b/bokehjs/src/lib/models/tools/edit/common.ts new file mode 100644 index 00000000000..0d06547e356 --- /dev/null +++ b/bokehjs/src/lib/models/tools/edit/common.ts @@ -0,0 +1,4 @@ +import type {MultiLine} from "../../glyphs/multi_line" +import type {Patches} from "../../glyphs/patches" + +export type XsYsGlyph = MultiLine | Patches diff --git a/bokehjs/src/lib/models/tools/edit/edit_tool.ts b/bokehjs/src/lib/models/tools/edit/edit_tool.ts index 0fa88dbedee..8e91c2fa8b9 100644 --- a/bokehjs/src/lib/models/tools/edit/edit_tool.ts +++ b/bokehjs/src/lib/models/tools/edit/edit_tool.ts @@ -1,4 +1,5 @@ import type * as p from "core/properties" +import {isField} from "core/vectorization" import type {PointGeometry} from "core/geometry" import type {UIEvent, MoveEvent} from "core/ui_events" import type {Dimensions, SelectionMode} from "core/enums" @@ -7,15 +8,12 @@ import {includes} from "core/util/array" import {dict} from "core/util/object" import {isArray} from "core/util/types" import {unreachable} from "core/util/assert" +import type {Glyph} from "../../glyphs/glyph" import type {XYGlyph} from "../../glyphs/xy_glyph" import type {ColumnarDataSource} from "../../sources/columnar_data_source" import type {GlyphRenderer} from "../../renderers/glyph_renderer" import {GestureTool, GestureToolView} from "../gestures/gesture_tool" -export type HasXYGlyph = { - glyph: XYGlyph -} - export abstract class EditToolView extends GestureToolView { declare model: EditTool @@ -52,7 +50,7 @@ export abstract class EditToolView extends GestureToolView { if (!frame.bbox.contains(sx, sy)) { return null } - const renderer_view = this.plot_view.renderer_view(renderer) + const renderer_view = this.plot_view.views.find_one(renderer) if (renderer_view == null) { return null } @@ -111,7 +109,7 @@ export abstract class EditToolView extends GestureToolView { } } - _drag_points(ev: UIEvent, renderers: (GlyphRenderer & HasXYGlyph)[], dim: Dimensions = "both"): void { + _drag_points(ev: UIEvent, renderers: GlyphRenderer[], dim: Dimensions = "both"): void { if (this._basepoint == null) { return } @@ -126,16 +124,17 @@ export abstract class EditToolView extends GestureToolView { const [px, py] = basepoint const [dx, dy] = [x-px, y-py] // Type once dataspecs are typed - const glyph: any = renderer.glyph + const {glyph} = renderer const cds = renderer.data_source const data = dict(cds.data) - const [xkey, ykey] = [glyph.x.field, glyph.y.field] + const xkey = isField(glyph.x) ? glyph.x.field : null + const ykey = isField(glyph.y) ? glyph.y.field : null for (const index of cds.selected.indices) { - if (xkey && (dim == "width" || dim == "both")) { + if (xkey != null && (dim == "width" || dim == "both")) { const column = (data.get(xkey) ?? []) as number[] column[index] += dx } - if (ykey && (dim == "height" || dim == "both")) { + if (ykey != null && (dim == "height" || dim == "both")) { const column = (data.get(ykey) ?? []) as number[] column[index] += dy } @@ -145,7 +144,7 @@ export abstract class EditToolView extends GestureToolView { this._basepoint = [ev.sx, ev.sy] } - _pad_empty_columns(cds: ColumnarDataSource, coord_columns: string[]): void { + _pad_empty_columns(cds: ColumnarDataSource, coord_columns: (string | null)[]): void { // Pad ColumnDataSource non-coordinate columns with default values const {inferred_defaults} = cds const default_values = dict(cds.default_values) @@ -169,7 +168,7 @@ export abstract class EditToolView extends GestureToolView { } } - _select_event(ev: UIEvent, mode: SelectionMode, renderers: GlyphRenderer[]): GlyphRenderer[] { + _select_event(ev: UIEvent, mode: SelectionMode, renderers: GlyphRenderer[]): GlyphRenderer[] { // Process selection event on the supplied renderers and return selected renderers const frame = this.plot_view.frame const {sx, sy} = ev @@ -181,7 +180,7 @@ export abstract class EditToolView extends GestureToolView { for (const renderer of renderers) { const sm = renderer.get_selection_manager() const cds = renderer.data_source - const view = this.plot_view.renderer_view(renderer) + const view = this.plot_view.views.find_one(renderer) if (view != null) { const did_hit = sm.select([view], geometry, true, mode) if (did_hit) { diff --git a/bokehjs/src/lib/models/tools/edit/freehand_draw_tool.ts b/bokehjs/src/lib/models/tools/edit/freehand_draw_tool.ts index 6d17199e91b..9a5a0145c67 100644 --- a/bokehjs/src/lib/models/tools/edit/freehand_draw_tool.ts +++ b/bokehjs/src/lib/models/tools/edit/freehand_draw_tool.ts @@ -1,9 +1,10 @@ import type {UIEvent, PanEvent, TapEvent, KeyEvent} from "core/ui_events" import type * as p from "core/properties" +import {isField} from "core/vectorization" import {dict} from "core/util/object" import {isArray} from "core/util/types" -import type {HasXYGlyph} from "./edit_tool" import {EditTool, EditToolView} from "./edit_tool" +import type {XsYsGlyph} from "./common" import {GlyphRenderer} from "../../renderers/glyph_renderer" import {tool_icon_freehand_draw} from "styles/icons.css" @@ -22,34 +23,34 @@ export class FreehandDrawToolView extends EditToolView { } const [x, y] = point - const cds = renderer.data_source - const data = dict(cds.data) - const glyph: any = renderer.glyph - const [xkey, ykey] = [glyph.xs.field, glyph.ys.field] + const {glyph, data_source} = renderer + const xkey = isField(glyph.xs) ? glyph.xs.field : null + const ykey = isField(glyph.ys) ? glyph.ys.field : null + const data = dict(data_source.data) if (mode == "new") { - this._pop_glyphs(cds, this.model.num_objects) - if (xkey) { - cds.get_array(xkey).push([x]) + this._pop_glyphs(data_source, this.model.num_objects) + if (xkey != null) { + data_source.get_array(xkey).push([x]) } - if (ykey) { - cds.get_array(ykey).push([y]) + if (ykey != null) { + data_source.get_array(ykey).push([y]) } - this._pad_empty_columns(cds, [xkey, ykey]) + this._pad_empty_columns(data_source, [xkey, ykey]) } else if (mode == "add") { - if (xkey) { + if (xkey != null) { const column = data.get(xkey) ?? [] const xidx = column.length-1 - let xs = cds.get_array(xkey)[xidx] + let xs = data_source.get_array(xkey)[xidx] if (!isArray(xs)) { xs = Array.from(xs) column[xidx] = xs } xs.push(x) } - if (ykey) { + if (ykey != null) { const column = data.get(ykey) ?? [] const yidx = column.length-1 - let ys = cds.get_array(ykey)[yidx] + let ys = data_source.get_array(ykey)[yidx] if (!isArray(ys)) { ys = Array.from(ys) column[yidx] = ys @@ -57,7 +58,7 @@ export class FreehandDrawToolView extends EditToolView { ys.push(y) } } - this._emit_cds_changes(cds, true, true, emit) + this._emit_cds_changes(data_source, true, true, emit) } override _pan_start(ev: PanEvent): void { @@ -95,7 +96,7 @@ export namespace FreehandDrawTool { export type Props = EditTool.Props & { num_objects: p.Property - renderers: p.Property<(GlyphRenderer & HasXYGlyph)[]> + renderers: p.Property[]> } } @@ -114,7 +115,7 @@ export class FreehandDrawTool extends EditTool { this.define(({Int, List, Ref}) => ({ num_objects: [ Int, 0 ], - renderers: [ List(Ref(GlyphRenderer as any)), [] ], + renderers: [ List(Ref(GlyphRenderer)), [] ], })) this.register_alias("freehand_draw", () => new FreehandDrawTool()) diff --git a/bokehjs/src/lib/models/tools/edit/line_edit_tool.ts b/bokehjs/src/lib/models/tools/edit/line_edit_tool.ts index 99995264f7d..421e5b4c15d 100644 --- a/bokehjs/src/lib/models/tools/edit/line_edit_tool.ts +++ b/bokehjs/src/lib/models/tools/edit/line_edit_tool.ts @@ -1,4 +1,5 @@ import type {PanEvent, TapEvent} from "core/ui_events" +import {isField} from "core/vectorization" import {Dimensions} from "core/enums" import {dict} from "core/util/object" import {GlyphRenderer} from "../../renderers/glyph_renderer" @@ -14,7 +15,7 @@ export interface HasLineGlyph { export class LineEditToolView extends LineToolView { declare model: LineEditTool - _selected_renderer: GlyphRenderer | null + _selected_renderer: GlyphRenderer | null _drawing: boolean = false override _press(ev: TapEvent): void { @@ -50,11 +51,14 @@ export class LineEditToolView extends LineToolView { return } - const cds = this._selected_renderer.data_source + const {glyph} = this._selected_renderer + if (!isField(glyph.x) || !isField(glyph.y)) { + return + } - const glyph: any = this._selected_renderer.glyph const [xkey, ykey] = [glyph.x.field, glyph.y.field] + const cds = this._selected_renderer.data_source const x = cds.get_array(xkey) const y = cds.get_array(ykey) @@ -82,11 +86,12 @@ export class LineEditToolView extends LineToolView { if (this._selected_renderer == null) { return } - const point_glyph: any = this.model.intersection_renderer.glyph + const point_glyph = this.model.intersection_renderer.glyph const point_cds = this.model.intersection_renderer.data_source const data = dict(point_cds.data) - const [pxkey, pykey] = [point_glyph.x.field, point_glyph.y.field] - if (pxkey && pykey) { + const pxkey = isField(point_glyph.x) ? point_glyph.x.field : null + const pykey = isField(point_glyph.y) ? point_glyph.y.field : null + if (pxkey != null && pykey != null) { const x = data.get(pxkey) const y = data.get(pykey) if (x != null) { @@ -145,7 +150,7 @@ export namespace LineEditTool { export type Props = LineTool.Props & { dimensions: p.Property - renderers: p.Property<(GlyphRenderer & HasLineGlyph)[]> + renderers: p.Property[]> } } @@ -163,7 +168,7 @@ export class LineEditTool extends LineTool { this.prototype.default_view = LineEditToolView this.define(({List, Ref}) => ({ dimensions: [ Dimensions, "both" ], - renderers: [ List(Ref(GlyphRenderer as any)), [] ], + renderers: [ List(Ref(GlyphRenderer)), [] ], })) } diff --git a/bokehjs/src/lib/models/tools/edit/line_tool.ts b/bokehjs/src/lib/models/tools/edit/line_tool.ts index c6391af17da..fc415d6c6d7 100644 --- a/bokehjs/src/lib/models/tools/edit/line_tool.ts +++ b/bokehjs/src/lib/models/tools/edit/line_tool.ts @@ -1,31 +1,29 @@ import type * as p from "core/properties" +import {isField} from "core/vectorization" import {dict} from "core/util/object" import {isArray} from "core/util/types" import type {Line} from "../../glyphs/line" -import type {GlyphRenderer} from "../../renderers/glyph_renderer" +import {GlyphRenderer} from "../../renderers/glyph_renderer" import {EditTool, EditToolView} from "./edit_tool" -export type HasLineGlyph = { - glyph: Line -} - export abstract class LineToolView extends EditToolView { declare model: LineTool _set_intersection(x: number[] | number, y: number[] | number): void { - const point_glyph: any = this.model.intersection_renderer.glyph + const point_glyph = this.model.intersection_renderer.glyph const point_cds = this.model.intersection_renderer.data_source const data = dict(point_cds.data) - const [pxkey, pykey] = [point_glyph.x.field, point_glyph.y.field] - if (pxkey) { + const pxkey = isField(point_glyph.x) ? point_glyph.x.field : null + const pykey = isField(point_glyph.y) ? point_glyph.y.field : null + if (pxkey != null) { if (isArray(x)) { data.set(pxkey, x) } else { point_glyph.x = {value: x} } } - if (pykey) { + if (pykey != null) { if (isArray(y)) { data.set(pykey, y) } else { @@ -44,7 +42,7 @@ export namespace LineTool { export type Attrs = p.AttrsOf export type Props = EditTool.Props & { - intersection_renderer: p.Property<(GlyphRenderer & HasLineGlyph)> + intersection_renderer: p.Property> } } @@ -59,8 +57,8 @@ export abstract class LineTool extends EditTool { } static { - this.define(({AnyRef}) => ({ - intersection_renderer: [ AnyRef() ], + this.define(({Ref}) => ({ + intersection_renderer: [ Ref(GlyphRenderer) ], })) } } diff --git a/bokehjs/src/lib/models/tools/edit/point_draw_tool.ts b/bokehjs/src/lib/models/tools/edit/point_draw_tool.ts index ff2b39d932a..a64d93093c5 100644 --- a/bokehjs/src/lib/models/tools/edit/point_draw_tool.ts +++ b/bokehjs/src/lib/models/tools/edit/point_draw_tool.ts @@ -1,7 +1,8 @@ import type {PanEvent, TapEvent, KeyEvent} from "core/ui_events" import type * as p from "core/properties" +import {isField} from "core/vectorization" import {GlyphRenderer} from "../../renderers/glyph_renderer" -import type {HasXYGlyph} from "./edit_tool" +import type {XYGlyph} from "../../glyphs/xy_glyph" import {EditTool, EditToolView} from "./edit_tool" import {tool_icon_point_draw} from "styles/icons.css" @@ -20,23 +21,22 @@ export class PointDrawToolView extends EditToolView { return } - // Type once dataspecs are typed - const glyph: any = renderer.glyph - const cds = renderer.data_source - const [xkey, ykey] = [glyph.x.field, glyph.y.field] + const {glyph, data_source} = renderer + const xkey = isField(glyph.x) ? glyph.x.field : null + const ykey = isField(glyph.y) ? glyph.y.field : null const [x, y] = point - this._pop_glyphs(cds, this.model.num_objects) - if (xkey) { - cds.get_array(xkey).push(x) + this._pop_glyphs(data_source, this.model.num_objects) + if (xkey != null) { + data_source.get_array(xkey).push(x) } - if (ykey) { - cds.get_array(ykey).push(y) + if (ykey != null) { + data_source.get_array(ykey).push(y) } - this._pad_empty_columns(cds, [xkey, ykey]) + this._pad_empty_columns(data_source, [xkey, ykey]) - const {data} = cds - cds.setv({data}, {check_eq: false}) // XXX: inplace updates + const {data} = data_source + data_source.setv({data}, {check_eq: false}) // XXX: inplace updates } override _keyup(ev: KeyEvent): void { @@ -86,7 +86,7 @@ export namespace PointDrawTool { add: p.Property drag: p.Property num_objects: p.Property - renderers: p.Property<(GlyphRenderer & HasXYGlyph)[]> + renderers: p.Property[]> } } @@ -107,7 +107,7 @@ export class PointDrawTool extends EditTool { add: [ Bool, true ], drag: [ Bool, true ], num_objects: [ Int, 0 ], - renderers: [ List(Ref(GlyphRenderer as any)), [] ], + renderers: [ List(Ref(GlyphRenderer)), [] ], })) } diff --git a/bokehjs/src/lib/models/tools/edit/poly_draw_tool.ts b/bokehjs/src/lib/models/tools/edit/poly_draw_tool.ts index 9b1ace0f08d..00d642876c9 100644 --- a/bokehjs/src/lib/models/tools/edit/poly_draw_tool.ts +++ b/bokehjs/src/lib/models/tools/edit/poly_draw_tool.ts @@ -1,17 +1,13 @@ import type {UIEvent, PanEvent, TapEvent, MoveEvent, KeyEvent} from "core/ui_events" import type * as p from "core/properties" +import {isField} from "core/vectorization" import {dict} from "core/util/object" import {isArray} from "core/util/types" -import type {MultiLine} from "../../glyphs/multi_line" -import type {Patches} from "../../glyphs/patches" import {GlyphRenderer} from "../../renderers/glyph_renderer" import {PolyTool, PolyToolView} from "./poly_tool" +import type {XsYsGlyph} from "./common" import {tool_icon_poly_draw} from "styles/icons.css" -export interface HasPolyGlyph { - glyph: MultiLine | Patches -} - export class PolyDrawToolView extends PolyToolView { declare model: PolyDrawTool _drawing: boolean = false @@ -41,30 +37,31 @@ export class PolyDrawToolView extends PolyToolView { const cds = renderer.data_source const data = dict(cds.data) - const glyph: any = renderer.glyph - const [xkey, ykey] = [glyph.xs.field, glyph.ys.field] + const glyph = renderer.glyph + const xkey = isField(glyph.xs) ? glyph.xs.field : null + const ykey = isField(glyph.ys) ? glyph.ys.field : null if (mode == "new") { this._pop_glyphs(cds, this.model.num_objects) - if (xkey) { + if (xkey != null) { cds.get_array(xkey).push([x, x]) } - if (ykey) { + if (ykey != null) { cds.get_array(ykey).push([y, y]) } this._pad_empty_columns(cds, [xkey, ykey]) } else if (mode == "edit") { - if (xkey) { + if (xkey != null) { const column = data.get(xkey) ?? [] const xs = column[column.length-1] as number[] xs[xs.length-1] = x } - if (ykey) { + if (ykey != null) { const column = data.get(ykey) ?? [] const ys = column[column.length-1] as number[] ys[ys.length-1] = y } } else if (mode == "add") { - if (xkey) { + if (xkey != null) { const column = data.get(xkey) ?? [] const xidx = column.length-1 let xs = cds.get_array(xkey)[xidx] @@ -76,7 +73,7 @@ export class PolyDrawToolView extends PolyToolView { } xs.push(nx) } - if (ykey) { + if (ykey != null) { const column = data.get(ykey) ?? [] const yidx = column.length-1 let ys = cds.get_array(ykey)[yidx] @@ -98,18 +95,18 @@ export class PolyDrawToolView extends PolyToolView { } const xs: number[] = [] const ys: number[] = [] - for (let i=0; i(xkey)) { + const {glyph, data_source} = renderer + const xkey = isField(glyph.xs) ? glyph.xs.field : null + const ykey = isField(glyph.ys) ? glyph.ys.field : null + if (xkey != null) { + for (const array of data_source.get_array(xkey)) { xs.push(...array) } } - if (ykey) { - for (const array of cds.get_array(ykey)) { + if (ykey != null) { + for (const array of data_source.get_array(ykey)) { ys.push(...array) } } @@ -143,23 +140,23 @@ export class PolyDrawToolView extends PolyToolView { _remove(): void { const renderer = this.model.renderers[0] - const cds = renderer.data_source - const data = dict(cds.data) - const glyph: any = renderer.glyph - const [xkey, ykey] = [glyph.xs.field, glyph.ys.field] - if (xkey) { + const {glyph, data_source} = renderer + const xkey = isField(glyph.xs) ? glyph.xs.field : null + const ykey = isField(glyph.ys) ? glyph.ys.field : null + const data = dict(data_source.data) + if (xkey != null) { const column = data.get(xkey) ?? [] const xidx = column.length-1 - const xs = cds.get_array(xkey)[xidx] + const xs = data_source.get_array(xkey)[xidx] xs.splice(xs.length-1, 1) } - if (ykey) { + if (ykey != null) { const column = data.get(ykey) ?? [] const yidx = column.length-1 - const ys = cds.get_array(ykey)[yidx] + const ys = data_source.get_array(ykey)[yidx] ys.splice(ys.length-1, 1) } - this._emit_cds_changes(cds) + this._emit_cds_changes(data_source) } override _keyup(ev: KeyEvent): void { @@ -201,10 +198,10 @@ export class PolyDrawToolView extends PolyToolView { } const cds = renderer.data_source - // Type once dataspecs are typed - const glyph: any = renderer.glyph - const [xkey, ykey] = [glyph.xs.field, glyph.ys.field] - if (!xkey && !ykey) { + const {glyph} = renderer + const xkey = isField(glyph.xs) ? glyph.xs.field : null + const ykey = isField(glyph.ys) ? glyph.ys.field : null + if (xkey == null && ykey == null) { continue } const [x, y] = point @@ -213,11 +210,11 @@ export class PolyDrawToolView extends PolyToolView { const data = dict(cds.data) for (const index of cds.selected.indices) { let length, xs: any, ys: any - if (xkey) { + if (xkey != null) { const column = data.get(xkey) ?? [] xs = column[index] } - if (ykey) { + if (ykey != null) { const column = data.get(ykey) ?? [] ys = column[index] length = ys.length @@ -280,7 +277,7 @@ export namespace PolyDrawTool { export type Props = PolyTool.Props & { drag: p.Property num_objects: p.Property - renderers: p.Property<(GlyphRenderer & HasPolyGlyph)[]> + renderers: p.Property[]> } } @@ -300,7 +297,7 @@ export class PolyDrawTool extends PolyTool { this.define(({Bool, Int, List, Ref}) => ({ drag: [ Bool, true ], num_objects: [ Int, 0 ], - renderers: [ List(Ref(GlyphRenderer as any)), [] ], + renderers: [ List(Ref(GlyphRenderer)), [] ], })) } diff --git a/bokehjs/src/lib/models/tools/edit/poly_edit_tool.ts b/bokehjs/src/lib/models/tools/edit/poly_edit_tool.ts index 08de2fae965..d0858687f6e 100644 --- a/bokehjs/src/lib/models/tools/edit/poly_edit_tool.ts +++ b/bokehjs/src/lib/models/tools/edit/poly_edit_tool.ts @@ -1,21 +1,18 @@ import type {PanEvent, TapEvent, MoveEvent, KeyEvent, UIEvent} from "core/ui_events" +import {isField, isValue} from "core/vectorization" +import {assert} from "core/util/assert" import {isArray} from "core/util/types" import {dict} from "core/util/object" -import type {MultiLine} from "../../glyphs/multi_line" -import type {Patches} from "../../glyphs/patches" import {GlyphRenderer} from "../../renderers/glyph_renderer" import {PolyTool, PolyToolView} from "./poly_tool" +import type {XsYsGlyph} from "./common" import type * as p from "core/properties" import {tool_icon_poly_edit} from "styles/icons.css" -export interface HasPolyGlyph { - glyph: MultiLine | Patches -} - export class PolyEditToolView extends PolyToolView { declare model: PolyEditTool - _selected_renderer: GlyphRenderer | null + _selected_renderer: GlyphRenderer | null _drawing: boolean = false _cur_index: number | null = null @@ -33,8 +30,9 @@ export class PolyEditToolView extends PolyToolView { const vertex_selected = this._select_event(ev, "replace", [this.model.vertex_renderer]) const point_cds = this.model.vertex_renderer.data_source // Type once dataspecs are typed - const point_glyph: any = this.model.vertex_renderer.glyph - const [pxkey, pykey] = [point_glyph.x.field, point_glyph.y.field] + const point_glyph = this.model.vertex_renderer.glyph + const pxkey = isField(point_glyph.x) ? point_glyph.x.field : null + const pykey = isField(point_glyph.y) ? point_glyph.y.field : null if (vertex_selected.length != 0 && this._selected_renderer != null) { // Insert a new point after the selected vertex and enter draw mode const index = point_cds.selected.indices[0] @@ -43,10 +41,10 @@ export class PolyEditToolView extends PolyToolView { point_cds.selection_manager.clear() } else { point_cds.selected.indices = [index+1] - if (pxkey) { + if (pxkey != null) { point_cds.get_array(pxkey).splice(index+1, 0, x) } - if (pykey) { + if (pykey != null) { point_cds.get_array(pykey).splice(index+1, 0, y) } this._drawing = true @@ -86,39 +84,42 @@ export class PolyEditToolView extends PolyToolView { this._update_vertices(renderers[0]) } - _update_vertices(renderer: GlyphRenderer): void { - const glyph: any = renderer.glyph + _update_vertices(renderer: GlyphRenderer): void { + const {glyph} = renderer + const xkey = isField(glyph.xs) ? glyph.xs.field : null + const ykey = isField(glyph.ys) ? glyph.ys.field : null const data = dict(renderer.data_source.data) const index = this._cur_index - const [xkey, ykey] = [glyph.xs.field, glyph.ys.field] if (this._drawing) { return } - if ((index == null) && (xkey || ykey)) { + if (index == null && (xkey != null || ykey != null)) { return } let xs: number[] let ys: number[] - if (xkey && index != null) { // redundant xkey null check to satisfy build-time checks + if (xkey != null && index != null) { // redundant xkey null check to satisfy build-time checks const column = data.get(xkey) ?? [] xs = column[index] as number[] if (!isArray(xs)) { column[index] = xs = Array.from(xs) } } else { - xs = glyph.xs.value + assert(isValue(glyph.xs)) + xs = glyph.xs.value as number[] // TODO this cast is wrong } - if (ykey && index != null) { + if (ykey != null && index != null) { const column = data.get(ykey) ?? [] ys = column[index] as number[] if (!isArray(ys)) { column[index] = ys = Array.from(ys) } } else { - ys = glyph.ys.value + assert(isValue(glyph.ys)) + ys = glyph.ys.value as number[] // TODO this cast is wrong } this._selected_renderer = renderer this._set_vertices(xs, ys) @@ -132,7 +133,7 @@ export class PolyEditToolView extends PolyToolView { } const cds = renderer.data_source const data = dict(cds.data) - const glyph: any = renderer.glyph + const {glyph} = renderer const point = this._map_drag(ev.sx, ev.sy, renderer) if (point == null) { return @@ -141,12 +142,13 @@ export class PolyEditToolView extends PolyToolView { const indices = cds.selected.indices ;[x, y] = this._snap_to_vertex(ev, x, y) cds.selected.indices = indices - const [xkey, ykey] = [glyph.x.field, glyph.y.field] + const xkey = isField(glyph.x) ? glyph.x.field : null + const ykey = isField(glyph.y) ? glyph.y.field : null const index = indices[0] - if (xkey) { + if (xkey != null) { data.get(xkey)![index] = x } - if (ykey) { + if (ykey != null) { data.get(ykey)![index] = y } cds.change.emit() @@ -166,19 +168,20 @@ export class PolyEditToolView extends PolyToolView { let [x, y] = point const cds = renderer.data_source // Type once dataspecs are typed - const glyph: any = renderer.glyph - const [xkey, ykey] = [glyph.x.field, glyph.y.field] + const {glyph} = renderer + const xkey = isField(glyph.x) ? glyph.x.field : null + const ykey = isField(glyph.y) ? glyph.y.field : null const indices = cds.selected.indices ;[x, y] = this._snap_to_vertex(ev, x, y) const index = indices[0] cds.selected.indices = [index+1] - if (xkey) { + if (xkey != null) { const xs = cds.get_array(xkey) const nx = xs[index] xs[index] = x xs.splice(index+1, 0, nx) } - if (ykey) { + if (ykey != null) { const ys = cds.get_array(ykey) const ny = ys[index] ys[index] = y @@ -201,18 +204,17 @@ export class PolyEditToolView extends PolyToolView { if (renderer == null) { return } - const cds = renderer.data_source - // Type once dataspecs are typed - const glyph: any = renderer.glyph - const index = cds.selected.indices[0] - const [xkey, ykey] = [glyph.x.field, glyph.y.field] - if (xkey) { - cds.get_array(xkey).splice(index, 1) + const {glyph, data_source} = renderer + const index = data_source.selected.indices[0] + const xkey = isField(glyph.x) ? glyph.x.field : null + const ykey = isField(glyph.y) ? glyph.y.field : null + if (xkey != null) { + data_source.get_array(xkey).splice(index, 1) } - if (ykey) { - cds.get_array(ykey).splice(index, 1) + if (ykey != null) { + data_source.get_array(ykey).splice(index, 1) } - cds.change.emit() + data_source.change.emit() this._emit_cds_changes(this._selected_renderer.data_source) } @@ -296,7 +298,7 @@ export namespace PolyEditTool { export type Attrs = p.AttrsOf export type Props = PolyTool.Props & { - renderers: p.Property<(GlyphRenderer & HasPolyGlyph)[]> + renderers: p.Property[]> } } @@ -314,7 +316,7 @@ export class PolyEditTool extends PolyTool { this.prototype.default_view = PolyEditToolView this.define(({List, Ref}) => ({ - renderers: [ List(Ref(GlyphRenderer as any)), [] ], + renderers: [ List(Ref(GlyphRenderer)), [] ], })) } diff --git a/bokehjs/src/lib/models/tools/edit/poly_tool.ts b/bokehjs/src/lib/models/tools/edit/poly_tool.ts index 78c0a2cbfef..7a9878328f6 100644 --- a/bokehjs/src/lib/models/tools/edit/poly_tool.ts +++ b/bokehjs/src/lib/models/tools/edit/poly_tool.ts @@ -1,37 +1,32 @@ import type * as p from "core/properties" +import {isField} from "core/vectorization" import type {UIEvent} from "core/ui_events" import {dict} from "core/util/object" import {isArray} from "core/util/types" import {assert} from "core/util/assert" -import type {MultiLine} from "../../glyphs/multi_line" -import type {Patches} from "../../glyphs/patches" -import type {GlyphRenderer} from "../../renderers/glyph_renderer" - -import type {HasXYGlyph} from "./edit_tool" +import {GlyphRenderer} from "../../renderers/glyph_renderer" +import type {XYGlyph} from "../../glyphs/xy_glyph" import {EditTool, EditToolView} from "./edit_tool" -export interface HasPolyGlyph { - glyph: MultiLine | Patches -} - export abstract class PolyToolView extends EditToolView { declare model: PolyTool _set_vertices(xs: number[] | number, ys: number[] | number): void { const {vertex_renderer} = this.model assert(vertex_renderer != null) - const point_glyph: any = vertex_renderer.glyph + const point_glyph = vertex_renderer.glyph const point_cds = vertex_renderer.data_source - const [pxkey, pykey] = [point_glyph.x.field, point_glyph.y.field] + const pxkey = isField(point_glyph.x) ? point_glyph.x.field : null + const pykey = isField(point_glyph.y) ? point_glyph.y.field : null const data = dict(point_cds.data) - if (pxkey) { + if (pxkey != null) { if (isArray(xs)) { data.set(pxkey, xs) } else { point_glyph.x = {value: xs} } } - if (pykey) { + if (pykey != null) { if (isArray(ys)) { data.set(pykey, ys) } else { @@ -51,15 +46,16 @@ export abstract class PolyToolView extends EditToolView { const vertex_selected = this._select_event(ev, "replace", [this.model.vertex_renderer]) const point_ds = this.model.vertex_renderer.data_source // Type once dataspecs are typed - const point_glyph: any = this.model.vertex_renderer.glyph - const [pxkey, pykey] = [point_glyph.x.field, point_glyph.y.field] + const point_glyph = this.model.vertex_renderer.glyph + const pxkey = isField(point_glyph.x) ? point_glyph.x.field : null + const pykey = isField(point_glyph.y) ? point_glyph.y.field : null if (vertex_selected.length != 0) { const index = point_ds.selected.indices[0] const data = dict(point_ds.data) - if (pxkey) { + if (pxkey != null) { x = data.get(pxkey)![index] as number } - if (pykey) { + if (pykey != null) { y = data.get(pykey)![index] as number } point_ds.selection_manager.clear() @@ -73,7 +69,7 @@ export namespace PolyTool { export type Attrs = p.AttrsOf export type Props = EditTool.Props & { - vertex_renderer: p.Property<(GlyphRenderer & HasXYGlyph) | null> + vertex_renderer: p.Property | null> } } @@ -88,8 +84,8 @@ export abstract class PolyTool extends EditTool { } static { - this.define(({AnyRef, Nullable}) => ({ - vertex_renderer: [ Nullable(AnyRef()), null ], + this.define(({Ref, Nullable}) => ({ + vertex_renderer: [ Nullable(Ref(GlyphRenderer)), null ], })) } } diff --git a/bokehjs/src/lib/models/tools/gestures/range_tool.ts b/bokehjs/src/lib/models/tools/gestures/range_tool.ts index 4373821634f..f51feb1e6a7 100644 --- a/bokehjs/src/lib/models/tools/gestures/range_tool.ts +++ b/bokehjs/src/lib/models/tools/gestures/range_tool.ts @@ -1,17 +1,25 @@ -import {Tool, ToolView} from "../tool" +import {GestureTool, GestureToolView} from "../gestures/gesture_tool" import {OnOffButton} from "../on_off_button" import type {PlotView} from "../../plots/plot" import {BoxAnnotation} from "../../annotations/box_annotation" import {Range} from "../../ranges/range" import type {RangeState} from "../../plots/range_manager" +import type {PanEvent, TapEvent, MoveEvent, KeyEvent, EventType} from "core/ui_events" import {logger} from "core/logging" import type * as p from "core/properties" -import {assert} from "core/util/assert" -import {isNumber, non_null} from "core/util/types" +import {assert, unreachable} from "core/util/assert" +import {isNumber} from "core/util/types" import {tool_icon_range} from "styles/icons.css" import {Node} from "../../coordinates/node" +import type {CoordinateMapper, LRTB} from "core/util/bbox" +import type {CoordinateUnits} from "core/enums" +import type {Scale} from "../../scales/scale" +import {Enum} from "core/kinds" -export class RangeToolView extends ToolView { +const StartGesture = Enum("pan", "tap", "none") +type StartGesture = typeof StartGesture["__type__"] + +export class RangeToolView extends GestureToolView { declare model: RangeTool declare readonly parent: PlotView @@ -36,7 +44,7 @@ export class RangeToolView extends ToolView { if (state == "pan") { this.model.update_ranges_from_overlay() } else if (state == "pan:end") { - const ranges = [this.model.x_range, this.model.y_range].filter(non_null) + const ranges = [this.model.x_range, this.model.y_range].filter((r) => r != null) this.parent.trigger_ranges_update_event(ranges) } }) @@ -46,6 +54,158 @@ export class RangeToolView extends ToolView { this.model.update_constraints() }) } + + protected _mappers(): LRTB { + const mapper = (units: CoordinateUnits, scale: Scale, + view: CoordinateMapper, canvas: CoordinateMapper) => { + switch (units) { + case "canvas": return canvas + case "screen": return view + case "data": return scale + } + } + + const {overlay} = this.model + const {frame, canvas} = this.plot_view + const {x_scale, y_scale} = frame + const {x_view, y_view} = frame.bbox + const {x_screen, y_screen} = canvas.bbox + + return { + left: mapper(overlay.left_units, x_scale, x_view, x_screen), + right: mapper(overlay.right_units, x_scale, x_view, x_screen), + top: mapper(overlay.top_units, y_scale, y_view, y_screen), + bottom: mapper(overlay.bottom_units, y_scale, y_view, y_screen), + } + } + + protected _invert_lrtb({left, right, top, bottom}: LRTB): LRTB { + const lrtb = this._mappers() + + const {x_range, y_range} = this.model + const has_x = x_range != null + const has_y = y_range != null + + return { + left: has_x ? lrtb.left.invert(left) : this.model.nodes.left, + right: has_x ? lrtb.right.invert(right) : this.model.nodes.right, + top: has_y ? lrtb.top.invert(top) : this.model.nodes.top, + bottom: has_y ? lrtb.bottom.invert(bottom) : this.model.nodes.bottom, + } + } + + protected _compute_limits(curr_point: [number, number]): [[number, number], [number, number]] { + const dims = (() => { + const {x_range, y_range} = this.model + const has_x = x_range != null + const has_y = y_range != null + + if (has_x && has_y) { + return "both" + } else if (has_x) { + return "width" + } else if (has_y) { + return "height" + } else { + unreachable() + } + })() + + assert(this._base_point != null) + let base_point = this._base_point + if (this.model.overlay.symmetric) { + const [cx, cy] = base_point + const [dx, dy] = curr_point + base_point = [cx - (dx - cx), cy - (dy - cy)] + } + + const {frame} = this.plot_view + return this.model._get_dim_limits(base_point, curr_point, frame, dims) + } + + protected _base_point: [number, number] | null + + override _tap(ev: TapEvent): void { + assert(this.model.start_gesture == "tap") + + const {sx, sy} = ev + const {frame} = this.plot_view + if (!frame.bbox.contains(sx, sy)) { + return + } + + if (this._base_point == null) { + this._base_point = [sx, sy] + } else { + this._update_overlay(sx, sy) + this._base_point = null + } + } + + override _move(ev: MoveEvent): void { + if (this._base_point != null && this.model.start_gesture == "tap") { + const {sx, sy} = ev + this._update_overlay(sx, sy) + } + } + + override _pan_start(ev: PanEvent): void { + assert(this.model.start_gesture == "pan") + assert(this._base_point == null) + + const {sx, sy} = ev + const {frame} = this.plot_view + if (!frame.bbox.contains(sx, sy)) { + return + } + + this._base_point = [sx, sy] + } + + protected _update_overlay(sx: number, sy: number): void { + const [sxlim, sylim] = this._compute_limits([sx, sy]) + const [[left, right], [top, bottom]] = [sxlim, sylim] + this.model.overlay.update(this._invert_lrtb({left, right, top, bottom})) + this.model.update_ranges_from_overlay() + } + + override _pan(ev: PanEvent): void { + if (this._base_point == null) { + return + } + + const {sx, sy} = ev + this._update_overlay(sx, sy) + } + + override _pan_end(ev: PanEvent): void { + if (this._base_point == null) { + return + } + + const {sx, sy} = ev + this._update_overlay(sx, sy) + + this._base_point = null + } + + protected get _is_selecting(): boolean { + return this._base_point != null + } + + protected _stop(): void { + this._base_point = null + } + + override _keyup(ev: KeyEvent): void { + if (!this.model.active) { + return + } + + if (ev.key == "Escape" && this._is_selecting) { + this._stop() + } + } } const DEFAULT_RANGE_OVERLAY = () => { @@ -75,18 +235,19 @@ const DEFAULT_RANGE_OVERLAY = () => { export namespace RangeTool { export type Attrs = p.AttrsOf - export type Props = Tool.Props & { + export type Props = GestureTool.Props & { x_range: p.Property y_range: p.Property x_interaction: p.Property y_interaction: p.Property overlay: p.Property + start_gesture: p.Property } } export interface RangeTool extends RangeTool.Attrs {} -export class RangeTool extends Tool { +export class RangeTool extends GestureTool { declare properties: RangeTool.Props declare __view_type__: RangeToolView @@ -98,11 +259,12 @@ export class RangeTool extends Tool { this.prototype.default_view = RangeToolView this.define(({Bool, Ref, Nullable}) => ({ - x_range: [ Nullable(Ref(Range)), null ], - y_range: [ Nullable(Ref(Range)), null ], - x_interaction: [ Bool, true ], - y_interaction: [ Bool, true ], - overlay: [ Ref(BoxAnnotation), DEFAULT_RANGE_OVERLAY ], + x_range: [ Nullable(Ref(Range)), null ], + y_range: [ Nullable(Ref(Range)), null ], + x_interaction: [ Bool, true ], + y_interaction: [ Bool, true ], + overlay: [ Ref(BoxAnnotation), DEFAULT_RANGE_OVERLAY ], + start_gesture: [ StartGesture, "none" ], })) this.override({ @@ -174,7 +336,7 @@ export class RangeTool extends Tool { } } - private _nodes = Node.frame.freeze() + readonly nodes = Node.frame.freeze() update_overlay_from_ranges(): void { const {x_range, y_range} = this @@ -182,10 +344,10 @@ export class RangeTool extends Tool { const has_y = y_range != null this.overlay.update({ - left: has_x ? x_range.start : this._nodes.left, - right: has_x ? x_range.end : this._nodes.right, - top: has_y ? y_range.end : this._nodes.top, - bottom: has_y ? y_range.start : this._nodes.bottom, + left: has_x ? x_range.start : this.nodes.left, + right: has_x ? x_range.end : this.nodes.right, + top: has_y ? y_range.end : this.nodes.top, + bottom: has_y ? y_range.start : this.nodes.bottom, }) if (!has_x && !has_y) { @@ -197,6 +359,20 @@ export class RangeTool extends Tool { override tool_name = "Range Tool" override tool_icon = tool_icon_range + get event_type(): EventType | EventType[] { + switch (this.start_gesture) { + case "pan": return "pan" as "pan" + case "tap": return ["tap" as "tap", "move" as "move"] + case "none": return [] + } + } + + readonly default_order = 40 + + override supports_auto(): boolean { + return true + } + override tool_button(): OnOffButton { return new OnOffButton({tool: this}) } diff --git a/bokehjs/src/lib/models/tools/gestures/region_select_tool.ts b/bokehjs/src/lib/models/tools/gestures/region_select_tool.ts index 44543c0cefa..e02f940f34c 100644 --- a/bokehjs/src/lib/models/tools/gestures/region_select_tool.ts +++ b/bokehjs/src/lib/models/tools/gestures/region_select_tool.ts @@ -2,6 +2,7 @@ import {SelectTool, SelectToolView} from "./select_tool" import type {BoxAnnotation} from "../../annotations/box_annotation" import type {PolyAnnotation} from "../../annotations/poly_annotation" import type {DataRendererView} from "../../renderers/data_renderer" +import {RegionSelectionMode} from "core/enums" import type {SelectionMode} from "core/enums" import type {Geometry} from "core/geometry" import type {KeyModifiers} from "core/ui_events" @@ -26,7 +27,7 @@ export abstract class RegionSelectToolView extends SelectToolView { const r_views: DataRendererView[] = [] for (const r of renderers) { - const r_view = this.plot_view.renderer_view(r) + const r_view = this.plot_view.views.find_one(r) if (r_view != null) { r_views.push(r_view) } @@ -47,6 +48,7 @@ export namespace RegionSelectTool { export type Attrs = p.AttrsOf export type Props = SelectTool.Props & { + mode: p.Property continuous: p.Property persistent: p.Property greedy: p.Property @@ -60,6 +62,7 @@ export abstract class RegionSelectTool extends SelectTool { declare __view_type__: RegionSelectToolView declare overlay: BoxAnnotation | PolyAnnotation + declare mode: RegionSelectionMode constructor(attrs?: Partial) { super(attrs) @@ -67,9 +70,10 @@ export abstract class RegionSelectTool extends SelectTool { static { this.define(({Bool}) => ({ + mode: [ RegionSelectionMode, "replace" ], continuous: [ Bool, false ], persistent: [ Bool, false ], - greedy: [ Bool, false ], + greedy: [ Bool, false ], })) } } diff --git a/bokehjs/src/lib/models/tools/gestures/select_tool.ts b/bokehjs/src/lib/models/tools/gestures/select_tool.ts index a3b9ecfe6f0..36797aabe23 100644 --- a/bokehjs/src/lib/models/tools/gestures/select_tool.ts +++ b/bokehjs/src/lib/models/tools/gestures/select_tool.ts @@ -6,13 +6,14 @@ import type {DataSource} from "../../sources/data_source" import {compute_renderers} from "../../util" import type * as p from "core/properties" import type {KeyEvent, KeyModifiers} from "core/ui_events" -import {SelectionMode} from "core/enums" +import type {SelectionMode} from "core/enums" import {SelectionGeometry} from "core/bokeh_events" import type {Geometry} from "core/geometry" import {Signal0} from "core/signaling" import type {MenuItem} from "core/util/menus" import {unreachable} from "core/util/assert" import {uniq} from "core/util/array" +import * as icons from "styles/icons.css" export abstract class SelectToolView extends GestureToolView { declare model: SelectTool @@ -146,7 +147,6 @@ export namespace SelectTool { export type Props = GestureTool.Props & { renderers: p.Property - mode: p.Property } } @@ -163,17 +163,18 @@ export abstract class SelectTool extends GestureTool { super(attrs) } + declare mode: SelectionMode + static { this.define(({List, Ref, Or, Auto}) => ({ renderers: [ Or(List(Ref(DataRenderer)), Auto), "auto" ], - mode: [ SelectionMode, "replace" ], })) } override get menu(): MenuItem[] | null { return [ { - icon: "bk-tool-icon-replace-mode", + icon: icons.tool_icon_replace_mode, tooltip: "Replace the current selection", active: () => this.mode == "replace", handler: () => { @@ -181,7 +182,7 @@ export abstract class SelectTool extends GestureTool { this.active = true }, }, { - icon: "bk-tool-icon-append-mode", + icon: icons.tool_icon_append_mode, tooltip: "Append to the current selection (Shift)", active: () => this.mode == "append", handler: () => { @@ -189,7 +190,7 @@ export abstract class SelectTool extends GestureTool { this.active = true }, }, { - icon: "bk-tool-icon-intersect-mode", + icon: icons.tool_icon_intersect_mode, tooltip: "Intersect with the current selection (Ctrl)", active: () => this.mode == "intersect", handler: () => { @@ -197,7 +198,7 @@ export abstract class SelectTool extends GestureTool { this.active = true }, }, { - icon: "bk-tool-icon-subtract-mode", + icon: icons.tool_icon_subtract_mode, tooltip: "Subtract from the current selection (Shift+Ctrl)", active: () => this.mode == "subtract", handler: () => { @@ -205,7 +206,7 @@ export abstract class SelectTool extends GestureTool { this.active = true }, }, { - icon: "bk-tool-icon-xor-mode", + icon: icons.tool_icon_xor_mode, tooltip: "Symmetric difference with the current selection", active: () => this.mode == "xor", handler: () => { @@ -215,13 +216,13 @@ export abstract class SelectTool extends GestureTool { }, null, { - icon: "bk-tool-icon-invert-selection", + icon: icons.tool_icon_invert_selection, tooltip: "Invert the current selection", handler: () => { this.invert.emit() }, }, { - icon: "bk-tool-icon-clear-selection", + icon: icons.tool_icon_clear_selection, tooltip: "Clear the current selection and/or selection overlay (Esc)", handler: () => { this.clear.emit() diff --git a/bokehjs/src/lib/models/tools/gestures/tap_tool.ts b/bokehjs/src/lib/models/tools/gestures/tap_tool.ts index 3743a4a842c..47e4e8b11ca 100644 --- a/bokehjs/src/lib/models/tools/gestures/tap_tool.ts +++ b/bokehjs/src/lib/models/tools/gestures/tap_tool.ts @@ -5,12 +5,13 @@ import {execute} from "core/util/callbacks" import type * as p from "core/properties" import type {TapEvent, KeyModifiers} from "core/ui_events" import type {PointGeometry} from "core/geometry" -import type {SelectionMode} from "core/enums" +import {SelectionMode} from "core/enums" import {TapBehavior, TapGesture} from "core/enums" -import {non_null} from "core/util/types" +import {prepend} from "core/util/arrayable" +import type {MenuItem} from "core/util/menus" import type {ColumnarDataSource} from "../../sources/columnar_data_source" import type {DataRendererView} from "../../renderers/data_renderer" -import {tool_icon_tap_select} from "styles/icons.css" +import {tool_icon_tap_select, tool_icon_toggle_mode} from "styles/icons.css" export type TapToolCallback = CallbackLike1 this.plot_view.renderer_view(r)).filter(non_null) + const r_views = renderers.map((r) => this.plot_view.views.find_one(r)).filter((r) => r != null) const did_hit = sm.select(r_views, geometry, final, mode) if (did_hit) { const [rv] = r_views @@ -79,7 +80,7 @@ export class TapToolView extends SelectToolView { protected _inspect(geometry: PointGeometry, modifiers?: KeyModifiers): void { for (const r of this.computed_renderers) { - const rv = this.plot_view.renderer_view(r) + const rv = this.plot_view.views.find_one(r) if (rv == null) { continue } @@ -111,6 +112,7 @@ export namespace TapTool { export type Attrs = p.AttrsOf export type Props = SelectTool.Props & { + mode: p.Property behavior: p.Property gesture: p.Property modifiers: p.Property @@ -132,16 +134,13 @@ export class TapTool extends SelectTool { this.prototype.default_view = TapToolView this.define(({Any, Nullable}) => ({ + mode: [ SelectionMode, "toggle" ], behavior: [ TapBehavior, "select" ], gesture: [ TapGesture, "tap"], modifiers: [ Modifiers, {} ], callback: [ Nullable(Any /*TODO*/), null ], })) - this.override({ - mode: "xor", - }) - this.register_alias("click", () => new TapTool({behavior: "inspect"})) this.register_alias("tap", () => new TapTool()) this.register_alias("doubletap", () => new TapTool({gesture: "doubletap"})) @@ -151,4 +150,21 @@ export class TapTool extends SelectTool { override tool_icon = tool_icon_tap_select override event_type = "tap" as "tap" override default_order = 10 + + override get menu(): MenuItem[] | null { + const menu = super.menu + if (menu == null) { + return null + } else { + return prepend(menu, { + icon: tool_icon_toggle_mode, + tooltip: "Toggle the current selection", + active: () => this.mode == "toggle", + handler: () => { + this.mode = "toggle" + this.active = true + }, + }) + } + } } diff --git a/bokehjs/src/lib/models/tools/gestures/wheel_zoom_tool.ts b/bokehjs/src/lib/models/tools/gestures/wheel_zoom_tool.ts index f9a741e2ec4..f3538efd2ff 100644 --- a/bokehjs/src/lib/models/tools/gestures/wheel_zoom_tool.ts +++ b/bokehjs/src/lib/models/tools/gestures/wheel_zoom_tool.ts @@ -3,12 +3,13 @@ import {Modifiers, satisfies_modifiers, print_modifiers} from "./common" import {DataRenderer} from "../../renderers/data_renderer" import type {Scale} from "../../scales/scale" import {CompositeScale} from "../../scales/composite_scale" +import {GroupBy} from "../../misc/group_by" import {scale_range} from "core/util/zoom" import type * as p from "core/properties" import type {PinchEvent, ScrollEvent} from "core/ui_events" import {Dimensions} from "core/enums" import {logger} from "core/logging" -import {assert} from "core/util/assert" +import type {Geometry} from "core/geometry" import {tool_icon_wheel_zoom} from "styles/icons.css" import {Enum, List, Ref, Or, Auto} from "core/kinds" @@ -82,23 +83,70 @@ export class WheelZoomToolView extends GestureToolView { } })() + const data_renderers = (() => { + const {renderers} = this.model + const data_renderers = new Set(renderers != "auto" ? renderers : this.plot_view.model.data_renderers) + + if (!this.model.hit_test) { + return data_renderers + } else { + const collected_renderers = new Set() + const hit_renderers = new Set() + + for (const renderer of data_renderers) { + if (renderer.coordinates == null) { + collected_renderers.add(renderer) + continue + } + + const geometry: Geometry = (() => { + switch (this.model.hit_test_mode) { + case "point": return {type: "point", sx, sy} + case "hline": return {type: "span", sx, sy, direction: "v"} + case "vline": return {type: "span", sx, sy, direction: "h"} + } + })() + + const rv = this.plot_view.views.get_one(renderer) + const did_hit = rv.hit_test(geometry) + if (did_hit != null && !did_hit.is_empty()) { + hit_renderers.add(rv.model) + } + } + + if (hit_renderers.size != 0) { + const {hit_test_behavior} = this.model + if (hit_test_behavior == "only_hit") { + for (const hit of hit_renderers) { + collected_renderers.add(hit) + } + } else { + for (const group of hit_test_behavior.query_groups(hit_renderers, data_renderers)) { + for (const renderer of group) { + if (renderer instanceof DataRenderer && data_renderers.has(renderer)) { + collected_renderers.add(renderer) + } + } + } + } + } + + return [...collected_renderers] + } + })() + const x_frame_scales = new Set(x_frame_scales_) const y_frame_scales = new Set(y_frame_scales_) const x_renderer_scales = new Set() const y_renderer_scales = new Set() - const {renderers} = this.model - const data_renderers = renderers != "auto" ? renderers : this.plot_view.model.data_renderers - for (const renderer of data_renderers) { if (renderer.coordinates == null) { continue } - const rv = this.plot_view.renderer_view(renderer) - assert(rv != null) - + const rv = this.plot_view.views.get_one(renderer) const {x_scale, y_scale} = rv.coordinates if (x_scale instanceof CompositeScale) { @@ -115,7 +163,7 @@ export class WheelZoomToolView extends GestureToolView { } const [x_all_scales, y_all_scales] = (() => { - if (renderers == "auto") { + if (this.model.renderers == "auto") { return [ new Set([...x_frame_scales, ...x_renderer_scales]), new Set([...y_frame_scales, ...y_renderer_scales]), @@ -197,6 +245,9 @@ export namespace WheelZoomTool { dimensions: p.Property renderers: p.Property level: p.Property + hit_test: p.Property + hit_test_mode: p.Property<"point" | "hline" | "vline"> + hit_test_behavior: p.Property maintain_focus: p.Property zoom_on_axis: p.Property zoom_together: p.Property @@ -218,10 +269,13 @@ export class WheelZoomTool extends GestureTool { static { this.prototype.default_view = WheelZoomToolView - this.define(({Bool, Float, NonNegative, Int}) => ({ + this.define(({Bool, Float, NonNegative, Int, Ref, Or}) => ({ dimensions: [ Dimensions, "both" ], renderers: [ Renderers, "auto" ], level: [ NonNegative(Int), 0 ], + hit_test: [ Bool, false ], + hit_test_mode: [ Enum("point", "hline", "vline"), "point" ], + hit_test_behavior: [ Or(Ref(GroupBy), Enum("only_hit")), "only_hit" ], maintain_focus: [ Bool, true ], zoom_on_axis: [ Bool, true ], zoom_together: [ ZoomTogether, "all" ], diff --git a/bokehjs/src/lib/models/tools/inspectors/crosshair_tool.ts b/bokehjs/src/lib/models/tools/inspectors/crosshair_tool.ts index 99081b728e7..6c6b5efc10a 100644 --- a/bokehjs/src/lib/models/tools/inspectors/crosshair_tool.ts +++ b/bokehjs/src/lib/models/tools/inspectors/crosshair_tool.ts @@ -148,6 +148,8 @@ export class CrosshairTool extends InspectTool { })) this.register_alias("crosshair", () => new CrosshairTool()) + this.register_alias("xcrosshair", () => new CrosshairTool({dimensions: "width"})) + this.register_alias("ycrosshair", () => new CrosshairTool({dimensions: "height"})) } override tool_name = "Crosshair" diff --git a/bokehjs/src/lib/models/tools/inspectors/hover_tool.ts b/bokehjs/src/lib/models/tools/inspectors/hover_tool.ts index 8d7ff45403e..d8b404a5ada 100644 --- a/bokehjs/src/lib/models/tools/inspectors/hover_tool.ts +++ b/bokehjs/src/lib/models/tools/inspectors/hover_tool.ts @@ -1,7 +1,7 @@ -import type {ViewStorage, IterViews} from "core/build_views" -import {build_view, build_views, remove_views} from "core/build_views" +import type {ViewStorage, IterViews, ViewOf} from "core/build_views" +import {build_view, build_views, remove_views, traverse_views} from "core/build_views" import {display, div, empty, span, undisplay} from "core/dom" -import {Anchor, HoverMode, LinePolicy, MutedPolicy, PointPolicy, TooltipAttachment} from "core/enums" +import {Anchor, HoverMode, LinePolicy, MutedPolicy, PointPolicy, TooltipAttachment, BuiltinFormatter} from "core/enums" import type {Geometry, GeometryData, PointGeometry, SpanGeometry} from "core/geometry" import * as hittest from "core/hittest" import type * as p from "core/properties" @@ -13,14 +13,15 @@ import {color2css, color2hex} from "core/util/color" import {enumerate} from "core/util/iterator" import type {CallbackLike1} from "core/util/callbacks" import {execute} from "core/util/callbacks" -import type {Formatters} from "core/util/templating" -import {FormatterType, replace_placeholders} from "core/util/templating" +import type {Formatters, Index} from "core/util/templating" +import {replace_placeholders} from "core/util/templating" import {isFunction, isNumber, isString, is_undefined} from "core/util/types" import {tool_icon_hover} from "styles/icons.css" import * as styles from "styles/tooltips.css" import {Tooltip} from "../../ui/tooltip" -import type {TemplateView} from "../../dom/template" -import {Template} from "../../dom/template" +import {DOMElement} from "../../dom/dom_element" +import {PlaceholderView} from "../../dom/placeholder" +import {TemplateView} from "../../dom/template" import type {GlyphView} from "../../glyphs/glyph" import {HAreaView} from "../../glyphs/harea" import {HAreaStepView} from "../../glyphs/harea_step" @@ -104,7 +105,7 @@ export class HoverToolView extends InspectToolView { protected readonly _ttviews: ViewStorage = new Map() protected _template_el?: HTMLElement - protected _template_view?: TemplateView + protected _template_view?: ViewOf override *children(): IterViews { yield* super.children() @@ -119,7 +120,7 @@ export class HoverToolView extends InspectToolView { await this._update_ttmodels() const {tooltips} = this.model - if (tooltips instanceof Template) { + if (tooltips instanceof DOMElement) { this._template_view = await build_view(tooltips, {parent: this.plot_view.canvas}) this._template_view.render() } @@ -243,7 +244,7 @@ export class HoverToolView extends InspectToolView { for (const r of this.computed_renderers) { const sm = r.get_selection_manager() - const rview = this.plot_view.renderer_view(r) + const rview = this.plot_view.views.find_one(r) if (rview != null) { sm.inspect(rview, geometry) } @@ -264,7 +265,7 @@ export class HoverToolView extends InspectToolView { } const ds = selection_manager.source - const renderer_view = this.plot_view.renderer_view(renderer) + const renderer_view = this.plot_view.views.find_one(renderer) if (renderer_view == null) { return } @@ -277,7 +278,7 @@ export class HoverToolView extends InspectToolView { const {glyph} = renderer_view - const tooltips: [number, number, HTMLElement | null][] = [] + const tooltips: [number, number, Node | null][] = [] if (glyph instanceof PatchView) { const [snap_sx, snap_sy] = [sx, sy] @@ -454,7 +455,7 @@ export class HoverToolView extends InspectToolView { tooltip.clear() } else { const {content} = tooltip - assert(content instanceof Element) + assert(content instanceof Node) empty(content) for (const [,, node] of in_frame) { if (node != null) { @@ -499,7 +500,7 @@ export class HoverToolView extends InspectToolView { continue } - const glyph_renderer_view = this.plot_view.renderer_view(renderer) + const glyph_renderer_view = this.plot_view.views.find_one(renderer) if (glyph_renderer_view == null) { continue } @@ -612,30 +613,39 @@ export class HoverToolView extends InspectToolView { return el } - _render_tooltips(ds: ColumnarDataSource, vars: TooltipVars): HTMLElement | null { + _render_tooltips(ds: ColumnarDataSource, vars: TooltipVars): Element | null { const {tooltips} = this.model const i = vars.index if (isString(tooltips)) { const content = replace_placeholders({html: tooltips}, ds, i, this.model.formatters, vars) return div(content) - } - - if (isFunction(tooltips)) { + } else if (isFunction(tooltips)) { return tooltips(ds, vars) - } - - if (tooltips instanceof Template) { - this._template_view!.update(ds, i, vars) - return this._template_view!.el - } - - if (tooltips != null) { + } else if (tooltips instanceof DOMElement) { + const {_template_view} = this + assert(_template_view != null) + this._update_template(_template_view, ds, i, vars) + return _template_view.el.cloneNode(true) as HTMLElement + } else if (tooltips != null) { const template = this._template_el ?? (this._template_el = this._create_template(tooltips)) return this._render_template(template, tooltips, ds, vars) + } else { + return null } + } - return null + protected _update_template(template_view: ViewOf, ds: ColumnarDataSource, i: Index | null, vars: TooltipVars): void { + const {formatters} = this.model + if (template_view instanceof TemplateView) { + template_view.update(ds, i, vars, formatters) + } else { + traverse_views([template_view], (view) => { + if (view instanceof PlaceholderView) { + view.update(ds, i, vars, formatters) + } + }) + } } } @@ -643,7 +653,7 @@ export namespace HoverTool { export type Attrs = p.AttrsOf export type Props = InspectTool.Props & { - tooltips: p.Property HTMLElement)> + tooltips: p.Property HTMLElement)> formatters: p.Property renderers: p.Property mode: p.Property @@ -671,12 +681,12 @@ export class HoverTool extends InspectTool { this.prototype.default_view = HoverToolView this.define(({Any, Bool, Str, List, Tuple, Dict, Or, Ref, Func, Auto, Nullable}) => ({ - tooltips: [ Nullable(Or(Ref(Template), Str, List(Tuple(Str, Str)), Func<[ColumnarDataSource, TooltipVars], HTMLElement>())), [ + tooltips: [ Nullable(Or(Ref(DOMElement), Str, List(Tuple(Str, Str)), Func<[ColumnarDataSource, TooltipVars], HTMLElement>())), [ ["index", "$index" ], ["data (x, y)", "($x, $y)" ], ["screen (x, y)", "($sx, $sy)"], ]], - formatters: [ Dict(Or(Ref(CustomJSHover), FormatterType)), {} ], + formatters: [ Dict(Or(Ref(CustomJSHover), BuiltinFormatter)), {} ], renderers: [ Or(List(Ref(DataRenderer)), Auto), "auto" ], mode: [ HoverMode, "mouse" ], muted_policy: [ MutedPolicy, "show" ], diff --git a/bokehjs/src/lib/models/tools/tool.ts b/bokehjs/src/lib/models/tools/tool.ts index 9643df88d56..c23cdc28262 100644 --- a/bokehjs/src/lib/models/tools/tool.ts +++ b/bokehjs/src/lib/models/tools/tool.ts @@ -50,6 +50,8 @@ export type ToolAliases = { click: TapTool tap: TapTool crosshair: CrosshairTool + xcrosshair: CrosshairTool + ycrosshair: CrosshairTool box_select: BoxSelectTool xbox_select: BoxSelectTool ybox_select: BoxSelectTool diff --git a/bokehjs/src/lib/models/tools/toolbar.ts b/bokehjs/src/lib/models/tools/toolbar.ts index 2486249b726..9065f0c475f 100644 --- a/bokehjs/src/lib/models/tools/toolbar.ts +++ b/bokehjs/src/lib/models/tools/toolbar.ts @@ -259,6 +259,8 @@ export class ToolbarView extends UIElementView { import {Struct, Ref, Nullable, List, Or} from "core/kinds" const GestureToolLike = Or(Ref(GestureTool), Ref(ToolProxy)) +type GestureToolLike = GestureTool | ToolProxy + const GestureEntry = Struct({ tools: List(GestureToolLike), active: Nullable(GestureToolLike), @@ -280,20 +282,14 @@ type GesturesMap = typeof GesturesMap["__type__"] type GestureType = keyof GesturesMap // XXX: add appropriate base classes to get rid of this -export type Drag = Tool -export const Drag = Tool export type Inspection = Tool export const Inspection = Tool -export type Scroll = Tool -export const Scroll = Tool -export type Tap = Tool -export const Tap = Tool type ActiveGestureToolsProps = { - active_drag: p.Property | "auto" | null> - active_scroll: p.Property | "auto" | null> - active_tap: p.Property | "auto" | null> - active_multi: p.Property | "auto" | null> + active_drag: p.Property + active_scroll: p.Property + active_tap: p.Property + active_multi: p.Property } export namespace Toolbar { @@ -352,11 +348,11 @@ export class Toolbar extends UIElement { tools: [ List(Or(Ref(Tool), Ref(ToolProxy))), [] ], logo: [ Nullable(Logo), "normal" ], autohide: [ Bool, false ], - active_drag: [ Nullable(Or(Ref(Drag), Auto)), "auto" ], - active_inspect: [ Nullable(Or(Ref(Inspection), List(Ref(Inspection)), Auto)), "auto" ], - active_scroll: [ Nullable(Or(Ref(Scroll), Auto)), "auto" ], - active_tap: [ Nullable(Or(Ref(Tap), Auto)), "auto" ], - active_multi: [ Nullable(Or(Ref(GestureTool), Auto)), "auto" ], + active_drag: [ Nullable(Or(GestureToolLike, Auto)), "auto" ], + active_inspect: [ Nullable(Or(Ref(Inspection), List(Ref(Inspection)), Ref(ToolProxy), Auto)), "auto" ], + active_scroll: [ Nullable(Or(GestureToolLike, Auto)), "auto" ], + active_tap: [ Nullable(Or(GestureToolLike, Auto)), "auto" ], + active_multi: [ Nullable(Or(GestureToolLike, Auto)), "auto" ], })) this.internal(({List, Bool, Ref, Or, Null, Auto}) => { @@ -409,13 +405,13 @@ export class Toolbar extends UIElement { return is } - const new_inspectors = this.tools.filter(t => isa(t, InspectTool)) as ToolLike[] + const new_inspectors = this.tools.filter(t => isa(t, InspectTool)) this.inspectors = new_inspectors - const new_help = this.tools.filter(t => isa(t, HelpTool)) as ToolLike[] + const new_help = this.tools.filter(t => isa(t, HelpTool)) this.help = new_help - const new_actions = this.tools.filter(t => isa(t, ActionTool)) as ToolLike[] + const new_actions = this.tools.filter(t => isa(t, ActionTool)) this.actions = new_actions const new_gestures = create_gesture_map() @@ -500,6 +496,10 @@ export class Toolbar extends UIElement { return et == "tap" || et == "pan" || tool.supports_auto() } + const is_active_gesture = (active_tool: ToolLike): boolean => { + return this.tools.includes(active_tool) || (active_tool instanceof Tool && this.tools.some((tool) => tool instanceof ToolProxy && tool.tools.includes(active_tool))) + } + for (const [event_role, gesture] of entries(this.gestures)) { const et = event_role as EventRole const active_attr = _get_active_attr(et) @@ -513,9 +513,8 @@ export class Toolbar extends UIElement { } } } else if (active_tool != null) { - // TODO: allow to activate a proxy of tools with any child? - if (includes(this.tools, active_tool)) { - _activate_gesture(active_tool as ToolLike) // XXX: remove this cast + if (is_active_gesture(active_tool)) { + _activate_gesture(active_tool) } else { this[active_attr] = null } diff --git a/bokehjs/src/lib/models/ui/dialog.ts b/bokehjs/src/lib/models/ui/dialog.ts index 714bd89e541..565fee27b1e 100644 --- a/bokehjs/src/lib/models/ui/dialog.ts +++ b/bokehjs/src/lib/models/ui/dialog.ts @@ -19,6 +19,9 @@ import {Or, Ref} from "core/kinds" import dialogs_css, * as dialogs from "styles/dialogs.css" import icons_css from "styles/icons.css" +// Make sure this at least an order of magnitude lower than --bokeh-top-level. +const base_z_index = 1000 + const UIElementLike = Or(Ref(UIElement), Ref(DOMNode)) type UIElementLike = typeof UIElementLike["__type__"] @@ -276,6 +279,10 @@ export class DialogView extends UIElementView { }) this._has_rendered = true + + if (this.model.visible) { + this.bring_to_front() + } } get resizable(): LRTB { @@ -543,9 +550,6 @@ export class DialogView extends UIElementView { this.render_to(document.body) this.r_after_render() } - if (!_stacking_order.includes(this)) { - _stacking_order.push(this) - } this.bring_to_front() } else { remove(_stacking_order, this) @@ -571,6 +575,9 @@ export class DialogView extends UIElementView { } bring_to_front(): void { + if (!_stacking_order.includes(this)) { + _stacking_order.push(this) + } const pinned = find(_stacking_order, (view) => view._pinned) if (pinned != null) { remove(_stacking_order, pinned) @@ -583,7 +590,7 @@ export class DialogView extends UIElementView { for (const [dialog_view, i] of enumerate(_stacking_order)) { dialog_view._stacking.replace(":host", { - "z-index": `${i}`, + "z-index": `${base_z_index + i}`, }) } } diff --git a/bokehjs/src/lib/models/ui/menus/menu.ts b/bokehjs/src/lib/models/ui/menus/menu.ts index 27981e8fdb1..2819d1bc225 100644 --- a/bokehjs/src/lib/models/ui/menus/menu.ts +++ b/bokehjs/src/lib/models/ui/menus/menu.ts @@ -11,7 +11,6 @@ import {ToolIcon} from "core/enums" import type {ViewStorage, IterViews} from "core/build_views" import {build_views, remove_views} from "core/build_views" import {reversed as reverse} from "core/util/array" -import {isNotNull} from "core/util/types" import {execute} from "core/util/callbacks" import menus_css, * as menus from "styles/menus_.css" @@ -31,7 +30,7 @@ export class MenuView extends UIElementView { await super.lazy_initialize() const menus = this.model.items .map((item) => item instanceof ActionItem ? item.menu : null) - .filter(isNotNull) + .filter((item) => item != null) await build_views(this._menu_views, menus, {parent: this}) } diff --git a/bokehjs/src/lib/models/ui/pane.ts b/bokehjs/src/lib/models/ui/pane.ts index 8407adc91da..1e083f5bc0a 100644 --- a/bokehjs/src/lib/models/ui/pane.ts +++ b/bokehjs/src/lib/models/ui/pane.ts @@ -49,10 +49,11 @@ export class PaneView extends UIElementView { for (const element_view of this.element_views) { const is_new = created_elements.has(element_view) + const target = element_view.rendering_target() ?? this.shadow_el if (is_new) { - element_view.render_to(this.shadow_el) + element_view.render_to(target) } else { - this.shadow_el.append(element_view.el) + target.append(element_view.el) } } this.r_after_render() @@ -75,7 +76,8 @@ export class PaneView extends UIElementView { super.render() for (const element_view of this.element_views) { - element_view.render_to(this.shadow_el) + const target = element_view.rendering_target() ?? this.shadow_el + element_view.render_to(target) } } diff --git a/bokehjs/src/lib/models/ui/tooltip.ts b/bokehjs/src/lib/models/ui/tooltip.ts index ff8f5c921bd..e62606505c0 100644 --- a/bokehjs/src/lib/models/ui/tooltip.ts +++ b/bokehjs/src/lib/models/ui/tooltip.ts @@ -76,13 +76,19 @@ export class TooltipView extends UIElementView { override async lazy_initialize(): Promise { await super.lazy_initialize() + await this._build_content() + } + + protected async _build_content(): Promise { + if (this._element_view != null) { + this._element_view.remove() + this._element_view = null + } const {content} = this.model if (content instanceof Model) { this._element_view = await build_view(content, {parent: this}) } - - this.render() } private _scroll_listener?: () => void @@ -113,9 +119,20 @@ export class TooltipView extends UIElementView { this._observer.disconnect() this._observer.observe(this.target) this.render() + this.after_render() + }) + this.on_change(content, async () => { + await this._build_content() + this.render() + this.after_render() + }) + this.on_change([closable, interactive], () => { + this.render() + this.after_render() + }) + this.on_change([position, attachment, visible], () => { + this._reposition() }) - this.on_change([content, closable, interactive], () => this.render()) - this.on_change([position, attachment, visible], () => this._reposition()) } override disconnect_signals(): void { @@ -148,6 +165,8 @@ export class TooltipView extends UIElementView { } } + private _has_rendered: boolean = false + override render(): void { super.render() @@ -170,66 +189,67 @@ export class TooltipView extends UIElementView { this.el.classList.toggle(tooltips.show_arrow, this.model.show_arrow) this.el.classList.toggle(tooltips.non_interactive, !this.model.interactive) + this._has_rendered = true + } + + override _after_render(): void { + super._after_render() this._reposition() } - private _anchor_to_align(anchor: Anchor): [VAlign, HAlign] { - switch (anchor) { - case "top_left": - return ["top", "left"] - case "top": - case "top_center": - return ["top", "center"] - case "top_right": - return ["top", "right"] - - case "left": - case "center_left": - return ["center", "left"] - case "center": - case "center_center": - return ["center", "center"] - case "right": - case "center_right": - return ["center", "right"] - - case "bottom_left": - return ["bottom", "left"] - case "bottom": - case "bottom_center": - return ["bottom", "center"] - case "bottom_right": - return ["bottom", "right"] - } + override _after_resize(): void { + super._after_resize() + this._reposition() + } + + private _anchor_to_align(anchor: Anchor): {v: VAlign, h: HAlign} { + anchor = (() => { + switch (anchor) { + case "top": return "top_center" + case "bottom": return "bottom_center" + case "left": return "center_left" + case "right": return "center_right" + default: return anchor + } + })() + const [v, h] = anchor.split("_") as [VAlign, HAlign] + return {v, h} } protected _reposition(): void { + // Append to `body` to deal with CSS' `contain` interaction + // with `position: fixed`. We assume initial containment + // block in this function, but `contain` can introduce a + // new containment block and offset tooltip's position. + const target = document.body.shadowRoot ?? document.body + + if (!this._has_rendered) { + this.render_to(target) + this.after_render() + return // render() calls _reposition() + } + const {position, visible} = this.model if (position == null || !visible) { this.el.remove() return } - // Append to `body` to deal with CSS' `contain` interaction - // with `position: fixed`. We assume initial containment - // block in this function, but `contain` can introduce a - // new containment block and offset tooltip's position. - (document.body.shadowRoot ?? document.body).append(this.el) + target.append(this.el) const bbox = bounding_box(this.target) - const [sx, sy] = (() => { if (isString(position)) { - const [valign, halign] = this._anchor_to_align(position) + const {v: v_align, h: h_align} = this._anchor_to_align(position) const sx = (() => { - switch (halign) { + switch (h_align) { case "left": return bbox.left case "center": return bbox.hcenter case "right": return bbox.right } })() const sy = (() => { - switch (valign) { + switch (v_align) { case "top": return bbox.top case "center": return bbox.vcenter case "bottom": return bbox.bottom @@ -260,12 +280,12 @@ export class TooltipView extends UIElementView { const {attachment} = this.model if (attachment == "auto") { if (isString(position)) { - const [valign, halign] = this._anchor_to_align(position) - if (halign != "center") { - return halign == "left" ? "left" : "right" + const {v: v_align, h: h_align} = this._anchor_to_align(position) + if (h_align != "center") { + return h_align == "left" ? "left" : "right" } - if (valign != "center") { - return valign == "top" ? "above" : "below" + if (v_align != "center") { + return v_align == "top" ? "above" : "below" } } return "horizontal" diff --git a/bokehjs/src/lib/models/widgets/color_picker.ts b/bokehjs/src/lib/models/widgets/color_picker.ts index 0208fe3def2..62173931cec 100644 --- a/bokehjs/src/lib/models/widgets/color_picker.ts +++ b/bokehjs/src/lib/models/widgets/color_picker.ts @@ -21,7 +21,7 @@ export class ColorPickerView extends InputWidgetView { type: "color", class: inputs.input, name: this.model.name, - value: this.model.color, + value: color2hexrgb(this.model.color), disabled: this.model.disabled, }) } diff --git a/bokehjs/src/lib/models/widgets/dropdown.ts b/bokehjs/src/lib/models/widgets/dropdown.ts index 0e7e67edb0a..d2a78eca2ef 100644 --- a/bokehjs/src/lib/models/widgets/dropdown.ts +++ b/bokehjs/src/lib/models/widgets/dropdown.ts @@ -1,13 +1,11 @@ import {AbstractButton, AbstractButtonView} from "./abstract_button" - import {ButtonClick, MenuItemClick} from "core/bokeh_events" import type {StyleSheetLike} from "core/dom" -import {div, display, undisplay} from "core/dom" +import {div, display, undisplay, empty} from "core/dom" import type * as p from "core/properties" import {isString} from "core/util/types" import type {CallbackLike1} from "core/util/callbacks" import {execute} from "core/util/callbacks" - import * as buttons from "styles/buttons.css" import dropdown_css, * as dropdown from "styles/dropdown.css" import carets_css, * as carets from "styles/caret.css" @@ -16,47 +14,43 @@ export class DropdownView extends AbstractButtonView { declare model: Dropdown protected _open: boolean = false - - protected menu: HTMLElement + protected menu_el: HTMLElement override stylesheets(): StyleSheetLike[] { return [...super.stylesheets(), dropdown_css, carets_css] } + override connect_signals(): void { + super.connect_signals() + + const {menu} = this.model.properties + this.on_change(menu, () => this.rebuild_menu()) + } + override render(): void { super.render() const caret = div({class: [carets.caret, carets.down]}) if (!this.model.is_split) { - this.button_el.appendChild(caret) + this.button_el.append(caret) } else { const toggle = this._render_button(caret) toggle.classList.add(buttons.dropdown_toggle) toggle.addEventListener("click", () => this._toggle_menu()) - this.group_el.appendChild(toggle) + this.group_el.append(toggle) } - const items = this.model.menu.map((item, i) => { - if (item == null) { - return div({class: dropdown.divider}) - } else { - const label = isString(item) ? item : item[0] - const el = div(label) - el.addEventListener("click", () => this._item_click(i)) - return el - } - }) - - this.menu = div({class: [dropdown.menu, dropdown.below]}, items) - this.shadow_el.appendChild(this.menu) - undisplay(this.menu) + this.menu_el = div({class: [dropdown.menu, dropdown.below]}) + this.shadow_el.append(this.menu_el) + this.rebuild_menu() + undisplay(this.menu_el) } protected _show_menu(): void { if (!this._open) { this._open = true - display(this.menu) + display(this.menu_el) const listener = (event: MouseEvent) => { if (!event.composedPath().includes(this.el)) { @@ -71,7 +65,7 @@ export class DropdownView extends AbstractButtonView { protected _hide_menu(): void { if (this._open) { this._open = false - undisplay(this.menu) + undisplay(this.menu_el) } } @@ -99,6 +93,7 @@ export class DropdownView extends AbstractButtonView { const item = this.model.menu[i] if (item != null) { const value_or_callback = isString(item) ? item : item[1] + if (isString(value_or_callback)) { this.model.trigger_event(new MenuItemClick(value_or_callback)) } else { @@ -106,6 +101,22 @@ export class DropdownView extends AbstractButtonView { } } } + + rebuild_menu(): void { + empty(this.menu_el) + + const items = this.model.menu.map((item, i) => { + if (item == null) { + return div({class: dropdown.divider}) + } else { + const label = isString(item) ? item : item[0] + const el = div(label) + el.addEventListener("click", () => this._item_click(i)) + return el + } + }) + this.menu_el.append(...items) + } } export namespace Dropdown { diff --git a/bokehjs/src/lib/models/widgets/file_input.ts b/bokehjs/src/lib/models/widgets/file_input.ts index 35593e8dd37..0133be2d2e5 100644 --- a/bokehjs/src/lib/models/widgets/file_input.ts +++ b/bokehjs/src/lib/models/widgets/file_input.ts @@ -1,4 +1,4 @@ -import {InputWidget, InputWidgetView} from "./input_widget" +import {InputWidget, InputWidgetView, ClearInput} from "./input_widget" import type {StyleSheetLike} from "core/dom" import {input} from "core/dom" import {isString} from "core/util/types" @@ -10,19 +10,32 @@ export class FileInputView extends InputWidgetView { declare model: FileInput declare input_el: HTMLInputElement + override connect_signals(): void { + super.connect_signals() + + this.model.on_event(ClearInput, () => { + this.model.setv({ + value: "", // p.unset, + mime_type: "", // p.unset, + filename: "", // p.unset, + }) + this.input_el.value = "" + }) + } + override stylesheets(): StyleSheetLike[] { return [...super.stylesheets(), buttons_css] } protected _render_input(): HTMLElement { - const {multiple, disabled} = this.model + const {multiple, disabled, directory} = this.model const accept = (() => { const {accept} = this.model return isString(accept) ? accept : accept.join(",") })() - return this.input_el = input({type: "file", class: inputs.input, multiple, accept, disabled}) + return this.input_el = input({type: "file", class: inputs.input, multiple, accept, disabled, webkitdirectory: directory}) } override render(): void { @@ -40,18 +53,32 @@ export class FileInputView extends InputWidgetView { const values: string[] = [] const filenames: string[] = [] const mime_types: string[] = [] + const {directory, multiple} = this.model + const accept = (() => { + const {accept} = this.model + return isString(accept) ? accept : accept.join(",") + })() for (const file of files) { const data_url = await this._read_file(file) const [, mime_type="",, value=""] = data_url.split(/[:;,]/, 4) - values.push(value) - filenames.push(file.name) - mime_types.push(mime_type) + if (directory) { + const ext = file.name.split(".").pop() + if ((accept.length > 0 && isString(ext)) ? accept.includes(`.${ext}`) : true) { + filenames.push(file.webkitRelativePath) + values.push(value) + mime_types.push(mime_type) + } + } else { + filenames.push(file.name) + values.push(value) + mime_types.push(mime_type) + } } const [value, filename, mime_type] = (() =>{ - if (this.model.multiple) { + if (directory || multiple) { return [values, filenames, mime_types] } else if (files.length != 0) { return [values[0], filenames[0], mime_types[0]] @@ -87,6 +114,7 @@ export namespace FileInput { filename: p.Property accept: p.Property multiple: p.Property + directory: p.Property } } @@ -109,6 +137,7 @@ export class FileInput extends InputWidget { filename: [ Or(Str, List(Str)), p.unset, {readonly: true} ], accept: [ Or(Str, List(Str)), "" ], multiple: [ Bool, false ], + directory: [ Bool, false ], })) } } diff --git a/bokehjs/src/lib/models/widgets/input_widget.ts b/bokehjs/src/lib/models/widgets/input_widget.ts index 5fc4ecdf72e..860bd35defb 100644 --- a/bokehjs/src/lib/models/widgets/input_widget.ts +++ b/bokehjs/src/lib/models/widgets/input_widget.ts @@ -10,12 +10,26 @@ import type {StyleSheetLike} from "core/dom" import {div, label} from "core/dom" import {View} from "core/view" import type * as p from "core/properties" +import {server_event, ModelEvent} from "core/bokeh_events" import inputs_css, * as inputs from "styles/widgets/inputs.css" import icons_css from "styles/icons.css" export type HTMLInputElementLike = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement +@server_event("clear_input") +export class ClearInput extends ModelEvent { + constructor(readonly model: InputWidget) { + super() + this.origin = model + } + + static override from_values(values: object): ClearInput { + const {model} = values as {model: InputWidget} + return new ClearInput(model) + } +} + export abstract class InputWidgetView extends ControlView { declare model: InputWidget diff --git a/bokehjs/src/lib/models/widgets/palette_select.ts b/bokehjs/src/lib/models/widgets/palette_select.ts index 6bd001ced92..847cc8f90a0 100644 --- a/bokehjs/src/lib/models/widgets/palette_select.ts +++ b/bokehjs/src/lib/models/widgets/palette_select.ts @@ -57,7 +57,10 @@ export class PaletteSelectView extends InputWidgetView { const [_name, colors] = item const {swatch_width, swatch_height} = this.model - const img = canvas({width: `${swatch_width}`, height: `${swatch_height}`}) + const width = swatch_width + const height = swatch_height == "auto" ? swatch_width : swatch_height + + const img = canvas({width, height}) const ctx = img.getContext("2d")! const n = colors.length diff --git a/bokehjs/src/lib/models/widgets/paragraph.ts b/bokehjs/src/lib/models/widgets/paragraph.ts index 880281e634b..da444692fc0 100644 --- a/bokehjs/src/lib/models/widgets/paragraph.ts +++ b/bokehjs/src/lib/models/widgets/paragraph.ts @@ -8,7 +8,7 @@ export class ParagraphView extends MarkupView { override render(): void { super.render() // This overrides default user-agent styling and helps layout work - const content = paragraph({style: {margin: 0}}) + const content = paragraph({style: {margin: "0px"}}) if (this.has_math_disabled()) { content.textContent = this.model.text diff --git a/bokehjs/src/lib/models/widgets/sliders/categorical_slider.ts b/bokehjs/src/lib/models/widgets/sliders/categorical_slider.ts index bb6d94f367c..18bfc0fe82d 100644 --- a/bokehjs/src/lib/models/widgets/sliders/categorical_slider.ts +++ b/bokehjs/src/lib/models/widgets/sliders/categorical_slider.ts @@ -33,7 +33,7 @@ export class CategoricalSliderView extends AbstractSliderView { protected _calc_from([value]: number[]): string { const {categories} = this.model - return categories[value] + return categories[value | 0] // value may not be an integer due to noUiSlider's FP math } pretty(value: number | string): string { diff --git a/bokehjs/src/lib/models/widgets/tables/data_cube.ts b/bokehjs/src/lib/models/widgets/tables/data_cube.ts index 13750ebaf85..5cb6579f368 100644 --- a/bokehjs/src/lib/models/widgets/tables/data_cube.ts +++ b/bokehjs/src/lib/models/widgets/tables/data_cube.ts @@ -28,7 +28,6 @@ function groupCellFormatter(_row: number, _cell: number, _value: unknown, _colum }) const titleElement = span({ class: "slick-group-title", - level, }, title) return `${toggle.outerHTML}${titleElement.outerHTML}` diff --git a/bokehjs/src/lib/models/widgets/tables/data_table.ts b/bokehjs/src/lib/models/widgets/tables/data_table.ts index 798935ffa21..20e10e92911 100644 --- a/bokehjs/src/lib/models/widgets/tables/data_table.ts +++ b/bokehjs/src/lib/models/widgets/tables/data_table.ts @@ -110,19 +110,22 @@ export class TableDataProvider implements DataProvider { } sort(columns: SortColumn[]): void { - let cols = columns.map((column) => [column.sortCol.field, column.sortAsc ? 1 : -1] as const) + let cols = columns.map((column) => [column.sortCol as ColumnType, column.sortAsc ? 1 : -1] as const) if (cols.length == 0) { - cols = [[DTINDEX_NAME, 1]] + cols = [[{field: DTINDEX_NAME}, 1]] } const records = this.getRecords() const old_index = this.index.slice() this.index.sort((i0, i1) => { - for (const [field, sign] of cols) { - const v0 = records[old_index.indexOf(i0)][field!] - const v1 = records[old_index.indexOf(i1)][field!] + for (const [col, sign] of cols) { + const v0 = records[old_index.indexOf(i0)][col.field!] + const v1 = records[old_index.indexOf(i1)][col.field!] + if (col.sorter != null) { + return sign * col.sorter.compute(v0, v1) + } if (v0 === v1) { continue } diff --git a/bokehjs/src/lib/models/widgets/tables/definitions.ts b/bokehjs/src/lib/models/widgets/tables/definitions.ts index 4ff1b4eee7d..36337e76ef1 100644 --- a/bokehjs/src/lib/models/widgets/tables/definitions.ts +++ b/bokehjs/src/lib/models/widgets/tables/definitions.ts @@ -1,7 +1,8 @@ import type {Column} from "@bokeh/slickgrid" import type {CellEditor} from "./cell_editors" +import type {Comparison} from "../../../models/comparisons" export type Item = {[key: string]: any} -export type ColumnType = Column & {model?: CellEditor} +export type ColumnType = Column & {model?: CellEditor, sorter?: Comparison | null} export const DTINDEX_NAME = "__bkdt_internal_index__" diff --git a/bokehjs/src/lib/models/widgets/tables/table_column.ts b/bokehjs/src/lib/models/widgets/tables/table_column.ts index a91077a7c5e..b3dea5f0682 100644 --- a/bokehjs/src/lib/models/widgets/tables/table_column.ts +++ b/bokehjs/src/lib/models/widgets/tables/table_column.ts @@ -5,6 +5,7 @@ import type {ColumnType} from "./definitions" import type * as p from "core/properties" import {unique_id} from "core/util/string" import {Sort} from "core/enums" +import {Comparison} from "../../../models/comparisons" import {Model} from "../../../model" export namespace TableColumn { @@ -19,6 +20,7 @@ export namespace TableColumn { sortable: p.Property default_sort: p.Property visible: p.Property + sorter: p.Property } } @@ -41,6 +43,7 @@ export class TableColumn extends Model { sortable: [ Bool, true ], default_sort: [ Sort, "ascending" ], visible: [ Bool, true ], + sorter: [ Nullable(Ref(Comparison)), null ], })) } @@ -55,6 +58,7 @@ export class TableColumn extends Model { editor: this.editor.default_view, sortable: this.sortable, defaultSortAsc: this.default_sort == "ascending", + sorter: this.sorter, } } } diff --git a/bokehjs/src/lib/package.json b/bokehjs/src/lib/package.json index 0af2d73a247..a11ced8134c 100644 --- a/bokehjs/src/lib/package.json +++ b/bokehjs/src/lib/package.json @@ -1,6 +1,6 @@ { "name": "@bokeh/lib", - "version": "3.5.0-dev.3", + "version": "3.6.0-dev.1", "private": true, "description": "Internal package for bokehjs' implementation", "license": "BSD-3-Clause", diff --git a/bokehjs/src/server/package.json b/bokehjs/src/server/package.json index 8f9e876b3c9..282c49a06c3 100644 --- a/bokehjs/src/server/package.json +++ b/bokehjs/src/server/package.json @@ -1,6 +1,6 @@ { "name": "@bokeh/server", - "version": "3.5.0-dev.3", + "version": "3.6.0-dev.1", "private": true, "description": "Internal package for bokehjs' server", "license": "BSD-3-Clause", @@ -15,7 +15,7 @@ "chalk": "^4.1.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "ws": "^8.16.0", + "ws": "^8.18.0", "yargs": "^17.7.2" } } diff --git a/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_hover_over_interaction_handles.blf b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_hover_over_interaction_handles.blf new file mode 100644 index 00000000000..99c1805a01e --- /dev/null +++ b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_hover_over_interaction_handles.blf @@ -0,0 +1,6 @@ +Figure bbox=[0, 0, 150, 150] + Canvas bbox=[0, 0, 150, 150] + CartesianFrame bbox=[20, 5, 125, 123] + BoxAnnotation bbox=[41, 26, 83, 82] + LinearAxis bbox=[20, 128, 125, 22] + LinearAxis bbox=[0, 5, 20, 123] diff --git a/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_hover_over_interaction_handles.png b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_hover_over_interaction_handles.png new file mode 100644 index 00000000000..5b9abdee1b4 Binary files /dev/null and b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_hover_over_interaction_handles.png differ diff --git a/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_interaction_handles.blf b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_interaction_handles.blf new file mode 100644 index 00000000000..7cfa4e70aad --- /dev/null +++ b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_interaction_handles.blf @@ -0,0 +1,72 @@ +GridPlot bbox=[0, 0, 750, 300] + GridBox bbox=[0, 0, 750, 300] + Figure bbox=[0, 0, 150, 150] + Canvas bbox=[0, 0, 150, 150] + CartesianFrame bbox=[20, 42, 125, 86] + BoxAnnotation bbox=[41, 56, 83, 58] + LinearAxis bbox=[20, 128, 125, 22] + LinearAxis bbox=[0, 42, 20, 86] + Title bbox=[20, 0, 125, 42] + Figure bbox=[150, 0, 150, 150] + Canvas bbox=[0, 0, 150, 150] + CartesianFrame bbox=[20, 42, 125, 86] + BoxAnnotation bbox=[41, 56, 83, 58] + LinearAxis bbox=[20, 128, 125, 22] + LinearAxis bbox=[0, 42, 20, 86] + Title bbox=[20, 0, 125, 42] + Figure bbox=[300, 0, 150, 150] + Canvas bbox=[0, 0, 150, 150] + CartesianFrame bbox=[20, 42, 125, 86] + BoxAnnotation bbox=[41, 56, 83, 58] + LinearAxis bbox=[20, 128, 125, 22] + LinearAxis bbox=[0, 42, 20, 86] + Title bbox=[20, 0, 125, 42] + Figure bbox=[450, 0, 150, 150] + Canvas bbox=[0, 0, 150, 150] + CartesianFrame bbox=[20, 42, 125, 86] + BoxAnnotation bbox=[41, 56, 83, 58] + LinearAxis bbox=[20, 128, 125, 22] + LinearAxis bbox=[0, 42, 20, 86] + Title bbox=[20, 0, 125, 42] + Figure bbox=[600, 0, 150, 150] + Canvas bbox=[0, 0, 150, 150] + CartesianFrame bbox=[20, 42, 125, 86] + BoxAnnotation bbox=[41, 56, 83, 58] + LinearAxis bbox=[20, 128, 125, 22] + LinearAxis bbox=[0, 42, 20, 86] + Title bbox=[20, 0, 125, 42] + Figure bbox=[0, 150, 150, 150] + Canvas bbox=[0, 0, 150, 150] + CartesianFrame bbox=[20, 42, 125, 86] + BoxAnnotation bbox=[41, 56, 83, 58] + LinearAxis bbox=[20, 128, 125, 22] + LinearAxis bbox=[0, 42, 20, 86] + Title bbox=[20, 0, 125, 42] + Figure bbox=[150, 150, 150, 150] + Canvas bbox=[0, 0, 150, 150] + CartesianFrame bbox=[20, 42, 125, 86] + BoxAnnotation bbox=[41, 56, 83, 58] + LinearAxis bbox=[20, 128, 125, 22] + LinearAxis bbox=[0, 42, 20, 86] + Title bbox=[20, 0, 125, 42] + Figure bbox=[300, 150, 150, 150] + Canvas bbox=[0, 0, 150, 150] + CartesianFrame bbox=[20, 42, 125, 86] + BoxAnnotation bbox=[41, 56, 83, 58] + LinearAxis bbox=[20, 128, 125, 22] + LinearAxis bbox=[0, 42, 20, 86] + Title bbox=[20, 0, 125, 42] + Figure bbox=[450, 150, 150, 150] + Canvas bbox=[0, 0, 150, 150] + CartesianFrame bbox=[20, 42, 125, 86] + BoxAnnotation bbox=[41, 56, 83, 58] + LinearAxis bbox=[20, 128, 125, 22] + LinearAxis bbox=[0, 42, 20, 86] + Title bbox=[20, 0, 125, 42] + Figure bbox=[600, 150, 150, 150] + Canvas bbox=[0, 0, 150, 150] + CartesianFrame bbox=[20, 42, 125, 86] + BoxAnnotation bbox=[41, 56, 83, 58] + LinearAxis bbox=[20, 128, 125, 22] + LinearAxis bbox=[0, 42, 20, 86] + Title bbox=[20, 0, 125, 42] diff --git a/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_interaction_handles.png b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_interaction_handles.png new file mode 100644 index 00000000000..4c3c6fd51d3 Binary files /dev/null and b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_interaction_handles.png differ diff --git a/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_visual_overrides_in_interaction_handles.blf b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_visual_overrides_in_interaction_handles.blf new file mode 100644 index 00000000000..99c1805a01e --- /dev/null +++ b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_visual_overrides_in_interaction_handles.blf @@ -0,0 +1,6 @@ +Figure bbox=[0, 0, 150, 150] + Canvas bbox=[0, 0, 150, 150] + CartesianFrame bbox=[20, 5, 125, 123] + BoxAnnotation bbox=[41, 26, 83, 82] + LinearAxis bbox=[20, 128, 125, 22] + LinearAxis bbox=[0, 5, 20, 123] diff --git a/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_visual_overrides_in_interaction_handles.png b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_visual_overrides_in_interaction_handles.png new file mode 100644 index 00000000000..f1aabed17ad Binary files /dev/null and b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_visual_overrides_in_interaction_handles.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13756__doesn't_correctly_position_Tooltip_when_using_nodes.blf b/bokehjs/test/baselines/linux/Bug__in_issue_#13756__doesn't_correctly_position_Tooltip_when_using_nodes.blf index c9b30f83900..91ab8a1aeb9 100644 --- a/bokehjs/test/baselines/linux/Bug__in_issue_#13756__doesn't_correctly_position_Tooltip_when_using_nodes.blf +++ b/bokehjs/test/baselines/linux/Bug__in_issue_#13756__doesn't_correctly_position_Tooltip_when_using_nodes.blf @@ -1,7 +1,7 @@ Figure bbox=[0, 0, 300, 300] Canvas bbox=[0, 0, 300, 300] CartesianFrame bbox=[29, 5, 266, 273] - Tooltip bbox=[0, 0, 92, 29] + Tooltip bbox=[-110, 360, 92, 29] GlyphRenderer bbox=[41, 17, 242, 249] Scatter bbox=[41, 17, 242, 249] BoxAnnotation bbox=[41, 141, 121, 125] diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13766__doesn't_allow_to_rebuild_Dropdown.menu_on_change.blf b/bokehjs/test/baselines/linux/Bug__in_issue_#13766__doesn't_allow_to_rebuild_Dropdown.menu_on_change.blf new file mode 100644 index 00000000000..52d40eaad30 --- /dev/null +++ b/bokehjs/test/baselines/linux/Bug__in_issue_#13766__doesn't_allow_to_rebuild_Dropdown.menu_on_change.blf @@ -0,0 +1 @@ +Dropdown bbox=[0, 0, 106, 28] diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13766__doesn't_allow_to_rebuild_Dropdown.menu_on_change.png b/bokehjs/test/baselines/linux/Bug__in_issue_#13766__doesn't_allow_to_rebuild_Dropdown.menu_on_change.png new file mode 100644 index 00000000000..a2e74523c7f Binary files /dev/null and b/bokehjs/test/baselines/linux/Bug__in_issue_#13766__doesn't_allow_to_rebuild_Dropdown.menu_on_change.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13827__doesn't_allow_to_respect_maintain_focus=false_when_zooming.blf b/bokehjs/test/baselines/linux/Bug__in_issue_#13827__doesn't_allow_to_respect_maintain_focus=false_when_zooming.blf new file mode 100644 index 00000000000..1d4a2dc351b --- /dev/null +++ b/bokehjs/test/baselines/linux/Bug__in_issue_#13827__doesn't_allow_to_respect_maintain_focus=false_when_zooming.blf @@ -0,0 +1,7 @@ +Figure bbox=[0, 0, 200, 200] + Canvas bbox=[0, 0, 200, 200] + CartesianFrame bbox=[20, 5, 175, 173] + GlyphRenderer bbox=[20, 5, 175, 173] + Line bbox=[20, 5, 175, 173] + LinearAxis bbox=[20, 178, 175, 22] + LinearAxis bbox=[0, 5, 20, 173] diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13827__doesn't_allow_to_respect_maintain_focus=false_when_zooming.png b/bokehjs/test/baselines/linux/Bug__in_issue_#13827__doesn't_allow_to_respect_maintain_focus=false_when_zooming.png new file mode 100644 index 00000000000..34e3a54aa1e Binary files /dev/null and b/bokehjs/test/baselines/linux/Bug__in_issue_#13827__doesn't_allow_to_respect_maintain_focus=false_when_zooming.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13895__allows_elements_associated_with_renderers_to_overflow_the_canvas.blf b/bokehjs/test/baselines/linux/Bug__in_issue_#13895__allows_elements_associated_with_renderers_to_overflow_the_canvas.blf new file mode 100644 index 00000000000..2b21ed31aa2 --- /dev/null +++ b/bokehjs/test/baselines/linux/Bug__in_issue_#13895__allows_elements_associated_with_renderers_to_overflow_the_canvas.blf @@ -0,0 +1,7 @@ +Figure bbox=[0, 0, 200, 200] + Canvas bbox=[0, 0, 200, 200] + CartesianFrame bbox=[29, 5, 166, 173] + GlyphRenderer bbox=[-137, -1725, 1992, 2076] + Circle bbox=[-137, -1725, 1992, 2076] + LinearAxis bbox=[29, 178, 166, 22] + LinearAxis bbox=[0, 5, 29, 173] diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13895__allows_elements_associated_with_renderers_to_overflow_the_canvas.png b/bokehjs/test/baselines/linux/Bug__in_issue_#13895__allows_elements_associated_with_renderers_to_overflow_the_canvas.png new file mode 100644 index 00000000000..e816d86e7e6 Binary files /dev/null and b/bokehjs/test/baselines/linux/Bug__in_issue_#13895__allows_elements_associated_with_renderers_to_overflow_the_canvas.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13912__doesn't_allow_stacking_Dialog_above_non-floating_UI_elements.blf b/bokehjs/test/baselines/linux/Bug__in_issue_#13912__doesn't_allow_stacking_Dialog_above_non-floating_UI_elements.blf new file mode 100644 index 00000000000..4169743b76d --- /dev/null +++ b/bokehjs/test/baselines/linux/Bug__in_issue_#13912__doesn't_allow_stacking_Dialog_above_non-floating_UI_elements.blf @@ -0,0 +1,28 @@ +Column bbox=[0, 0, 310, 310] + DataTable bbox=[5, 5, 300, 300] + Dialog bbox=[50, 50, 200, 200] + Figure bbox=[1, 28, 198, 170] + Canvas bbox=[0, 0, 198, 170] + CartesianFrame bbox=[36, 5, 132, 143] + GlyphRenderer bbox=[42, 12, 120, 130] + Circle bbox=[42, 12, 120, 130] + LinearAxis bbox=[36, 148, 132, 22] + LinearAxis bbox=[0, 5, 36, 143] + Title bbox=[36, 5, 132, 0] + ToolbarPanel bbox=[168, 5, 30, 143] + Toolbar bbox=[168, 5, 30, 143] + OnOffButton bbox=[0, 25, 30, 30] + OnOffButton bbox=[0, 56, 30, 30] + Dialog bbox=[70, 70, 200, 200] + Figure bbox=[1, 28, 198, 170] + Canvas bbox=[0, 0, 198, 170] + CartesianFrame bbox=[36, 5, 132, 143] + GlyphRenderer bbox=[42, 12, 120, 130] + Circle bbox=[42, 12, 120, 130] + LinearAxis bbox=[36, 148, 132, 22] + LinearAxis bbox=[0, 5, 36, 143] + Title bbox=[36, 5, 132, 0] + ToolbarPanel bbox=[168, 5, 30, 143] + Toolbar bbox=[168, 5, 30, 143] + OnOffButton bbox=[0, 25, 30, 30] + OnOffButton bbox=[0, 56, 30, 30] diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13912__doesn't_allow_stacking_Dialog_above_non-floating_UI_elements.png b/bokehjs/test/baselines/linux/Bug__in_issue_#13912__doesn't_allow_stacking_Dialog_above_non-floating_UI_elements.png new file mode 100644 index 00000000000..252d4fa01c1 Binary files /dev/null and b/bokehjs/test/baselines/linux/Bug__in_issue_#13912__doesn't_allow_stacking_Dialog_above_non-floating_UI_elements.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13923__doesn't_allow_to_rebuild_views_when_Tooltip.contents_changes.blf b/bokehjs/test/baselines/linux/Bug__in_issue_#13923__doesn't_allow_to_rebuild_views_when_Tooltip.contents_changes.blf new file mode 100644 index 00000000000..e9596106122 --- /dev/null +++ b/bokehjs/test/baselines/linux/Bug__in_issue_#13923__doesn't_allow_to_rebuild_views_when_Tooltip.contents_changes.blf @@ -0,0 +1 @@ +Tooltip bbox=[0, 0, 114, 29] diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13923__doesn't_allow_to_rebuild_views_when_Tooltip.contents_changes.png b/bokehjs/test/baselines/linux/Bug__in_issue_#13923__doesn't_allow_to_rebuild_views_when_Tooltip.contents_changes.png new file mode 100644 index 00000000000..5a54468a299 Binary files /dev/null and b/bokehjs/test/baselines/linux/Bug__in_issue_#13923__doesn't_allow_to_rebuild_views_when_Tooltip.contents_changes.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13964__doesn't_allow_using_'constructor'_key_in_maps_or_plain_objects_in_may_have_refs_contexts.blf b/bokehjs/test/baselines/linux/Bug__in_issue_#13964__doesn't_allow_using_'constructor'_key_in_maps_or_plain_objects_in_may_have_refs_contexts.blf new file mode 100644 index 00000000000..be32de93e38 --- /dev/null +++ b/bokehjs/test/baselines/linux/Bug__in_issue_#13964__doesn't_allow_using_'constructor'_key_in_maps_or_plain_objects_in_may_have_refs_contexts.blf @@ -0,0 +1 @@ +TextInput bbox=[0, 0, 203, 31] diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#14013__doesn't_allow_to_respect_secondary_ranges_when_masking_data_in_MultiPolygons_glyph.blf b/bokehjs/test/baselines/linux/Bug__in_issue_#14013__doesn't_allow_to_respect_secondary_ranges_when_masking_data_in_MultiPolygons_glyph.blf new file mode 100644 index 00000000000..72695bb48d6 --- /dev/null +++ b/bokehjs/test/baselines/linux/Bug__in_issue_#14013__doesn't_allow_to_respect_secondary_ranges_when_masking_data_in_MultiPolygons_glyph.blf @@ -0,0 +1,8 @@ +Figure bbox=[0, 0, 300, 150] + Canvas bbox=[0, 0, 300, 150] + CartesianFrame bbox=[84, 5, 211, 123] + GlyphRenderer bbox=[105, 100, 169, 62] + MultiPolygons bbox=[105, 100, 169, 62] + LinearAxis bbox=[84, 128, 211, 22] + LinearAxis bbox=[45, 5, 39, 123] + LinearAxis bbox=[0, 5, 45, 123] diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#14013__doesn't_allow_to_respect_secondary_ranges_when_masking_data_in_MultiPolygons_glyph.png b/bokehjs/test/baselines/linux/Bug__in_issue_#14013__doesn't_allow_to_respect_secondary_ranges_when_masking_data_in_MultiPolygons_glyph.png new file mode 100644 index 00000000000..82a24237cd2 Binary files /dev/null and b/bokehjs/test/baselines/linux/Bug__in_issue_#14013__doesn't_allow_to_respect_secondary_ranges_when_masking_data_in_MultiPolygons_glyph.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#14013__doesn't_allow_to_respect_secondary_ranges_when_masking_data_in_Patches_glyph.blf b/bokehjs/test/baselines/linux/Bug__in_issue_#14013__doesn't_allow_to_respect_secondary_ranges_when_masking_data_in_Patches_glyph.blf new file mode 100644 index 00000000000..b3bc5392773 --- /dev/null +++ b/bokehjs/test/baselines/linux/Bug__in_issue_#14013__doesn't_allow_to_respect_secondary_ranges_when_masking_data_in_Patches_glyph.blf @@ -0,0 +1,8 @@ +Figure bbox=[0, 0, 300, 150] + Canvas bbox=[0, 0, 300, 150] + CartesianFrame bbox=[84, 5, 211, 123] + GlyphRenderer bbox=[105, 100, 169, 62] + Patches bbox=[105, 100, 169, 62] + LinearAxis bbox=[84, 128, 211, 22] + LinearAxis bbox=[45, 5, 39, 123] + LinearAxis bbox=[0, 5, 45, 123] diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#14013__doesn't_allow_to_respect_secondary_ranges_when_masking_data_in_Patches_glyph.png b/bokehjs/test/baselines/linux/Bug__in_issue_#14013__doesn't_allow_to_respect_secondary_ranges_when_masking_data_in_Patches_glyph.png new file mode 100644 index 00000000000..82a24237cd2 Binary files /dev/null and b/bokehjs/test/baselines/linux/Bug__in_issue_#14013__doesn't_allow_to_respect_secondary_ranges_when_masking_data_in_Patches_glyph.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#8766__doesn't_allow_activation_of_proxied_box_zoom_tools.blf b/bokehjs/test/baselines/linux/Bug__in_issue_#8766__doesn't_allow_activation_of_proxied_box_zoom_tools.blf new file mode 100644 index 00000000000..223c2ed96de --- /dev/null +++ b/bokehjs/test/baselines/linux/Bug__in_issue_#8766__doesn't_allow_activation_of_proxied_box_zoom_tools.blf @@ -0,0 +1,29 @@ +GridPlot bbox=[0, 0, 300, 930] + Toolbar bbox=[0, 0, 300, 30] + OnOffButton bbox=[215, 0, 30, 30] + OnOffButton bbox=[245, 0, 30, 30] + GridBox bbox=[0, 30, 300, 900] + Figure bbox=[0, 0, 300, 300] + Canvas bbox=[0, 0, 300, 300] + CartesianFrame bbox=[29, 5, 266, 273] + GlyphRenderer bbox=[41, 17, 242, 249] + Scatter bbox=[41, 17, 242, 249] + LinearAxis bbox=[29, 278, 266, 22] + LinearAxis bbox=[0, 5, 29, 273] + Title bbox=[29, 5, 266, 0] + Figure bbox=[0, 300, 300, 300] + Canvas bbox=[0, 0, 300, 300] + CartesianFrame bbox=[29, 5, 266, 273] + GlyphRenderer bbox=[41, 17, 242, 249] + Scatter bbox=[41, 17, 242, 249] + LinearAxis bbox=[29, 278, 266, 22] + LinearAxis bbox=[0, 5, 29, 273] + Title bbox=[29, 5, 266, 0] + Figure bbox=[0, 600, 300, 300] + Canvas bbox=[0, 0, 300, 300] + CartesianFrame bbox=[29, 5, 266, 273] + GlyphRenderer bbox=[41, 17, 242, 249] + Scatter bbox=[41, 17, 242, 249] + LinearAxis bbox=[29, 278, 266, 22] + LinearAxis bbox=[0, 5, 29, 273] + Title bbox=[29, 5, 266, 0] diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#8766__doesn't_allow_activation_of_proxied_box_zoom_tools.png b/bokehjs/test/baselines/linux/Bug__in_issue_#8766__doesn't_allow_activation_of_proxied_box_zoom_tools.png new file mode 100644 index 00000000000..d33be06aafe Binary files /dev/null and b/bokehjs/test/baselines/linux/Bug__in_issue_#8766__doesn't_allow_activation_of_proxied_box_zoom_tools.png differ diff --git a/bokehjs/test/baselines/linux/DataTable__should_allow_sorting_with_a_CustomJSCompare.blf b/bokehjs/test/baselines/linux/DataTable__should_allow_sorting_with_a_CustomJSCompare.blf new file mode 100644 index 00000000000..c30a6b35507 --- /dev/null +++ b/bokehjs/test/baselines/linux/DataTable__should_allow_sorting_with_a_CustomJSCompare.blf @@ -0,0 +1 @@ +DataTable bbox=[0, 0, 600, 400] diff --git a/bokehjs/test/baselines/linux/DataTable__should_allow_sorting_with_a_CustomJSCompare.png b/bokehjs/test/baselines/linux/DataTable__should_allow_sorting_with_a_CustomJSCompare.png new file mode 100644 index 00000000000..00b86685953 Binary files /dev/null and b/bokehjs/test/baselines/linux/DataTable__should_allow_sorting_with_a_CustomJSCompare.png differ diff --git a/bokehjs/test/baselines/linux/DataTable__should_allow_sorting_with_a_NanCompare.blf b/bokehjs/test/baselines/linux/DataTable__should_allow_sorting_with_a_NanCompare.blf new file mode 100644 index 00000000000..c30a6b35507 --- /dev/null +++ b/bokehjs/test/baselines/linux/DataTable__should_allow_sorting_with_a_NanCompare.blf @@ -0,0 +1 @@ +DataTable bbox=[0, 0, 600, 400] diff --git a/bokehjs/test/baselines/linux/DataTable__should_allow_sorting_with_a_NanCompare.png b/bokehjs/test/baselines/linux/DataTable__should_allow_sorting_with_a_NanCompare.png new file mode 100644 index 00000000000..e2af5a5b146 Binary files /dev/null and b/bokehjs/test/baselines/linux/DataTable__should_allow_sorting_with_a_NanCompare.png differ diff --git a/bokehjs/test/baselines/linux/Glyph_models__should_support_Text-like_glyphs__with_outline_shape.blf b/bokehjs/test/baselines/linux/Glyph_models__should_support_Text-like_glyphs__with_outline_shape.blf new file mode 100644 index 00000000000..bea42292938 --- /dev/null +++ b/bokehjs/test/baselines/linux/Glyph_models__should_support_Text-like_glyphs__with_outline_shape.blf @@ -0,0 +1,9 @@ +Figure bbox=[0, 0, 1000, 400] + Canvas bbox=[0, 0, 1000, 400] + CartesianFrame bbox=[5, 5, 990, 390] + GlyphRenderer bbox=[60, 356, 880, 0] + Text bbox=[60, 356, 880, 0] + GlyphRenderer bbox=[60, 239, 880, 0] + TeXGlyph bbox=[60, 239, 880, 0] + GlyphRenderer bbox=[60, 83, 880, 0] + TeXGlyph bbox=[60, 83, 880, 0] diff --git a/bokehjs/test/baselines/linux/Glyph_models__should_support_Text-like_glyphs__with_outline_shape.png b/bokehjs/test/baselines/linux/Glyph_models__should_support_Text-like_glyphs__with_outline_shape.png new file mode 100644 index 00000000000..99f53b4576c Binary files /dev/null and b/bokehjs/test/baselines/linux/Glyph_models__should_support_Text-like_glyphs__with_outline_shape.png differ diff --git a/bokehjs/test/baselines/linux/HoverTool__should_support_formatting_with_templated_and_regular_tooltips.blf b/bokehjs/test/baselines/linux/HoverTool__should_support_formatting_with_templated_and_regular_tooltips.blf new file mode 100644 index 00000000000..433570303c7 --- /dev/null +++ b/bokehjs/test/baselines/linux/HoverTool__should_support_formatting_with_templated_and_regular_tooltips.blf @@ -0,0 +1,7 @@ +Figure bbox=[0, 0, 300, 300] + Canvas bbox=[0, 0, 300, 300] + CartesianFrame bbox=[26, 5, 269, 273] + GlyphRenderer bbox=[38, 17, 245, 249] + Circle bbox=[38, 17, 245, 249] + LinearAxis bbox=[26, 278, 269, 22] + LinearAxis bbox=[0, 5, 26, 273] diff --git a/bokehjs/test/baselines/linux/HoverTool__should_support_formatting_with_templated_and_regular_tooltips.png b/bokehjs/test/baselines/linux/HoverTool__should_support_formatting_with_templated_and_regular_tooltips.png new file mode 100644 index 00000000000..8fdb7cd9f90 Binary files /dev/null and b/bokehjs/test/baselines/linux/HoverTool__should_support_formatting_with_templated_and_regular_tooltips.png differ diff --git a/bokehjs/test/baselines/linux/Icons__should_support_all_icons_defined_in_less_icons.less.png b/bokehjs/test/baselines/linux/Icons__should_support_all_icons_defined_in_less_icons.less.png index a9e6107f211..abbe4bf8477 100644 Binary files a/bokehjs/test/baselines/linux/Icons__should_support_all_icons_defined_in_less_icons.less.png and b/bokehjs/test/baselines/linux/Icons__should_support_all_icons_defined_in_less_icons.less.png differ diff --git a/bokehjs/test/baselines/linux/LabelSet_annotation__should_support_basic_positioning_using_CSS_rendering.png b/bokehjs/test/baselines/linux/LabelSet_annotation__should_support_basic_positioning_using_CSS_rendering.png index cde0afa1524..c9ce1d631c1 100644 Binary files a/bokehjs/test/baselines/linux/LabelSet_annotation__should_support_basic_positioning_using_CSS_rendering.png and b/bokehjs/test/baselines/linux/LabelSet_annotation__should_support_basic_positioning_using_CSS_rendering.png differ diff --git a/bokehjs/test/baselines/linux/Legend_annotation__should_support_LegendItemClick_events.blf b/bokehjs/test/baselines/linux/Legend_annotation__should_support_LegendItemClick_events.blf new file mode 100644 index 00000000000..ad0c20db70d --- /dev/null +++ b/bokehjs/test/baselines/linux/Legend_annotation__should_support_LegendItemClick_events.blf @@ -0,0 +1,12 @@ +Figure bbox=[0, 0, 200, 200] + Canvas bbox=[0, 0, 200, 200] + CartesianFrame bbox=[0, 0, 180, 178] + GlyphRenderer bbox=[8, 8, 164, 81] + Scatter bbox=[8, 8, 164, 81] + GlyphRenderer bbox=[8, 49, 164, 80] + Scatter bbox=[8, 49, 164, 80] + GlyphRenderer bbox=[8, 89, 164, 81] + Scatter bbox=[8, 89, 164, 81] + LinearAxis bbox=[0, 178, 180, 22] + LinearAxis bbox=[180, 0, 20, 178] + Legend bbox=[0, 0, 90, 86] diff --git a/bokehjs/test/baselines/linux/Legend_annotation__should_support_LegendItemClick_events.png b/bokehjs/test/baselines/linux/Legend_annotation__should_support_LegendItemClick_events.png new file mode 100644 index 00000000000..f04ca6e360b Binary files /dev/null and b/bokehjs/test/baselines/linux/Legend_annotation__should_support_LegendItemClick_events.png differ diff --git a/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='none'.blf b/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='none'.blf new file mode 100644 index 00000000000..4355e2e91f0 --- /dev/null +++ b/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='none'.blf @@ -0,0 +1,5 @@ +Figure bbox=[0, 0, 400, 200] + Canvas bbox=[0, 0, 400, 200] + CartesianFrame bbox=[29, 5, 366, 173] + LinearAxis bbox=[29, 178, 366, 22] + LinearAxis bbox=[0, 5, 29, 173] diff --git a/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='none'.png b/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='none'.png new file mode 100644 index 00000000000..0133b24e472 Binary files /dev/null and b/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='none'.png differ diff --git a/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='pan'.blf b/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='pan'.blf new file mode 100644 index 00000000000..4355e2e91f0 --- /dev/null +++ b/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='pan'.blf @@ -0,0 +1,5 @@ +Figure bbox=[0, 0, 400, 200] + Canvas bbox=[0, 0, 400, 200] + CartesianFrame bbox=[29, 5, 366, 173] + LinearAxis bbox=[29, 178, 366, 22] + LinearAxis bbox=[0, 5, 29, 173] diff --git a/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='pan'.png b/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='pan'.png new file mode 100644 index 00000000000..051d0f7755c Binary files /dev/null and b/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='pan'.png differ diff --git a/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='tap'.blf b/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='tap'.blf new file mode 100644 index 00000000000..4355e2e91f0 --- /dev/null +++ b/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='tap'.blf @@ -0,0 +1,5 @@ +Figure bbox=[0, 0, 400, 200] + Canvas bbox=[0, 0, 400, 200] + CartesianFrame bbox=[29, 5, 366, 173] + LinearAxis bbox=[29, 178, 366, 22] + LinearAxis bbox=[0, 5, 29, 173] diff --git a/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='tap'.png b/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='tap'.png new file mode 100644 index 00000000000..051d0f7755c Binary files /dev/null and b/bokehjs/test/baselines/linux/RangeTool__should_support_start_gesture='tap'.png differ diff --git a/bokehjs/test/baselines/linux/Tools__should_support_TapTool's_setup_menu.png b/bokehjs/test/baselines/linux/Tools__should_support_TapTool's_setup_menu.png index 750a780ffd3..f8f8d4d4642 100644 Binary files a/bokehjs/test/baselines/linux/Tools__should_support_TapTool's_setup_menu.png and b/bokehjs/test/baselines/linux/Tools__should_support_TapTool's_setup_menu.png differ diff --git a/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior='only_hit'.blf b/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior='only_hit'.blf new file mode 100644 index 00000000000..c84b1708696 --- /dev/null +++ b/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior='only_hit'.blf @@ -0,0 +1,25 @@ +Figure bbox=[0, 0, 300, 300] + Canvas bbox=[0, 0, 300, 300] + CartesianFrame bbox=[22, 5, 273, 273] + GlyphRenderer bbox=[49, 255, 219, 18] + Line bbox=[49, 255, 219, 18] + GlyphRenderer bbox=[49, 214, 219, 46] + Line bbox=[49, 214, 219, 46] + GlyphRenderer bbox=[49, 201, 219, 18] + Line bbox=[49, 201, 219, 18] + GlyphRenderer bbox=[49, 173, 219, 19] + Line bbox=[49, 173, 219, 19] + GlyphRenderer bbox=[49, 146, 219, 18] + Line bbox=[49, 146, 219, 18] + GlyphRenderer bbox=[49, 119, 219, 18] + Line bbox=[49, 119, 219, 18] + GlyphRenderer bbox=[49, 91, 219, 19] + Line bbox=[49, 91, 219, 19] + GlyphRenderer bbox=[49, 64, 219, 18] + Line bbox=[49, 64, 219, 18] + GlyphRenderer bbox=[49, 37, 219, 18] + Line bbox=[49, 37, 219, 18] + GlyphRenderer bbox=[49, 10, 219, 18] + Line bbox=[49, 10, 219, 18] + LinearAxis bbox=[22, 278, 273, 22] + CategoricalAxis bbox=[0, 5, 22, 273] diff --git a/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior='only_hit'.png b/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior='only_hit'.png new file mode 100644 index 00000000000..f949c132032 Binary files /dev/null and b/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior='only_hit'.png differ diff --git a/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior=GroupByModels().blf b/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior=GroupByModels().blf new file mode 100644 index 00000000000..25f8c46899f --- /dev/null +++ b/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior=GroupByModels().blf @@ -0,0 +1,25 @@ +Figure bbox=[0, 0, 300, 300] + Canvas bbox=[0, 0, 300, 300] + CartesianFrame bbox=[22, 5, 273, 273] + GlyphRenderer bbox=[49, 255, 219, 18] + Line bbox=[49, 255, 219, 18] + GlyphRenderer bbox=[49, 214, 219, 46] + Line bbox=[49, 214, 219, 46] + GlyphRenderer bbox=[49, 201, 219, 18] + Line bbox=[49, 201, 219, 18] + GlyphRenderer bbox=[49, 160, 219, 45] + Line bbox=[49, 160, 219, 45] + GlyphRenderer bbox=[49, 146, 219, 18] + Line bbox=[49, 146, 219, 18] + GlyphRenderer bbox=[49, 105, 219, 46] + Line bbox=[49, 105, 219, 46] + GlyphRenderer bbox=[49, 91, 219, 19] + Line bbox=[49, 91, 219, 19] + GlyphRenderer bbox=[49, 51, 219, 45] + Line bbox=[49, 51, 219, 45] + GlyphRenderer bbox=[49, 37, 219, 18] + Line bbox=[49, 37, 219, 18] + GlyphRenderer bbox=[49, -4, 219, 45] + Line bbox=[49, -4, 219, 45] + LinearAxis bbox=[22, 278, 273, 22] + CategoricalAxis bbox=[0, 5, 22, 273] diff --git a/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior=GroupByModels().png b/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior=GroupByModels().png new file mode 100644 index 00000000000..b78ee5876b3 Binary files /dev/null and b/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior=GroupByModels().png differ diff --git a/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior=GroupByName().blf b/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior=GroupByName().blf new file mode 100644 index 00000000000..25f8c46899f --- /dev/null +++ b/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior=GroupByName().blf @@ -0,0 +1,25 @@ +Figure bbox=[0, 0, 300, 300] + Canvas bbox=[0, 0, 300, 300] + CartesianFrame bbox=[22, 5, 273, 273] + GlyphRenderer bbox=[49, 255, 219, 18] + Line bbox=[49, 255, 219, 18] + GlyphRenderer bbox=[49, 214, 219, 46] + Line bbox=[49, 214, 219, 46] + GlyphRenderer bbox=[49, 201, 219, 18] + Line bbox=[49, 201, 219, 18] + GlyphRenderer bbox=[49, 160, 219, 45] + Line bbox=[49, 160, 219, 45] + GlyphRenderer bbox=[49, 146, 219, 18] + Line bbox=[49, 146, 219, 18] + GlyphRenderer bbox=[49, 105, 219, 46] + Line bbox=[49, 105, 219, 46] + GlyphRenderer bbox=[49, 91, 219, 19] + Line bbox=[49, 91, 219, 19] + GlyphRenderer bbox=[49, 51, 219, 45] + Line bbox=[49, 51, 219, 45] + GlyphRenderer bbox=[49, 37, 219, 18] + Line bbox=[49, 37, 219, 18] + GlyphRenderer bbox=[49, -4, 219, 45] + Line bbox=[49, -4, 219, 45] + LinearAxis bbox=[22, 278, 273, 22] + CategoricalAxis bbox=[0, 5, 22, 273] diff --git a/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior=GroupByName().png b/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior=GroupByName().png new file mode 100644 index 00000000000..b78ee5876b3 Binary files /dev/null and b/bokehjs/test/baselines/linux/WheelZoomTool__should_support_zooming_sub-coordinates__with_hit_test=true__with_hit_test_mode=hline__and_hit_test_behavior=GroupByName().png differ diff --git a/bokehjs/test/codebase/size.ts b/bokehjs/test/codebase/size.ts index c6b88f8dc04..f875758f6b6 100644 --- a/bokehjs/test/codebase/size.ts +++ b/bokehjs/test/codebase/size.ts @@ -6,7 +6,7 @@ const build_dir = normalize(`${__dirname}/../..`) // build/test/codebase -> buil const LIMITS = new Map([ // es2020 - ["js/bokeh.min.js", 1050], + ["js/bokeh.min.js", 1100], ["js/bokeh-widgets.min.js", 350], ["js/bokeh-tables.min.js", 350], ["js/bokeh-api.min.js", 150], diff --git a/bokehjs/test/defaults/index.ts b/bokehjs/test/defaults/index.ts index c6bc34b6359..72be794a6a2 100644 --- a/bokehjs/test/defaults/index.ts +++ b/bokehjs/test/defaults/index.ts @@ -175,33 +175,64 @@ function check_matching_defaults(context: string[], name: string, python_default } if (!is_equal(py_v, js_v)) { - // compare arrays of objects - if (isArray(js_v) && isArray(py_v)) { + function compare_recursively(js_v: unknown, py_v: unknown): boolean { let equal = true - if (js_v.length != py_v.length) { - equal = false - } else { - for (let i = 0; i < js_v.length; i++) { - const js_vi = js_v[i] - const py_vi = py_v[i] - - if (is_object(js_vi) && is_object(py_vi) && js_vi.name == py_vi.name) { - const py_attrs = {...get_defaults(py_vi.name), ...py_vi.attributes} - if (!check_matching_defaults([...context, `${name}.${k}[${i}]`], js_vi.name, py_attrs, js_vi.attributes)) { + if (isArray(js_v) && isArray(py_v)) { + if (js_v.length != py_v.length) { + equal = false + } else { + for (let i = 0; i < js_v.length; i++) { + const js_vi = js_v[i] + const py_vi = py_v[i] + + if (is_object(js_vi) && is_object(py_vi) && js_vi.name == py_vi.name) { + const py_attrs = {...get_defaults(py_vi.name), ...py_vi.attributes} + if (!check_matching_defaults([...context, `${name}.${k}[${i}]`], js_vi.name, py_attrs, js_vi.attributes)) { + equal = false + break + } + } else if ((isArray(js_vi) && isArray(py_vi)) || (isPlainObject(js_vi) && isPlainObject(py_vi))) { + equal = compare_recursively(js_vi, py_vi) + } else if (!is_equal(js_vi, py_vi)) { + equal = false + break + } + } + } + } else if (isPlainObject(js_v) && isPlainObject(py_v)) { + const js_d = dict(js_v) + const py_d = dict(py_v) + + if (!is_equal(new Set(js_d.keys()), new Set(py_d.keys()))) { // TODO can't compare objects of type [object Generator] + equal = false + } else { + for (const key of js_d.keys()) { + const js_vi = js_v[key] + const py_vi = py_v[key] + + if (is_object(js_vi) && is_object(py_vi) && js_vi.name == py_vi.name) { + const py_attrs = {...get_defaults(py_vi.name), ...py_vi.attributes} + if (!check_matching_defaults([...context, `${name}.${k}[${key}]`], js_vi.name, py_attrs, js_vi.attributes)) { + equal = false + break + } + } else if ((isArray(js_vi) && isArray(py_vi)) || (isPlainObject(js_vi) && isPlainObject(py_vi))) { + equal = compare_recursively(js_vi, py_vi) + } else if (!is_equal(js_vi, py_vi)) { equal = false break } - } else if (!is_equal(js_vi, py_vi)) { - equal = false - break } } } - if (equal) { - continue - } + return equal + } + + const equal = compare_recursively(js_v, py_v) + if (equal) { + continue } different.push(`${[...context, `${name}.${k}`].join(" -> ")}: bokehjs defaults to ${to_string(js_v)} but python defaults to ${to_string(py_v)}`) diff --git a/bokehjs/test/devtools/server.ts b/bokehjs/test/devtools/server.ts index 5b9f33362c4..f2081401269 100644 --- a/bokehjs/test/devtools/server.ts +++ b/bokehjs/test/devtools/server.ts @@ -84,6 +84,11 @@ app.get("/integration/metrics", using_report(({metrics}, _, res) => { res.render("test/devtools/metrics.html", {title: "Integration Tests Metrics", metrics, js: js_path}) })) +app.post("/ajax/dummy_data", async (_req, res) => { + res.setHeader("Content-Type", "application/json") + res.end(JSON.stringify({x: [0, 1, 2], y: [1, 2, 3], radius: [0.5, 0.7, 1.1], color: ["red", "green", "blue"]})) +}) + app.get("/examples", async (_req, res) => { const dir = await fs.promises.opendir("examples") const entries = [] @@ -254,7 +259,7 @@ const {host, port} = argv const server = app.listen(port, host) server.on("listening", () => { - console.log(`listening on ${host}:${port}`) + console.log(`listening on http://${host}:${port}`) process.send?.("ready") }) server.on("error", (error) => { diff --git a/bokehjs/test/integration/annotations/box_annotation.ts b/bokehjs/test/integration/annotations/box_annotation.ts index dff5f36547f..b1a9c7abe38 100644 --- a/bokehjs/test/integration/annotations/box_annotation.ts +++ b/bokehjs/test/integration/annotations/box_annotation.ts @@ -6,6 +6,8 @@ import {BoxAnnotation, Node} from "@bokehjs/models" import type {OutputBackend} from "@bokehjs/core/enums" import {paint} from "@bokehjs/core/util/defer" import type {PlotView} from "@bokehjs/models/plots/plot" +import {gridplot} from "@bokehjs/api/gridplot" +import {entries} from "@bokehjs/core/util/object" describe("BoxAnnotation annotation", () => { @@ -347,4 +349,88 @@ describe("BoxAnnotation annotation", () => { const p = fig([200, 200], {renderers: [box], x_range: [0, 6], y_range: [0, 6]}) await display(p) }) + + it("should support interaction handles", async () => { + function plot(attrs: Partial) { + const box = new BoxAnnotation({ + left: 1, right: 5, top: 5, bottom: 1, + editable: true, + line_color: "blue", + use_handles: true, + ...attrs, + }) + + return fig([150, 150], { + title: entries(attrs).map(([k, v]) => `${k}=${v}`).join("\n"), + renderers: [box], + x_range: [0, 6], y_range: [0, 6], + }) + } + + const plots = [[ + plot({movable: "none", resizable: "none"}), + plot({movable: "both", resizable: "none"}), + plot({movable: "none", resizable: "all"}), + plot({movable: "both", resizable: "all"}), + plot({movable: "none", resizable: "x"}), + ], [ + plot({movable: "none", resizable: "y"}), + plot({movable: "none", resizable: "left"}), + plot({movable: "none", resizable: "right"}), + plot({movable: "none", resizable: "top"}), + plot({movable: "none", resizable: "bottom"}), + ]] + + const gp = gridplot(plots, {toolbar_location: null}) + await display(gp) + }) + + it("should support hover over interaction handles", async () => { + const box = new BoxAnnotation({ + left: 1, right: 5, top: 5, bottom: 1, + editable: true, + use_handles: true, + movable: "both", + resizable: "all", + line_color: "blue", + hover_fill_color: "green", + }) + + box.handles.all.hover_fill_color = "red" + box.handles.all.hover_fill_alpha = 0.7 + + const p = fig([150, 150], { + renderers: [box], + x_range: [0, 6], y_range: [0, 6], + }) + + const {view: pv} = await display(p) + await actions(pv).hover(xy(3, 3)) + await pv.ready + }) + + it("should support visual overrides in interaction handles", async () => { + const box = new BoxAnnotation({ + left: 1, right: 5, top: 5, bottom: 1, + editable: true, + use_handles: true, + movable: "both", + resizable: "all", + line_color: "blue", + }) + + box.handles.all.fill_color = "red" + box.handles.all.fill_alpha = 0.7 + + box.handles.resize = box.handles.all.clone() + box.handles.resize.hatch_color = "blue" + box.handles.resize.hatch_pattern = "@" + + const p = fig([150, 150], { + renderers: [box], + x_range: [0, 6], y_range: [0, 6], + }) + + await display(p) + }) }) diff --git a/bokehjs/test/integration/annotations/legend.ts b/bokehjs/test/integration/annotations/legend.ts index 337c126f6e7..5265a579510 100644 --- a/bokehjs/test/integration/annotations/legend.ts +++ b/bokehjs/test/integration/annotations/legend.ts @@ -1,4 +1,6 @@ import {display, fig} from "../_util" +import {PlotActions, xy} from "../../interactive" +import {expect} from "../../unit/assertions" import {Legend, LegendItem, LinearAxis} from "@bokehjs/models" import {Random} from "@bokehjs/core/util/random" @@ -7,6 +9,8 @@ import type {CircleArgs, LineArgs} from "@bokehjs/api/glyph_api" import type {Orientation} from "@bokehjs/core/enums" import {Location} from "@bokehjs/core/enums" import {linspace} from "@bokehjs/core/util/array" +import {LegendItemClick} from "@bokehjs/core/bokeh_events" +import type {Scatter} from "@bokehjs/models/glyphs" describe("Legend annotation", () => { it("should support various combinations of locations and orientations", async () => { @@ -351,4 +355,39 @@ describe("Legend annotation", () => { test(plot({orientation: "vertical"}), "vertical") test_grid("vertical") }) + + it("should support LegendItemClick events", async () => { + const p = fig([200, 200], {y_axis_location: "right", min_border: 0}) + + const r0 = p.scatter({x: [1, 2, 3], y: [3, 4, 5], size: 10, marker: "circle", color: "red"}) + const r1 = p.scatter({x: [1, 2, 3], y: [2, 3, 4], size: 15, marker: "circle", color: "blue"}) + const r2 = p.scatter({x: [1, 2, 3], y: [1, 2, 3], size: 20, marker: "circle", color: "green"}) + + const items = [ + new LegendItem({label: "Item #0", renderers: [r0]}), + new LegendItem({label: "Item #1", renderers: [r1]}), + new LegendItem({label: "Item #2", renderers: [r2]}), + ] + + const legend = new Legend({items, location: "top_left", margin: 0}) + p.add_layout(legend) + + const clicked: LegendItem[] = [] + legend.on_event(LegendItemClick, ({item}) => { + clicked.push(item) + item.renderers.forEach((r) => (r.glyph as Scatter).marker = {value: "triangle"}) + }) + + const {view: pv} = await display(p) + + const actions = new PlotActions(pv, {units: "screen"}) + await actions.tap(xy(50, 20)) + await pv.ready + await actions.tap(xy(50, 40)) + await pv.ready + await actions.tap(xy(50, 60)) + await pv.ready + + expect(clicked).to.be.equal(items) + }) }) diff --git a/bokehjs/test/integration/cross.ts b/bokehjs/test/integration/cross.ts index 03453df5e60..b5b49d9237f 100644 --- a/bokehjs/test/integration/cross.ts +++ b/bokehjs/test/integration/cross.ts @@ -1,16 +1,21 @@ +import {expect, expect_instanceof} from "../unit/assertions" import {display} from "./_util" +import {actions, xy} from "../interactive" import json5 from "json5" import type {DocJson} from "@bokehjs/document" import {Document} from "@bokehjs/document" +import {GlyphRenderer} from "@bokehjs/models" +import {PlotView} from "@bokehjs/models/plots/plot" +import {GridPlotView} from "@bokehjs/models/plots/grid_plot" async function test(name: string) { const response = await fetch(`/cases/${name}`) const text = await response.text() const doc_json = json5.parse(text) const doc = Document.from_json(doc_json) - return await display(doc) + return await display(doc, null) } describe("Bug", () => { @@ -47,4 +52,30 @@ describe("Bug", () => { await test("regressions/issue_13637_empty_map.json5") }) }) + + describe("in issue #8766", () => { + it("doesn't allow activation of proxied box zoom tools", async () => { + const {views} = await test("regressions/issue_8766.json5") + + const [gp] = views + expect_instanceof(gp, GridPlotView) + const gb = gp.grid_box_view + + for (const pv of gb.child_views) { + expect_instanceof(pv, PlotView) + + await actions(pv).pan(xy(0.5, 0.5), xy(1.5, 1.5)) + await pv.ready + + const [gr] = pv.model.renderers.filter((r) => r instanceof GlyphRenderer) + expect(gr.data_source.selected.indices).to.be.equal([1]) + } + }) + }) + + describe("in issue #13964", () => { + it.no_image("doesn't allow using 'constructor' key in maps or plain objects in may have refs contexts", async () => { + await test("regressions/issue_13964.json5") + }) + }) }) diff --git a/bokehjs/test/integration/glyphs/glyphs.ts b/bokehjs/test/integration/glyphs/glyphs.ts index a0e0f2cb68d..da5cd485d14 100644 --- a/bokehjs/test/integration/glyphs/glyphs.ts +++ b/bokehjs/test/integration/glyphs/glyphs.ts @@ -1,6 +1,7 @@ import {display, fig, row, column} from "../_util" import {Range1d, HoverTool, ColumnDataSource, Circle, Rect, GlyphRenderer} from "@bokehjs/models" +import type {DisplayMode} from "@bokehjs/models/glyphs/tex_glyph" import type {Direction, OutputBackend} from "@bokehjs/core/enums" import type {Color} from "@bokehjs/core/types" import {hatch_aliases} from "@bokehjs/core/visuals/patterns" @@ -706,6 +707,64 @@ describe("Glyph models", () => { }) }) + describe("should support Text-like glyphs", () => { + it("with outline_shape", async () => { + const p = fig([1000, 400], { + x_range: [5, 95], y_range: [0, 50], + x_axis_type: null, y_axis_type: null, + background_fill_color: "ivory", + }) + + const x = [10, 20, 30, 40, 50, 60, 70, 80, 90] + const padding = 5 + + p.text({ + anchor: "center", + x, + y: 5, + text: ["none", "circle", "square", "ellipse", "box\nrectangle", "trapezoid", "parallelogram", "diamond", "triangle"], + outline_shape: ["none", "circle", "square", "ellipse", "box", "trapezoid", "parallelogram", "diamond", "triangle"], + background_fill_color: "white", + background_fill_alpha: 1.0, + padding, + border_line_color: "black", + text_font_size: "0.9em", + }) + + function tex(display: DisplayMode, y: number, color: Color) { + const r = String.raw + p.tex({ + anchor: "center", + x, + y, + text: [ + r`\emptyset`, + r`x^{y^z}`, + r`\frac{1}{x^2\cdot y}`, + r`\int_{-\infty}^{\infty} \frac{1}{x} dx`, + r`F = G \left( \frac{m_1 m_2}{r^2} \right)`, + r`\delta`, + r`\sqrt[3]{\gamma}`, + r`x^2`, + r`y_{\rho \theta}`, + ], + outline_shape: ["none", "circle", "square", "ellipse", "box", "trapezoid", "parallelogram", "diamond", "triangle"], + background_fill_color: color, + background_fill_alpha: 0.8, + padding, + border_line_color: "black", + text_font_size: "1.0em", + display, + }) + } + + tex("inline", 20, "yellow") + tex("block", 40, "pink") + + await display(p) + }) + }) + it("should support VArea", async () => { function p(output_backend: OutputBackend) { const p = fig([200, 300], {output_backend, title: output_backend}) diff --git a/bokehjs/test/integration/regressions.ts b/bokehjs/test/integration/regressions.ts index 534a3d49c26..6d62fd7b2bd 100644 --- a/bokehjs/test/integration/regressions.ts +++ b/bokehjs/test/integration/regressions.ts @@ -17,7 +17,8 @@ import { LinearColorMapper, Plot, TeX, - Toolbar, ToolProxy, PanTool, PolySelectTool, LassoSelectTool, HoverTool, ZoomInTool, ZoomOutTool, RangeTool, WheelPanTool, + Toolbar, ToolProxy, + PanTool, PolySelectTool, LassoSelectTool, HoverTool, ZoomInTool, ZoomOutTool, RangeTool, WheelPanTool, WheelZoomTool, TileRenderer, WMTSTileSource, ImageURLTexture, Row, Column, Spacer, @@ -33,11 +34,11 @@ import { } from "@bokehjs/models" import { - InlineStyleSheet, + InlineStyleSheet, HTML, } from "@bokehjs/models/dom" import { - Button, Toggle, Select, MultiSelect, MultiChoice, RadioGroup, RadioButtonGroup, + Button, Dropdown, Toggle, Select, MultiSelect, MultiChoice, RadioGroup, RadioButtonGroup, Div, TextInput, DatePicker, AutocompleteInput, } from "@bokehjs/models/widgets" @@ -3952,4 +3953,173 @@ describe("Bug", () => { await display(dialog, [450, 250]) }) }) + + describe("in issue #13912", () => { + it("doesn't allow stacking Dialog above non-floating UI elements", async () => { + const source = new ColumnDataSource({ + data: { + col1: range(0, 10).map((i) => String.fromCodePoint(0x61 + i)), + col2: range(0, 10), + col3: range(0, 10).map((i) => 1_000_000 + i), + }, + }) + + const columns = [ + new TableColumn({field: "col1", title: "col1"}), + new TableColumn({field: "col2", title: "col2"}), + new TableColumn({field: "col3", title: "col3"}), + ] + const data_table = new DataTable({source, columns, width: 300, height: 300}) + + const plot0 = figure({sizing_mode: "stretch_both", tools: "pan,hover"}) + plot0.circle({x: 0, y: 0, radius: 1, color: "red"}) + + const dialog0 = new Dialog({ + title: "Dialog #0", + content: plot0, + stylesheets: [` + :host { + position: absolute; /* the default is fixed */ + left: 50px; + top: 50px; + width: 200px; + height: 200px; + } + `], + }) + + const plot1 = figure({sizing_mode: "stretch_both", tools: "pan,hover"}) + plot1.circle({x: 0, y: 0, radius: 1, color: "blue"}) + + const dialog1 = new Dialog({ + title: "Dialog #1", + content: plot1, + stylesheets: [` + :host { + position: absolute; /* the default is fixed */ + left: 70px; + top: 70px; + width: 200px; + height: 200px; + } + `], + }) + + const layout = new Column({children: [data_table, dialog0, dialog1]}) + const {view} = await display(layout, [350, 350]) + + const pv1 = view.owner.get_one(plot1) + await actions(pv1).hover(xy(0, 0)) + }) + }) + + describe("in issue #13895", () => { + it("allows elements associated with renderers to overflow the canvas", async () => { + const box = div({ + style: { + width: "250px", + height: "250px", + overflow: "scroll", + }, + }) + const p = fig([200, 200], {x_range: [0, 1], y_range: [0, 1]}) + p.circle({x: [0, 10], y: [0, 10], radius: 1}) + await display(p, [300, 300], box) + }) + }) + + describe("in issue #13923", () => { + it("doesn't allow to rebuild views when Tooltip.contents changes", async () => { + const box = div({ + style: { + width: "150px", + height: "50px", + }, + }) + const content = new HTML({html: ["HTML content"]}) + const tooltip = new Tooltip({content, attachment: "right", target: box, position: "center_left", visible: true}) + const {view} = await display(tooltip, [200, 100], box) + + tooltip.content = new HTML({html: ["New HTML content"]}) + await view.ready + }) + }) + + describe("in issue #13766", () => { + it("doesn't allow to rebuild Dropdown.menu on change", async () => { + const dropdown = new Dropdown({menu: ["Action 1", "Action 2"], label: "Click action"}) + const {view} = await display(dropdown, [150, 200]) + + await mouse_click(view.button_el) // TODO make tap(view.el) work + await view.ready + + dropdown.menu = ["New Action 1", "New Action 2", "New Action 3"] + await view.ready + }) + }) + + describe("in issue #13827", () => { + it("doesn't allow to respect maintain_focus=false when zooming", async () => { + const p = fig([200, 200], { + x_range: new Range1d({bounds: [1, 5], start: 1, end: 2}), + y_range: new Range1d({bounds: [2, 7], start: 4, end: 6.5}), + tools: "reset,pan", + }) + + p.line({ + x: [1, 2, 3, 4, 5], + y: [6, 7, 2, 4, 5], + }) + + const wheel_zoom = new WheelZoomTool({maintain_focus: false}) + p.add_tools(wheel_zoom) + p.toolbar.active_scroll = wheel_zoom + + const {view} = await display(p) + const ac = actions(view, {units: "screen"}) + + for (const _ of range(0, 10)) { + await ac.scroll_down(xy(100, 100)) + await view.ready + } + }) + }) + + describe("in issue #14013", () => { + async function test(fn: (p: Figure) => GlyphRenderer) { + const p = fig([300, 150]) + + p.x_range = new Range1d({start: 0, end: 1000}) + p.y_range = new Range1d({start: -1000, end: 1000}) + + // Set the second Y axis range to be offset from the primary Y axis range + p.extra_y_ranges = { + y_range2: new Range1d({start: 250, end: -750}), + } + + p.add_layout(new LinearAxis({y_range_name: "y_range2"}), "left") + + const gr = fn(p) + gr.y_range_name = "y_range2" + + const {view} = await display(p) + + const [sx0, sx1] = view.frame.x_scale.r_compute(500, 500) + const [sy0, sy1] = view.frame.y_scale.r_compute(-500, 550) + + await actions(view, {units: "screen"}).pan(xy(sx0, sy0), xy(sx1, sy1)) + } + + const coords = [[100, 0], [900, 0], [900, -500], [100, -500]] + const xs = coords.map(([x, _]) => x) + const ys = coords.map(([_, y]) => y) + + it("doesn't allow to respect secondary ranges when masking data in Patches glyph", async () => { + await test((p) => p.patches([xs], [ys])) + }) + + it("doesn't allow to respect secondary ranges when masking data in MultiPolygons glyph", async () => { + await test((p) => p.multi_polygons([[[xs]]], [[[ys]]])) + }) + }) }) diff --git a/bokehjs/test/integration/tables.ts b/bokehjs/test/integration/tables.ts index 39af9eb9563..b6c3da235bc 100644 --- a/bokehjs/test/integration/tables.ts +++ b/bokehjs/test/integration/tables.ts @@ -1,7 +1,7 @@ import {display} from "./_util" import {mouse_click} from "../interactive" -import {ColumnDataSource} from "@bokehjs/models" +import {ColumnDataSource, CustomJSCompare, NanCompare} from "@bokehjs/models" import {DataTable, TableColumn} from "@bokehjs/models/widgets/tables" describe("DataTable", () => { @@ -20,4 +20,43 @@ describe("DataTable", () => { await mouse_click(el) await view.ready }) + + it("should allow sorting with a NanCompare", async () => { + const source = new ColumnDataSource({data: {foo: [10, 20, 30, 40], bar: [3.4, NaN, 0, -10]}}) + + const foo_col = new TableColumn({field: "foo", title: "Foo", width: 200}) + const bar_col = new TableColumn({field: "bar", title: "Bar", width: 200, sorter: new NanCompare()}) + const columns = [foo_col, bar_col] + + const table = new DataTable({source, columns, sortable: true}) + const {view} = await display(table, [600, 400]) + + const el = view.shadow_el.querySelectorAll(".slick-header-column")[2] + await mouse_click(el) + await view.ready + }) + + it("should allow sorting with a CustomJSCompare", async () => { + const source = new ColumnDataSource({data: {foo: [10, 20, 30, 40], bar: ["a 1", "a 10", "a 100", "a 2"]}}) + + const foo_col = new TableColumn({field: "foo", title: "Foo", width: 200}) + + const sorter = new CustomJSCompare({ + code: ` + const xn = Number(x.split(" ")[1]) + const yn = Number(y.split(" ")[1]) + return xn == yn ? 0 : (xn < yn ? -1 : 1) + `, + }) + const bar_col = new TableColumn({field: "bar", title: "Bar", width: 200, sorter}) + const columns = [foo_col, bar_col] + + const table = new DataTable({source, columns, sortable: true}) + const {view} = await display(table, [600, 400]) + + const el = view.shadow_el.querySelectorAll(".slick-header-column")[2] + await mouse_click(el) + await view.ready + }) + }) diff --git a/bokehjs/test/integration/tools/box_edit_tool.ts b/bokehjs/test/integration/tools/box_edit_tool.ts index dcd12374a2b..2a3a435b802 100644 --- a/bokehjs/test/integration/tools/box_edit_tool.ts +++ b/bokehjs/test/integration/tools/box_edit_tool.ts @@ -3,12 +3,12 @@ import {PlotActions, xy} from "../../interactive" import type {BoxLikeGlyph} from "@bokehjs/models/tools/edit/box_edit_tool" import {BoxEditTool} from "@bokehjs/models/tools/edit/box_edit_tool" +import type {GlyphRenderer} from "@bokehjs/models" import type {Figure} from "@bokehjs/api/figure" -import type {TypedGlyphRenderer} from "@bokehjs/api/glyph_api" describe("BoxEditTool", () => { describe("should support moving", () => { - async function move(glyph: (p: Figure) => TypedGlyphRenderer) { + async function move(glyph: (p: Figure) => GlyphRenderer) { const box_edit = new BoxEditTool() const p = fig([200, 200], { x_range: [-1, 2], diff --git a/bokehjs/test/integration/tools/hover_tool.ts b/bokehjs/test/integration/tools/hover_tool.ts new file mode 100644 index 00000000000..68d122c188c --- /dev/null +++ b/bokehjs/test/integration/tools/hover_tool.ts @@ -0,0 +1,59 @@ +import {display, fig} from "../_util" +import {PlotActions, xy} from "../../interactive" + +import {HoverTool} from "@bokehjs/models" +import {Div, ValueRef, Index, Styles} from "@bokehjs/models/dom" + +describe("HoverTool", () => { + it("should support formatting with templated and regular tooltips", async () => { + const p = fig([300, 300]) + p.circle({ + x: [0, 1, 2], + y: [0, 1, 2], + radius: [1.4325234, 1.1994322, 1.921211523], + fill_color: ["red", "green", "blue"], + fill_alpha: 0.5, + }) + + const grid = new Div({ + style: new Styles({ + display: "grid", + grid_template_columns: "auto auto", + column_gap: "10px", + }), + children: [ + "index:", new Div({children: ["#", new Index()]}), + "(x, y):", new Div({children: ["(", new ValueRef({field: "x"}), ", ", new ValueRef({field: "y"}), ")"]}), + "radius:", new ValueRef({field: "radius", format: "%.2f", formatter: "printf"}), + ], + }) + + const hover_templated = new HoverTool({ + description: "Templated hover", + tooltips: grid, + attachment: "left", + point_policy: "follow_mouse", + }) + p.add_tools(hover_templated) + + const hover_regular = new HoverTool({ + description: "Regular hover", + tooltips: [ + ["index", "$index"], + ["(x,y)", "(@x, @y)"], + ["radius", "@radius{%.2f}"], + ], + formatters: { + "@radius": "printf", + }, + attachment: "right", + point_policy: "follow_mouse", + }) + p.add_tools(hover_regular) + + const {view} = await display(p) + const actions = new PlotActions(view) + await actions.hover(xy(1, 1)) + await view.ready + }) +}) diff --git a/bokehjs/test/integration/tools/range_tool.ts b/bokehjs/test/integration/tools/range_tool.ts new file mode 100644 index 00000000000..a444159deb2 --- /dev/null +++ b/bokehjs/test/integration/tools/range_tool.ts @@ -0,0 +1,64 @@ +import {display, fig} from "../_util" +import {PlotActions, xy} from "../../interactive" + +import {PanTool, RangeTool, Range1d} from "@bokehjs/models" + +describe("RangeTool", () => { + it("should support start_gesture='none'", async () => { + const x_range = new Range1d({start: 4, end: 8}) + + const pan_tool = new PanTool() + const range_tool = new RangeTool({x_range, start_gesture: "none"}) + + const p = fig([400, 200], { + x_range: [0, 10], + y_range: [0, 2], + tools: [pan_tool, range_tool], + }) + + const {view} = await display(p) + const actions = new PlotActions(view) + await actions.pan(xy(2, 1), xy(4, 1), 2) + await view.ready + }) + + it("should support start_gesture='pan'", async () => { + const x_range = new Range1d({start: 4, end: 8}) + + const pan_tool = new PanTool() + const range_tool = new RangeTool({x_range, start_gesture: "pan"}) + + const p = fig([400, 200], { + x_range: [0, 10], + y_range: [0, 2], + tools: [pan_tool, range_tool], + active_drag: range_tool, + }) + + const {view} = await display(p) + const actions = new PlotActions(view) + await actions.pan(xy(2, 1), xy(4, 1), 2) + await view.ready + }) + + it("should support start_gesture='tap'", async () => { + const x_range = new Range1d({start: 4, end: 8}) + + const pan_tool = new PanTool() + const range_tool = new RangeTool({x_range, start_gesture: "tap"}) + + const p = fig([400, 200], { + x_range: [0, 10], + y_range: [0, 2], + tools: [pan_tool, range_tool], + }) + + const {view} = await display(p) + const actions = new PlotActions(view) + await actions.tap(xy(2, 1)) + await actions.hover(xy(2, 1), xy(4, 1), 2) + await actions.tap(xy(4, 1)) + await actions.hover(xy(4, 1), xy(6, 1), 2) + await view.ready + }) +}) diff --git a/bokehjs/test/integration/tools/wheel_zoom_tool.ts b/bokehjs/test/integration/tools/wheel_zoom_tool.ts index b74cbe101e8..cc6bd8ac65e 100644 --- a/bokehjs/test/integration/tools/wheel_zoom_tool.ts +++ b/bokehjs/test/integration/tools/wheel_zoom_tool.ts @@ -1,16 +1,19 @@ import {display, fig, row} from "../_util" -import {PlotActions, xy} from "../../interactive" +import {PlotActions, actions, xy} from "../../interactive" import type {Plot} from "@bokehjs/models" -import {Range1d, FactorRange, WheelZoomTool, LinearScale, CategoricalScale} from "@bokehjs/models" +import {Range1d, FactorRange, WheelZoomTool, LinearScale, CategoricalScale, GroupByModels, GroupByName} from "@bokehjs/models" import {DataRenderer} from "@bokehjs/models/renderers/data_renderer" import {enumerate} from "@bokehjs/core/util/iterator" +import {Category10_10} from "@bokehjs/api/palettes" describe("WheelZoomTool", () => { it("should support zooming sub-coordinates", async () => { + const factors = ["A", "B", "C"] + function plot(title: string) { const x_range = new Range1d({start: 0, end: 10}) - const y_range = new FactorRange({factors: ["A", "B", "C"]}) + const y_range = new FactorRange({factors}) const p = fig([300, 300], {x_range, y_range, title, tools: ["hover"]}) @@ -40,7 +43,7 @@ describe("WheelZoomTool", () => { const p2 = plot("Sub-coordinate zoom level 1") function data_renderers(p: Plot): DataRenderer[] { - return p.renderers.filter((r): r is DataRenderer => r instanceof DataRenderer) + return p.renderers.filter((r) => r instanceof DataRenderer) } const wheel_zoom1 = new WheelZoomTool({renderers: data_renderers(p1), level: 0}) @@ -53,19 +56,112 @@ describe("WheelZoomTool", () => { const {view} = await display(row([p0, p1, p2])) - const delta = 5*120 - const pv1 = view.owner.get_one(p1) const actions1 = new PlotActions(pv1) - await actions1.scroll(xy(5, 1.5 /*B*/), delta) + await actions1.scroll_down(xy(5, factors.indexOf("B") + 0.5), 5) await pv1.ready const pv2 = view.owner.get_one(p2) const actions2 = new PlotActions(pv2) - await actions2.scroll(xy(5, 1.5 /*B*/), delta) + await actions2.scroll_down(xy(5, factors.indexOf("B") + 0.5), 5) await pv2.ready }) + describe("should support zooming sub-coordinates", () => { + describe("with hit_test=true", () => { + const factors = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"] + + function plot(fn: (renderers: DataRenderer[]) => WheelZoomTool) { + const x_range = new Range1d({start: 0, end: 10}) + const y_range = new FactorRange({factors}) + + const p = fig([300, 300], {x_range, y_range, tools: []}) + + const renderers = [] + for (const [color, i] of enumerate(Category10_10)) { + const xy = p.subplot({ + x_source: p.x_range, + y_source: new FactorRange({factors: ["u", "v", "w"]}), + x_target: p.x_range, + y_target: new Range1d({start: i, end: i + 1}), + x_scale: new LinearScale(), + y_scale: new CategoricalScale(), + }) + + const gr = xy.line({ + x: [1, 2, 3, 4, 5, 6, 7, 8, 9], + y: ["u", "v", "w", "u", "v", "w", "u", "v", "w"], + color, + line_width: 2, + name: i % 2 == 0 ? "even" : "odd", + }) + renderers.push(gr) + } + + const wheel_zoom = fn(renderers) + p.add_tools(wheel_zoom) + p.toolbar.active_scroll = wheel_zoom + + return p + } + + describe("with hit_test_mode=hline", async () => { + it("and hit_test_behavior='only_hit'", async () => { + const p = plot((renderers) => { + return new WheelZoomTool({ + renderers, + level: 1, + dimensions: "height", + hit_test: true, + hit_test_mode: "hline", + hit_test_behavior: "only_hit", + }) + }) + + const {view: pv} = await display(p) + await actions(pv).scroll_up(xy(5, factors.indexOf("B") + 0.5), 3) + await pv.ready + }) + + it("and hit_test_behavior=GroupByName()", async () => { + const p = plot((renderers) => { + return new WheelZoomTool({ + renderers, + level: 1, + dimensions: "height", + hit_test: true, + hit_test_mode: "hline", + hit_test_behavior: new GroupByName(), + }) + }) + + const {view: pv} = await display(p) + await actions(pv).scroll_up(xy(5, factors.indexOf("B") + 0.5), 3) + await pv.ready + }) + + it("and hit_test_behavior=GroupByModels()", async () => { + const p = plot((renderers) => { + const even = renderers.filter((_, i) => i % 2 == 0) + const odd = renderers.filter((_, i) => i % 2 == 1) + return new WheelZoomTool({ + renderers, + level: 1, + dimensions: "height", + hit_test: true, + hit_test_mode: "hline", + hit_test_behavior: new GroupByModels({groups: [even, odd]}), + }) + }) + + const {view: pv} = await display(p) + await actions(pv).scroll_up(xy(5, factors.indexOf("B") + 0.5), 3) + await pv.ready + }) + }) + }) + }) + it("should notify when modifiers aren't satisfied", async () => { const wheel_zoom = new WheelZoomTool({modifiers: {ctrl: true}}) const p = fig([200, 200], {tools: [wheel_zoom]}) @@ -73,7 +169,7 @@ describe("WheelZoomTool", () => { const {view} = await display(p) const actions1 = new PlotActions(view) - await actions1.scroll(xy(2, 2), 120) + await actions1.scroll_down(xy(2, 2), 1) await view.ready }) }) diff --git a/bokehjs/test/integration/tools/zoom_in_tool.ts b/bokehjs/test/integration/tools/zoom_in_tool.ts index d0a5ea82dcb..c7d84121597 100644 --- a/bokehjs/test/integration/tools/zoom_in_tool.ts +++ b/bokehjs/test/integration/tools/zoom_in_tool.ts @@ -39,16 +39,14 @@ describe("ZoomInTool", () => { const p2 = plot("Sub-coordinate zoom level 1") function data_renderers(p: Plot): DataRenderer[] { - return p.renderers.filter((r): r is DataRenderer => r instanceof DataRenderer) + return p.renderers.filter((r) => r instanceof DataRenderer) } const zoom_in1 = new ZoomInTool({renderers: data_renderers(p1), level: 0}) p1.add_tools(zoom_in1) - p1.toolbar.active_scroll = zoom_in1 const zoom_in2 = new ZoomInTool({renderers: data_renderers(p2), level: 1}) p2.add_tools(zoom_in2) - p2.toolbar.active_scroll = zoom_in2 const {view} = await display(row([p0, p1, p2])) diff --git a/bokehjs/test/integration/tools/zoom_out_tool.ts b/bokehjs/test/integration/tools/zoom_out_tool.ts index 625ab2b5d3c..f61fb7127f2 100644 --- a/bokehjs/test/integration/tools/zoom_out_tool.ts +++ b/bokehjs/test/integration/tools/zoom_out_tool.ts @@ -39,16 +39,14 @@ describe("ZoomOutTool", () => { const p2 = plot("Sub-coordinate zoom level 1") function data_renderers(p: Plot): DataRenderer[] { - return p.renderers.filter((r): r is DataRenderer => r instanceof DataRenderer) + return p.renderers.filter((r) => r instanceof DataRenderer) } const zoom_in1 = new ZoomOutTool({renderers: data_renderers(p1), level: 0}) p1.add_tools(zoom_in1) - p1.toolbar.active_scroll = zoom_in1 const zoom_in2 = new ZoomOutTool({renderers: data_renderers(p2), level: 1}) p2.add_tools(zoom_in2) - p2.toolbar.active_scroll = zoom_in2 const {view} = await display(row([p0, p1, p2])) diff --git a/bokehjs/test/interactive.ts b/bokehjs/test/interactive.ts index a9b0f83b1dd..87dbeea5214 100644 --- a/bokehjs/test/interactive.ts +++ b/bokehjs/test/interactive.ts @@ -2,6 +2,7 @@ import type {PlotView} from "@bokehjs/models/plots/plot_canvas" import {MouseButton, offset_bbox} from "@bokehjs/core/dom" import {linspace, zip, last} from "@bokehjs/core/util/array" import {delay} from "@bokehjs/core/util/defer" +import {assert} from "@bokehjs/core/util/assert" import type {KeyModifiers} from "@bokehjs/core/ui_events" import {UIGestures} from "@bokehjs/core/ui_gestures" @@ -62,6 +63,8 @@ const _pointer_common: Partial = { const MOVE_PRESSURE = 0.0 const HOLD_PRESSURE = 0.5 +const DELTA = -120 // [px] one unit of scroll up; typical deltaY for deltaMode == DOM_DELTA_PIXEL (WheelEvent) in Chromium + export async function tap(el: Element): Promise { const ev0 = new PointerEvent("pointerdown", {..._pointer_common, pressure: HOLD_PRESSURE, buttons: MouseButton.Left}) el.dispatchEvent(ev0) @@ -125,6 +128,16 @@ export class PlotActions { await this.emit(this._scroll(xy, delta)) } + async scroll_up(xy: Point, count: number = 1): Promise { + assert(count >= 1) + await this.scroll(xy, count*(+DELTA)) + } + + async scroll_down(xy: Point, count: number = 1): Promise { + assert(count >= 1) + await this.scroll(xy, count*(-DELTA)) + } + async hover(xy0: Point, xy1?: Point, n?: number): Promise { await this.emit(this._hover({type: "line", xy0, xy1: xy1 ?? xy0, n})) } diff --git a/bokehjs/test/package.json b/bokehjs/test/package.json index 6e16aee879b..56fd9568225 100644 --- a/bokehjs/test/package.json +++ b/bokehjs/test/package.json @@ -1,6 +1,6 @@ { "name": "@bokeh/test", - "version": "3.5.0-dev.3", + "version": "3.6.0-dev.1", "private": true, "description": "Internal package for bokehjs' testing framework and tests", "license": "BSD-3-Clause", @@ -15,23 +15,23 @@ "@types/cli-progress": "^3.11.5", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", - "@types/node": "^20.12.4", + "@types/node": "^20.14.7", "@types/nunjucks": "^3.2.6", "@types/pngjs": "^6.0.4", "@types/sinon": "^17.0.2", "@types/source-map-support": "^0.5.10", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", - "chrome-remote-interface": "^0.33.0", + "chrome-remote-interface": "^0.33.2", "cli-progress": "^3.12.0", "cors": "^2.8.5", - "devtools-protocol": "^0.0.1282316", + "devtools-protocol": "^0.0.1325906", "express": "^4.19.2", "json5": "^2.2.3", "nunjucks": "^3.2.4", "path-browserify": "^1.0.1", "pngjs": "^7.0.0", - "sinon": "^17.0.1", + "sinon": "^18.0.0", "source-map-support": "^0.5.21", "ts-node": "^10.9.2", "yargs": "^17.7.2" diff --git a/bokehjs/test/unit/api/plotting.ts b/bokehjs/test/unit/api/plotting.ts index d26aa9a453f..299488d51a0 100644 --- a/bokehjs/test/unit/api/plotting.ts +++ b/bokehjs/test/unit/api/plotting.ts @@ -1,11 +1,96 @@ import {expect} from "assertions" import {figure} from "@bokehjs/api/plotting" -import {LinearAxis} from "@bokehjs/models" +import {ColumnDataSource, LinearAxis} from "@bokehjs/models" describe("in api/plotting module", () => { describe("figure()", () => { describe("glyph methods", () => { + it("should validate arguments", () => { + const source = new ColumnDataSource() + + const gr0 = figure().circle() + expect(gr0.glyph.x).to.be.equal({field: "x"}) + expect(gr0.glyph.y).to.be.equal({field: "y"}) + expect(gr0.glyph.radius).to.be.equal({field: "radius"}) + expect(gr0.data_source).to.not.be.equal(source) + + const gr1 = figure().circle({source}) + expect(gr1.glyph.x).to.be.equal({field: "x"}) + expect(gr1.glyph.y).to.be.equal({field: "y"}) + expect(gr1.glyph.radius).to.be.equal({field: "radius"}) + expect(gr1.data_source).to.be.equal(source) + + const gr2 = figure().circle(5, 10, 0.5) + expect(gr2.glyph.x).to.be.equal({value: 5}) + expect(gr2.glyph.y).to.be.equal({value: 10}) + expect(gr2.glyph.radius).to.be.equal({value: 0.5}) + expect(gr2.data_source).to.not.be.equal(source) + + const gr3 = figure().circle(5, 10, 0.5, {source}) + expect(gr3.glyph.x).to.be.equal({value: 5}) + expect(gr3.glyph.y).to.be.equal({value: 10}) + expect(gr3.glyph.radius).to.be.equal({value: 0.5}) + expect(gr3.data_source).to.be.equal(source) + + const gr4 = figure().circle([1, 2, 3], [4, 5, 6], [7, 8, 9]) + expect(gr4.glyph.x).to.be.equal({field: "x"}) + expect(gr4.glyph.y).to.be.equal({field: "y"}) + expect(gr4.glyph.radius).to.be.equal({field: "radius"}) + expect(gr4.data_source).to.not.be.equal(source) + + const gr5 = figure().circle([1, 2, 3], [4, 5, 6], [7, 8, 9], {source}) + expect(gr5.glyph.x).to.be.equal({field: "x"}) + expect(gr5.glyph.y).to.be.equal({field: "y"}) + expect(gr5.glyph.radius).to.be.equal({field: "radius"}) + expect(gr5.data_source).to.be.equal(source) + + const gr6 = figure().circle({field: "X"}, {field: "Y"}, 0.5) + expect(gr6.glyph.x).to.be.equal({field: "X"}) + expect(gr6.glyph.y).to.be.equal({field: "Y"}) + expect(gr6.glyph.radius).to.be.equal({value: 0.5}) + expect(gr6.data_source).to.not.be.equal(source) + + const gr7 = figure().circle({field: "X"}, {field: "Y"}, 0.5, {source}) + expect(gr7.glyph.x).to.be.equal({field: "X"}) + expect(gr7.glyph.y).to.be.equal({field: "Y"}) + expect(gr7.glyph.radius).to.be.equal({value: 0.5}) + expect(gr7.data_source).to.be.equal(source) + + const gr8 = figure().circle({field: "X"}, {field: "Y"}, {field: "R"}) + expect(gr8.glyph.x).to.be.equal({field: "X"}) + expect(gr8.glyph.y).to.be.equal({field: "Y"}) + expect(gr8.glyph.radius).to.be.equal({field: "R"}) + expect(gr8.data_source).to.not.be.equal(source) + + const gr9 = figure().circle({field: "X"}, {field: "Y"}, {field: "R"}, {source}) + expect(gr9.glyph.x).to.be.equal({field: "X"}) + expect(gr9.glyph.y).to.be.equal({field: "Y"}) + expect(gr9.glyph.radius).to.be.equal({field: "R"}) + expect(gr9.data_source).to.be.equal(source) + + const gr10 = figure().circle({field: "X"}, {field: "Y"}, {field: "R"}, {x: {field: "X1"}, source}) + expect(gr10.glyph.x).to.be.equal({field: "X1"}) + expect(gr10.glyph.y).to.be.equal({field: "Y"}) + expect(gr10.glyph.radius).to.be.equal({field: "R"}) + expect(gr10.data_source).to.be.equal(source) + + expect(() => { + // @ts-ignore TS2575: No overload expects 2 arguments, (...) + figure().circle(5, 10) + }).to.throw(Error, /^wrong number of arguments/) + + expect(() => { + // @ts-ignore TS2559: Type '0' has no properties in common with type 'Partial' + figure().circle(5, 10, 0.5, 0) + }).to.throw(Error, /^expected optional arguments/) + + expect(() => { + // @ts-ignore TS2353: Object literal may only specify known properties, (...) + figure().circle(5, 10, {source}, 0) + }).to.throw(Error, /^invalid value for 'radius' parameter at position 2/) + }) + it("should support '_units' auxiliary properties", () => { const p = figure() const attrs = {x: 0, y: 0, inner_radius: 1, outer_radius: 2} diff --git a/bokehjs/test/unit/base.ts b/bokehjs/test/unit/base.ts index 706c6380ff9..46081777a6b 100644 --- a/bokehjs/test/unit/base.ts +++ b/bokehjs/test/unit/base.ts @@ -31,6 +31,7 @@ describe("default model resolver", () => { "Annulus", "AnotherModel", "Arc", + "AreaVisuals", "Arrow", "ArrowHead", "Ascii", @@ -49,6 +50,7 @@ describe("default model resolver", () => { "BooleanFormatter", "BoxAnnotation", "BoxEditTool", + "BoxInteractionHandles", "BoxSelectTool", "BoxZoomTool", "BuiltinIcon", @@ -85,6 +87,7 @@ describe("default model resolver", () => { "Column", "ColumnDataSource", "ColumnarDataSource", + "Comparison", "CompositeScale", "CompositeTicker", "ContinuousAxis", @@ -99,10 +102,12 @@ describe("default model resolver", () => { "CumSum", "CustomAction", "CustomJS", + "CustomJSCompare", "CustomJSExpr", "CustomJSFilter", "CustomJSHover", "CustomJSTickFormatter", + "CustomJSTicker", "CustomJSTransform", "CustomLabelingPolicy", "DataCube", @@ -163,6 +168,8 @@ describe("default model resolver", () => { "GridBox", "GridPlot", "GroupBox", + "GroupByModels", + "GroupByName", "GroupFilter", "GroupingInfo", "GuideRenderer", @@ -243,6 +250,7 @@ describe("default model resolver", () => { "MultiSelect", "MultipleDatePicker", "MultipleDatetimePicker", + "NanCompare", "NoOverlap", "Node", "NodeCoordinates", diff --git a/bokehjs/test/unit/core/bokeh_events.ts b/bokehjs/test/unit/core/bokeh_events.ts new file mode 100644 index 00000000000..4655a3b25e0 --- /dev/null +++ b/bokehjs/test/unit/core/bokeh_events.ts @@ -0,0 +1,63 @@ +import {expect, expect_instanceof} from "assertions" +import {display} from "../_util" + +import {Serializer} from "@bokehjs/core/serialization" +import type {BokehEvent} from "@bokehjs/core/bokeh_events" +import {server_event, UserEvent} from "@bokehjs/core/bokeh_events" +import type {Model} from "@bokehjs/model" +import type {MessageSent, Patch} from "@bokehjs/document" +import {TextInput} from "@bokehjs/models/widgets" + +@server_event("some_library.do_something") +class DoSomething extends UserEvent { + constructor(override readonly values: {target: Model, action: "do_this" | "do_that"}) { + super(values) + } +} + +describe("BokehEvent", () => { + it("should support user defined events", async () => { + const collected_events: BokehEvent[] = [] + + const widget = new TextInput() + widget.on_event(DoSomething, (event) => { + collected_events.push(event) + }) + + const {view, doc} = await display(widget, null) + + const new_event = new DoSomething({target: widget, action: "do_that"}) + + const serializer = new Serializer({references: new Map([[widget, widget.ref()]])}) + const new_rep = serializer.encode(new_event) + expect(new_rep).to.be.equal({ + type: "event", + name: "some_library.do_something", + values: { + type: "map", + entries: [ + ["model", null], + ["target", widget.ref()], + ["action", "do_that"], + ], + }, + }) + + const msg: MessageSent = { + kind: "MessageSent", + msg_type: "bokeh_event", + msg_data: new_rep, + } + const patch: Patch = {events: [msg]} + doc.apply_json_patch(patch) + await view.ready + + expect(collected_events.length).to.be.equal(1) + + const [event] = collected_events + expect_instanceof(event, DoSomething) + + expect(event.origin).to.be.null + expect(event.values).to.be.equal({target: widget, action: "do_that"}) + }) +}) diff --git a/bokehjs/test/unit/core/dom.ts b/bokehjs/test/unit/core/dom.ts new file mode 100644 index 00000000000..198af0fbf61 --- /dev/null +++ b/bokehjs/test/unit/core/dom.ts @@ -0,0 +1,38 @@ +import {expect} from "assertions" + +import {div} from "@bokehjs/core/dom" + +describe("core/dom module", () => { + it("support element constructors", () => { + const el0 = div({id: "el0", style: "width: 100px; height: 200px; z-index: 1;"}) + expect(el0.isConnected).to.be.false + expect(el0.id).to.be.equal("el0") + expect(el0.style.width).to.be.equal("100px") + expect(el0.style.height).to.be.equal("200px") + expect(el0.style.zIndex).to.be.equal("1") + + const el1 = div({id: "el1", style: {width: "100px", height: "200px", zIndex: "1"}}) + expect(el1.isConnected).to.be.false + expect(el1.id).to.be.equal("el1") + expect(el1.style.width).to.be.equal("100px") + expect(el1.style.height).to.be.equal("200px") + expect(el1.style.zIndex).to.be.equal("1") + }) + + describe("support element constructors with styles", () => { + it("using camel CSS property names", () => { + const el = div({style: {borderTopLeftRadius: "1.5em"}}) + expect(el.style.borderTopLeftRadius).to.be.equal("1.5em") + }) + + it("using dashed CSS property names", () => { + const el = div({style: {"border-top-left-radius": "1.5em"}}) + expect(el.style.borderTopLeftRadius).to.be.equal("1.5em") + }) + + it("using snake CSS property names", () => { + const el = div({style: {border_top_left_radius: "1.5em"}}) + expect(el.style.borderTopLeftRadius).to.be.equal("1.5em") + }) + }) +}) diff --git a/bokehjs/test/unit/core/enums.ts b/bokehjs/test/unit/core/enums.ts index 695188526be..024d2e77241 100644 --- a/bokehjs/test/unit/core/enums.ts +++ b/bokehjs/test/unit/core/enums.ts @@ -157,6 +157,10 @@ describe("enums module", () => { expect([...enums.Orientation]).to.be.equal(["vertical", "horizontal"]) }) + it("should have OutlineShapeName", () => { + expect([...enums.OutlineShapeName]).to.be.equal(["none", "box", "rectangle", "square", "circle", "ellipse", "trapezoid", "parallelogram", "diamond", "triangle"]) + }) + it("should have OutputBackend", () => { expect([...enums.OutputBackend]).to.be.equal(["canvas", "svg", "webgl"]) }) @@ -189,8 +193,12 @@ describe("enums module", () => { expect([...enums.RoundingFunction]).to.be.equal(["round", "nearest", "floor", "rounddown", "ceil", "roundup"]) }) + it("should have RegionSelectionMode", () => { + expect([...enums.RegionSelectionMode]).to.be.equal(["replace", "append", "intersect", "subtract", "xor"]) + }) + it("should have SelectionMode", () => { - expect([...enums.SelectionMode]).to.be.equal(["replace", "append", "intersect", "subtract", "xor"]) + expect([...enums.SelectionMode]).to.be.equal(["replace", "append", "intersect", "subtract", "xor", "toggle"]) }) it("should have Side", () => { diff --git a/bokehjs/test/unit/core/util/svg.tsx b/bokehjs/test/unit/core/util/svg.tsx index 07b62eea1e0..61f7620cd14 100644 --- a/bokehjs/test/unit/core/util/svg.tsx +++ b/bokehjs/test/unit/core/util/svg.tsx @@ -3,7 +3,7 @@ import {compare_on_dom} from "../../../framework" import {SVGRenderingContext2D} from "@bokehjs/core/util/svg" import {Random} from "@bokehjs/core/util/random" import {load_image} from "@bokehjs/core/util/image" -import * as DOM from "@bokehjs/core/dom" +import * as _DOM from "@bokehjs/core/dom" // used by jsxFactory in tsconfig.json declare global { namespace JSX { diff --git a/bokehjs/test/unit/core/util/templating.ts b/bokehjs/test/unit/core/util/templating.ts index 6d93d8f9022..16da6d7c4e5 100644 --- a/bokehjs/test/unit/core/util/templating.ts +++ b/bokehjs/test/unit/core/util/templating.ts @@ -10,8 +10,8 @@ describe("templating module", () => { describe("DEFAULT_FORMATTERS", () => { - it("should have 3 entries", () => { - expect(keys(tmpl.DEFAULT_FORMATTERS).length).to.be.equal(3) + it("should have 5 entries", () => { + expect(keys(tmpl.DEFAULT_FORMATTERS).length).to.be.equal(5) }) it("should have a numeral formatter", () => { @@ -65,7 +65,7 @@ describe("templating module", () => { it("should return basic_formatter for null format", () => { const f = tmpl.get_formatter("@x") - expect(f).to.be.equal(tmpl.basic_formatter) + expect(f).to.be.equal(tmpl.DEFAULT_FORMATTERS.basic) }) it("should return numeral formatter for specs not in formatters dict", () => { diff --git a/bokehjs/test/unit/core/view.ts b/bokehjs/test/unit/core/view.ts new file mode 100644 index 00000000000..f06cacf553a --- /dev/null +++ b/bokehjs/test/unit/core/view.ts @@ -0,0 +1,91 @@ +import {expect, expect_not_null} from "assertions" + +import {HasProps} from "@bokehjs/core/has_props" +import {View} from "@bokehjs/core/view" +import type {ViewStorage} from "@bokehjs/core/build_views" +import {build_view, build_views, remove_views} from "@bokehjs/core/build_views" +import type * as p from "@bokehjs/core/properties" +import {Ref, List} from "@bokehjs/core/kinds" + +class SomeModelView extends View { + declare model: SomeModel + + protected _children_views: ViewStorage = new Map() + + override *children() { + yield* super.children() + yield* this._children_views.values() + } + + override async lazy_initialize(): Promise { + await super.lazy_initialize() + await build_views(this._children_views, this.model.children, {parent: this}) + } + + override remove(): void { + remove_views(this._children_views) + super.remove() + } +} + +export namespace SomeModel { + export type Attrs = p.AttrsOf + export type Props = HasProps.Props & { + children: p.Property + } +} + +export interface SomeModel extends SomeModel.Attrs {} + +export class SomeModel extends HasProps { + declare properties: SomeModel.Props + declare __view_type__: SomeModelView + + constructor(attrs?: Partial) { + super(attrs) + } + + static { + this.prototype.default_view = SomeModelView + + this.define({ + children: [ List(Ref(HasProps)), [] ], + }) + } +} + +describe("core/view", () => { + + describe("View", () => { + it("should support ViewQuery", async () => { + const obj0 = new SomeModel() + const obj1 = new SomeModel() + const obj2 = new SomeModel() + const obj3 = new SomeModel({children: [obj0]}) + const obj4 = new SomeModel({children: [obj1, obj2]}) + const obj5 = new SomeModel({children: [obj3, obj4]}) + + const view5 = await build_view(obj5, {parent: null}) + + const view0 = view5.views.find_one(obj0) + expect_not_null(view0) + const view1 = view5.views.find_one(obj1) + expect_not_null(view1) + const view2 = view5.views.find_one(obj2) + expect_not_null(view2) + const view3 = view5.views.find_one(obj3) + expect_not_null(view3) + const view4 = view5.views.find_one(obj4) + expect_not_null(view4) + + expect([...view5.views.all_views()]).to.be.equal([view5, view3, view0, view4, view1, view2]) + + expect([...view0.children()]).to.be.equal([]) + expect([...view1.children()]).to.be.equal([]) + expect([...view2.children()]).to.be.equal([]) + expect([...view3.children()]).to.be.equal([view0]) + expect([...view4.children()]).to.be.equal([view1, view2]) + expect([...view5.children()]).to.be.equal([view3, view4]) + }) + }) +}) diff --git a/bokehjs/test/unit/models/graphs/graph_hit_test_policy.ts b/bokehjs/test/unit/models/graphs/graph_hit_test_policy.ts index 116fac1b407..7a65e1dee6f 100644 --- a/bokehjs/test/unit/models/graphs/graph_hit_test_policy.ts +++ b/bokehjs/test/unit/models/graphs/graph_hit_test_policy.ts @@ -61,8 +61,8 @@ describe("GraphHitTestPolicy", () => { ys: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], }, }) - const node_renderer = new GlyphRenderer({data_source: node_source, glyph: new Circle()}) as GlyphRenderer & {glyph: Circle} - const edge_renderer = new GlyphRenderer({data_source: edge_source, glyph: new MultiLine()}) as GlyphRenderer & {glyph: MultiLine} + const node_renderer = new GlyphRenderer({data_source: node_source, glyph: new Circle()}) + const edge_renderer = new GlyphRenderer({data_source: edge_source, glyph: new MultiLine()}) gr = new GraphRenderer({ node_renderer, diff --git a/bokehjs/test/unit/models/plots/plot.ts b/bokehjs/test/unit/models/plots/plot.ts index cb810af21ac..f30a1f662d9 100644 --- a/bokehjs/test/unit/models/plots/plot.ts +++ b/bokehjs/test/unit/models/plots/plot.ts @@ -90,10 +90,10 @@ describe("Plot module", () => { const glyph = new GlyphRenderer({data_source: new ColumnDataSource(), glyph: new Rect()}) const plot = new Plot({renderers: [graph, glyph]}) const {view: plot_view} = await display(plot) - expect(plot_view.renderer_view(graph.node_renderer)).to.be.instanceof(GlyphRendererView) - expect(plot_view.renderer_view(graph.edge_renderer)).to.be.instanceof(GlyphRendererView) - expect(plot_view.renderer_view(graph)).to.be.instanceof(GraphRendererView) - expect(plot_view.renderer_view(glyph)).to.be.instanceof(GlyphRendererView) + expect(plot_view.views.find_one(graph.node_renderer)).to.be.instanceof(GlyphRendererView) + expect(plot_view.views.find_one(graph.edge_renderer)).to.be.instanceof(GlyphRendererView) + expect(plot_view.views.find_one(graph)).to.be.instanceof(GraphRendererView) + expect(plot_view.views.find_one(glyph)).to.be.instanceof(GlyphRendererView) }) it("should perform standard reset actions by default", async () => { diff --git a/bokehjs/test/unit/models/sources/ajax_data_source.ts b/bokehjs/test/unit/models/sources/ajax_data_source.ts index 9d90d5c1d33..db5da8ee66c 100644 --- a/bokehjs/test/unit/models/sources/ajax_data_source.ts +++ b/bokehjs/test/unit/models/sources/ajax_data_source.ts @@ -7,158 +7,181 @@ import {AjaxDataSource} from "@bokehjs/models/sources/ajax_data_source" import type {WebDataSource, AdapterFn} from "@bokehjs/models/sources/web_data_source" import type {Data} from "@bokehjs/core/types" import {last} from "@bokehjs/core/util/array" +import {poll} from "@bokehjs/core/util/defer" describe("ajax_data_source module", () => { describe("AjaxDataSource", () => { - let requests: sinon.SinonFakeXMLHttpRequest[] - let xhr: sinon.SinonFakeXMLHttpRequestStatic - - before_each(() => { - requests = [] - xhr = sinon.useFakeXMLHttpRequest() - xhr.onCreate = (xhr) => requests.push(xhr) - }) - - after_each(() => { - xhr.restore() - }) - - describe("do_load method", () => { - - it("should replace", async () => { - const s = new AjaxDataSource({data_url: "http://foo.com"}) - expect(s.data).to.be.equal({}) - - const xhr0 = s.prepare_request() - last(requests).respond(200, {}, '{"foo": [10, 20], "bar": [1, 2]}') - await s.do_load(xhr0, "replace", 10) - expect(s.data).to.be.equal({foo: [10, 20], bar: [1, 2]}) - - const xhr1 = s.prepare_request() - last(requests).respond(200, {}, '{"foo": [100, 200], "bar": [1.1, 2.2]}') - await s.do_load(xhr1, "replace", 10) - expect(s.data).to.be.equal({foo: [100, 200], bar: [1.1, 2.2]}) - - // max size ignored when replacing - const xhr2 = s.prepare_request() - last(requests).respond(200, {}, '{"foo": [1000, 2000], "bar": [10.1, 20.2]}') - await s.do_load(xhr2, "replace", 1) - expect(s.data).to.be.equal({foo: [1000, 2000], bar: [10.1, 20.2]}) + describe("with fake XMLHttpRequest", () => { + let requests: sinon.SinonFakeXMLHttpRequest[] + let xhr: sinon.SinonFakeXMLHttpRequestStatic + + before_each(() => { + requests = [] + xhr = sinon.useFakeXMLHttpRequest() + xhr.onCreate = (xhr) => requests.push(xhr) }) - it("should append up to max_size", async () => { - const s = new AjaxDataSource({data_url: "http://foo.com"}) - expect(s.data).to.be.equal({}) - - const xhr0 = s.prepare_request() - last(requests).respond(200, {}, '{"foo": [10, 20], "bar": [1, 2]}') - await s.do_load(xhr0, "append", 3) - expect(s.data).to.be.equal({foo: [10, 20], bar: [1, 2]}) - - const xhr1 = s.prepare_request() - last(requests).respond(200, {}, '{"foo": [100, 200], "bar": [1.1, 2.2]}') - await s.do_load(xhr1, "append", 3) - expect(s.data).to.be.equal({foo: [20, 100, 200], bar: [2, 1.1, 2.2]}) + after_each(() => { + xhr.restore() }) - it("should use a CustomJS adapter", async () => { - const code = ` - export default (_args, _obj, data) => { - const foo = [] - const bar = [] - for (const [pt0, pt1] of data.response.points) { - foo.push(pt0) - bar.push(pt1) + describe("do_load method", () => { + + it("should replace", async () => { + const s = new AjaxDataSource({data_url: "http://foo.com"}) + expect(s.data).to.be.equal({}) + + const xhr0 = s.prepare_request() + last(requests).respond(200, {}, '{"foo": [10, 20], "bar": [1, 2]}') + await s.do_load(xhr0, "replace", 10) + expect(s.data).to.be.equal({foo: [10, 20], bar: [1, 2]}) + + const xhr1 = s.prepare_request() + last(requests).respond(200, {}, '{"foo": [100, 200], "bar": [1.1, 2.2]}') + await s.do_load(xhr1, "replace", 10) + expect(s.data).to.be.equal({foo: [100, 200], bar: [1.1, 2.2]}) + + // max size ignored when replacing + const xhr2 = s.prepare_request() + last(requests).respond(200, {}, '{"foo": [1000, 2000], "bar": [10.1, 20.2]}') + await s.do_load(xhr2, "replace", 1) + expect(s.data).to.be.equal({foo: [1000, 2000], bar: [10.1, 20.2]}) + }) + + it("should append up to max_size", async () => { + const s = new AjaxDataSource({data_url: "http://foo.com"}) + expect(s.data).to.be.equal({}) + + const xhr0 = s.prepare_request() + last(requests).respond(200, {}, '{"foo": [10, 20], "bar": [1, 2]}') + await s.do_load(xhr0, "append", 3) + expect(s.data).to.be.equal({foo: [10, 20], bar: [1, 2]}) + + const xhr1 = s.prepare_request() + last(requests).respond(200, {}, '{"foo": [100, 200], "bar": [1.1, 2.2]}') + await s.do_load(xhr1, "append", 3) + expect(s.data).to.be.equal({foo: [20, 100, 200], bar: [2, 1.1, 2.2]}) + }) + + it("should use a CustomJS adapter", async () => { + const code = ` + export default (_args, _obj, data) => { + const foo = [] + const bar = [] + for (const [pt0, pt1] of data.response.points) { + foo.push(pt0) + bar.push(pt1) + } + return {foo, bar} } - return {foo, bar} - } - ` - const cb = new CustomJS({code}) - const s = new AjaxDataSource({data_url: "http://foo.com", adapter: cb as AdapterFn}) - expect(s.data).to.be.equal({}) - - const xhr = s.prepare_request() - last(requests).respond(200, {}, '{"points": [[10, 1], [20, 2]]}') - await s.do_load(xhr, "replace", 10) - expect(s.data).to.be.equal({foo: [10, 20], bar: [1, 2]}) - }) - - it("should use a JavaScript function adapter", async () => { - function execute(_obj: WebDataSource, data: {response: {points: [number, number][]}}): Data { - const foo: number[] = [] - const bar: number[] = [] - for (const [pt0, pt1] of data.response.points) { - foo.push(pt0) - bar.push(pt1) + ` + const cb = new CustomJS({code}) + const s = new AjaxDataSource({data_url: "http://foo.com", adapter: cb as AdapterFn}) + expect(s.data).to.be.equal({}) + + const xhr = s.prepare_request() + last(requests).respond(200, {}, '{"points": [[10, 1], [20, 2]]}') + await s.do_load(xhr, "replace", 10) + expect(s.data).to.be.equal({foo: [10, 20], bar: [1, 2]}) + }) + + it("should use a JavaScript function adapter", async () => { + function execute(_obj: WebDataSource, data: {response: {points: [number, number][]}}): Data { + const foo: number[] = [] + const bar: number[] = [] + for (const [pt0, pt1] of data.response.points) { + foo.push(pt0) + bar.push(pt1) + } + return {foo, bar} } - return {foo, bar} - } - - const s = new AjaxDataSource({data_url: "http://foo.com", adapter: {execute}}) - expect(s.data).to.be.equal({}) - - const xhr = s.prepare_request() - last(requests).respond(200, {}, '{"points": [[10, 1], [20, 2]]}') - await s.do_load(xhr, "replace", 10) - expect(s.data).to.be.equal({foo: [10, 20], bar: [1, 2]}) - }) - }) - describe("prepare_request method", () => { + const s = new AjaxDataSource({data_url: "http://foo.com", adapter: {execute}}) + expect(s.data).to.be.equal({}) - it("should return an xhr with withCredentials = False", () => { - const s = new AjaxDataSource({data_url: "http://foo.com"}) - const xhr = s.prepare_request() - expect(xhr).to.be.instanceof(XMLHttpRequest) - expect(xhr.withCredentials).to.be.false + const xhr = s.prepare_request() + last(requests).respond(200, {}, '{"points": [[10, 1], [20, 2]]}') + await s.do_load(xhr, "replace", 10) + expect(s.data).to.be.equal({foo: [10, 20], bar: [1, 2]}) + }) }) - it("should return an xhr with method set from this.method", () => { - const s0 = new AjaxDataSource({data_url: "http://foo.com"}) - s0.prepare_request() - expect(last(requests).method).to.be.equal("POST") - - const s1 = new AjaxDataSource({data_url: "http://foo.com", method: "POST"}) - s1.prepare_request() - expect(last(requests).method).to.be.equal("POST") - - const s2 = new AjaxDataSource({data_url: "http://foo.com", method: "GET"}) - s2.prepare_request() - expect(last(requests).method).to.be.equal("GET") + describe("prepare_request method", () => { + + it("should return an xhr with withCredentials = False", () => { + const s = new AjaxDataSource({data_url: "http://foo.com"}) + const xhr = s.prepare_request() + expect(xhr).to.be.instanceof(XMLHttpRequest) + expect(xhr.withCredentials).to.be.false + }) + + it("should return an xhr with method set from this.method", () => { + const s0 = new AjaxDataSource({data_url: "http://foo.com"}) + s0.prepare_request() + expect(last(requests).method).to.be.equal("POST") + + const s1 = new AjaxDataSource({data_url: "http://foo.com", method: "POST"}) + s1.prepare_request() + expect(last(requests).method).to.be.equal("POST") + + const s2 = new AjaxDataSource({data_url: "http://foo.com", method: "GET"}) + s2.prepare_request() + expect(last(requests).method).to.be.equal("GET") + }) + + it("should return an xhr with Content-Type header set to json", () => { + const s = new AjaxDataSource({data_url: "http://foo.com"}) + s.prepare_request() + expect(last(requests).requestHeaders).to.be.equal({"Content-Type": "application/json"}) + }) + + it("should return an xhr with additional headers set from this.http_headers", () => { + const s = new AjaxDataSource({data_url: "http://foo.com", http_headers: {foo: "bar", baz: "10"}}) + s.prepare_request() + expect(last(requests).requestHeaders).to.be.equal({"Content-Type": "application/json", foo: "bar", baz: "10"}) + }) }) - it("should return an xhr with Content-Type header set to json", () => { - const s = new AjaxDataSource({data_url: "http://foo.com"}) - s.prepare_request() - expect(last(requests).requestHeaders).to.be.equal({"Content-Type": "application/json"}) - }) + describe("get_column() method", () => { - it("should return an xhr with additional headers set from this.http_headers", () => { - const s = new AjaxDataSource({data_url: "http://foo.com", http_headers: {foo: "bar", baz: "10"}}) - s.prepare_request() - expect(last(requests).requestHeaders).to.be.equal({"Content-Type": "application/json", foo: "bar", baz: "10"}) + it("should return empty lists for not-yet-existant columns", () => { + const s = new AjaxDataSource({data_url: "http://foo.com"}) + const c = s.get_column("foo") + expect(c).to.be.equal([]) + const n = s.get_length() + expect(n).to.be.equal(0) + }) }) - }) - describe("get_column() method", () => { + describe("initialize method", () => { - it("should return empty lists for not-yet-existant columns", () => { - const s = new AjaxDataSource({data_url: "http://foo.com"}) - const c = s.get_column("foo") - expect(c).to.be.equal([]) - const n = s.get_length() - expect(n).to.be.equal(0) + it("should call get_data", () => { + const spy = sinon.spy(AjaxDataSource.prototype, "get_data") + try { + const s = new AjaxDataSource({data_url: "http://foo.com"}) + s.destroy() + expect(spy.calledOnce).to.be.true + } finally { + spy.restore() + } + }) }) }) - describe("initialize method", () => { - - it("should call get_data", () => { - const s = new AjaxDataSource({data_url: "http://foo.com"}) - const spy = sinon.spy(s, "get_data") - s.initialize() - expect(spy.calledOnce).to.be.true + describe("with real XMLHttpRequest", () => { + describe("get_data() method", () => { + it("should support If-Modified-Since header", async () => { + const spy = sinon.spy(XMLHttpRequest.prototype, "setRequestHeader") + try { + const source = new AjaxDataSource({data_url: "/ajax/dummy_data", polling_interval: 100, if_modified: true}) + await poll(() => spy.callCount >= 5, 100, 1000) + source.destroy() + expect(spy.calledWith(sinon.match("If-Modified-Since"), sinon.match.string)).to.be.true + } finally { + spy.restore() + } + }) }) }) }) diff --git a/bokehjs/test/unit/models/tickers/customjs_ticker.ts b/bokehjs/test/unit/models/tickers/customjs_ticker.ts new file mode 100644 index 00000000000..daf4fd0ad3c --- /dev/null +++ b/bokehjs/test/unit/models/tickers/customjs_ticker.ts @@ -0,0 +1,106 @@ +import {expect} from "assertions" + +import {CustomJSTicker} from "@bokehjs/models/tickers/customjs_ticker" +import type {FactorTickSpec} from "@bokehjs/models/tickers/categorical_ticker" +import {FactorRange} from "@bokehjs/models/ranges/factor_range" +import {Range1d} from "@bokehjs/models/ranges/range1d" + +describe("CustomJSTicker Model", () => { + + describe("Continuous get_ticks method", () => { + it("should handle case with no major_code", () => { + const ticker = new CustomJSTicker() + const range = new Range1d({start: -1, end: 11}) + const ticks = ticker.get_ticks(0, 10, range, NaN) + expect(ticks.major).to.be.equal([]) + expect(ticks.minor).to.be.equal([]) + }) + + it("should return major_code result", () => { + const ticker = new CustomJSTicker({major_code: "return [2,4,6,8]"}) + const range = new Range1d({start: -1, end: 11}) + const ticks = ticker.get_ticks(0, 10, range, NaN) + expect(ticks.major).to.be.equal([2, 4, 6, 8]) + expect(ticks.minor).to.be.equal([]) + }) + + it("should pass start and end to major_code", () => { + const ticker = new CustomJSTicker({major_code: "return [cb_data.start, cb_data.end]"}) + const range = new Range1d({start: -1, end: 11}) + const ticks = ticker.get_ticks(0, 10, range, NaN) + expect(ticks.major).to.be.equal([0, 10]) + expect(ticks.minor).to.be.equal([]) + }) + + it("should pass range to major_code", () => { + const ticker = new CustomJSTicker({major_code: "return [cb_data.range.start, cb_data.range.end]"}) + const range = new Range1d({start: -1, end: 11}) + const ticks = ticker.get_ticks(0, 10, range, NaN) + expect(ticks.major).to.be.equal([-1, 11]) + expect(ticks.minor).to.be.equal([]) + }) + + it("should pass cross_loc to major_code", () => { + const ticker = new CustomJSTicker({major_code: "return [cb_data.cross_loc]"}) + const range = new Range1d({start: -1, end: 11}) + const ticks = ticker.get_ticks(0, 10, range, 20) + expect(ticks.major).to.be.equal([20]) + expect(ticks.minor).to.be.equal([]) + }) + + }) + + describe("Categorical get_ticks method", () => { + + it("should handle case with no major_code", () => { + const ticker = new CustomJSTicker() + const range = new FactorRange({factors: ["foo", "bar", "baz"]}) + const ticks = ticker.get_ticks(0, 10, range, NaN) as FactorTickSpec + expect(ticks.major).to.be.equal([]) + expect(ticks.minor).to.be.equal([]) + expect(ticks.mids).to.be.equal([]) + expect(ticks.tops).to.be.equal([]) + }) + + it("should handle case where range has factors", () => { + const ticker = new CustomJSTicker({major_code: "return['foo', 'baz']"}) + const range = new FactorRange({factors: ["foo", "bar", "baz"]}) + const ticks = ticker.get_ticks(0, 3, range, NaN) as FactorTickSpec + expect(ticks.major).to.be.equal(["foo", "baz"]) + expect(ticks.minor).to.be.equal([]) + expect(ticks.mids).to.be.equal([]) + expect(ticks.tops).to.be.equal([]) + }) + + it("should pass start and end to major_code", () => { + const ticker = new CustomJSTicker({major_code: "return [cb_data.start.toString(), cb_data.end.toString()]"}) + const range = new FactorRange({factors: ["foo", "bar", "baz"]}) + const ticks = ticker.get_ticks(0, 10, range, NaN) as FactorTickSpec + expect(ticks.major).to.be.equal(["0", "10"]) + expect(ticks.minor).to.be.equal([]) + expect(ticks.mids).to.be.equal([]) + expect(ticks.tops).to.be.equal([]) + }) + + it("should pass range to major_code", () => { + const ticker = new CustomJSTicker({major_code: "return cb_data.range.factors"}) + const range = new FactorRange({factors: ["foo", "bar", "baz"]}) + const ticks = ticker.get_ticks(0, 10, range, NaN) as FactorTickSpec + expect(ticks.major).to.be.equal(["foo", "bar", "baz"]) + expect(ticks.minor).to.be.equal([]) + expect(ticks.mids).to.be.equal([]) + expect(ticks.tops).to.be.equal([]) + }) + + it("should pass cross_loc to major_code", () => { + const ticker = new CustomJSTicker({major_code: "return [cb_data.cross_loc.toString()]"}) + const range = new FactorRange({factors: ["foo", "bar", "baz"]}) + const ticks = ticker.get_ticks(0, 10, range, 20) as FactorTickSpec + expect(ticks.major).to.be.equal(["20"]) + expect(ticks.minor).to.be.equal([]) + expect(ticks.mids).to.be.equal([]) + expect(ticks.tops).to.be.equal([]) + }) + + }) +}) diff --git a/bokehjs/test/unit/models/tools/edit/freehand_draw_tool.ts b/bokehjs/test/unit/models/tools/edit/freehand_draw_tool.ts index 3f1ff3bf3fe..be828254a62 100644 --- a/bokehjs/test/unit/models/tools/edit/freehand_draw_tool.ts +++ b/bokehjs/test/unit/models/tools/edit/freehand_draw_tool.ts @@ -12,7 +12,6 @@ import {Range1d} from "@bokehjs/models/ranges/range1d" import {Selection} from "@bokehjs/models/selections/selection" import {GlyphRenderer} from "@bokehjs/models/renderers/glyph_renderer" import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source" -import type {HasXYGlyph} from "@bokehjs/models/tools/edit/edit_tool" import type {FreehandDrawToolView} from "@bokehjs/models/tools/edit/freehand_draw_tool" import {FreehandDrawTool} from "@bokehjs/models/tools/edit/freehand_draw_tool" @@ -46,13 +45,13 @@ async function make_testcase(): Promise { ys: {field: "ys"}, }) - const glyph_renderer = new GlyphRenderer({glyph, data_source}) + const glyph_renderer = new GlyphRenderer({glyph, data_source}) const glyph_renderer_view = await build_view(glyph_renderer, {parent: plot_view}) const draw_tool = new FreehandDrawTool({ active: true, default_overrides: {z: "Test"}, - renderers: [glyph_renderer as GlyphRenderer & HasXYGlyph], + renderers: [glyph_renderer], }) plot.add_tools(draw_tool) await plot_view.ready diff --git a/bokehjs/test/unit/models/tools/edit/poly_edit_tool.ts b/bokehjs/test/unit/models/tools/edit/poly_edit_tool.ts index 53fb5ca1a54..4743b56855f 100644 --- a/bokehjs/test/unit/models/tools/edit/poly_edit_tool.ts +++ b/bokehjs/test/unit/models/tools/edit/poly_edit_tool.ts @@ -14,7 +14,6 @@ import {Range1d} from "@bokehjs/models/ranges/range1d" import {Selection} from "@bokehjs/models/selections/selection" import {GlyphRenderer} from "@bokehjs/models/renderers/glyph_renderer" import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source" -import type {HasXYGlyph} from "@bokehjs/models/tools/edit/edit_tool" import type {PolyEditToolView} from "@bokehjs/models/tools/edit/poly_edit_tool" import {PolyEditTool} from "@bokehjs/models/tools/edit/poly_edit_tool" @@ -25,7 +24,7 @@ export interface PolyEditTestCase { data_source: ColumnDataSource draw_tool_view: PolyEditToolView glyph_view: PatchesView - glyph_renderer: GlyphRenderer + glyph_renderer: GlyphRenderer vertex_glyph_view: CircleView vertex_source: ColumnDataSource vertex_renderer: GlyphRenderer @@ -67,8 +66,8 @@ async function make_testcase(): Promise { const draw_tool = new PolyEditTool({ active: true, default_overrides: {z: "Test"}, - renderers: [glyph_renderer as any], - vertex_renderer: vertex_renderer as GlyphRenderer & HasXYGlyph, + renderers: [glyph_renderer], + vertex_renderer, }) plot.add_tools(draw_tool) await plot_view.ready diff --git a/bokehjs/test/unit/models/tools/gestures/wheel_zoom_tool.ts b/bokehjs/test/unit/models/tools/gestures/wheel_zoom_tool.ts index ab44464bf28..db7e908c73a 100644 --- a/bokehjs/test/unit/models/tools/gestures/wheel_zoom_tool.ts +++ b/bokehjs/test/unit/models/tools/gestures/wheel_zoom_tool.ts @@ -1,7 +1,7 @@ import {expect} from "assertions" import {display} from "../../../_util" -import type {Tool} from "@bokehjs/models/tools/tool" +import type {GestureTool} from "@bokehjs/models/tools/gestures/gesture_tool" import {WheelZoomTool} from "@bokehjs/models/tools/gestures/wheel_zoom_tool" import {Range1d} from "@bokehjs/models/ranges/range1d" import type {PlotView} from "@bokehjs/models/plots/plot" @@ -21,7 +21,7 @@ function xy_axis(plot_view: PlotView) { } // frame dimensions is 300x300, thus zooming at {sx: 150, sy: 150} causes the x/y ranges to zoom equally -async function make_plot(tool: T): Promise<{view: PlotView, tool_view: ViewOf}> { +async function make_plot(tool: T): Promise<{view: PlotView, tool_view: ViewOf}> { const plot = new Plot({ x_range: new Range1d({start: -1, end: 1}), y_range: new Range1d({start: -1, end: 1}), diff --git a/bokehjs/test/unit/models/tools/inspectors/hover_tool.ts b/bokehjs/test/unit/models/tools/inspectors/hover_tool.ts index 2bc4a15c607..68e1b584b5c 100644 --- a/bokehjs/test/unit/models/tools/inspectors/hover_tool.ts +++ b/bokehjs/test/unit/models/tools/inspectors/hover_tool.ts @@ -29,7 +29,7 @@ async function make_testcase(): Promise<{hover_view: HoverToolView, data_source: const {view: plot_view} = await display(plot) const hover_view = plot_view.owner.get_one(hover_tool) - const glyph_view = plot_view.owner.get_one(glyph_renderer.glyph) as CircleView + const glyph_view = plot_view.owner.get_one(glyph_renderer.glyph) return {hover_view, data_source, glyph_view} } diff --git a/bokehjs/test/unit/models/widgets/file_input.ts b/bokehjs/test/unit/models/widgets/file_input.ts index 2ecd244285f..d9bfb2af48e 100644 --- a/bokehjs/test/unit/models/widgets/file_input.ts +++ b/bokehjs/test/unit/models/widgets/file_input.ts @@ -2,6 +2,8 @@ import {expect} from "assertions" import {display} from "../../_util" import {FileInput} from "@bokehjs/models/widgets" +import type {MessageSent, Patch} from "@bokehjs/document" +import {zip} from "@bokehjs/core/util/array" // FileList doesn't have a constructor (https://www.w3.org/TR/FileAPI/#filelist-section) class _FileList extends Array implements FileList { @@ -63,4 +65,85 @@ describe("FileInputView", () => { expect(model.filename).to.be.equal(["foo.txt", "bar.txt", "baz.txt"]) expect(model.mime_type).to.be.equal(["text/plain", "text/plain", "text/plain"]) }) + + it("should support ClearInput server-sent event", async () => { + const file_input = new FileInput({accept: ".csv,.json.,.txt", multiple: false}) + const {view, doc} = await display(file_input, null) + + const file = new File(["foo bar"], "foo.txt", {type: "text/plain"}) + const files = new _FileList(file) + + await view.load_files(files) + expect(file_input.filename).to.be.equal("foo.txt") + + const msg: MessageSent = { + kind: "MessageSent", + msg_type: "bokeh_event", + msg_data: { + type: "event", + name: "clear_input", + values: { + type: "map", + entries: [ + ["model", file_input.ref()], + ], + }, + }, + } + + const patch: Patch = {events: [msg]} + doc.apply_json_patch(patch) + await view.ready + + expect(file_input.filename).to.be.equal("") // TODO should be `unset` + }) + + it("should upload a directory", async () => { + const model = new FileInput({directory: true}) + const {view} = await display(model, null) + + const getFileList = () => { + const dt = new DataTransfer() + const filenames = ["foo", "bar", "baz"] + for (const filename of filenames) { + const file = new File([filename], `${filename}.txt`, {type: "text/plain"}) + // To set the `webkitRelativePath` property as it is a read-only + Object.defineProperty(file, "webkitRelativePath", {value: `subdir/${filename}.txt`}) + dt.items.add(file) + } + return dt.files + } + + const files = getFileList() + await view.load_files(files) + + expect(model.value).to.be.equal([btoa("foo"), btoa("bar"), btoa("baz")]) + expect(model.filename).to.be.equal(["subdir/foo.txt", "subdir/bar.txt", "subdir/baz.txt"]) + expect(model.mime_type).to.be.equal(["text/plain", "text/plain", "text/plain"]) + }) + + it("should upload a directory with accept", async () => { + const model = new FileInput({directory: true, accept: ".txt"}) + const {view} = await display(model, null) + + const getFileList = () => { + const dt = new DataTransfer() + const filenames = ["foo", "bar", "baz"] + const exts = ["txt", "csv", "json"] + for (const [ filename, ext ] of zip(filenames, exts)) { + const file = new File([filename], `${filename}.${ext}`, {type: "text/plain"}) + // To set the `webkitRelativePath` property as it is a read-only + Object.defineProperty(file, "webkitRelativePath", {value: `subdir/${filename}.${ext}`}) + dt.items.add(file) + } + return dt.files + } + + const files = getFileList() + await view.load_files(files) + + expect(model.value).to.be.equal([btoa("foo")]) + expect(model.filename).to.be.equal(["subdir/foo.txt"]) + expect(model.mime_type).to.be.equal(["text/plain"]) + }) }) diff --git a/bokehjs/test/unit/regressions.ts b/bokehjs/test/unit/regressions.ts index 0ff6409b771..4fced46f46d 100644 --- a/bokehjs/test/unit/regressions.ts +++ b/bokehjs/test/unit/regressions.ts @@ -18,8 +18,10 @@ import { CopyTool, CustomJS, DataRange1d, + EqHistColorMapper, GlyphRenderer, HoverTool, + Image, IndexFilter, Legend, LegendItem, @@ -50,13 +52,14 @@ import { import { Button, + CategoricalSlider, } from "@bokehjs/models/widgets" import {version} from "@bokehjs/version" import {Model} from "@bokehjs/model" import * as p from "@bokehjs/core/properties" import {is_equal} from "@bokehjs/core/util/eq" -import {linspace} from "@bokehjs/core/util/array" +import {linspace, range} from "@bokehjs/core/util/array" import {keys} from "@bokehjs/core/util/object" import {ndarray} from "@bokehjs/core/util/ndarray" import {BitSet} from "@bokehjs/core/util/bitset" @@ -68,13 +71,14 @@ import type {DocJson, DocumentEvent} from "@bokehjs/document" import {Document, ModelChangedEvent, MessageSentEvent} from "@bokehjs/document" import {DocumentReady, RangesUpdate} from "@bokehjs/core/bokeh_events" import {gridplot} from "@bokehjs/api/gridplot" -import {Spectral11, Viridis256} from "@bokehjs/api/palettes" +import {Spectral11, Viridis11, Viridis256} from "@bokehjs/api/palettes" import {defer, paint, poll} from "@bokehjs/core/util/defer" import type {Field} from "@bokehjs/core/vectorization" import {UIElement, UIElementView} from "@bokehjs/models/ui/ui_element" import type {GlyphRendererView} from "@bokehjs/models/renderers/glyph_renderer" import {ImageURLView} from "@bokehjs/models/glyphs/image_url" +import {ImageView} from "@bokehjs/models/glyphs/image" import {CopyToolView} from "@bokehjs/models/tools/actions/copy_tool" import {TableDataProvider, DataTable} from "@bokehjs/models/widgets/tables/data_table" import {TableColumn} from "@bokehjs/models/widgets/tables/table_column" @@ -1548,4 +1552,116 @@ describe("Bug", () => { expect(table.source.selected.indices.slice().sort()).to.be.equal([0, 2].sort()) }) }) + + describe("in issue #13831", () => { + it("allows addition of new indices to selection by default", async () => { + const tap_tool = new TapTool() + const p = fig([200, 200], {tools: [tap_tool]}) + const cr = p.circle({x: [0, 1, 2, 3], y: [0, 1, 2, 3], radius: [0.5, 0.75, 1.0, 1.25], color: ["red", "green", "blue", "yellow"], alpha: 0.8}) + const ds = cr.data_source + + const {view} = await display(p) + const pv = actions(view) + + expect(ds.selected.indices).to.be.equal([]) + + async function tap_at(x: number, y: number) { + await pv.tap(xy(x, y)) + await view.ready + } + + await tap_at(0, 0) // select red + expect(ds.selected.indices).to.be.equal([0]) + + await tap_at(0, 0) // deselect red + expect(ds.selected.indices).to.be.equal([]) + + await tap_at(0, 0) // select red + expect(ds.selected.indices).to.be.equal([0]) + + await tap_at(1, 1) // select green + expect(ds.selected.indices).to.be.equal([1]) + + await tap_at(2, 2) // select blue + expect(ds.selected.indices).to.be.equal([2]) + + await tap_at(3, 3) // select yellow + expect(ds.selected.indices).to.be.equal([3]) + + await tap_at(4, 0) // deselect + expect(ds.selected.indices).to.be.equal([]) + + await tap_at(2.5, 2.5) // select blue and yellow + expect(ds.selected.indices).to.be.equal([2, 3]) + + await tap_at(2.5, 2.5) // deselect blue and yellow + expect(ds.selected.indices).to.be.equal([]) + + await tap_at(2, 2) // select blue + expect(ds.selected.indices).to.be.equal([2]) + + await tap_at(2.5, 2.5) // deselect blue and select yellow + expect(ds.selected.indices).to.be.equal([3]) + }) + }) + + describe("in issue #13951", () => { + it("doesn't allow inheriting image data in Image-like glyphs", async () => { + const p = fig([400, 400]) + + const color_mapper = new EqHistColorMapper({palette: Spectral11}) + const gr = p.image({image: [scalar_image()], x: 0, y: 0, dw: 10, dh: 10, color_mapper}) + + const nonselection_color_mapper = new EqHistColorMapper({palette: Viridis11}) + gr.nonselection_glyph = new Image({x: 0, y: 0, dw: 10, dh: 10, color_mapper: nonselection_color_mapper}) + + const {view} = await display(p) + const grv = view.views.get_one(gr) + + expect_instanceof(grv.glyph, ImageView) + expect_instanceof(grv.selection_glyph, ImageView) + expect_instanceof(grv.nonselection_glyph, ImageView) + expect_instanceof(grv.decimated_glyph, ImageView) + expect_instanceof(grv.hover_glyph, ImageView) + expect_instanceof(grv.muted_glyph, ImageView) + + expect(grv.glyph.inherited_image_data).to.be.false + expect(grv.selection_glyph.inherited_image_data).to.be.true + expect(grv.nonselection_glyph.inherited_image_data).to.be.false + expect(grv.decimated_glyph.inherited_image_data).to.be.true + expect(grv.hover_glyph.inherited_image_data).to.be.true + expect(grv.muted_glyph.inherited_image_data).to.be.true + + expect(grv.glyph.inherited_image_width).to.be.false + expect(grv.selection_glyph.inherited_image_width).to.be.true + expect(grv.nonselection_glyph.inherited_image_width).to.be.false + expect(grv.decimated_glyph.inherited_image_width).to.be.true + expect(grv.hover_glyph.inherited_image_width).to.be.true + expect(grv.muted_glyph.inherited_image_width).to.be.true + + expect(grv.glyph.inherited_image_height).to.be.false + expect(grv.selection_glyph.inherited_image_height).to.be.true + expect(grv.nonselection_glyph.inherited_image_height).to.be.false + expect(grv.decimated_glyph.inherited_image_height).to.be.true + expect(grv.hover_glyph.inherited_image_height).to.be.true + expect(grv.muted_glyph.inherited_image_height).to.be.true + }) + }) + + describe("in issue #13965", () => { + it("doesn't allow to correctly index categories in CategoricalSlider", async () => { + const categories = range(0, 20).map((i) => `${i}`) + const slider = new CategoricalSlider({categories, value: "0"}) + const {view} = await display(slider, [300, 50]) + + const el = view.shadow_el.querySelector(".noUi-handle") + expect_not_null(el) + + // The expectation is that no errors accumulate during sliding. + for (const _ of categories) { + el.dispatchEvent(new KeyboardEvent("keydown", {key: "ArrowRight"})) + await view.ready + } + }) + }) }) diff --git a/bokehjs/test/unit/tsconfig.json b/bokehjs/test/unit/tsconfig.json index a2ad43e4609..5969edb955e 100644 --- a/bokehjs/test/unit/tsconfig.json +++ b/bokehjs/test/unit/tsconfig.json @@ -20,7 +20,7 @@ "importHelpers": true, "experimentalDecorators": true, "jsx": "react", - "jsxFactory": "DOM.createSVGElement", + "jsxFactory": "_DOM.createSVGElement", "module": "ES2022", "moduleResolution": "node", "isolatedModules": true, diff --git a/conda/environment-release-build.yml b/conda/environment-release-build.yml index b4efd08ccc1..3e63bef1286 100644 --- a/conda/environment-release-build.yml +++ b/conda/environment-release-build.yml @@ -38,7 +38,7 @@ dependencies: - pip: # docs - bokeh_sampledata - - pydata_sphinx_theme==0.15.2 + - pydata_sphinx_theme - sphinx >= 7.1.0 - sphinx-copybutton - sphinx-design diff --git a/conda/environment-test-3.10.yml b/conda/environment-test-3.10.yml index 6599dc33a8b..f8d81d05712 100644 --- a/conda/environment-test-3.10.yml +++ b/conda/environment-test-3.10.yml @@ -41,7 +41,7 @@ dependencies: - pytest-html - pytest-xdist - pytest-timeout - - requests >=1.2.3 + - requests >=1.2.3,!=2.32.* # see https://github.com/bokeh/bokeh/issues/13910 - urllib3 <2 # see https://github.com/bokeh/bokeh/issues/13152 - scipy - pooch # required in scipy.datasets @@ -63,7 +63,7 @@ dependencies: # pip dependencies - pip - pip: - - mypy >=1.10 + - mypy >=1.11 # docs - bokeh_sampledata - pydata_sphinx_theme diff --git a/conda/environment-test-3.11.yml b/conda/environment-test-3.11.yml index 6bb48d1a970..63bed737011 100644 --- a/conda/environment-test-3.11.yml +++ b/conda/environment-test-3.11.yml @@ -41,7 +41,7 @@ dependencies: - pytest-html - pytest-xdist - pytest-timeout - - requests >=1.2.3 + - requests >=1.2.3,!=2.32.* # see https://github.com/bokeh/bokeh/issues/13910 - urllib3 <2 # see https://github.com/bokeh/bokeh/issues/13152 - scipy - pooch # required in scipy.datasets @@ -63,7 +63,7 @@ dependencies: # pip dependencies - pip - pip: - - mypy >=1.10 + - mypy >=1.11 # docs - bokeh_sampledata - pydata_sphinx_theme diff --git a/conda/environment-test-3.12.yml b/conda/environment-test-3.12.yml index 4f39a9e085a..3344031925a 100644 --- a/conda/environment-test-3.12.yml +++ b/conda/environment-test-3.12.yml @@ -41,7 +41,7 @@ dependencies: - pytest-html - pytest-xdist - pytest-timeout - - requests >=1.2.3 + - requests >=1.2.3,!=2.32.* # see https://github.com/bokeh/bokeh/issues/13910 - urllib3 <2 # see https://github.com/bokeh/bokeh/issues/13152 - scipy - pooch # required in scipy.datasets @@ -63,7 +63,7 @@ dependencies: # pip dependencies - pip - pip: - - mypy >=1.10 + - mypy >=1.11 # docs - bokeh_sampledata - pydata_sphinx_theme diff --git a/conda/environment-test-minimal-deps.yml b/conda/environment-test-minimal-deps.yml index 9c39a5f264a..def5a240a1b 100644 --- a/conda/environment-test-minimal-deps.yml +++ b/conda/environment-test-minimal-deps.yml @@ -40,7 +40,7 @@ dependencies: - pytest-html - pytest-xdist - pytest-timeout - - requests >=1.2.3 + - requests >=1.2.3,!=2.32.* # see https://github.com/bokeh/bokeh/issues/13910 - urllib3 <2 # see https://github.com/bokeh/bokeh/issues/13152 - selenium 4.2 @@ -53,7 +53,7 @@ dependencies: # pip dependencies - pip - pip: - - mypy >=1.10 + - mypy >=1.11 # docs # omit bokeh_sampledata - pydata_sphinx_theme diff --git a/docs/CHANGELOG b/docs/CHANGELOG index 698be2451b1..dc1562889ca 100644 --- a/docs/CHANGELOG +++ b/docs/CHANGELOG @@ -1,3 +1,85 @@ +2024-07-04 3.5: +-------------------- + * bugfixes: + - #8766 [component: bokehjs] box_select not working as active_drag for gridplot + - #12638 [component: bokehjs] Make Slider and Image available in BokehJS (JS only) + - #13515 [component: tests] CI unit-test on Python 3.9 fails systematically + - #13623 [component: bokehjs] [BUG] TextAreaInput resizable=False doesn't set resize: none + - #13720 [component: docs] Explanation is part of the code block in the latex_axis_labels_titles_labels example + - #13766 [component: bokehjs] [BUG] Dropdown doesnt update properly when .menu changed in 3.4 + - #13771 [component: bokehjs] GMap example not showing glyphs + - #13787 [component: bokehjs] [BUG] DataTable inside Dialog + - #13789 [component: docs] [BUG] JS errors on Bokeh docs page + - #13824 [component: docs] activate pydata-sphinx-theme version banner + - #13834 [component: build] bokehjs' build fails on Windows with no explanation + - #13844 [component: build] [BUG] cannot build environment with pixi due to `firefox =>96` constraint + - #13848 Can't build extensions on Windows + - #13852 [component: bokehjs] [BUG] and ignored in tooltips on Firefox + - #13878 [component: bokehjs] Bokeh.Plotting.figure.circle in bokehjs can't find `x` and `y` in a ColumnDataSource + - #13894 [component: bokehjs] `Tooltip` is initially attached to the DOM when it shouldn't be + - #13895 [component: bokehjs] Inner canvas layers need `overflow: hidden` after PR #13863 + - #13897 [component: docs] [BUG] Typing of layouts.gridplot expects wrong type if ncols given + - #13902 Release build fails after PR #13901 + - #13910 [component: tests] Tests/CI fail due to a regression in requests 2.32.0 + - #13912 [component: bokehjs] [BUG] Dialog and Datatable don't get along well + - #13919 [component: bokehjs] [BUG] Bokeh 3.5 no longer allow custom properties for HTMLAttr + - #13923 [component: bokehjs] `Tooltip.content` doesn't rebuild views on change + - #13948 [component: docs] Version banner warns about unknown version on latest docs after 3.4.2 release + - #13951 [component: bokehjs] [BUG] Image Glyph causing tons of unnecessary calls to set_data + - #13822 [component: docs] remove consecutive "the" and fix typos in touched files + - #13879 [component: docs] Fix broken example in user guide advanced bokehjs + - #13884 [component: docs] silence warning in `theme_glyphs.py` + - #13908 [component: bokehjs] Fix `CartesianFrame`'s position and generalize `rendering_target()` + - #13926 [component: docs] Remove `blob/main` and `tree/master` from references + - #13927 [component: docs] update path in readme for server examples + - #13939 [component: docs] correct version string to match dev name from switcher.json + - #13940 [component: bokehjs] Fix initialization of `AjaxDataSource` and add tests + + * features: + - #8289 [component: bokehjs] Permanent labels on Networkx graph + - #10439 [component: bokehjs] Activate wheel zoom by default with BokehJS + - #12185 [component: bokehjs] [FEATURE] Legend click events + - #12759 [component: bokehjs] [FEATURE] Support using CSS Variables in place of colors + - #13599 [FEATURE] Support formatters when using Template as HoverTool + - #13646 [FEATURE] Support BoxSelectTool-like range-setting for the RangeTool + - #13652 [FEATURE] `HTMLLabel` to support `stylesheets` and `css_classes` + - #13673 [FEATURE] Add Carbon Theme + - #13728 [FEATURE] Create option for wheel zoom tool to apply only to subplot nearest to the cursor position + - #13792 [component: server] [FEATURE] Allow bokeh server embed script to forward credentials + - #13861 [FEATURE] Support directory upload and clearing inputs from Python for `FileInput` widget + - #13935 [FEATURE] Allow extensions to register server side events + - #13936 [component: bokehjs] `AjaxDataSource.if_modified` not implemented + - #13467 [component: bokehjs] Add scale up boundary to datetimetickformatter + - #13810 Add support for `BoxAnnotation.inverted` + - #13890 Add support for server sent bokeh/model/UI events + - #13906 Add support for resize and drag handles to `BoxAnnotation` + + * tasks: + - #11745 [component: docs] Need examples of callbacks with BokehJS + - #13791 [component: docs] [DOC] Boxplot example: some whiskers and vbar are rendered slightly asymmetrically and boldly + - #13831 [component: bokehjs] Tap tool default mode should select+unselect, but not append + - #13856 [component: build] Move sampledata files to pip/conda installable package + - #13634 Drop support for Python 3.9 and modernize the codebase + - #13686 [component: examples] added metadata to spans and strips example + - #13731 [component: examples] Add metadata to the data_models.py in plotting example + - #13735 [component: bokehjs] Improve type safety of DOM elements on `core/dom` + - #13747 [component: build] Upgrade to TypeScript 5.4 + - #13770 [component: bokehjs] Remove legacy font measurement logic + - #13778 [component: bokehjs] Simplify eslint's configuration + - #13802 [component: build] Update bokehjs' dependencies and clear dependabot alerts + - #13839 [component: server] remove unnecessary code from ProtocolHandler.handle + - #13840 Upgrade to mypy 1.10.0 and ruff 0.4.2 + - #13847 Upgrade CI actions to most recent versions + - #13860 Remove unused code from `bokeh.models.plots` + - #13862 [component: bokehjs] Redesign `CartesianFrame` as an internal model/view + - #13863 [component: bokehjs] Generalize bbox handling in UI views (DOM and canvas) + - #13877 [component: bokehjs] Replace `PlotView.renderer_view()` with generic `ViewQuery` + - #13882 Remove old deprecations + - #13889 [component: docs] Repository cloning issues + - #13892 Unify definitions of enums in `bokeh.core.enums` + - #13925 [component: build] Add `http://` to devtools server + - #13961 Final preparations for 3.5.0 release + 2024-03-14 3.4: -------------------- * bugfixes: diff --git a/docs/bokeh/Makefile b/docs/bokeh/Makefile index 578e4ddd128..8733ce02757 100644 --- a/docs/bokeh/Makefile +++ b/docs/bokeh/Makefile @@ -18,7 +18,6 @@ clean: -rm -rf source/docs/gallery/* -rm -rf source/docs/examples/* -# XXX -W was removed due to issue https://github.com/bokeh/bokeh/issues/13156 html: @start=$$(date +%s) \ ; sphinx-build -W -b html -d $(BUILDDIR)/doctrees $(SPHINXOPTS) source $(BUILDDIR)/html \ diff --git a/docs/bokeh/docserver.py b/docs/bokeh/docserver.py index 4645e3c0e9e..2672a810982 100644 --- a/docs/bokeh/docserver.py +++ b/docs/bokeh/docserver.py @@ -14,7 +14,7 @@ python docserver.py or more commonly via executing ``make serve`` in this directory. It is possible -to combine this usage with other make targets in a single invovation, e.g. +to combine this usage with other make targets in a single invocation, e.g. make clean html serve @@ -34,7 +34,7 @@ from pathlib import Path import flask -from flask import redirect, url_for +from flask import redirect from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop from tornado.wsgi import WSGIContainer @@ -52,10 +52,10 @@ @app.route("/") def root(): - return redirect(url_for("en/latest/index.html")) + return redirect("en/latest/index.html") -@app.route("/switcher.json") +@app.route("/en/switcher.json") def switcher(): return flask.send_from_directory(SPHINX_TOP, "switcher.json") diff --git a/docs/bokeh/source/_images/icons/ToggleMode.png b/docs/bokeh/source/_images/icons/ToggleMode.png new file mode 100644 index 00000000000..50f4bb4093e Binary files /dev/null and b/docs/bokeh/source/_images/icons/ToggleMode.png differ diff --git a/docs/bokeh/source/_static/custom.css b/docs/bokeh/source/_static/custom.css index 45fe999452b..5616bdf79e0 100644 --- a/docs/bokeh/source/_static/custom.css +++ b/docs/bokeh/source/_static/custom.css @@ -105,24 +105,6 @@ img.image-border { .bd-footer .btn-primary { filter: brightness(90%); background-color: #C02942; border-color: #C02942; } .bd-footer .btn-primary:hover { filter: brightness(100%); background-color: #C02942; border-color: #C02942; } -/* CSS for our custom version warning banner */ -.version-alert a { - text-decoration: underline; - color: #ddd; -} - -#banner .version-alert { - background-color: #CF462A; - padding: 1em; - color: #fff; - font-weight: 600; font-size: 16px; -} - -.version-alert a { - text-decoration: underline; - color: #ddd; -} - /* CSS for our custom gallery */ div.bk-gallery { width: 100%; diff --git a/docs/bokeh/source/_static/custom.js b/docs/bokeh/source/_static/custom.js index ab1b638e748..7343014b019 100644 --- a/docs/bokeh/source/_static/custom.js +++ b/docs/bokeh/source/_static/custom.js @@ -6,21 +6,3 @@ window.addEventListener('DOMContentLoaded', function () { }); } }) - -// Display a version warning banner if necessary -$(document).ready(function () { - const randid = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); - $.getJSON('/switcher.json?v=' + randid , function (data) { - // old versions have a unified latest/x.y.z, things are split starting with 3.0 - if (BOKEH_CURRENT_VERSION != data[1].version) { - let msg - if (data.findIndex((elt) => elt.version == BOKEH_CURRENT_VERSION) < 0 ) { - msg = "DEVELOPMENT / PRE-RELEASE" - } else { - msg = "PREVIOUS RELEASE" - } - const content = $('
This page is documentation for a ' + msg + ' version. For the latest release, go to https://docs.bokeh.org/en/latest/
') - $('#banner').append(content); - } - }) -}) diff --git a/docs/bokeh/source/conf.py b/docs/bokeh/source/conf.py index 51a35aa82fe..9340bc00a19 100644 --- a/docs/bokeh/source/conf.py +++ b/docs/bokeh/source/conf.py @@ -7,6 +7,7 @@ # Standard library imports import os +import re from datetime import date from sphinx.util import logging @@ -24,7 +25,7 @@ project = "Bokeh" -version = settings.docs_version() or __version__ +release = version = settings.docs_version() or __version__ # -- Sphinx configuration ----------------------------------------------------- @@ -169,6 +170,16 @@ html_title = f"{project} {version} Documentation" +# avoid CORS error on local docs build for the switcher.json file +if "BOKEH_DOCS_VERSION" in os.environ: + json_url = "https://docs.bokeh.org/switcher.json" +else: + json_url = "../switcher.json" +if "dev" in version or "rc" in version: + version_match = "dev-" + re.match(r"\d\.\d+", version).group() +else: + version_match = version + # html_logo configured in navbar-logo.html html_theme_options = { @@ -189,10 +200,11 @@ "show_nav_level": 2, "show_toc_level": 1, "switcher": { - "json_url": "https://docs.bokeh.org/switcher.json", - "version_match": version, + "json_url": json_url, + "version_match": version_match, }, "use_edit_page_button": False, + "show_version_warning_banner": True, "header_links_before_dropdown": 8, } diff --git a/docs/bokeh/source/docs/dev_guide/creating_issues.rst b/docs/bokeh/source/docs/dev_guide/creating_issues.rst index 4f334274e1c..f142e64af4c 100644 --- a/docs/bokeh/source/docs/dev_guide/creating_issues.rst +++ b/docs/bokeh/source/docs/dev_guide/creating_issues.rst @@ -74,6 +74,15 @@ Software version info bokeh info + or alternatively use: + + .. code-block:: python + + from bokeh.util.info import print_info + print_info() + + in your scripts and/or MREs (minimal reproducible examples). + This provides you with a list of the versions of relevant software packages. Copy and paste this information into your bug report. diff --git a/docs/bokeh/source/docs/dev_guide/documentation.rst b/docs/bokeh/source/docs/dev_guide/documentation.rst index d14e21a404d..d9a6aa66562 100644 --- a/docs/bokeh/source/docs/dev_guide/documentation.rst +++ b/docs/bokeh/source/docs/dev_guide/documentation.rst @@ -130,13 +130,6 @@ root level of your *source checkout* directory to update ``bkdev``: using the environment file you originally used to create ``bkdev``. -Some of the examples in the documentation require additional sample data. Use -this command to automatically download and install the necessary package: - -.. code-block:: sh - - pip install bokeh_sampledata - 2. Set environment variable ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/bokeh/source/docs/dev_guide/setup.rst b/docs/bokeh/source/docs/dev_guide/setup.rst index 156e0a1fc05..a973d9b84b4 100644 --- a/docs/bokeh/source/docs/dev_guide/setup.rst +++ b/docs/bokeh/source/docs/dev_guide/setup.rst @@ -262,22 +262,9 @@ different local version instead, set the ``BOKEHJS_ACTION`` environment variable older**, you most likely also need to delete the ``bokehjs/build`` folder in your local environment before building and installing a fresh BokehJS. -.. _contributor_guide_setup_sample_data: - -7. Download sample data ------------------------ - -Several tests and examples require Bokeh's sample data to be available on your -hard drive. After :ref:`installing ` -Bokeh, use the following command to download and install the data: - -.. code-block:: sh - - pip install bokeh_sampledata - .. _contributor_guide_setup_environment_variables: -8. Set environment variables +7. Set environment variables ---------------------------- Bokeh uses :ref:`environment variables ` to control several @@ -480,7 +467,7 @@ is called. .. _contributor_guide_setup_test_setup: -9. Test your local setup +8. Test your local setup ------------------------ Run the following tests to check that everything is installed and set up @@ -500,14 +487,16 @@ You should see output similar to: .. code-block:: sh - Python version : 3.9.7 | packaged by conda-forge | (default, Sep 29 2021, 19:20:46) - IPython version : 7.20.0 - Tornado version : 6.1 - Bokeh version : 3.0.0dev1+20.g6c394d579 - BokehJS static path : /opt/anaconda/envs/test/lib/python3.9/site-packages/bokeh/server/static - node.js version : v16.12.0 - npm version : 7.24.2 - Operating system : Linux-5.11.0-40-generic-x86_64-with-glibc2.31 + Python version : 3.12.3 | packaged by conda-forge | (main, Apr 15 2024, 18:38:13) [GCC 12.3.0] + IPython version : 8.19.0 + Tornado version : 6.3.3 + NumPy version : 2.0.0 + Bokeh version : 3.5.1 + BokehJS static path : /opt/anaconda/envs/test/lib/python3.12/site-packages/bokeh/server/static + node.js version : v20.12.2 + npm version : 10.8.2 + jupyter_bokeh version : (not installed) + Operating system : Linux-5.15.0-86-generic-x86_64-with-glibc2.35 Run examples ~~~~~~~~~~~~ diff --git a/docs/bokeh/source/docs/gallery.json b/docs/bokeh/source/docs/gallery.json index 07d1b9950f7..b80bb23c0db 100644 --- a/docs/bokeh/source/docs/gallery.json +++ b/docs/bokeh/source/docs/gallery.json @@ -38,7 +38,7 @@ }, { "name": "transform_markers.py", - "alt": "Thumbnail link to the examples/basic/data/transform_markers.py example shows a scatter plot of the Palmer penuguins dataset, color- and marker-mapped by species. The x-axis is flipper length (mm) and the y-axis is body mass (g)." + "alt": "Thumbnail link to the examples/basic/data/transform_markers.py example shows a scatter plot of the Palmer penguins dataset, color- and marker-mapped by species. The x-axis is flipper length (mm) and the y-axis is body mass (g)." }, { "name": "transform_jitter.py", @@ -70,7 +70,7 @@ }, { "name": "intervals.py", - "alt": "Thumbnail link to the examples/basic/bars/intervals.py example shows an interval chart showing Olympic sprint time data as intervals using blue horizontal bars for times, with a bar for earch year on the y-axis." + "alt": "Thumbnail link to the examples/basic/bars/intervals.py example shows an interval chart showing Olympic sprint time data as intervals using blue horizontal bars for times, with a bar for each year on the y-axis." }, { "name": "mixed.py", @@ -78,7 +78,7 @@ }, { "name": "nested_colormapped.py", - "alt": "Thumbnail link to the examples/basic/bars/nested_colormapped.py example shows a grouped bar chart on a hierachical categorical axis of years for different categories of fruit. Each group has a silver, blue, and red bar corresponding to random values for years 2015, 2016 and 2017." + "alt": "Thumbnail link to the examples/basic/bars/nested_colormapped.py example shows a grouped bar chart on a hierarchical categorical axis of years for different categories of fruit. Each group has a silver, blue, and red bar corresponding to random values for years 2015, 2016 and 2017." }, { "name": "pandas_groupby_colormapped.py", @@ -98,7 +98,7 @@ }, { "name": "nested.py", - "alt": "Thumbnail link to the examples/basic/bars/nested.py example shows a grouped bar chart on a hierachical categorical axis of years for different categories of fruit. Each group has three blue bars corresponding to random values for years 2015, 2016 and 2017." + "alt": "Thumbnail link to the examples/basic/bars/nested.py example shows a grouped bar chart on a hierarchical categorical axis of years for different categories of fruit. Each group has three blue bars corresponding to random values for years 2015, 2016 and 2017." }, { "name": "colors.py", @@ -236,7 +236,7 @@ }, { "name": "image.py", - "alt": "Thumbnail link to the examples/topics/images/image.py example shows a plot with a colormapped two-dimensonal sine surface." + "alt": "Thumbnail link to the examples/topics/images/image.py example shows a plot with a colormapped two-dimensional sine surface." }, { "name": "image_origin_anchor.py", @@ -282,7 +282,7 @@ }, { "name": "slope_graph.py", - "alt": "Thumbnail link to the examples/topics/categorical/slope_graph.py example shows a slope graph of the CO2 emmisions of selected countries in the years 2000 and 2010." + "alt": "Thumbnail link to the examples/topics/categorical/slope_graph.py example shows a slope graph of the CO2 emissions of selected countries in the years 2000 and 2010." }, { "name": "heatmap_unemployment.py", @@ -364,7 +364,7 @@ }, { "name": "burtin.py", - "alt": "Thumbnail link to the examples/topics/pie/burtin.py example shows a reproduction of Burtin's hitorical antibiotics plot." + "alt": "Thumbnail link to the examples/topics/pie/burtin.py example shows a reproduction of Burtin's historical antibiotics plot." } ], "topics/stats": [ @@ -406,7 +406,7 @@ "interaction/linking": [ { "name": "linked_brushing.py", - "alt": "Thumbnail link to the examples/interaction/linking/linked_brushing.py example shows a two different scatter plots of the Palmer penuguin dataset with linked selections." + "alt": "Thumbnail link to the examples/interaction/linking/linked_brushing.py example shows a two different scatter plots of the Palmer penguin dataset with linked selections." }, { "name": "linked_crosshair.py", @@ -456,7 +456,7 @@ }, { "name": "date_picker.py", - "alt": "Thumbnail link to the examples/interaction/widgets/date_picker.py example shows a simple example of a widget for seleting dates and date ranges." + "alt": "Thumbnail link to the examples/interaction/widgets/date_picker.py example shows a simple example of a widget for selecting dates and date ranges." }, { "name": "dropdown.py", @@ -476,55 +476,55 @@ "name": "movies", "url": "https://demo.bokeh.org/movies", "alt": "Thumbnail featuring an interactive query tool for a set of IMDB data. Tool features a default graph of the Tomato Meter (x-axis) against the number of reviews (y-axis). Graph can be refined using multiple variables including cast names, director name, number of Oscar wins, year released, end year released, genre, dollar at the box office, and more.", - "desc": "An interactive query tool for a set of IMDB data.(source code)" + "desc": "An interactive query tool for a set of IMDB data.(source code)" }, { "name": "selection_histogram", "url": "https://demo.bokeh.org/selection_histogram", "alt": "Thumbnail featuring axis histograms for selected and non-selected points in a scatter plot. Axes unlabeled.", - "desc": "Shows axis histograms for selected and non-selected points in a scatter plot. (source code)" + "desc": "Shows axis histograms for selected and non-selected points in a scatter plot. (source code)" }, { "name": "weather", "url": "https://demo.bokeh.org/weather", "alt": "Thumbnail featuring interactive weather statistics for three cities. Features drop down for city and distribution. X-axis features date, the y-axis features temperature.", - "desc": "Interactive weather statistics for three cities. (source code)" + "desc": "Interactive weather statistics for three cities. (source code)" }, { "name": "sliders", "url": "https://demo.bokeh.org/sliders", "alt": "Thumbnail of plotted trigonometric function with sliders for offset, amplitude, phase, and frequency.", - "desc": "A basic demo that has sliders for controlling a plotted trigonometric function. (source code)" + "desc": "A basic demo that has sliders for controlling a plotted trigonometric function. (source code)" }, { "name": "crossfilter", "url": "https://demo.bokeh.org/crossfilter", "alt": "Thumbnail of example scatter plot with drop downs for x-axis, y-axis, color, and size. Default graph features mpg on the x-axis, hp on the y-axis, and shows a downward exponential trend.", - "desc": "Explore the autompg data set by selecting and highlighting different dimensions. (source code)" + "desc": "Explore the autompg data set by selecting and highlighting different dimensions. (source code)" }, { "name": "gapminder", "url": "https://demo.bokeh.org/gapminder", "alt": "Thumbnail of a page featuring a reproduction of the Gapminder demo and containing an embedded TED talk video added using a custom page template. Gapminder demo shows children per woman (x-axis), life expectancy at birth in years (y-axis), by nation, over the years (slider), and a play button that allows data to play across slider range of 1964 - 2012.", - "desc": "A reproduction of the famous Gapminder demo, with embedded video added using a custom page template. (source code)" + "desc": "A reproduction of the famous Gapminder demo, with embedded video added using a custom page template. (source code)" }, { "name": "stocks", "url": "https://demo.bokeh.org/stocks", "alt": "Thumbnail image of linked plots, summary statistics, and correlations for market data. Contains two drop down selections for different investment options. Left features a scatterplot of option one's returns vs option two's returns. Below features a line plot of each individual option. Right shows basic statistics of each option and each option's returns.", - "desc": "Linked plots, summary statistics, and correlations for market data. (source code)" + "desc": "Linked plots, summary statistics, and correlations for market data. (source code)" }, { "name": "surface3d", "url": "https://demo.bokeh.org/surface3d", "alt": "Thumbnail for surface3d example. Axes are rotated slightly and shown with perspective. A 3D surface is plotted, and colored with a heat map corresponding to z axis value.", - "desc": "An updating 3d plot that demonstrates using Bokeh custom extensions to wrap third-party JavaScript libraries. (source code)" + "desc": "An updating 3d plot that demonstrates using Bokeh custom extensions to wrap third-party JavaScript libraries. (source code)" }, { "name": "export_csv", "url": "https://demo.bokeh.org/export_csv", "alt": "Thumbnail for export_csv example. Image shows an interface for filtering data with a slider widget, and writing the results by clicking a button.", - "desc": "Link sliders filter a data table that can be saved as a CSV. (source code)" + "desc": "Link sliders filter a data table that can be saved as a CSV. (source code)" } ] } diff --git a/docs/bokeh/source/docs/gallery.rst b/docs/bokeh/source/docs/gallery.rst index 35899569b86..06b8b7147ab 100644 --- a/docs/bokeh/source/docs/gallery.rst +++ b/docs/bokeh/source/docs/gallery.rst @@ -53,7 +53,7 @@ code and interact with a live plot. .. tab-item:: Interaction - Bokeh has many interative tools and widgets. Only a subset have + Bokeh has many interactive tools and widgets. Only a subset have thumbnails here. For more information see the :ref:`ug_interaction` chapter of the users guide. diff --git a/docs/bokeh/source/docs/reference/models/comparisons.rst b/docs/bokeh/source/docs/reference/models/comparisons.rst new file mode 100644 index 00000000000..31dbf0e63e1 --- /dev/null +++ b/docs/bokeh/source/docs/reference/models/comparisons.rst @@ -0,0 +1,8 @@ +.. _bokeh.models.comparisons: + +comparisons +----------- + +.. automodule:: bokeh.models.comparisons + :members: + :undoc-members: diff --git a/docs/bokeh/source/docs/releases/3.4.1.rst b/docs/bokeh/source/docs/releases/3.4.1.rst new file mode 100644 index 00000000000..1d22c767a88 --- /dev/null +++ b/docs/bokeh/source/docs/releases/3.4.1.rst @@ -0,0 +1,21 @@ +.. _release-3-4-1: + +3.4.1 +===== + +Bokeh version ``3.4.1`` (April 2024) is a patch release that fixes a number of +minor bugs/regressions and docs issues. + +Changes +------- + +* Fixed configuration of secondary glyphs in ``GraphRendererView`` (:bokeh-pull:`13808`) +* Allowed to update the order of ``LayoutDOM.children`` (:bokeh-pull:`13807`) +* Allowed to update ``InputWidget.{title,description}`` (:bokeh-pull:`13805`) +* Allowed ``DataRange1d`` to respect ``min_interval`` and ``max_interval`` (:bokeh-pull:`13819`) +* Fixed WebGL scaling of anti-aliasing by pixel ratio (:bokeh-pull:`13783`) +* Enabled RUF001 to lint confusable characters (:bokeh-pull:`13788`) +* Removed references to Twitter from documentation (:bokeh-pull:`13775`) +* Enabled documentation build on Windows (:bokeh-pull:`13776`) +* Various fixes to documentation and docstrings (:bokeh-pull:`13793`, :bokeh-pull:`13798`, + :bokeh-pull:`13781`, :bokeh-pull:`13818`, :bokeh-pull:`13820`, :bokeh-pull:`13821`) diff --git a/docs/bokeh/source/docs/releases/3.4.2.rst b/docs/bokeh/source/docs/releases/3.4.2.rst new file mode 100644 index 00000000000..06929034287 --- /dev/null +++ b/docs/bokeh/source/docs/releases/3.4.2.rst @@ -0,0 +1,15 @@ +.. _release-3-4-2: + +3.4.2 +===== + +Bokeh version ``3.4.2`` (June 2024) is a patch release that fixes a number of +minor bugs/regressions and docs issues. + +Changes +------- + +* Added `"toggle"` select mode and made it the default in `TapTool` (:bokeh-pull:`13808`) +* Made projection computations in glyphs aware of inherited properties (:bokeh-pull:`13832`) +* Improvements to documentation and its infrastructure (:bokeh-pull:`13823`, :bokeh-pull:`13876`, :bokeh-pull:`13901` and :bokeh-pull:`13903`) +* Robustified bokeh/bokehjs tests and their infrastructure (:bokeh-pull:`13843` and :bokeh-pull:`13851`) diff --git a/docs/bokeh/source/docs/releases/3.4.3.rst b/docs/bokeh/source/docs/releases/3.4.3.rst new file mode 100644 index 00000000000..4226f59ecd2 --- /dev/null +++ b/docs/bokeh/source/docs/releases/3.4.3.rst @@ -0,0 +1,19 @@ +.. _release-3-4-3: + +3.4.3 +===== + +Bokeh version ``3.4.3`` (July 2024) is a patch release that fixes a number of +minor bugs/regressions and docs issues. + +Changes +------- + +* Fixed performance regression related to inherited image data in ``Image``-like glyphs (:bokeh-pull:`13952`) +* Fixed spurious warning about unknown bokeh version in the documentation (:bokeh-pull:`13949`) +* Fixed handling of certain classes of objects in ``HasProps`` internals (:bokeh-pull:`13970`) +* Restored support for ``BOKEH_MINIFIED=no`` in resources (:bokeh-pull:`13974`) +* Updated the location of ``*.d.ts`` files in ``package.json`` (:bokeh-pull:`13975`) +* Improved type hints of ``gridplot()`` (:bokeh-pull:`13914`) +* Fixed merging of plots in grid plots when only one plot is involved (:bokeh-pull:`13978`) +* Fixed indexing of categories in ``CategoricalSlider`` widget (:bokeh-pull:`13966`) diff --git a/docs/bokeh/source/docs/releases/3.5.0.rst b/docs/bokeh/source/docs/releases/3.5.0.rst index b26f58bf5c2..6c383322470 100644 --- a/docs/bokeh/source/docs/releases/3.5.0.rst +++ b/docs/bokeh/source/docs/releases/3.5.0.rst @@ -3,9 +3,18 @@ 3.5.0 ===== -Bokeh version ``3.5.0`` (May 2024) is a minor milestone of Bokeh project. +Bokeh version ``3.5.0`` (July 2024) is a minor milestone of Bokeh project. * Added support for ``BoxAnnotation.inverted`` property (:bokeh-pull:`13810`) * Added support for key modifiers to ``WheelZoomTool`` and ``WheelPanTool`` (:bokeh-pull:`13815`) * Allowed auto-activation of tools using wheel/scroll events when modifiers are set (:bokeh-pull:`13815`) * Added support for CSS variable based styling to plot renderers (:bokeh-pull:`13828`) +* Added support for outline shapes to text-like glyphs (``Text``, ``TeX`` and ``MathML``) (:bokeh-pull:`13620`) +* Added support for range setting gesture to ``RangeTool`` and allowed a choice of gesture (pan, tap or none) (:bokeh-pull:`13855`) +* Added support for server-sent events, in particular for ``ClearInput`` event on input widgets (:bokeh-pull:`13890`) +* Added support for ``Legend`` item click events and ``Legend.{on_click,js_on_click}()`` APIs (:bokeh-pull:`13922`) +* Added support for wheel zoom of renderers under the cursor when using sub-coordinates (:bokeh-pull:`13826`) +* Added support for directory upload for ``FileInput`` (:bokeh-pull:`13873`) +* Added support for formatters to ``ValueRef`` model and improved tooltip templating in ``HoverTool`` (:bokeh-pull:`13650`) +* Added support for interaction handles (move, resize) to ``BoxAnnotation`` (:bokeh-pull:`13906`) +* Dropped support for Python 3.9 and modernized the codebase (:bokeh-pull:`13634`) diff --git a/docs/bokeh/source/docs/releases/3.6.0.rst b/docs/bokeh/source/docs/releases/3.6.0.rst new file mode 100644 index 00000000000..9b576dc0cb7 --- /dev/null +++ b/docs/bokeh/source/docs/releases/3.6.0.rst @@ -0,0 +1,8 @@ +.. _release-3-6-0: + +3.6.0 +===== + +Bokeh version ``3.6.0`` (??? 2024) is a minor milestone of Bokeh project. + +* Improved streaming corner cases and added NumPy to ``bokeh info`` (:bokeh-pull:`14007`) diff --git a/docs/bokeh/source/docs/user_guide/advanced/bokehjs.rst b/docs/bokeh/source/docs/user_guide/advanced/bokehjs.rst index c50b05a60c6..657a99c3133 100644 --- a/docs/bokeh/source/docs/user_guide/advanced/bokehjs.rst +++ b/docs/bokeh/source/docs/user_guide/advanced/bokehjs.rst @@ -278,17 +278,6 @@ and hover policy. Here is an example of a ``bar`` chart and the plot it generate plt.show(plt.gridplot([[p1, p2], [p3, p4]], {width: 350, height: 350})); -.. _ug_advanced_bokehjs_issues: - -Known Issues ------------- - -* :bokeh-issue:`11016` figure name passed to `renderer.glyph.name` but not `renderer.name` -* :bokeh-issue:`11034` Palettes not accessible by name for `ColorMapper` objects in BokehJS -* :bokeh-issue:`11035` `Bokeh.Widgets.Div()` missing `tools`, required by `Bokeh.Plotting.gridplot()` -* :bokeh-issue:`11036` Making axis range padding persistent requires changing `._initial_range_padding` as well -* :bokeh-issue:`11037` Using `sizing_mode` in gridplot layouts requires explicit assignment -* :bokeh-issue:`11038` Calling `figure({title:"some title"})` replaces Title object with string, prevents subsequent updates to title text Minimal example --------------- diff --git a/docs/bokeh/source/docs/user_guide/styling/plots.rst b/docs/bokeh/source/docs/user_guide/styling/plots.rst index 8a665549609..6f5820f99c8 100644 --- a/docs/bokeh/source/docs/user_guide/styling/plots.rst +++ b/docs/bokeh/source/docs/user_guide/styling/plots.rst @@ -364,6 +364,18 @@ As a shortcut, you can also supply the list of ticks directly to an axis' .. bokeh-plot:: __REPO__/examples/styling/plots/fixed_ticker.py :source-position: above +``CustomJSTicker`` +'''''''''''''''''' + +To fully customize the location of axis ticks, use the |CustomJSTicker| in +combination with a JavaScript snippet as its ``major_code`` and ``minor_code`` +properties. + +These code snippets should return lists of tick locations: + +.. bokeh-plot:: __REPO__/examples/styling/plots/custom_js_ticker.py + :source-position: above + Tick lines ~~~~~~~~~~ @@ -843,6 +855,7 @@ You can see a complete example with output in the section .. |BasicTickFormatter| replace:: :class:`~bokeh.models.formatters.BasicTickFormatter` .. |CategoricalTickFormatter| replace:: :class:`~bokeh.models.formatters.CategoricalTickFormatter` .. |DatetimeTickFormatter| replace:: :class:`~bokeh.models.formatters.DatetimeTickFormatter` +.. |CustomJSTicker| replace:: :class:`~bokeh.models.tickers.CustomJSTicker` .. |CustomJSTickFormatter| replace:: :class:`~bokeh.models.formatters.CustomJSTickFormatter` .. |LogTickFormatter| replace:: :class:`~bokeh.models.formatters.LogTickFormatter` .. |NumeralTickFormatter| replace:: :class:`~bokeh.models.formatters.NumeralTickFormatter` diff --git a/docs/bokeh/switcher.json b/docs/bokeh/switcher.json index 20aa105f632..55d67d40921 100644 --- a/docs/bokeh/switcher.json +++ b/docs/bokeh/switcher.json @@ -1,15 +1,17 @@ [ { "name": "latest", - "url": "https://docs.bokeh.org/en/latest/" + "version": "3.5.0", + "url": "https://docs.bokeh.org/en/latest/", + "preferred": true }, { - "version": "3.4.0", - "url": "https://docs.bokeh.org/en/3.4.0/" + "version": "3.4.2", + "url": "https://docs.bokeh.org/en/3.4.2/" }, { - "version": "3.3.0", - "url": "https://docs.bokeh.org/en/3.3.0/" + "version": "3.3.4", + "url": "https://docs.bokeh.org/en/3.3.4/" }, { "version": "3.2.2", @@ -24,7 +26,8 @@ "url": "https://docs.bokeh.org/en/2.4.3/" }, { - "name": "dev (3.5)", - "url": "https://docs.bokeh.org/en/dev-3.5/" + "name": "dev (3.6)", + "version": "dev-3.6", + "url": "https://docs.bokeh.org/en/dev-3.6/" } ] diff --git a/examples/interaction/js_callbacks/doc_js_events.py b/examples/interaction/js_callbacks/doc_js_events.py index 61f6a92d5e2..770c31e4c36 100644 --- a/examples/interaction/js_callbacks/doc_js_events.py +++ b/examples/interaction/js_callbacks/doc_js_events.py @@ -2,7 +2,8 @@ from bokeh.events import Event from bokeh.io import curdoc -from bokeh.models import Button, CustomJS +from bokeh.layouts import column +from bokeh.models import Button, CustomJS, FileInput def py_ready(event: Event): @@ -27,16 +28,19 @@ def py_connection_lost(event: Event): curdoc().on_event("connection_lost", py_connection_lost) curdoc().js_on_event("connection_lost", js_connection_lost) +file_input = FileInput() + def py_clicked(event: Event): print("CLICKED!") + file_input.clear() js_clicked = CustomJS(code=""" const html = "
CLICKED!
" document.body.insertAdjacentHTML("beforeend", html) """) -button = Button(label="Click me") -button.on_event("button_click", py_clicked) +button = Button(label="Click me to clear the selected file") +button.on_click(py_clicked) # or button.on_event("button_click", py_clicked) button.js_on_event("button_click", js_clicked) -curdoc().add_root(button) +curdoc().add_root(column(file_input, button)) diff --git a/examples/interaction/legends/legend_click.py b/examples/interaction/legends/legend_click.py new file mode 100644 index 00000000000..b724041d81a --- /dev/null +++ b/examples/interaction/legends/legend_click.py @@ -0,0 +1,27 @@ +from bokeh.core.enums import MarkerType +from bokeh.models import CustomJS +from bokeh.plotting import figure, show + +p = figure(width=300, height=300, title="Click on legend entries to change\nmarkers of the corresponding glyphs") + +p.scatter(x=[0, 1, 2], y=[1, 2, 3], size=[10, 20, 30], marker="circle", color="red", fill_alpha=0.5, legend_label="Red item") +p.scatter(x=[0, 1, 2], y=[2, 3, 4], size=[10, 20, 30], marker="circle", color="green", fill_alpha=0.5, legend_label="Green item") +p.scatter(x=[0, 1, 2], y=[3, 4, 5], size=[10, 20, 30], marker="circle", color="blue", fill_alpha=0.5, legend_label="Blue item") + +callback = CustomJS( + args=dict(markers=list(MarkerType)), + code=""" +export default ({markers}, {item}) => { + for (const renderer of item.renderers) { + const {value: marker} = renderer.glyph.marker + const i = markers.indexOf(marker) + const j = (i + 1) % markers.length + renderer.glyph.marker = {value: markers[j]} + } +} + """, +) +p.legend.js_on_click(callback) +p.legend.location = "top_left" + +show(p) diff --git a/examples/interaction/tools/box_select_handles.py b/examples/interaction/tools/box_select_handles.py new file mode 100644 index 00000000000..8201414f642 --- /dev/null +++ b/examples/interaction/tools/box_select_handles.py @@ -0,0 +1,33 @@ +import numpy as np + +from bokeh.layouts import row +from bokeh.models import BoxSelectTool +from bokeh.palettes import Spectral11 +from bokeh.plotting import figure, show + +N = 4000 +x = np.random.random(size=N)*100 +y = np.random.random(size=N)*100 +radii = np.random.random(size=N)*1.5 +colors = np.random.choice(Spectral11, size=N) + +TOOLS="pan,box_select,wheel_zoom,zoom_in,zoom_out,undo,redo,reset,save" + +p0 = figure(tools=TOOLS, active_drag="box_select", width=400, height=400, title="Box selection overlay with handles") +p0.circle(x, y, radius=radii, fill_color=colors, fill_alpha=0.6, line_color=None) + +box_select = p0.select(BoxSelectTool) +box_select.persistent = True +box_select.overlay.hover_fill_color = "yellow" +box_select.overlay.use_handles = True +box_select.overlay.handles.all.hover_fill_color = "red" +box_select.overlay.handles.all.hover_fill_alpha = 0.7 + +p1 = figure(tools=TOOLS, active_drag="box_select", width=400, height=400, title="Box selection overlay without handles") +p1.circle(x, y, radius=radii, fill_color=colors, fill_alpha=0.6, line_color=None) + +box_select = p1.select(BoxSelectTool) +box_select.persistent = True +box_select.overlay.hover_fill_color = "green" + +show(row([p0, p1])) diff --git a/examples/interaction/tools/hover_tooltip_advanced.py b/examples/interaction/tools/hover_tooltip_advanced.py new file mode 100644 index 00000000000..8032c247a76 --- /dev/null +++ b/examples/interaction/tools/hover_tooltip_advanced.py @@ -0,0 +1,62 @@ +import numpy as np + +from bokeh.models import HoverTool, Styles +from bokeh.models.dom import HTML, Index, ValueRef +from bokeh.palettes import Spectral11 +from bokeh.plotting import figure, show + +N = 1000 +x = np.random.random(size=N) * 100 +y = np.random.random(size=N) * 100 +radii = np.random.random(size=N) * 1.5 +colors = np.random.choice(Spectral11, size=N) + +p = figure( + title="Demonstrates hover tool with advanced and regular tooltip formatting side-by-side", + tools="pan,wheel_zoom,box_select,crosshair", +) + +p.circle(x, y, radius=radii, fill_color=colors, fill_alpha=0.6, line_color=None) + +x_ref = ValueRef(style=dict(background_color="cyan"), field="x") +y_ref = ValueRef(style=dict(background_color="lime"), field="y") + +def span(name: str, color: str): + return f"""{name}""" + +grid = HTML( + style=Styles( + display="grid", + grid_template_columns="auto auto", + column_gap="10px", + ), + html=[ + """
index:
#""", Index(), "
", + f"
({span('x', 'cyan')}, {span('y', 'lime')}):
(", x_ref, ", ", y_ref, ")
", + "
radius:
", ValueRef(field="radius", format="%.2f", formatter="printf"), + ], +) + +hover_advanced = HoverTool( + description="Advanced hover", + tooltips=grid, + attachment="left", +) +p.add_tools(hover_advanced) + +hover_regular = HoverTool( + description="Regular hover", + tooltips=[ + ("index", "$index"), + ("(x,y)", "(@x, @y)"), + ("radius", "@radius{%.2f}"), + ], + formatters={ + "@radius": "printf", + }, + attachment="right", + +) +p.add_tools(hover_regular) + +show(p) diff --git a/examples/interaction/tools/range_tool.py b/examples/interaction/tools/range_tool.py index e2278fdf6bd..b9ab490365b 100644 --- a/examples/interaction/tools/range_tool.py +++ b/examples/interaction/tools/range_tool.py @@ -31,7 +31,7 @@ x_axis_type="datetime", y_axis_type=None, tools="", toolbar_location=None, background_fill_color="#efefef") -range_tool = RangeTool(x_range=p.x_range) +range_tool = RangeTool(x_range=p.x_range, start_gesture="pan") range_tool.overlay.fill_color = "navy" range_tool.overlay.fill_alpha = 0.2 diff --git a/examples/interaction/tools/subcoordinates_zoom.py b/examples/interaction/tools/subcoordinates_zoom.py index f55b06d07a1..9a6cd34cfff 100644 --- a/examples/interaction/tools/subcoordinates_zoom.py +++ b/examples/interaction/tools/subcoordinates_zoom.py @@ -3,8 +3,9 @@ from bokeh.core.properties import field from bokeh.io import show from bokeh.layouts import column, row -from bokeh.models import (ColumnDataSource, CustomJS, Div, FactorRange, HoverTool, - Range1d, Switch, WheelZoomTool, ZoomInTool, ZoomOutTool) +from bokeh.models import (ColumnDataSource, CustomJS, Div, FactorRange, + GroupByModels, HoverTool, Range1d, Select, + Switch, WheelZoomTool, ZoomInTool, ZoomOutTool) from bokeh.palettes import Category10 from bokeh.plotting import figure @@ -25,7 +26,7 @@ x_range = Range1d(start=time.min(), end=time.max()) y_range = FactorRange(factors=channels) -p = figure(x_range=x_range, y_range=y_range, lod_threshold=None, tools="pan,reset") +p = figure(x_range=x_range, y_range=y_range, lod_threshold=None, tools="pan,reset,xcrosshair") source = ColumnDataSource(data=dict(time=time)) renderers = [] @@ -43,16 +44,32 @@ renderers.append(line) level = 1 +hit_test = False +only_hit = True -ywheel_zoom = WheelZoomTool(renderers=renderers, level=level, dimensions="height") -xwheel_zoom = WheelZoomTool(renderers=renderers, level=level, dimensions="width") +group_by = GroupByModels(groups=[renderers[0::2], renderers[1::2]]) +behavior = "only_hit" if only_hit else group_by + +ywheel_zoom = WheelZoomTool(renderers=renderers, level=level, hit_test=hit_test, hit_test_mode="hline", hit_test_behavior=behavior, dimensions="height") +xwheel_zoom = WheelZoomTool(renderers=renderers, level=level, hit_test=hit_test, hit_test_mode="hline", hit_test_behavior=behavior, dimensions="width") zoom_in = ZoomInTool(renderers=renderers, level=level, dimensions="height") zoom_out = ZoomOutTool(renderers=renderers, level=level, dimensions="height") p.add_tools(ywheel_zoom, xwheel_zoom, zoom_in, zoom_out, hover) p.toolbar.active_scroll = ywheel_zoom -on_change = CustomJS( +level_switch = Switch(active=level == 1) +hit_test_switch = Switch(active=hit_test) +behavior_select = Select( + disabled=not hit_test_switch.active, + value=behavior, + options=[ + ("only_hit", "Only hit renderers"), + (group_by, "Even/Odd groups of renderers"), + ], +) + +level_switch.js_on_change("active", CustomJS( args=dict(tools=[ywheel_zoom, zoom_in, zoom_out]), code=""" export default ({tools}, obj) => { @@ -61,10 +78,30 @@ tool.level = level } } -""") +""")) + +hit_test_switch.js_on_change("active", CustomJS( + args=dict(tool=ywheel_zoom, select=behavior_select), + code=""" +export default ({tool, select}, obj) => { + tool.hit_test = obj.active + select.disabled = !obj.active +} +""")) + +behavior_select.js_on_change("value", CustomJS( + args=dict(tool=ywheel_zoom), + code=""" +export default ({tool}, obj) => { + tool.hit_test_behavior = obj.value +} +""")) -label = Div(text="Zoom sub-coordinates:") -widget = Switch(active=level == 1) -widget.js_on_change("active", on_change) +layout = column( + row(Div(text="Enable zooming of sub-coordinates:"), level_switch), + row(Div(text="Enable hit-testing based zooming:"), hit_test_switch), + row(Div(text="Hit test behavior:"), behavior_select), + p, +) -show(column(row(label, widget), p)) +show(layout) diff --git a/examples/interaction/widgets/data_table_column_sort.py b/examples/interaction/widgets/data_table_column_sort.py new file mode 100644 index 00000000000..f4bf62ed25a --- /dev/null +++ b/examples/interaction/widgets/data_table_column_sort.py @@ -0,0 +1,17 @@ +from bokeh.io import show +from bokeh.models import ColumnDataSource, CustomJSCompare, DataTable, TableColumn + +source = ColumnDataSource(data=dict(foo=["AB 1", "AB 10", "AB 2"])) + +# sort values like "AB ###" by the numeric portion +sorter = CustomJSCompare(code=""" + const xn = Number(x.split(" ")[1]) + const yn = Number(y.split(" ")[1]) + return (xn == yn) ? 0 : (xn < yn) ? -1 : 1 +""") + +columns = [TableColumn(field="foo", title="Foo", sorter=sorter)] + +table = DataTable(source=source, columns=columns, width=400, height=280) + +show(table) diff --git a/examples/server/api/iframe_embed/README.md b/examples/server/api/iframe_embed/README.md new file mode 100644 index 00000000000..14a1e0aec2b --- /dev/null +++ b/examples/server/api/iframe_embed/README.md @@ -0,0 +1,9 @@ +This example demonstrates embedding in an iframe using srcdoc under restrictive CSP. + +To view the example, run: + + python main.py + +in this directory, and navigate to: + + http://localhost:5000 diff --git a/examples/server/api/iframe_embed/bokeh_server.py b/examples/server/api/iframe_embed/bokeh_server.py new file mode 100644 index 00000000000..e3452205f75 --- /dev/null +++ b/examples/server/api/iframe_embed/bokeh_server.py @@ -0,0 +1,14 @@ +import numpy as np + +from bokeh.io import curdoc +from bokeh.plotting import figure + +N = 4000 +x = np.random.random(size=N) * 100 +y = np.random.random(size=N) * 100 +radii = np.random.random(size=N) * 1.5 + +p = figure(tools="", toolbar_location=None) +p.circle(x, y, radius=radii, fill_alpha=0.6) + +curdoc().add_root(p) diff --git a/examples/server/api/iframe_embed/main.py b/examples/server/api/iframe_embed/main.py new file mode 100644 index 00000000000..ec835463179 --- /dev/null +++ b/examples/server/api/iframe_embed/main.py @@ -0,0 +1,67 @@ +'''This example demonstrates embedding in an iframe using srcdoc under restrictive CSP. + +To view the example, run: + + python main.py + +in this directory, and navigate to: + + http://localhost:5000 + +''' +import atexit +import subprocess + +from flask import Flask, render_template_string + +from bokeh.client import pull_session +from bokeh.embed.server import server_html_page_for_session +from bokeh.resources import INLINE + +app_html = """ + + + +

+ This is an example of cross-origin embedding using an iframe under restrictive Content Security Policy (CSP). + Under strict CSP iframe embedding with `src` does not work: +

+ +

But it is still possible to embed with `srcdoc` attribute and using `data-absolute-url`:

+ + + + +""" + +app = Flask(__name__) + +bokeh_process = subprocess.Popen( + ['python', '-m', 'bokeh', 'serve', '--port=5151', '--allow-websocket-origin=localhost:5000', '--allow-websocket-origin=127.0.0.1:5000', 'bokeh_server.py'], + stdout=subprocess.PIPE, +) + +@atexit.register +def kill_server(): + bokeh_process.kill() + + +@app.after_request +def add_security_headers(resp): + resp.headers["Content-Security-Policy"] = "frame-ancestors 'none'" + return resp + +@app.route('/') +def home(): + app_url = "http://localhost:5151/bokeh_server" + with pull_session(url=app_url) as session: + code = server_html_page_for_session(session=session, resources=INLINE, title='test') + return render_template_string(app_html, code=code, app_url=app_url) + + +if __name__ == '__main__': + app.run() diff --git a/examples/server/app/README.md b/examples/server/app/README.md index e2c7225adb3..4dac511380e 100644 --- a/examples/server/app/README.md +++ b/examples/server/app/README.md @@ -16,85 +16,85 @@ The demos container here are: clustering - + Demonstrates different scikit-learn clustering algorithms on a few different data sets. contour_animated - + Using a Python callback to animate a contour plot. crossfilter - + Explore the "autompg" data set by selecting and highlighting different dimensions dash - + Demonstrates use of custom Bootstrap template with a Bokeh application export_csv - + Query a data table and save the results to a CSV file fourier_animated - + A continuously updating demonstration of Fourier synthesis using periodic callbacks gapminder - + A reproduction of the famous Gapminder demo, with embedded video added using a custom page template movies - + An interactive query tool for a set of IMDB data ohlc - + A simulated streaming OHLC chart with MACD indicator and selectable moving averages using periodic callbacks and the efficient streaming API selection_histogram - + Shows axis histograms for selected and unselected points in a scatter plot sliders - + A basic demo that has sliders for controlling a plotted trigonometric function spectrogram - + A live audio spectrogram that connects NumPy to interactive web visualizations stocks - + Linked plots, summary statistics, and correlations for market data surface3d - + An updating 3d plot that demonstrates using Bokeh custom extensions to wrap third-party JavaScript libraries diff --git a/examples/server/app/clustering/README.md b/examples/server/app/clustering/README.md index 6ef146f84ad..76d2f6ef149 100644 --- a/examples/server/app/clustering/README.md +++ b/examples/server/app/clustering/README.md @@ -19,7 +19,7 @@ To install using pip, execute the command: ## Running To view the app directly from a Bokeh server, navigate to the parent directory -[`examples/app`](https://github.com/bokeh/bokeh/tree/master/examples/app), +[`examples/server/app`](https://github.com/bokeh/bokeh/blob/-/examples/server/app), and execute the command: bokeh serve --show clustering diff --git a/examples/server/app/crossfilter/README.md b/examples/server/app/crossfilter/README.md index 94183146598..61905a28d2e 100644 --- a/examples/server/app/crossfilter/README.md +++ b/examples/server/app/crossfilter/README.md @@ -20,7 +20,7 @@ To install using pip, execute the command: ## Running To view the app directly from a Bokeh server, navigate to the parent directory -[`examples/app`](https://github.com/bokeh/bokeh/tree/master/examples/app), +[`examples/server/app`](https://github.com/bokeh/bokeh/blob/-/examples/server/app), and execute the command: bokeh serve --show crossfilter diff --git a/examples/server/app/dash/README.md b/examples/server/app/dash/README.md index a873ba1f707..b6cfcfe7e3b 100644 --- a/examples/server/app/dash/README.md +++ b/examples/server/app/dash/README.md @@ -11,7 +11,7 @@ No additional packages or steps are required to run this example. ## Running To view the app directly from a Bokeh server, navigate to the parent directory -[`examples/app`](https://github.com/bokeh/bokeh/tree/master/examples/app), +[`examples/server/app`](https://github.com/bokeh/bokeh/blob/-/examples/server/app), and execute the command bokeh serve --show dash diff --git a/examples/server/app/export_csv/README.md b/examples/server/app/export_csv/README.md index 748fe00c940..0b87af31d5f 100644 --- a/examples/server/app/export_csv/README.md +++ b/examples/server/app/export_csv/README.md @@ -19,7 +19,7 @@ To install using pip, execute the command: ## Running To view the app directly from a Bokeh server, navigate to the parent directory -[`examples/app`](https://github.com/bokeh/bokeh/tree/master/examples/app), +[`examples/server/app`](https://github.com/bokeh/bokeh/blob/-/examples/server/app), and execute the command: bokeh serve --show export_csv diff --git a/examples/server/app/faces/README.md b/examples/server/app/faces/README.md index d9f6df4575b..dd6d6499945 100644 --- a/examples/server/app/faces/README.md +++ b/examples/server/app/faces/README.md @@ -15,7 +15,7 @@ install OpenCV using conda, execute the command: ## Running To view the app directly from a Bokeh server, navigate to the parent directory -[`examples/app`](https://github.com/bokeh/bokeh/tree/master/examples/app), +[`examples/server/app`](https://github.com/bokeh/bokeh/blob/-/examples/server/app), and execute the command: bokeh serve --show faces diff --git a/examples/server/app/gapminder/README.md b/examples/server/app/gapminder/README.md index 796527fe289..5b1612746c9 100644 --- a/examples/server/app/gapminder/README.md +++ b/examples/server/app/gapminder/README.md @@ -18,7 +18,7 @@ directory. ## Running To view the app directly from a Bokeh server, navigate to the parent directory -[`examples/app`](https://github.com/bokeh/bokeh/tree/master/examples/app), +[`examples/server/app`](https://github.com/bokeh/bokeh/blob/-/examples/server/app), and execute the command: bokeh serve --show gapminder diff --git a/examples/server/app/movies/README.md b/examples/server/app/movies/README.md index f2d01cafdec..9dbde352690 100644 --- a/examples/server/app/movies/README.md +++ b/examples/server/app/movies/README.md @@ -28,7 +28,7 @@ To install using pip, execute the command: ## Running To view the app directly from a Bokeh server, navigate to the parent directory -[`examples/app`](https://github.com/bokeh/bokeh/tree/master/examples/app), and +[`examples/server/app`](https://github.com/bokeh/bokeh/blob/-/examples/server/app), and execute the command: bokeh serve --show movies diff --git a/examples/server/app/ohlc/README.md b/examples/server/app/ohlc/README.md index 47961613a66..eafa82bbc7c 100644 --- a/examples/server/app/ohlc/README.md +++ b/examples/server/app/ohlc/README.md @@ -12,7 +12,7 @@ No additional packages or steps are required to run this example. ## Running To view the app directly from a Bokeh server, navigate to the parent directory -[`examples/app`](https://github.com/bokeh/bokeh/tree/master/examples/app), +[`examples/server/app`](https://github.com/bokeh/bokeh/blob/-/examples/server/app), and execute the command: bokeh serve --show ohlc diff --git a/examples/server/app/selection_histogram.py b/examples/server/app/selection_histogram.py index 3f66e97d935..6509454c3b5 100644 --- a/examples/server/app/selection_histogram.py +++ b/examples/server/app/selection_histogram.py @@ -85,7 +85,7 @@ def update(attr, old, new): hhist1, hhist2 = hzeros, hzeros vhist1, vhist2 = vzeros, vzeros else: - neg_inds = np.ones_like(x, dtype=np.bool) + neg_inds = np.ones_like(x, dtype=bool) neg_inds[inds] = False hhist1, _ = np.histogram(x[inds], bins=hedges) vhist1, _ = np.histogram(y[inds], bins=vedges) diff --git a/examples/server/app/simple_hdf5/README.md b/examples/server/app/simple_hdf5/README.md index 5cd67322538..eb58a392b3f 100644 --- a/examples/server/app/simple_hdf5/README.md +++ b/examples/server/app/simple_hdf5/README.md @@ -25,7 +25,7 @@ To install using pip, execute the command: ## Running To view the app directly from a Bokeh server, navigate to the parent directory -[`examples/app`](https://github.com/bokeh/bokeh/tree/master/examples/app), +[`examples/server/app`](https://github.com/bokeh/bokeh/blob/-/examples/server/app), and execute the command: bokeh serve --show simple_hdf5 diff --git a/examples/server/app/spectrogram/README.md b/examples/server/app/spectrogram/README.md index 45b5d604636..4571c8f15b5 100644 --- a/examples/server/app/spectrogram/README.md +++ b/examples/server/app/spectrogram/README.md @@ -27,7 +27,7 @@ If pyaudio is not installed, this example will use simulated audio data. ## Running To view the app directly from a Bokeh server, navigate to the parent directory -[`examples/app`](https://github.com/bokeh/bokeh/tree/master/examples/app), +[`examples/server/app`](https://github.com/bokeh/bokeh/blob/-/examples/server/app), and execute the command: bokeh serve --show spectrogram diff --git a/examples/server/app/stocks/README.md b/examples/server/app/stocks/README.md index d8d565ed028..b9286fed662 100644 --- a/examples/server/app/stocks/README.md +++ b/examples/server/app/stocks/README.md @@ -7,7 +7,7 @@ Create a simple stocks correlation dashboard. ## Running To view the app directly from a Bokeh server, navigate to the parent directory -[`examples/app`](https://github.com/bokeh/bokeh/tree/branch-3.0/examples/app), +[`examples/server/app`](https://github.com/bokeh/bokeh/blob/-/examples/server/app), and execute the command: bokeh serve --show stocks diff --git a/examples/server/app/surface3d/README.md b/examples/server/app/surface3d/README.md index caaf4bef549..8373706aa23 100644 --- a/examples/server/app/surface3d/README.md +++ b/examples/server/app/surface3d/README.md @@ -13,7 +13,7 @@ No additional packages or steps are required to run this example. ## Running To view the app directly from a Bokeh server, navigate to the parent directory -[`examples/app`](https://github.com/bokeh/bokeh/tree/master/examples/app), +[`examples/server/app`](https://github.com/bokeh/bokeh/blob/-/examples/server/app), and execute the command: bokeh serve --show surface3d diff --git a/examples/server/app/weather/README.md b/examples/server/app/weather/README.md index ed6caf32aca..95e85e8fa27 100644 --- a/examples/server/app/weather/README.md +++ b/examples/server/app/weather/README.md @@ -23,7 +23,7 @@ To install using pip, execute the command: ## Running To view the app directly from a Bokeh server, navigate to the parent directory -[`examples/app`](https://github.com/bokeh/bokeh/tree/master/examples/app), +[`examples/server/app`](https://github.com/bokeh/bokeh/blob/-/examples/server/app), and execute the command: bokeh serve --show weather diff --git a/examples/styling/mathtext/latex_outline_shapes.py b/examples/styling/mathtext/latex_outline_shapes.py new file mode 100644 index 00000000000..61ff177e1f0 --- /dev/null +++ b/examples/styling/mathtext/latex_outline_shapes.py @@ -0,0 +1,57 @@ +from typing import Literal + +from bokeh.io import show +from bokeh.plotting import figure + +p = figure( + x_range=(5, 85), y_range=(0, 50), + width=1000, height=400, + x_axis_type=None, y_axis_type=None, + toolbar_location=None, + background_fill_color="ivory", +) + +x = [10, 20, 30, 40, 50, 60, 70, 80, 90] +padding = 5 + +p.text( + anchor="center", + x=x, + y=5, + text=["none", "circle", "square", "ellipse", "box\nrectangle", "trapezoid", "parallelogram", "diamond", "triangle"], + outline_shape=["none", "circle", "square", "ellipse", "box", "trapezoid", "parallelogram", "diamond", "triangle"], + background_fill_color="white", + background_fill_alpha=1.0, + padding=padding, + border_line_color="black", + text_font_size="1.2em", +) + +def tex(display: Literal["inline", "block"], y: float, color: str): + p.tex( + anchor="center", + x=x, + y=y, + text=[ + r"\emptyset", + r"x^{y^z}", + r"\frac{1}{x^2\cdot y}", + r"\int_{-\infty}^{\infty} \frac{1}{x} dx", + r"F = G \left( \frac{m_1 m_2}{r^2} \right)", + r"\delta", + r"\sqrt[3]{\gamma}", + r"x^2", + r"y_{\rho \theta}", + ], + outline_shape=["none", "circle", "square", "ellipse", "box", "trapezoid", "parallelogram", "diamond", "triangle"], + background_fill_color=color, + background_fill_alpha=0.8, + padding=padding, + border_line_color="black", + display=display, + ) + +tex("inline", 20, "yellow") +tex("block", 40, "pink") + +show(p) diff --git a/examples/styling/plots/custom_js_ticker.py b/examples/styling/plots/custom_js_ticker.py new file mode 100644 index 00000000000..839fbe13f14 --- /dev/null +++ b/examples/styling/plots/custom_js_ticker.py @@ -0,0 +1,34 @@ +from bokeh.models import CustomJSTicker +from bokeh.plotting import figure, show + +xticker = CustomJSTicker( + # always three equally spaced ticks + major_code=""" + const {start, end} = cb_data.range + const interval = (end-start) / 4 + return [start + interval, start + 2*interval, start + 3*interval] + """, + # minor ticks in between the major ticks + minor_code=""" + const {start, end, major_ticks} = cb_data + return [ + (start+major_ticks[0])/2, + (major_ticks[0]+major_ticks[1])/2, + (major_ticks[1]+major_ticks[2])/2, + (major_ticks[2]+end)/2, + ] + """, +) + +yticker = CustomJSTicker(major_code="return ['a', 'c', 'e', 'g']") + +p = figure(y_range=list("abcdefg")) +p.scatter([1, 2, 3, 4, 5], ["a", "d", "b", "f", "c"], size=30) + +p.xaxis.ticker = xticker + +# keep the grid lines at all original tick locations +p.ygrid.ticker = p.yaxis.ticker +p.yaxis.ticker = yticker + +show(p) diff --git a/pyproject.toml b/pyproject.toml index 214bcac68a4..5ded5cc8fb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,9 +133,9 @@ warn_return_any = false warn_unreachable = true [tool.coverage.report] -exclude_lines = [ - "pragma: no cover", +exclude_also = [ "if TYPE_CHECKING:", + "@overload", ] [tool.ruff] @@ -145,7 +145,7 @@ lint.exclude = [ 'node_modules', ] lint.per-file-ignores = {"__init__.py" = ["F403"]} -lint.select = ["B", "COM", "E", "F", "RUF", "TID", "UP", "W"] +lint.select = ["B", "COM", "E", "F", "NPY", "RUF", "TID", "UP", "W"] lint.ignore = [ 'B005', # Using .strip() with multi-character strings is misleading the reader 'B006', # Do not use mutable data structures for argument defaults @@ -167,6 +167,7 @@ lint.ignore = [ 'E702', # Multiple statements on one line (semicolon) 'E731', # Do not assign a lambda expression, use a def 'E741', # Ambiguous variable name: I + 'NPY002', # Replace legacy np.random.{method_name} call with np.random.Generator 'UP035', # Import from `collections.abc` instead: `Iterator` 'RUF012', # Mutable class attributes should be annotated with `typing.ClassVar` 'TID252', # Prefer absolute imports over relative imports from parent modules @@ -269,5 +270,6 @@ module = [ "tests.unit.bokeh.document.test_callbacks__document", "tests.unit.bokeh.embed.test_standalone", #"tests.unit.bokeh.models._util_models", + "tests.unit.bokeh.test_layouts__typing", ] ignore_errors = false diff --git a/scripts/ci/install_downstream_packages.sh b/scripts/ci/install_downstream_packages.sh index e153c82024c..b01b26fa081 100644 --- a/scripts/ci/install_downstream_packages.sh +++ b/scripts/ci/install_downstream_packages.sh @@ -30,8 +30,3 @@ banner "dask/dask" 2> /dev/null pip install pytest-timeout pytest-cov pytest-rerunfailures pytest-repeat git clone https://github.com/dask/dask.git pip install -e "./dask[test]" # "test" extra installs additional testing dependencies - -banner "pandas_bokeh" 2> /dev/null -pip install pandas_bokeh -pip install geopandas -git clone https://github.com/PatrikHlobil/Pandas-Bokeh.git diff --git a/scripts/ci/run_downstream_tests.sh b/scripts/ci/run_downstream_tests.sh index 7b620edc060..fbfab0d30cb 100644 --- a/scripts/ci/run_downstream_tests.sh +++ b/scripts/ci/run_downstream_tests.sh @@ -39,12 +39,6 @@ pytest panel/tests banner "Holoviews" 2> /dev/null nosetests holoviews/tests/plotting/bokeh -popd || exit - -banner "PandasBokeh" 2> /dev/null -pytest Pandas-Bokeh/Tests/test_PandasBokeh.py - -banner "GeoPandasBokeh" 2> /dev/null -pytest Pandas-Bokeh/Tests/test_GeoPandasBokeh.py +popd exit 0 diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile deleted file mode 100644 index 91b74a2d3c7..00000000000 --- a/scripts/docker/Dockerfile +++ /dev/null @@ -1,49 +0,0 @@ -FROM ubuntu:22.04 - -ARG CHROME_DEB=google-chrome-stable_current_amd64.deb -ARG FIXUID_VERSION=0.5.1 - -ENV DEBIAN_FRONTEND=noninteractive - -# fonts-dejavu-core needed for headless chrome unicode characters. -RUN apt update -y && \ - apt upgrade -y && \ - apt install -y curl fonts-dejavu-core git sudo unzip - -# User and group setup using fixuid. -RUN addgroup --gid 1000 docker && \ - adduser --uid 1000 --ingroup docker --home /home/docker --shell /bin/bash --disabled-password --gecos "" docker && \ - USER=docker && \ - GROUP=docker && \ - ARCH="$(dpkg --print-architecture)" && \ - curl -fsSL "https://github.com/boxboat/fixuid/releases/download/v$FIXUID_VERSION/fixuid-$FIXUID_VERSION-linux-$ARCH.tar.gz" | tar -C /usr/local/bin -xzf - && \ - chown root:root /usr/local/bin/fixuid && \ - chmod 4755 /usr/local/bin/fixuid && \ - mkdir -p /etc/fixuid && \ - printf "user: $USER\ngroup: $GROUP\n" > /etc/fixuid/config.yml - -# Cannot 'snap install chromium' in docker container, so instead install google-chrome deb. -RUN curl -LO https://dl.google.com/linux/direct/$CHROME_DEB && \ - apt install --no-install-recommends -y ./$CHROME_DEB && \ - rm $CHROME_DEB && \ - apt autoremove -y && \ - apt clean -y && \ - rm -rf /var/lib/apt/lists/* - -# Download correct version of chromedriver and put in path. -RUN CHROME_VERSION=$(google-chrome --version | awk '{print $NF}' | sed 's/\.[0-9]\+$//g') && \ - DRIVER_VERSION=$(curl https://chromedriver.storage.googleapis.com/LATEST_RELEASE_$CHROME_VERSION) && \ - curl -LO https://chromedriver.storage.googleapis.com/$DRIVER_VERSION/chromedriver_linux64.zip && \ - unzip chromedriver_linux64.zip && \ - mv chromedriver /usr/bin && \ - rm chromedriver_linux64.zip - -EXPOSE 5006 - -COPY entrypoint.sh /usr/bin/entrypoint.sh -RUN chmod a+x /usr/bin/entrypoint.sh -ENTRYPOINT ["/usr/bin/entrypoint.sh"] - -ENV BOKEH_IN_DOCKER=1 -USER docker:docker -WORKDIR /bokeh diff --git a/scripts/docker/bokeh_docker_build.sh b/scripts/docker/bokeh_docker_build.sh deleted file mode 100644 index d6ed0bbea03..00000000000 --- a/scripts/docker/bokeh_docker_build.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -set -eu - -echo "Start of $0" - -bash scripts/ci/install_node_modules.sh -pip install -ve . - -python -m bokeh info - -echo "End of $0" diff --git a/scripts/docker/bokeh_docker_from_wheel.sh b/scripts/docker/bokeh_docker_from_wheel.sh deleted file mode 100644 index 13fcb61d2fe..00000000000 --- a/scripts/docker/bokeh_docker_from_wheel.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# This installs Bokeh from a single wheel in the dist directory and runs some of the Python tests. - -set -eu - -echo "Start of $0" - -pip install dist/bokeh*.whl -bash scripts/ci/install_node_modules.sh - -python -m bokeh info - -# Bokeh Python tests. -pytest tests/test_defaults.py -pytest tests/unit -k "not firefox" -#pytest tests/integration - -echo "End of $0" diff --git a/scripts/docker/bokeh_docker_test.sh b/scripts/docker/bokeh_docker_test.sh deleted file mode 100644 index 5a5c14062db..00000000000 --- a/scripts/docker/bokeh_docker_test.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# Note do not exit on error, as want remaining tests to run. -set -u - -echo "Start of $0" - -python -m bokeh info - -# Run some of the tests. -pytest tests/codebase -cd bokehjs && node make test - -echo "End of $0" diff --git a/scripts/docker/docker_run.sh b/scripts/docker/docker_run.sh deleted file mode 100755 index 383dc4da062..00000000000 --- a/scripts/docker/docker_run.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Usage: docker_run.sh bokeh-dev:latest -# Can put env vars first, e.g. -# BOKEH_DOCKER_PY=3.10 docker_run.sh bokeh-dev:latest - -set -eu - -if [ $# -ne 1 ]; then - echo "Usage: docker_run.sh , e.g. docker_run.sh bokeh/bokeh-dev:latest" - exit 1 -fi - -IMAGE_AND_TAG=$1 -UID_GID="`id -u`:`id -g`" - -# Environment variables that are passed in to Docker container. -ENV_VARS="" -for name in BOKEH_DOCKER_CONDA BOKEH_DOCKER_PY BOKEH_DOCKER_BUILD BOKEH_DOCKER_TEST BOKEH_DOCKER_CHROME_VERSION BOKEH_DOCKER_FROM_WHEEL; do - if [ -n "${!name+set}" ]; then - ENV_VARS="$ENV_VARS -e $name=${!name}" - fi -done - -INTERACTIVE="-it" -if [ "${BOKEH_DOCKER_INTERACTIVE:-1}" == 0 ]; then - INTERACTIVE="" -fi -if [ "${BOKEH_DOCKER_CHROME_VERSION:-0}" == 1 ]; then - # If only want chrome version, do not need to run interactively. - INTERACTIVE="" -fi - -CMD="docker run -v $PWD:/bokeh -u $UID_GID -p 5006:5006 $ENV_VARS $INTERACTIVE $IMAGE_AND_TAG" -echo $CMD -$CMD diff --git a/scripts/docker/entrypoint.sh b/scripts/docker/entrypoint.sh deleted file mode 100644 index 37215e621f3..00000000000 --- a/scripts/docker/entrypoint.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/bash - -set -eu - -CONDA_DIR=/home/docker/conda_bokeh -DEFAULT_PY=3.10 -ENV_NAME=bkdev -MINICONDA_SCRIPT=Miniconda3-latest-Linux-x86_64.sh - -eval "$(fixuid -q)" - -if [ "${BOKEH_DOCKER_CHROME_VERSION:-0}" == 1 ]; then - # Print numerical chrome version and exit. - google-chrome --version | awk '{print $NF}' - exit 1 -fi - -if [ ! -d .git ] || [ ! -f conda/environment-test-3.10.yml ]; then - echo "Directory does not contain Bokeh git repo." - exit 2 -fi - -if [ "${BOKEH_DOCKER_CONDA:-1}" == 1 ]; then - # Check environment file exists. - BOKEH_DOCKER_PY=${BOKEH_DOCKER_PY:-$DEFAULT_PY} - ENV_YML_FILE=conda/environment-test-$BOKEH_DOCKER_PY.yml - if [ ! -f $ENV_YML_FILE ]; then - echo "Cannot find environment file $ENV_YML_FILE" - exit 3 - fi - - if [ ! -f "$CONDA_DIR/condabin/conda" ]; then - # Install miniconda into $CONDA_DIR on docker filesystem. - START_DIR=$(pwd) - cd /tmp - curl -LO "http://repo.continuum.io/miniconda/$MINICONDA_SCRIPT" - bash $MINICONDA_SCRIPT -p $CONDA_DIR -b - rm $MINICONDA_SCRIPT - cd $START_DIR - fi - - # Activate conda in .bashrc and in this shell. - $CONDA_DIR/condabin/conda init bash > /dev/null - . $CONDA_DIR/etc/profile.d/conda.sh - - if [ ! -d "$CONDA_DIR/envs/$ENV_NAME" ]; then - # Create conda environment and install required packagaes. - conda env create -n $ENV_NAME -f $ENV_YML_FILE - fi - - # Ensure conda environment is activated in this shell and in new shells. - conda activate $ENV_NAME - echo "conda activate $ENV_NAME" >> ~/.bashrc -fi - -google-chrome --version - -if [ "${BOKEH_DOCKER_BUILD:-0}" == 1 ]; then - bash scripts/docker/bokeh_docker_build.sh -fi - -if [ "${BOKEH_DOCKER_TEST:-0}" == 1 ]; then - bash scripts/docker/bokeh_docker_test.sh -fi - -if [ "${BOKEH_DOCKER_FROM_WHEEL:-0}" == 1 ]; then - bash scripts/docker/bokeh_docker_from_wheel.sh -fi - -/bin/bash "$@" diff --git a/src/bokeh/_sri/3.5.0.json b/src/bokeh/_sri/3.5.0.json new file mode 100644 index 00000000000..2d9a8e04361 --- /dev/null +++ b/src/bokeh/_sri/3.5.0.json @@ -0,0 +1,14 @@ +{ + "bokeh-3.5.0.js": "b01IQSh8eJbkTzrEcxdRixHfPO2DV/o84xr3MFYpDaTixBx/uPlFq+fb1dYt8yCm", + "bokeh-3.5.0.min.js": "Mo/T6DgIbjG7Dcw+pYyVGGiwvPRnN+0PzsQu5peG3NJjyx5O+dhbOj0+0YHj5BQR", + "bokeh-api-3.5.0.js": "zerxTOuGRvg+hKxY9zwC+HxoXGydUvbMnrTQOQHpRzAT68OP5dZQC50jJ7fknSCQ", + "bokeh-api-3.5.0.min.js": "RFelXXN7R5Mvjh+SIGob4C/7MJyHFMPTfRSHFFILn3/j6+1gtY0h+fQOcV3ZlcUe", + "bokeh-gl-3.5.0.js": "AJsyQW7Eq27dzmLDSnvRQXMDIddbp4wQRo6D5OFwEGznqaHMnlhrwixtw/v6Le91", + "bokeh-gl-3.5.0.min.js": "9DwrgsqLoC+APvBdhDX6Kw6DiMVpij+qlCNIRd+h31DR5c4B+80t7+Kugqsictox", + "bokeh-mathjax-3.5.0.js": "/uqzQGoW4WvumgLCBmgWB8tiSKWYIpGCIIAn70QrneTqXTllZVfs88pICqjZYa+9", + "bokeh-mathjax-3.5.0.min.js": "8zi+l5yWCf7wN8CDSG2y3clKgL5s4xRY1dDv3NSJpDycRPUatQiY+i+Il8pSHFbG", + "bokeh-tables-3.5.0.js": "ki1A167SoI1IIIeanCkX+RMsbMYjhiG4Zp6OYNIRiy5sf+b65+vpeTZFAE5nSmwV", + "bokeh-tables-3.5.0.min.js": "+HFjZpiHBkIdWeGS5Zo+Qd8W17couyhloAve0IhUzvhBJIB+1LSbLp5EGfXxK3Dg", + "bokeh-widgets-3.5.0.js": "DCQVZOcgaA4dZozQPpM7yUmycKfg4y6yEDiPGGj0BXlfBhW42dlpBRPS6314zQN5", + "bokeh-widgets-3.5.0.min.js": "zrVslzla+vpSxlyrdBC7VK4UXPsAFwrkdtTEq1bZs90U3LkFh9RS8WaS1/1nq0B/" +} \ No newline at end of file diff --git a/src/bokeh/command/subcommands/info.py b/src/bokeh/command/subcommands/info.py index d5b45c9e6d3..93d534d9ae1 100644 --- a/src/bokeh/command/subcommands/info.py +++ b/src/bokeh/command/subcommands/info.py @@ -17,13 +17,14 @@ .. code-block:: none - Python version : 3.11.3 | packaged by conda-forge | (main, Apr 6 2023, 08:57:19) [GCC 11.3.0] - IPython version : 8.13.2 - Tornado version : 6.3 - Bokeh version : 3.3.0 - BokehJS static path : /opt/anaconda/envs/test/lib/python3.11/site-packages/bokeh/server/static - node.js version : v18.16.1 - npm version : 9.5.1 + Python version : 3.12.3 | packaged by conda-forge | (main, Apr 15 2024, 18:38:13) [GCC 12.3.0] + IPython version : 8.19.0 + Tornado version : 6.3.3 + NumPy version : 2.0.0 + Bokeh version : 3.5.1 + BokehJS static path : /opt/anaconda/envs/test/lib/python3.12/site-packages/bokeh/server/static + node.js version : v20.12.2 + npm version : 10.8.2 jupyter_bokeh version : (not installed) Operating system : Linux-5.15.0-86-generic-x86_64-with-glibc2.35 @@ -38,7 +39,7 @@ .. code-block:: none - /opt/anaconda/envs/test/lib/python3.11/site-packages/bokeh/server/static + /opt/anaconda/envs/test/lib/python3.12/site-packages/bokeh/server/static ''' @@ -55,15 +56,11 @@ #----------------------------------------------------------------------------- # Standard library imports -import platform -import sys from argparse import Namespace # Bokeh imports -from bokeh import __version__ from bokeh.settings import settings -from bokeh.util.compiler import nodejs_version, npmjs_version -from bokeh.util.dependencies import import_optional +from bokeh.util.info import print_info # Bokeh imports from ..subcommand import Argument, Subcommand @@ -76,22 +73,6 @@ 'Info', ) -#----------------------------------------------------------------------------- -# Private API -#----------------------------------------------------------------------------- - -def if_installed(version_or_none: str | None) -> str: - ''' helper method to optionally return module version number or not installed - - :param version_or_none: - :return: - ''' - return version_or_none or "(not installed)" - -def _version(module_name: str, attr: str) -> str | None: - module = import_optional(module_name) - return getattr(module, attr) if module else None - #----------------------------------------------------------------------------- # General API #----------------------------------------------------------------------------- @@ -122,21 +103,16 @@ def invoke(self, args: Namespace) -> None: if args.static: print(settings.bokehjs_path()) else: - newline = '\n' - print(f"Python version : {sys.version.split(newline)[0]}") - print(f"IPython version : {if_installed(_version('IPython', '__version__'))}") - print(f"Tornado version : {if_installed(_version('tornado', 'version'))}") - print(f"Bokeh version : {__version__}") - print(f"BokehJS static path : {settings.bokehjs_path()}") - print(f"node.js version : {if_installed(nodejs_version())}") - print(f"npm version : {if_installed(npmjs_version())}") - print(f"jupyter_bokeh version : {if_installed(_version('jupyter_bokeh', '__version__'))}") - print(f"Operating system : {platform.platform()}") + print_info() #----------------------------------------------------------------------------- # Dev API #----------------------------------------------------------------------------- +#----------------------------------------------------------------------------- +# Private API +#----------------------------------------------------------------------------- + #----------------------------------------------------------------------------- # Code #----------------------------------------------------------------------------- diff --git a/src/bokeh/core/enums.py b/src/bokeh/core/enums.py index 47e480419bd..a88bce82243 100644 --- a/src/bokeh/core/enums.py +++ b/src/bokeh/core/enums.py @@ -94,6 +94,7 @@ class MyModel(Model): 'Anchor', 'AngleUnits', 'AutosizeMode', + 'BuiltinFormatter', 'ButtonType', 'CalendarPosition', 'ContextWhich', @@ -128,10 +129,12 @@ class MyModel(Model): 'NamedColor', 'NumeralLanguage', 'Orientation', + 'OutlineShapeName', 'OutputBackend', 'PaddingUnits', 'Palette', 'Place', + 'RegionSelectionMode', 'RenderLevel', 'ResetPolicy', 'Resizable', @@ -215,11 +218,10 @@ def enumeration(*values: Any, case_sensitive: bool = True, quote: bool = False) first element will be considered the default value when used to create |Enum| properties. - Keyword Args: case_sensitive (bool, optional) : Whether validation should consider case or not (default: True) - quote (bool, optional): + quote (bool, optional) : Whether values should be quoted in the string representations (default: False) @@ -250,92 +252,117 @@ def enumeration(*values: Any, case_sensitive: bool = True, quote: bool = False) return type("Enumeration", (Enumeration,), attrs)() #: Alignment (vertical or horizontal) of a child item -Align = enumeration("start", "center", "end") +AlignType = Literal["start", "center", "end"] +Align = enumeration(AlignType) #: Horizontal alignment of a child item -HAlign = enumeration("top", "center", "bottom") +HAlignType = Literal["top", "center", "bottom"] +HAlign = enumeration(HAlignType) #: Vertical alignment of a child item -VAlign = enumeration("left", "center", "right") +VAlignType = Literal["left", "center", "right"] +VAlign = enumeration(VAlignType) #: Specify to which items apply styling in a container (e.g. in a legend) -AlternationPolicy = enumeration("none", "even", "odd", "every") +AlternationPolicyType = Literal["none", "even", "odd", "every"] +AlternationPolicy = enumeration(AlternationPolicyType) #: Specify an anchor position on a box/frame -Anchor = enumeration( +AnchorType = Literal[ "top_left", "top_center", "top_right", "center_left", "center_center", "center_right", "bottom_left", "bottom_center", "bottom_right", "top", "left", "center", "right", "bottom", -) +] +Anchor = enumeration(AnchorType) #: Specify the units for an angle value -AngleUnits = enumeration("deg", "rad", "grad", "turn") +AngleUnitsType = Literal["deg", "rad", "grad", "turn"] +AngleUnits = enumeration(AngleUnitsType) #: Specify autosize mode for DataTable -AutosizeMode = enumeration("fit_columns", "fit_viewport", "force_fit", "none") +AutosizeModeType = Literal["fit_columns", "fit_viewport", "force_fit", "none"] +AutosizeMode = enumeration(AutosizeModeType) + +#: Names of built-in value formatters +BuiltinFormatterType = Literal["raw", "basic", "numeral", "printf", "datetime"] +BuiltinFormatter = enumeration(BuiltinFormatterType) #: Specify a style for button widgets -ButtonType = enumeration("default", "primary", "success", "warning", "danger", "light") +ButtonTypeType = Literal["default", "primary", "success", "warning", "danger", "light"] +ButtonType = enumeration(ButtonTypeType) #: Specify a position for the DatePicker calendar to display -CalendarPosition = enumeration("auto", "above", "below") +CalendarPositionType = Literal["auto", "above", "below"] +CalendarPosition = enumeration(CalendarPositionType) #: Specify which tick to add additional context to -ContextWhich = enumeration("start", "center", "end", "all") +ContextWhichType = Literal["start", "center", "end", "all"] +ContextWhich = enumeration(ContextWhichType) #: Specify units for mapping coordinates -CoordinateUnits = enumeration("canvas", "screen", "data") +CoordinateUnitsType = Literal["canvas", "screen", "data"] +CoordinateUnits = enumeration(CoordinateUnitsType) #: Specify a named dashing patter for stroking lines -DashPattern = enumeration("solid", "dashed", "dotted", "dotdash", "dashdot") +DashPatternType = Literal["solid", "dashed", "dotted", "dotdash", "dashdot"] +DashPattern = enumeration(DashPatternType) #: Specify a format for printing dates -DateFormat = enumeration("ATOM", "W3C", "RFC-3339", "ISO-8601", "COOKIE", "RFC-822", - "RFC-850", "RFC-1036", "RFC-1123", "RFC-2822", "RSS", "TIMESTAMP") +DateFormatType = Literal[ + "ATOM", "W3C", "RFC-3339", "ISO-8601", "COOKIE", "RFC-822", + "RFC-850", "RFC-1036", "RFC-1123", "RFC-2822", "RSS", "TIMESTAMP", +] +DateFormat = enumeration(DateFormatType) #: Specify a date/time scale -DatetimeUnits = enumeration("microseconds", "milliseconds", "seconds", "minsec", - "minutes", "hourmin", "hours", "days", "months", "years") +DatetimeUnitsType = Literal[ + "microseconds", "milliseconds", "seconds", "minsec", + "minutes", "hourmin", "hours", "days", "months", "years", +] +DatetimeUnits = enumeration(DatetimeUnitsType) #: Specify a vertical/horizontal dimension -Dimension = enumeration("width", "height") +DimensionType = Literal["width", "height"] +Dimension = enumeration(DimensionType) #: Specify a vertical/horizontal dimensions DimensionsType = Literal["width", "height", "both"] -Dimensions = enumeration("width", "height", "both") +Dimensions = enumeration(DimensionsType) #: Specify a stroke direction for circles, wedges, etc. -Direction = enumeration("clock", "anticlock") +DirectionType = Literal["clock", "anticlock"] +Direction = enumeration(DirectionType) #: Specify the flow behavior in CSS layouts. -FlowMode = enumeration("block", "inline") +FlowModeType = Literal["block", "inline"] +FlowMode = enumeration(FlowModeType) #: Specify the font style for rendering text -FontStyle = enumeration("normal", "italic", "bold", "bold italic") - -_hatch_patterns = ( - (" ", "blank"), - (".", "dot"), - ("o", "ring"), - ("-", "horizontal_line"), - ("|", "vertical_line"), - ("+", "cross"), - ('"', "horizontal_dash"), - (":", "vertical_dash"), - ("@", "spiral"), - ("/", "right_diagonal_line"), - ("\\", "left_diagonal_line"), - ("x", "diagonal_cross"), - (",", "right_diagonal_dash"), - ("`", "left_diagonal_dash"), - ("v", "horizontal_wave"), - (">", "vertical_wave"), - ("*", "criss_cross"), -) +FontStyleType = Literal["normal", "italic", "bold", "bold italic"] +FontStyle = enumeration(FontStyleType) #: Specify one of the built-in patterns for hatching fills -HatchPattern = enumeration(*list(zip(*_hatch_patterns))[1]) +HatchPatternType = Literal[ + "blank", + "dot", + "ring", + "horizontal_line", + "vertical_line", + "cross", + "horizontal_dash", + "vertical_dash", + "spiral", + "right_diagonal_line", + "left_diagonal_line", + "diagonal_cross", + "right_diagonal_dash", + "left_diagonal_dash", + "horizontal_wave", + "vertical_wave", + "criss_cross", +] +HatchPattern = enumeration(HatchPatternType) #: Specify one of the built-in patterns for hatching fills with a one-letter abbreviation #: @@ -360,144 +387,182 @@ def enumeration(*values: Any, case_sensitive: bool = True, quote: bool = False) #: "v" : horizontal_wave #: ">" : vertical_wave #: "*" : criss_cross -HatchPatternAbbreviation = enumeration(*next(iter(zip(*_hatch_patterns))), quote=True) +HatchPatternAbbreviationType = Literal[" ", ".", "o", "-", "|", "+", '"', ":", "@", "/", "\\", "x", ",", "`", "v", ">", "*"] +HatchPatternAbbreviation = enumeration(HatchPatternAbbreviationType, quote=True) #: Specify whether events should be combined or collected as-is when a Document hold is in effect HoldPolicyType = Literal["combine", "collect"] HoldPolicy = enumeration(HoldPolicyType) #: Specify a horizontal location in plot layouts -HorizontalLocation = enumeration("left", "right") +HorizontalLocationType = Literal["left", "right"] +HorizontalLocation = enumeration(HorizontalLocationType) #: Defines the coordinate space within an image -ImageOrigin = enumeration("bottom_left", "top_left", "bottom_right", "top_right") +ImageOriginType = Literal["bottom_left", "top_left", "bottom_right", "top_right"] +ImageOrigin = enumeration(ImageOriginType) #: Specify a distribution to use for the Jitter class JitterRandomDistributionType = Literal["uniform", "normal"] JitterRandomDistribution = enumeration(JitterRandomDistributionType) -#: +#: Keyboard modifier key used to configure tools or report in UI events KeyModifierType = Literal["shift", "ctrl", "alt"] -KeyModifier = enumeration("shift", "ctrl", "alt") +KeyModifier = enumeration(KeyModifierType) #: Specify how labels are oriented with respect to an axis LabelOrientationType = Literal["horizontal", "vertical", "parallel", "normal"] -LabelOrientation = enumeration("horizontal", "vertical", "parallel", "normal") +LabelOrientation = enumeration(LabelOrientationType) #: Specify whether a dimension or coordinate is latitude or longitude -LatLon = enumeration("lat", "lon") +LatLonType = Literal["lat", "lon"] +LatLon = enumeration(LatLonType) #: Specify how a legend should respond to click events -LegendClickPolicy = enumeration("none", "hide", "mute") +LegendClickPolicyType = Literal["none", "hide", "mute"] +LegendClickPolicy = enumeration(LegendClickPolicyType) -#: Specify a fixed location for a Bokeh legend +#: Specify a fixed location for a legend +LegendLocationType = AnchorType LegendLocation = Anchor #: Specify how stroked lines should be terminated -LineCap = enumeration("butt", "round", "square") +LineCapType = Literal["butt", "round", "square"] +LineCap = enumeration(LineCapType) #: Specify a named dash pattern for stroking lines -LineDash = enumeration("solid", "dashed", "dotted", "dotdash", "dashdot") +LineDashType = Literal["solid", "dashed", "dotted", "dotdash", "dashdot"] +LineDash = enumeration(LineDashType) #: Specify how stroked lines should be joined together -LineJoin = enumeration("miter", "round", "bevel") +LineJoinType = Literal["miter", "round", "bevel"] +LineJoin = enumeration(LineJoinType) #: Specify a location in plot layouts LocationType = Literal["above", "below", "left", "right"] Location = enumeration(LocationType) #: Specify a style for a Google map -MapType = enumeration("satellite", "roadmap", "terrain", "hybrid") +MapTypeType = Literal["satellite", "roadmap", "terrain", "hybrid"] +MapType = enumeration(MapTypeType) #: Specify one of the built-in marker types -MarkerType = enumeration( +MarkerTypeType = Literal[ "asterisk", "circle", "circle_cross", "circle_dot", "circle_x", "circle_y", "cross", "dash", "diamond", "diamond_cross", "diamond_dot", "dot", "hex", "hex_dot", "inverted_triangle", "plus", "square", "square_cross", "square_dot", "square_pin", "square_x", "star", "star_dot", "triangle", "triangle_dot", "triangle_pin", "x", "y", -) +] +MarkerType = enumeration(MarkerTypeType) #: Indicates in which dimensions an object (a renderer or an UI element) can be moved. -Movable = enumeration("none", "x", "y", "both") +MovableType = Literal["none", "x", "y", "both"] +Movable = enumeration(MovableType) #: Specify one of the CSS4 named colors (https://www.w3.org/TR/css-color-4/#named-colors) NamedColor = enumeration(*colors.named.__all__, case_sensitive=False) #: Specify a locale for printing numeric values -NumeralLanguage = enumeration("be-nl", "chs", "cs", "da-dk", "de-ch", "de", "en", +NumeralLanguageType = Literal["be-nl", "chs", "cs", "da-dk", "de-ch", "de", "en", "en-gb", "es-ES", "es", "et", "fi", "fr-CA", "fr-ch", "fr", "hu", "it", "ja", "nl-nl", "pl", "pt-br", - "pt-pt", "ru", "ru-UA", "sk", "th", "tr", "uk-UA") + "pt-pt", "ru", "ru-UA", "sk", "th", "tr", "uk-UA"] +NumeralLanguage = enumeration(NumeralLanguageType) #: Specify a vertical/horizontal orientation for something -Orientation = enumeration("horizontal", "vertical") +OrientationType = Literal["horizontal", "vertical"] +Orientation = enumeration(OrientationType) + +#: Names of pre-defined outline shapes (used in ``Text.outline_shape``) +OutlineShapeName = Literal["none", "box", "rectangle", "square", "circle", "ellipse", "trapezoid", "parallelogram", "diamond", "triangle"] +OutlineShapeName = enumeration(OutlineShapeName) #: Specify an output backend to render a plot area onto OutputBackendType = Literal["canvas", "svg", "webgl"] OutputBackend = enumeration(OutputBackendType) #: Whether range padding should be interpreted a percentage or and absolute quantity -PaddingUnits = enumeration("percent", "absolute") +PaddingUnitsType = Literal["percent", "absolute"] +PaddingUnits = enumeration(PaddingUnitsType) #: Specify the name of a palette from :ref:`bokeh.palettes` Palette = enumeration(*palettes.__palettes__) -#: +#: Placement of a layout element, in particular in border-style layouts PlaceType = Literal["above", "below", "left", "right", "center"] Place = enumeration(PlaceType) -#: Specify a position in the render order for a Bokeh renderer -RenderLevel = enumeration("image", "underlay", "glyph", "guide", "annotation", "overlay") +#: Specify a position in the render order for a renderer +RenderLevelType = Literal["image", "underlay", "glyph", "guide", "annotation", "overlay"] +RenderLevel = enumeration(RenderLevelType) #: What reset actions should occur on a Plot reset -ResetPolicy = enumeration("standard", "event_only") +ResetPolicyType = Literal["standard", "event_only"] +ResetPolicy = enumeration(ResetPolicyType) #: Indicates in which dimensions an object (a renderer or an UI element) can be resized. -Resizable = enumeration("none", "left", "right", "top", "bottom", "x", "y", "all") +ResizableType = Literal["none", "left", "right", "top", "bottom", "x", "y", "all"] +Resizable = enumeration(ResizableType) #: Specify which resolutions should be used for stripping of leading zeros -ResolutionType = enumeration("microseconds", "milliseconds", "seconds", "minsec", "minutes", "hourmin", "hours", "days", "months", "years") +ResolutionTypeType = Literal["microseconds", "milliseconds", "seconds", "minsec", "minutes", "hourmin", "hours", "days", "months", "years"] +ResolutionType = enumeration(ResolutionTypeType) #: Specify a policy for how numbers should be rounded -RoundingFunction = enumeration("round", "nearest", "floor", "rounddown", "ceil", "roundup") +RoundingFunctionType = Literal["round", "nearest", "floor", "rounddown", "ceil", "roundup"] +RoundingFunction = enumeration(RoundingFunctionType) #: Scrollbar policies -ScrollbarPolicy = enumeration("auto", "visible", "hidden") +ScrollbarPolicyType = Literal["auto", "visible", "hidden"] +ScrollbarPolicy = enumeration(ScrollbarPolicyType) + +#: Region selection modes +RegionSelectionModeType = Literal["replace", "append", "intersect", "subtract", "xor"] +RegionSelectionMode = enumeration(RegionSelectionModeType) #: Selection modes -SelectionMode = enumeration("replace", "append", "intersect", "subtract", "xor") +SelectionModeType = Literal[RegionSelectionModeType, "toggle"] +SelectionMode = enumeration(SelectionModeType) #: Sizing mode policies SizingModeType = Literal["stretch_width", "stretch_height", "stretch_both", "scale_width", "scale_height", "scale_both", "fixed", "inherit"] SizingMode = enumeration(SizingModeType) #: Individual sizing mode policies -SizingPolicy = enumeration("fixed", "fit", "min", "max") +SizingPolicyType = Literal["fixed", "fit", "min", "max"] +SizingPolicy = enumeration(SizingPolicyType) #: Specify sorting directions -SortDirection = enumeration("ascending", "descending") +SortDirectionType = Literal["ascending", "descending"] +SortDirection = enumeration(SortDirectionType) #: Specify units for mapping values -SpatialUnits = enumeration("screen", "data") +SpatialUnitsType = Literal["screen", "data"] +SpatialUnits = enumeration(SpatialUnitsType) #: Specify a start/end value -StartEnd = enumeration("start", "end") +StartEndType = Literal["start", "end"] +StartEnd = enumeration(StartEndType) #: Specify a mode for stepwise interpolation -StepMode = enumeration("before", "after", "center") +StepModeType = Literal["before", "after", "center"] +StepMode = enumeration(StepModeType) #: Specify the horizontal alignment for rendering text -TextAlign = enumeration("left", "right", "center") +TextAlignType = Literal["left", "right", "center"] +TextAlign = enumeration(TextAlignType) #: Specify the baseline location for rendering text -TextBaseline = enumeration("top", "middle", "bottom", "alphabetic", "hanging", "ideographic") +TextBaselineType = Literal["top", "middle", "bottom", "alphabetic", "hanging", "ideographic"] +TextBaseline = enumeration(TextBaselineType) #: Specify how textures used as canvas patterns should repeat -TextureRepetition = enumeration("repeat", "repeat_x", "repeat_y", "no_repeat") +TextureRepetitionType = Literal["repeat", "repeat_x", "repeat_y", "no_repeat"] +TextureRepetition = enumeration(TextureRepetitionType) #: Well known tool icon names -ToolIcon = enumeration( +ToolIconType = Literal[ "append_mode", "arrow_down_to_bar", "arrow_up_from_bar", @@ -568,22 +633,28 @@ def enumeration(*values: Any, case_sensitive: bool = True, quote: bool = False) "y_pan", "zoom_in", "zoom_out", -) +] +ToolIcon = enumeration(ToolIconType) #: Specify an attachment for tooltips -TooltipAttachment = enumeration("horizontal", "vertical", "left", "right", "above", "below") +TooltipAttachmentType = Literal["horizontal", "vertical", "left", "right", "above", "below"] +TooltipAttachment = enumeration(TooltipAttachmentType) #: Specify how a format string for a tooltip field should be interpreted -TooltipFieldFormatter = enumeration("numeral", "datetime", "printf") +TooltipFieldFormatterType = Literal["numeral", "datetime", "printf"] +TooltipFieldFormatter = enumeration(TooltipFieldFormatterType) #: Grid track (row/column) sizing policies -TrackPolicy = enumeration("auto", "min", "max", "flex", "fixed") +TrackPolicyType = Literal["auto", "min", "max", "flex", "fixed"] +TrackPolicy = enumeration(TrackPolicyType) #: Specify the vertical alignment for rendering text -VerticalAlign = enumeration("top", "middle", "bottom") +VerticalAlignType = Literal["top", "middle", "bottom"] +VerticalAlign = enumeration(VerticalAlignType) #: Specify a vertical location in plot layouts -VerticalLocation = enumeration("above", "below") +VerticalLocationType = Literal["above", "below"] +VerticalLocation = enumeration(VerticalLocationType) #----------------------------------------------------------------------------- # Private API diff --git a/src/bokeh/core/has_props.py b/src/bokeh/core/has_props.py index 058e705a59f..0f00c9b6d79 100644 --- a/src/bokeh/core/has_props.py +++ b/src/bokeh/core/has_props.py @@ -424,15 +424,9 @@ def set_from_json(self, name: str, value: Any, *, setter: Setter | None = None) ''' Set a property value on this object from JSON. Args: - name: (str) : name of the attribute to set + name (str) : name of the attribute to set - json: (JSON-value) : value to set to the attribute to - - models (dict or None, optional) : - Mapping of model ids to models (default: None) - - This is needed in cases where the attributes to update also - have values that have references. + value (JSON-value) : value to set to the attribute to setter(ClientSession or ServerSession or None, optional) : This is used to prevent "boomerang" updates to Bokeh apps. diff --git a/src/bokeh/core/property/dataspec.py b/src/bokeh/core/property/dataspec.py index 14fbe39483c..5c8c2e640ae 100644 --- a/src/bokeh/core/property/dataspec.py +++ b/src/bokeh/core/property/dataspec.py @@ -447,7 +447,7 @@ def make_descriptors(self, base_name: str): property as well as the associated units property are returned. Args: - name (str) : the name of the property these descriptors are for + base_name (str) : the name of the property these descriptors are for Returns: list[PropertyDescriptor] diff --git a/src/bokeh/core/property/descriptors.py b/src/bokeh/core/property/descriptors.py index 5079250b6e8..3a6b67055b0 100644 --- a/src/bokeh/core/property/descriptors.py +++ b/src/bokeh/core/property/descriptors.py @@ -203,7 +203,7 @@ def _warn(self) -> None: def __get__(self, obj: HasProps | None, owner: type[HasProps] | None) -> T: if obj is not None: - # Warn only when accesing descriptor's value, otherwise there would + # Warn only when accessing descriptor's value, otherwise there would # be a lot of spurious warnings from parameter resolution, etc. self._warn() return super().__get__(obj, owner) @@ -398,13 +398,7 @@ def set_from_json(self, obj: HasProps, value: Any, *, setter: Setter | None = No Args: obj: (HasProps) : instance to set the property value on - json: (JSON-value) : value to set to the attribute to - - models (dict or None, optional) : - Mapping of model ids to models (default: None) - - This is needed in cases where the attributes to update also - have values that have references. + value: (JSON-value) : value to set to the attribute to setter (ClientSession or ServerSession or None, optional) : This is used to prevent "boomerang" updates to Bokeh apps. @@ -786,9 +780,7 @@ def set_from_json(self, obj: HasProps, value: Any, *, setter: Setter | None = No Args: obj (HasProps) : - json (JSON-dict) : - - models(seq[Model], optional) : + value (JSON-dict) : setter (ClientSession or ServerSession or None, optional) : This is used to prevent "boomerang" updates to Bokeh apps. diff --git a/src/bokeh/core/property/wrappers.py b/src/bokeh/core/property/wrappers.py index 6c342f7962d..312f04916bb 100644 --- a/src/bokeh/core/property/wrappers.py +++ b/src/bokeh/core/property/wrappers.py @@ -69,6 +69,8 @@ class SomeModel(Model): TYPE_CHECKING, Any, Iterable, + MutableSequence, + Sequence, TypeVar, ) @@ -76,6 +78,8 @@ class SomeModel(Model): import numpy as np if TYPE_CHECKING: + import numpy.typing as npt + from ...document import Document from ...document.events import DocumentPatchedEvent from ...models.sources import ColumnarDataSource @@ -210,7 +214,7 @@ class PropertyValueList(PropertyValueContainer, list[T]): """ def __init__(self, *args, **kwargs) -> None: - return super().__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _saved_copy(self) -> list[T]: return list(self) @@ -270,7 +274,7 @@ class PropertyValueSet(PropertyValueContainer, set[T]): """ def __init__(self, *args, **kwargs) -> None: - return super().__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _saved_copy(self) -> set[T]: return set(self) @@ -303,7 +307,9 @@ def symmetric_difference_update(self, s: Iterable[T]) -> None: def update(self, *s: Iterable[T]) -> None: super().update(*s) -class PropertyValueDict(PropertyValueContainer, dict): +T_Val = TypeVar("T_Val") + +class PropertyValueDict(PropertyValueContainer, dict[str, T_Val]): """ A dict property value container that supports change notifications on mutating operations. @@ -342,7 +348,7 @@ class PropertyValueDict(PropertyValueContainer, dict): """ def __init__(self, *args, **kwargs) -> None: - return super().__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _saved_copy(self): return dict(self) @@ -377,7 +383,7 @@ def setdefault(self, *args): def update(self, *args, **kwargs): return super().update(*args, **kwargs) -class PropertyValueColumnData(PropertyValueDict): +class PropertyValueColumnData(PropertyValueDict[Sequence[Any]]): """ A property value container for ColumnData that supports change notifications on mutating operations. @@ -435,7 +441,7 @@ def update(self, *args, **kwargs): return result # don't wrap with notify_owner --- notifies owners explicitly - def _stream(self, doc: Document, source: ColumnarDataSource, new_data: dict[str, Any], + def _stream(self, doc: Document, source: ColumnarDataSource, new_data: dict[str, Sequence[Any] | npt.NDArray[Any]], rollover: int | None = None, setter: Setter | None = None) -> None: """ Internal implementation to handle special-casing stream events on ``ColumnDataSource`` columns. @@ -468,17 +474,41 @@ def _stream(self, doc: Document, source: ColumnarDataSource, new_data: dict[str, # is actually the already updated value. This is because the method # self._saved_copy() makes a shallow copy. for k in new_data: - if isinstance(self[k], np.ndarray) or isinstance(new_data[k], np.ndarray): - data = np.append(self[k], new_data[k]) - if rollover is not None and len(data) > rollover: - data = data[len(data) - rollover:] + old_seq = self[k] + new_seq = new_data[k] + + if isinstance(old_seq, np.ndarray) or isinstance(new_seq, np.ndarray): + # Special case for streaming with empty arrays, to allow this: + # + # data_source = ColumnDataSource(data={"DateTime": []}) + # data_source.stream({"DateTime": np.array([np.datetime64("now")])) + # + # See https://github.com/bokeh/bokeh/issues/14004. + if len(old_seq) == 0: + seq = new_seq + elif len(new_seq) == 0: + seq = old_seq + else: + seq = np.append(old_seq, new_seq) + + if rollover is not None and len(seq) > rollover: + seq = seq[len(seq) - rollover:] + # call dict.__setitem__ directly, bypass wrapped version on base class - dict.__setitem__(self, k, data) + dict.__setitem__(self, k, seq) else: - L = self[k] - L.extend(new_data[k]) - if rollover is not None and len(L) > rollover: - del L[:len(L) - rollover] + def apply_rollover(seq: MutableSequence[Any]) -> None: + if rollover is not None and len(seq) > rollover: + del seq[:len(seq) - rollover] + + if isinstance(old_seq, MutableSequence): + seq = old_seq + seq.extend(new_seq) + apply_rollover(seq) + else: + seq = [*old_seq, *new_seq] + apply_rollover(seq) + dict.__setitem__(self, k, seq) from ...document.events import ColumnsStreamedEvent self._notify_owners(old, hint=ColumnsStreamedEvent(doc, source, "data", new_data, rollover, setter)) diff --git a/src/bokeh/core/query.py b/src/bokeh/core/query.py index c0963b017ef..2c06e33a9dd 100644 --- a/src/bokeh/core/query.py +++ b/src/bokeh/core/query.py @@ -62,7 +62,7 @@ def find(objs: Iterable[Model], selector: SelectorType) -> Iterable[Model]: a selector. Args: - obj (Model) : object to test + objs (Iterable[Model]) : model objects to test selector (JSON-like) : query selector Yields: diff --git a/src/bokeh/document/callbacks.py b/src/bokeh/document/callbacks.py index 753b6686b13..a4ebdd470cb 100644 --- a/src/bokeh/document/callbacks.py +++ b/src/bokeh/document/callbacks.py @@ -40,6 +40,7 @@ from ..util.callback_manager import _check_callback from .events import ( DocumentPatchedEvent, + MessageSentEvent, ModelChangedEvent, RootAddedEvent, RootRemovedEvent, @@ -381,6 +382,14 @@ def change_callbacks(self) -> tuple[DocumentChangeCallback, ...]: ''' return tuple(self._change_callbacks.values()) + def send_event(self, event: Event) -> None: + ''' Send a bokeh/model/UI event to the client. + + ''' + document = self._document() + if document is not None: + self.trigger_on_change(MessageSentEvent(document, "bokeh_event", event)) + def trigger_event(self, event: Event) -> None: # This is fairly gorpy, we are not being careful with model vs doc events, etc. if isinstance(event, ModelEvent): diff --git a/src/bokeh/document/document.py b/src/bokeh/document/document.py index 28d15ea36c9..dec9b7686ec 100644 --- a/src/bokeh/document/document.py +++ b/src/bokeh/document/document.py @@ -350,7 +350,7 @@ def apply_json_patch(self, patch_json: PatchJson | Serialized[PatchJson], *, set ''' Apply a JSON patch object and process any resulting events. Args: - patch (JSON-data) : + patch_json (JSON-data) : The JSON-object containing the patch to apply. setter (ClientSession or ServerSession or None, optional) : @@ -496,7 +496,7 @@ def hold(self, policy: HoldPolicyType = "combine") -> None: hold will be applied according to the hold policy. Args: - hold ('combine' or 'collect', optional) + policy ('combine' or 'collect', optional) Whether events collected during a hold should attempt to be combined (default: 'combine') @@ -716,7 +716,7 @@ def set_select(self, selector: SelectorType | type[Model], updates: dict[str, An Args: selector (JSON-like query dictionary) : you can query by type or by name,i e.g. ``{"type": HoverTool}``, ``{"name": "mycircle"}`` - updates (dict) : + updates (dict) : Returns: None diff --git a/src/bokeh/driving.py b/src/bokeh/driving.py index 84f44099ca6..1a6aab26773 100644 --- a/src/bokeh/driving.py +++ b/src/bokeh/driving.py @@ -50,7 +50,6 @@ def update(i): from typing import ( Any, Callable, - Iterable, Iterator, Sequence, TypeVar, @@ -146,7 +145,7 @@ def linear(m: float = 1, b: float = 0) -> partial[Callable[[], None]]: Args: m (float) : a slope for the linear driver - x (float) : an offset for the linear driver + b (float) : an offset for the linear driver ''' def f(i: float) -> float: @@ -200,7 +199,7 @@ def f(i: float) -> float: T = TypeVar("T") -def _advance(f: Callable[[int], T]) -> Iterable[T]: +def _advance(f: Callable[[int], T]) -> Iterator[T]: ''' Yield a sequence generated by calling a given function with successively incremented integer values. diff --git a/src/bokeh/embed/standalone.py b/src/bokeh/embed/standalone.py index 415d9a1781a..f333dd9e991 100644 --- a/src/bokeh/embed/standalone.py +++ b/src/bokeh/embed/standalone.py @@ -133,21 +133,21 @@ def autoload_static(model: Model | Document, resources: Resources, script_path: return js, tag @overload -def components(models: Model, wrap_script: bool = ..., # type: ignore[overload-overlap] # XXX: mypy bug +def components(models: Model, wrap_script: bool = ..., wrap_plot_info: Literal[True] = ..., theme: ThemeLike = ...) -> tuple[str, str]: ... @overload def components(models: Model, wrap_script: bool = ..., wrap_plot_info: Literal[False] = ..., theme: ThemeLike = ...) -> tuple[str, RenderRoot]: ... @overload -def components(models: Sequence[Model], wrap_script: bool = ..., # type: ignore[overload-overlap] # XXX: mypy bug +def components(models: Sequence[Model], wrap_script: bool = ..., wrap_plot_info: Literal[True] = ..., theme: ThemeLike = ...) -> tuple[str, Sequence[str]]: ... @overload def components(models: Sequence[Model], wrap_script: bool = ..., wrap_plot_info: Literal[False] = ..., theme: ThemeLike = ...) -> tuple[str, Sequence[RenderRoot]]: ... @overload -def components(models: dict[str, Model], wrap_script: bool = ..., # type: ignore[overload-overlap] # XXX: mypy bug +def components(models: dict[str, Model], wrap_script: bool = ..., wrap_plot_info: Literal[True] = ..., theme: ThemeLike = ...) -> tuple[str, dict[str, str]]: ... @overload def components(models: dict[str, Model], wrap_script: bool = ..., wrap_plot_info: Literal[False] = ..., diff --git a/src/bokeh/events.py b/src/bokeh/events.py index 142722f8952..b5b4070ee40 100644 --- a/src/bokeh/events.py +++ b/src/bokeh/events.py @@ -76,11 +76,12 @@ def callback(event): ) # Bokeh imports -from .core.serialization import Deserializer +from .core.serialization import Deserializer, Serializable, Serializer if TYPE_CHECKING: from .core.types import GeometryData from .model import Model + from .models.annotations import Legend, LegendItem from .models.plots import Plot from .models.widgets.buttons import AbstractButton from .models.widgets.inputs import TextInput @@ -98,6 +99,7 @@ def callback(event): 'Event', 'LODEnd', 'LODStart', + 'LegendItemClick', 'MenuItemClick', 'ModelEvent', 'MouseEnter', @@ -144,7 +146,7 @@ class EventRep(TypedDict): name: str values: dict[str, Any] -class Event: +class Event(Serializable): ''' Base class for all Bokeh events. This base class is not typically useful to instantiate on its own. @@ -167,6 +169,16 @@ def __init_subclass__(cls): if hasattr(cls, "event_name"): _CONCRETE_EVENT_CLASSES[cls.event_name] = cls + def event_values(self) -> dict[str, Any]: + return {} + + def to_serializable(self, serializer: Serializer) -> BokehEventRep: + return BokehEventRep( + type="event", + name=self.event_name, + values=serializer.encode(self.event_values()), + ) + @classmethod def from_serializable(cls, rep: EventRep, decoder: Deserializer) -> Event: if not ("name" in rep and "values" in rep): @@ -260,6 +272,8 @@ def __init__(self, model: Model | None) -> None: ''' self.model = model + def event_values(self) -> dict[str, Any]: + return dict(**super().event_values(), model=self.model) class ButtonClick(ModelEvent): ''' Announce a button click event on a Bokeh button widget. @@ -274,6 +288,16 @@ def __init__(self, model: AbstractButton | None) -> None: raise ValueError(f"{clsname} event only applies to button and button group models") super().__init__(model=model) +class LegendItemClick(ModelEvent): + ''' Announce a click event on a Bokeh legend item. + + ''' + event_name = 'legend_item_click' + + def __init__(self, model: Legend, item: LegendItem) -> None: + self.item = item + super().__init__(model=model) + class MenuItemClick(ModelEvent): ''' Announce a button click event on a Bokeh menu item. @@ -721,6 +745,11 @@ class RotateStart(PointEvent): # Dev API #----------------------------------------------------------------------------- +class BokehEventRep(TypedDict): + type: Literal["event"] + name: str + values: Any + #----------------------------------------------------------------------------- # Code #----------------------------------------------------------------------------- diff --git a/src/bokeh/layouts.py b/src/bokeh/layouts.py index 6cda7b7bc1b..fd1a50fb481 100644 --- a/src/bokeh/layouts.py +++ b/src/bokeh/layouts.py @@ -187,23 +187,56 @@ def layout(*args: UIElement, children: list[UIElement] | None = None, sizing_mod _children = _parse_children_arg(*args, children=children) return _create_grid(_children, sizing_mode, **kwargs) + +@overload +def gridplot( + children: list[UIElement | None], + *, + sizing_mode: SizingModeType | None = None, + toolbar_location: LocationType | None = "above", + ncols: int, + width: int | None = None, + height: int | None = None, + toolbar_options: dict[ToolbarOptions, Any] | None = None, + merge_tools: bool = True, +) -> GridPlot: + ... + + +@overload def gridplot( - children: list[list[UIElement | None]], *, - sizing_mode: SizingModeType | None = None, - toolbar_location: LocationType | None = "above", - ncols: int | None = None, - width: int | None = None, - height: int | None = None, - toolbar_options: dict[ToolbarOptions, Any] | None = None, - merge_tools: bool = True) -> GridPlot: + children: list[list[UIElement | None]], + *, + sizing_mode: SizingModeType | None = None, + toolbar_location: LocationType | None = "above", + ncols: None = None, + width: int | None = None, + height: int | None = None, + toolbar_options: dict[ToolbarOptions, Any] | None = None, + merge_tools: bool = True, +) -> GridPlot: + ... + + +def gridplot( + children: list[UIElement | None] | list[list[UIElement | None]], + *, + sizing_mode: SizingModeType | None = None, + toolbar_location: LocationType | None = "above", + ncols: int | None = None, + width: int | None = None, + height: int | None = None, + toolbar_options: dict[ToolbarOptions, Any] | None = None, + merge_tools: bool = True, +) -> GridPlot: ''' Create a grid of plots rendered on separate canvases. The ``gridplot`` function builds a single toolbar for all the plots in the - grid. ``gridplot`` is designed to layout a set of plots. For general + grid. ``gridplot`` is designed to lay out a set of plots. For general grid layout, use the :func:`~bokeh.layouts.layout` function. Args: - children (list of lists of |Plot|): An array of plots to display in a + children (list or list of lists of |Plot|): An array of plots to display in a grid, given as a list of lists of Plot objects. To leave a position in the grid empty, pass None for that position in the children list. OR list of |Plot| if called with ncols. @@ -308,13 +341,20 @@ def merge(cls: type[Tool], group: list[Tool]) -> Tool | ToolProxy | None: if merge_tools: tools = group_tools(tools, merge=merge) + def map_to_proxy(active_tool: Tool | Literal["auto"] | None) -> ToolProxy | Tool: + if isinstance(active_tool, Tool): + for tool_or_proxy in tools: + if isinstance(tool_or_proxy, ToolProxy) and active_tool in tool_or_proxy.tools: + return tool_or_proxy + return active_tool + logos = [ toolbar.logo for toolbar in toolbars ] autohides = [ toolbar.autohide for toolbar in toolbars ] - active_drags = [ toolbar.active_drag for toolbar in toolbars ] - active_inspects = [ toolbar.active_inspect for toolbar in toolbars ] - active_scrolls = [ toolbar.active_scroll for toolbar in toolbars ] - active_taps = [ toolbar.active_tap for toolbar in toolbars ] - active_multis = [ toolbar.active_multi for toolbar in toolbars ] + active_drags = [ map_to_proxy(toolbar.active_drag) for toolbar in toolbars ] + active_inspects = [ map_to_proxy(toolbar.active_inspect) for toolbar in toolbars ] # TODO list[Tool] + active_scrolls = [ map_to_proxy(toolbar.active_scroll) for toolbar in toolbars ] + active_taps = [ map_to_proxy(toolbar.active_tap) for toolbar in toolbars ] + active_multis = [ map_to_proxy(toolbar.active_multi) for toolbar in toolbars ] V = TypeVar("V") def assert_unique(values: list[V], name: ToolbarOptions) -> V | UndefinedType: @@ -597,9 +637,7 @@ class ToolEntry: entries.remove(item) entries.remove(head) - if len(group) == 1: - computed.append(group[0]) - elif merge is not None and (tool := merge(cls, group)) is not None: + if merge is not None and (tool := merge(cls, group)) is not None: computed.append(tool) else: computed.append(ToolProxy(tools=group)) diff --git a/src/bokeh/models/__init__.py b/src/bokeh/models/__init__.py index 7cafc18536f..78be6dcff2d 100644 --- a/src/bokeh/models/__init__.py +++ b/src/bokeh/models/__init__.py @@ -35,6 +35,7 @@ axes, callbacks, canvas, + comparisons, coordinates, css, expressions, @@ -47,6 +48,7 @@ layouts, map_plots, mappers, + misc, nodes, plots, ranges, @@ -68,6 +70,7 @@ from .axes import * from .callbacks import * from .canvas import * +from .comparisons import * from .coordinates import * from .css import * from .expressions import * @@ -80,6 +83,7 @@ from .layouts import * from .map_plots import * from .mappers import * +from .misc import * from .nodes import * from .plots import * from .ranges import * @@ -107,6 +111,7 @@ *axes.__all__, *callbacks.__all__, *canvas.__all__, + *comparisons.__all__, *coordinates.__all__, *css.__all__, *expressions.__all__, @@ -119,6 +124,7 @@ *layouts.__all__, *map_plots.__all__, *mappers.__all__, + *misc.__all__, *nodes.__all__, *plots.__all__, *ranges.__all__, diff --git a/src/bokeh/models/annotations/geometry.py b/src/bokeh/models/annotations/geometry.py index 00c7336b5f8..1b2828d0f0a 100644 --- a/src/bokeh/models/annotations/geometry.py +++ b/src/bokeh/models/annotations/geometry.py @@ -43,6 +43,7 @@ Nullable, Override, Positive, + Required, Seq, UnitsSpec, field, @@ -54,6 +55,7 @@ ScalarHatchProps, ScalarLineProps, ) +from ...model import Model from ..common.properties import Coordinate from ..nodes import BoxNodes, Node from .annotation import Annotation, DataAnnotation @@ -66,6 +68,7 @@ __all__ = ( "Band", "BoxAnnotation", + "BoxInteractionHandles", "PolyAnnotation", "Slope", "Span", @@ -76,7 +79,77 @@ # General API #----------------------------------------------------------------------------- -class BoxAnnotation(Annotation): +class AreaVisuals(Model): + """ Allows to style line, fill and hatch visuals. """ + + # explicit __init__ to support Init signatures + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + line_props = Include(ScalarLineProps, help=""" + The {prop} values for the box. + """) + + fill_props = Include(ScalarFillProps, help=""" + The {prop} values for the box. + """) + + hatch_props = Include(ScalarHatchProps, help=""" + The {prop} values for the box. + """) + + hover_line_props = Include(ScalarLineProps, prefix="hover", help=""" + The {prop} values for the box when hovering over. + """) + + hover_fill_props = Include(ScalarFillProps, prefix="hover", help=""" + The {prop} values for the box when hovering over. + """) + + hover_hatch_props = Include(ScalarHatchProps, prefix="hover", help=""" + The {prop} values for the box when hovering over. + """) + +class BoxInteractionHandles(Model): + """ Defines interaction handles for box-like annotations. + + """ + + # explicit __init__ to support Init signatures + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + all = Required(Instance(AreaVisuals)) # move, resize + + move = Nullable(Instance(AreaVisuals)) + resize = Nullable(Instance(AreaVisuals)) # sides, corners + + sides = Nullable(Instance(AreaVisuals)) # left, right, top, bottom + corners = Nullable(Instance(AreaVisuals)) # top_left, top_right, bottom_left, bottom_right + + left = Nullable(Instance(AreaVisuals)) + right = Nullable(Instance(AreaVisuals)) + top = Nullable(Instance(AreaVisuals)) + bottom = Nullable(Instance(AreaVisuals)) + + top_left = Nullable(Instance(AreaVisuals)) + top_right = Nullable(Instance(AreaVisuals)) + bottom_left = Nullable(Instance(AreaVisuals)) + bottom_right = Nullable(Instance(AreaVisuals)) + +DEFAULT_BOX_ANNOTATION_HANDLES = lambda: \ + BoxInteractionHandles( + all=AreaVisuals( + fill_color="white", + fill_alpha=1.0, + line_color="black", + line_alpha=1.0, + hover_fill_color="lightgray", + hover_fill_alpha=1.0, + ), + ) + +class BoxAnnotation(Annotation, AreaVisuals): ''' Render a shaded rectangular region as an annotation. See :ref:`ug_basic_annotations_box_annotations` for information on plotting box annotations. @@ -217,34 +290,35 @@ def __init__(self, *args, **kwargs) -> None: This property is experimental and may change at any point. """) - inverted = Bool(default=False, help=""" - Inverts the geometry of the box, i.e. applies fill and hatch visuals - to the outside of the box instead of the inside. Visuals are applied - between the box and its parent, e.g. the frame. - """) + use_handles = Bool(default=False, help=""" + Whether to show interaction (move, resize, etc.) handles. - line_props = Include(ScalarLineProps, help=""" - The {prop} values for the box. - """) + If handles aren't used, then the whole annotation, its borders and corners + act as if they were interaction handles. - fill_props = Include(ScalarFillProps, help=""" - The {prop} values for the box. + .. note:: + This property is experimental and may change at any point. """) - hatch_props = Include(ScalarHatchProps, help=""" - The {prop} values for the box. - """) + handles = Instance(BoxInteractionHandles, default=DEFAULT_BOX_ANNOTATION_HANDLES, help=""" + Configure appearance of interaction handles. - hover_line_props = Include(ScalarLineProps, prefix="hover", help=""" - The {prop} values for the box when hovering over. - """) + Handles can be configured in bulk in an increasing level of specificity, + were each level, if defined, overrides the more generic setting: - hover_fill_props = Include(ScalarFillProps, prefix="hover", help=""" - The {prop} values for the box when hovering over. - """) + - `all` -> `move`, `resize` + - `resize` -> `sides`, `corners` + - `sides` -> `left`, `right`, `top`, `bottom` + - `corners` -> `top_left`, `top_right`, `bottom_left`, `bottom_right` - hover_hatch_props = Include(ScalarHatchProps, prefix="hover", help=""" - The {prop} values for the box when hovering over. + .. note:: + This property is experimental and may change at any point. + """).accepts(Instance(AreaVisuals), lambda obj: BoxInteractionHandles(all=obj)) + + inverted = Bool(default=False, help=""" + Inverts the geometry of the box, i.e. applies fill and hatch visuals + to the outside of the box instead of the inside. Visuals are applied + between the box and its parent, e.g. the frame. """) line_color = Override(default="#cccccc") diff --git a/src/bokeh/models/annotations/labels.py b/src/bokeh/models/annotations/labels.py index 35dccdc0724..7bb9871a29a 100644 --- a/src/bokeh/models/annotations/labels.py +++ b/src/bokeh/models/annotations/labels.py @@ -316,10 +316,10 @@ def __init__(self, *args, **kwargs) -> None: Offset the text by a number of pixels (can be positive or negative). Shifts the text in different directions based on the location of the title: - * above: shifts title right - * right: shifts title down - * below: shifts title right - * left: shifts title up + * above: shifts title right + * right: shifts title down + * below: shifts title right + * left: shifts title up """) diff --git a/src/bokeh/models/annotations/legends.py b/src/bokeh/models/annotations/legends.py index b1063fc7fef..35c929c537b 100644 --- a/src/bokeh/models/annotations/legends.py +++ b/src/bokeh/models/annotations/legends.py @@ -21,7 +21,7 @@ #----------------------------------------------------------------------------- # Standard library imports -from typing import Any +from typing import TYPE_CHECKING, Any # Bokeh imports from ...core.enums import ( @@ -70,6 +70,7 @@ NON_MATCHING_DATA_SOURCES_ON_LEGEND_ITEM_RENDERERS, NON_MATCHING_SCALE_BAR_UNIT, ) +from ...events import LegendItemClick from ...model import Model from ..formatters import TickFormatter from ..labeling import LabelingPolicy, NoOverlap @@ -80,6 +81,10 @@ from .annotation import Annotation from .dimensional import Dimensional, MetricLength +if TYPE_CHECKING: + from ...util.callback_manager import EventCallback as PyEventCallback + from ..callbacks import Callback as JsEventCallback + #----------------------------------------------------------------------------- # Globals and constants #----------------------------------------------------------------------------- @@ -518,6 +523,14 @@ def __init__(self, *args, **kwargs) -> None: """).accepts(List(Tuple(String, List(Instance(GlyphRenderer)))), lambda items: [LegendItem(label=item[0], renderers=item[1]) for item in items]) + def on_click(self, handler: PyEventCallback) -> None: + """ Set up a handler for legend item clicks. """ + self.on_event(LegendItemClick, handler) + + def js_on_click(self, handler: JsEventCallback) -> None: + """ Set up a JavaScript handler for legend item clicks. """ + self.js_on_event(LegendItemClick, handler) + class ScaleBar(Annotation): """ Represents a scale bar annotation. """ diff --git a/src/bokeh/models/comparisons.py b/src/bokeh/models/comparisons.py new file mode 100644 index 00000000000..221ba83eb51 --- /dev/null +++ b/src/bokeh/models/comparisons.py @@ -0,0 +1,123 @@ +#----------------------------------------------------------------------------- +# Copyright (c) Anaconda, Inc., and Bokeh Contributors. +# All rights reserved. +# +# The full license is in the file LICENSE.txt, distributed with this software. +#----------------------------------------------------------------------------- +''' Represent comparisons to be computed on the client (browser) side by +BokehJS. These comparisons may be useful for specifying how DataTable columns +are sorted. + +''' + +#----------------------------------------------------------------------------- +# Boilerplate +#----------------------------------------------------------------------------- +from __future__ import annotations + +import logging # isort:skip +log = logging.getLogger(__name__) + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +# Bokeh imports +from ..core.has_props import abstract +from ..core.properties import ( + AnyRef, + Bool, + Dict, + String, +) +from ..model import Model + +#----------------------------------------------------------------------------- +# Globals and constants +#----------------------------------------------------------------------------- + +__all__ = ( + 'Comparison', + 'CustomJSCompare', + 'NanCompare', +) + +#----------------------------------------------------------------------------- +# General API +#----------------------------------------------------------------------------- + +@abstract +class Comparison(Model): + ''' Base class for ``Comparison`` models that represent a comparison + to be carried out on the client-side. + + The JavaScript implementation should implement the following method: + + .. code-block:: typescript + + compute(x: unknown, y: unknown): -1 | 0 | 1 { + // compare and return -1, 0, or 1 + } + + ''' + + # explicit __init__ to support Init signatures + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + +class CustomJSCompare(Comparison): + ''' A client-side comparison performed by evaluating a user-supplied + JavaScript function. This comparison can be useful for DataTable columns. + + .. warning:: + The explicit purpose of this Bokeh Model is to embed *raw JavaScript + code* for a browser to execute. If any part of the code is derived + from untrusted user inputs, then you must take appropriate care to + sanitize the user input prior to passing to Bokeh. + + ''' + + # explicit __init__ to support Init signatures + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + args = Dict(String, AnyRef, help=""" + A mapping of names to Python objects. In particular those can be bokeh's models. + These objects are made available to the callback's code snippet as the values of + named parameters to the callback. There is no need to manually include the data + source of the associated glyph renderer, as it is available within the scope of + the code via `this` keyword (e.g. `this.data` will give access to raw data). + """) + + code = String(default="", help=""" + A snippet of JavaScript code to execute in the browser. The code is made into + the body of a generator function and all of the named objects in ``args`` + are available as parameters that the code can use. Must return -1, 0, or 1. + """) + +class NanCompare(Comparison): + ''' A client-side comparison that can sort NaN values first or last. This + comparison can be useful for DataTable columns. + + ''' + + # explicit __init__ to support Init signatures + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + ascending_first = Bool(default=False, help=""" + Whether NaN values should appear first or last in an ascending sort. + """) + +#----------------------------------------------------------------------------- +# Dev API +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Private API +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Code +#----------------------------------------------------------------------------- diff --git a/src/bokeh/models/dom.py b/src/bokeh/models/dom.py index d6edbb3c831..68b3d354d7f 100644 --- a/src/bokeh/models/dom.py +++ b/src/bokeh/models/dom.py @@ -24,13 +24,16 @@ from typing import Any # Bokeh imports +from ..core.enums import BuiltinFormatter from ..core.has_props import HasProps, abstract from ..core.properties import ( Bool, Dict, Either, + Enum, Instance, List, + Nullable, Required, String, ) @@ -136,7 +139,7 @@ def __init__(self, *args, **kwargs) -> None: groups = List(Instance(".models.renderers.RendererGroup")) @abstract -class Placeholder(DOMNode): +class Placeholder(DOMElement): # explicit __init__ to support Init signatures def __init__(self, *args, **kwargs) -> None: @@ -170,12 +173,38 @@ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) class ValueRef(Placeholder): + """ Allows to reference a value in a column of a data source. + """ # explicit __init__ to support Init signatures def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - field = Required(String) + field = Required(String, help=""" + The name of the field to reference, which is equivalent to using ``"@{field}``. + """) + + format = Nullable(String, default=None, help=""" + Optional format string, which is equivalent to using ``"@{field}{format}"``. + """) + + formatter = Either( + Enum(BuiltinFormatter), + Instance(".models.callbacks.CustomJS"), + Instance(".models.tools.CustomJSHover"), default="raw", help=""" + Either a named value formatter or an instance of ``CustomJS`` or ``CustomJSHover``. + + .. note:: + Custom JS formatters can return a value of any type, not necessarily a string. + If a non-string value is returned then, if it's an instance of DOM `Node`_ + (in particular it can be a DOM `Document`_ or a `DocumentFragment`_), then + it will be added to the DOM tree as-is, otherwise it will be converted to a + string and added verbatim. No HTML parsing is attempted in any case. + + .. _Node: https://developer.mozilla.org/en-US/docs/Web/API/Node + .. _Document: https://developer.mozilla.org/en-US/docs/Web/API/Document + .. _DocumentFragment: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment + """) class ColorRef(ValueRef): @@ -186,7 +215,7 @@ def __init__(self, *args, **kwargs) -> None: hex = Bool(default=True) swatch = Bool(default=True) -class HTML(DOMNode): +class HTML(DOMElement): """ A parsed HTML fragment with optional references to DOM nodes and UI elements. """ def __init__(self, *html: str | DOMNode | UIElement, **kwargs: Any) -> None: diff --git a/src/bokeh/models/glyphs.py b/src/bokeh/models/glyphs.py index 6db098a324f..8e7d4b4ed9c 100644 --- a/src/bokeh/models/glyphs.py +++ b/src/bokeh/models/glyphs.py @@ -7,8 +7,6 @@ ''' Display a variety of visual shapes whose attributes can be associated with data columns from ``ColumnDataSources``. - - The full list of glyphs is below: .. toctree:: @@ -40,6 +38,7 @@ from ..core.enums import ( Direction, ImageOrigin, + OutlineShapeName, Palette, StepMode, enumeration, @@ -1560,6 +1559,26 @@ def __init__(self, *args, **kwargs) -> None: This property is experimental and may change at any point. """) + outline_shape = DataSpec(Enum(OutlineShapeName), default="box", help=""" + Specify the shape of the outline for the text box. + + The default outline is of a text box is its bounding box (or rectangle). + This can be changed to a selection of pre-defined shapes, like circle, + ellipse, diamond, parallelogram, etc. Those shapes are circumscribed onto + the bounding box, so that the contents of a box fit inside those shapes. + + This property is in effect only when either border line, background fill + and/or background hatch properties are set. The user can choose ``"none"`` + to avoid drawing any shape, even if border or background visuals are set. + + .. note:: + This property is experimental and may change at any point. + + .. note:: + Currently hit testing only uses the bounding box of text contents + of the glyph, which is equivalent to using box/rectangle shape. + """) + text_props = Include(TextProps, help=""" The {prop} values for the text. """) diff --git a/src/bokeh/models/misc/__init__.py b/src/bokeh/models/misc/__init__.py new file mode 100644 index 00000000000..745657d17ec --- /dev/null +++ b/src/bokeh/models/misc/__init__.py @@ -0,0 +1,47 @@ +#----------------------------------------------------------------------------- +# Copyright (c) Anaconda, Inc., and Bokeh Contributors. +# All rights reserved. +# +# The full license is in the file LICENSE.txt, distributed with this software. +#----------------------------------------------------------------------------- +""" Various miscellaneous models. """ + +#----------------------------------------------------------------------------- +# Boilerplate +#----------------------------------------------------------------------------- +from __future__ import annotations + +import logging # isort:skip +log = logging.getLogger(__name__) + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +# Bokeh imports +from . import group_by +from .group_by import * + +#----------------------------------------------------------------------------- +# Globals and constants +#----------------------------------------------------------------------------- + +__all__ = ( + *group_by.__all__, +) + +#----------------------------------------------------------------------------- +# General API +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Dev API +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Private API +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Code +#----------------------------------------------------------------------------- diff --git a/src/bokeh/models/misc/group_by.py b/src/bokeh/models/misc/group_by.py new file mode 100644 index 00000000000..bed3f060707 --- /dev/null +++ b/src/bokeh/models/misc/group_by.py @@ -0,0 +1,77 @@ +#----------------------------------------------------------------------------- +# Copyright (c) Anaconda, Inc., and Bokeh Contributors. +# All rights reserved. +# +# The full license is in the file LICENSE.txt, distributed with this software. +#----------------------------------------------------------------------------- +""" Models for describing grouping behavior of collections of models. """ + +#----------------------------------------------------------------------------- +# Boilerplate +#----------------------------------------------------------------------------- +from __future__ import annotations + +import logging # isort:skip +log = logging.getLogger(__name__) + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +# Bokeh imports +from ...core.has_props import abstract +from ...core.properties import Instance, List, Required +from ...model import Model + +#----------------------------------------------------------------------------- +# Globals and constants +#----------------------------------------------------------------------------- + +__all__ = ( + "GroupByModels", + "GroupByName", +) + +#----------------------------------------------------------------------------- +# General API +#----------------------------------------------------------------------------- + +@abstract +class GroupBy(Model): + """ Base class for grouping behaviors. """ + + # explicit __init__ to support Init signatures + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + +class GroupByModels(GroupBy): + """ Group models by manually predefined groups. """ + + # explicit __init__ to support Init signatures + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + groups = Required(List(List(Instance(Model))), help=""" + Predefined groups of models. + """) + +class GroupByName(GroupBy): + """ Group models by their names (``Model.name`` property). """ + + # explicit __init__ to support Init signatures + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + +# TODO GroupByCustomJS(GroupBy) + +#----------------------------------------------------------------------------- +# Dev API +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Private API +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Code +#----------------------------------------------------------------------------- diff --git a/src/bokeh/models/tickers.py b/src/bokeh/models/tickers.py index f2943cad10c..5488e57332a 100644 --- a/src/bokeh/models/tickers.py +++ b/src/bokeh/models/tickers.py @@ -25,16 +25,20 @@ from ..core.enums import LatLon from ..core.has_props import abstract from ..core.properties import ( + AnyRef, Auto, + Dict, Either, Enum, Float, Instance, Int, + NonEmpty, Nullable, Override, Required, Seq, + String, ) from ..core.validation import error from ..core.validation.errors import MISSING_MERCATOR_DIMENSION @@ -46,21 +50,22 @@ #----------------------------------------------------------------------------- __all__ = ( - 'Ticker', - 'BinnedTicker', - 'ContinuousTicker', - 'FixedTicker', 'AdaptiveTicker', + 'BasicTicker', + 'BinnedTicker', + 'CategoricalTicker', 'CompositeTicker', - 'SingleIntervalTicker', + 'ContinuousTicker', + 'CustomJSTicker', + 'DatetimeTicker', 'DaysTicker', - 'MonthsTicker', - 'YearsTicker', - 'BasicTicker', + 'FixedTicker', 'LogTicker', 'MercatorTicker', - 'CategoricalTicker', - 'DatetimeTicker', + 'MonthsTicker', + 'SingleIntervalTicker', + 'Ticker', + 'YearsTicker', ) #----------------------------------------------------------------------------- @@ -77,6 +82,78 @@ class Ticker(Model): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) +class CustomJSTicker(Ticker): + ''' Generate tick locations that are computed by a user-defined function. + + A ``CustomJSTicker`` may be used with either a continuous (numeric) axis, + or a categorical axis. However, only basic, non-hierarchical categorical + axes (i.e. with a single level of factors) are supported. + + .. warning:: + The explicit purpose of this Bokeh Model is to embed *raw JavaScript + code* for a browser to execute. If any part of the code is derived + from untrusted user inputs, then you must take appropriate care to + sanitize the user input prior to passing to Bokeh. + + ''' + + # explicit __init__ to support Init signatures + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + args = Dict(String, AnyRef, help=""" + A mapping of names to Python objects. In particular those can be bokeh's models. + These objects are made available to the ticker's code snippet as the values of + named parameters to the callback. + """) + + major_code = String(default="", help=""" + Callback code to run in the browser to compute minor tick locations for the + current viewport. + + The ``cb_data`` parameter that is available to the callback code will contain + four specific fields: + + ``start`` + the computed start coordinate of the axis + + ``end`` + the computed end of the axis + + ``range`` + the Range model for this axis + + ``cross_loc`` + the coordinate that this axis intersects the orthogonal axis + """) + + minor_code = String(default="", help=""" + Callback code to run in the browser to compute minor tick locations for the + current viewport. + + .. note:: + Minor ticks are not used for categorical axes. This property will be + ignored when the range is a ``FactorRange``. + + The ``cb_data`` parameter that is available to the callback code will contain + five specific fields: + + ``major_ticks`` + the list of the current computed major tick locations + + ``start`` + the computed start coordinate of the axis + + ``end`` + the computed end of the axis + + ``range`` + the Range model for this axis + + ``cross_loc`` + the coordinate that this axis intersects the orthogonal axis + """) + @abstract class ContinuousTicker(Ticker): ''' A base class for non-categorical ticker types. @@ -169,10 +246,12 @@ class CompositeTicker(ContinuousTicker): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - tickers = Seq(Instance(Ticker), default=[], help=""" - A list of Ticker objects to combine at different scales in order + tickers = NonEmpty(Seq(Instance(Ticker)), help=""" + A list of ``Ticker`` objects to combine at different scales in order to generate tick values. The supplied tickers should be in order. - Specifically, if S comes before T, then it should be the case that:: + Specifically, if S comes before T, then it should be the case that: + + .. code-block:: javascript S.get_max_interval() < T.get_min_interval() diff --git a/src/bokeh/models/tools.py b/src/bokeh/models/tools.py index 0948ab4b4e5..07f8fe35815 100644 --- a/src/bokeh/models/tools.py +++ b/src/bokeh/models/tools.py @@ -51,6 +51,7 @@ Dimension, Dimensions, KeyModifierType, + RegionSelectionMode, SelectionMode, ToolIcon, TooltipAttachment, @@ -95,7 +96,7 @@ from ..util.strings import nice_join from .annotations import BoxAnnotation, PolyAnnotation, Span from .callbacks import Callback -from .dom import Template +from .dom import DOMElement from .glyphs import ( HStrip, Line, @@ -107,6 +108,7 @@ VStrip, XYGlyph, ) +from .misc.group_by import GroupBy, GroupByModels, GroupByName from .nodes import Node from .ranges import Range from .renderers import DataRenderer, GlyphRenderer @@ -319,12 +321,6 @@ def __init__(self, *args, **kwargs) -> None: all renderers on a plot. """) - mode = Enum(SelectionMode, default="replace", help=""" - Defines what should happen when a new selection is made. The default - is to replace the existing selection. Other options are to append to - the selection, intersect with it or subtract from it. - """) - @abstract class RegionSelectTool(SelectTool): ''' Base class for region selection tools (e.g. box, polygon, lasso). @@ -335,6 +331,18 @@ class RegionSelectTool(SelectTool): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) + mode = Enum(RegionSelectionMode, default="replace", help=""" + Defines what should happen when a new selection is made. The default + is to replace the existing selection. Other options are to append to + the selection, intersect with it or subtract from it. + + Defines what should happen when a new selection is made. + + The default is to replace the existing selection. Other options are to + append to the selection, intersect with it, subtract from it or compute + a symmetric difference with it. + """) + continuous = Bool(False, help=""" Whether a selection computation should happen continuously during selection gestures, or only once when the selection region is completed. @@ -389,25 +397,25 @@ def __init__(self, *args, **kwargs) -> None: A list of tools to add to the plot. """) - active_drag: Literal["auto"] | Drag | None = Either(Null, Auto, Instance(Drag), default="auto", help=""" + active_drag: Literal["auto"] | Drag | ToolProxy | None = Either(Null, Auto, Instance(Drag), Instance(ToolProxy), default="auto", help=""" Specify a drag tool to be active when the plot is displayed. """) - active_inspect: Literal["auto"] | InspectTool | tp.Sequence[InspectTool] | None = \ - Either(Null, Auto, Instance(InspectTool), Seq(Instance(InspectTool)), default="auto", help=""" + active_inspect: Literal["auto"] | InspectTool | ToolProxy | tp.Sequence[InspectTool] | None = \ + Either(Null, Auto, Instance(InspectTool), Instance(ToolProxy), Seq(Instance(InspectTool)), default="auto", help=""" Specify an inspection tool or sequence of inspection tools to be active when the plot is displayed. """) - active_scroll: Literal["auto"] | Scroll | None = Either(Null, Auto, Instance(Scroll), default="auto", help=""" + active_scroll: Literal["auto"] | Scroll | ToolProxy | None = Either(Null, Auto, Instance(Scroll), Instance(ToolProxy), default="auto", help=""" Specify a scroll/pinch tool to be active when the plot is displayed. """) - active_tap: Literal["auto"] | Tap | None = Either(Null, Auto, Instance(Tap), default="auto", help=""" + active_tap: Literal["auto"] | Tap | ToolProxy | None = Either(Null, Auto, Instance(Tap), Instance(ToolProxy), default="auto", help=""" Specify a tap/click tool to be active when the plot is displayed. """) - active_multi: Literal["auto"] | GestureTool | None = Either(Null, Auto, Instance(GestureTool), default="auto", help=""" + active_multi: Literal["auto"] | GestureTool | ToolProxy | None = Either(Null, Auto, Instance(GestureTool), Instance(ToolProxy), default="auto", help=""" Specify an active multi-gesture tool, for instance an edit tool or a range tool. @@ -524,6 +532,25 @@ def __init__(self, *args, **kwargs) -> None: A shaded annotation drawn to indicate the configured ranges. """) + start_gesture = Enum("pan", "tap", "none", default="none", help=""" + Which gesture will start a range update interaction in a new location. + + When the value is ``"pan"``, a new range starts at the location where + a pointer drag operation begins. The range is updated continuously while + the drag operation continues. Ending the drag operation sets the final + value of the range. + + When the value is ``"tap"``, a new range starts at the location where + a single tap is made. The range is updated continuously while the pointer + moves. Tapping at another location sets the final value of the range. + + When the value is ``"none"``, only existing range definitions may be + updated, by dragging their edges or interiors. + + Configuring this property allows to make this tool simultaneously co-exist + with another tool that would otherwise share a gesture. + """) + @error(NO_RANGE_TOOL_RANGES) def _check_no_range_tool_ranges(self): if self.x_range is None and self.y_range is None: @@ -620,6 +647,42 @@ def __init__(self, *args, **kwargs) -> None: """) # } + hit_test = Bool(default=False, help=""" + Whether to zoom only those renderer that are being pointed at. + + This setting only applies when zooming renderers that were configured with + sub-coordinates, otherwise it has no effect. + + If ``True``, then ``hit_test_mode`` property defines how hit testing + is performed and ``hit_test_behavior`` allows to configure other aspects + of this setup. See respective properties for details. + + .. note:: + This property is experimental and may change at any point + """) + + hit_test_mode = Enum("point", "hline", "vline", default="point", help=""" + Allows to configure what geometry to use when ``hit_test`` is enabled. + + Supported modes are ``"point"`` for single point hit testing, and ``hline`` + and ``vline`` for either horizontal or vertical span hit testing. + + .. note:: + This property is experimental and may change at any point + """) + + hit_test_behavior = Either(Instance(GroupBy), Enum("only_hit"), default="only_hit", help=""" + Allows to configure which renderers will be zoomed when ``hit_test`` is enabled. + + By default (``hit_only``) only actually hit renderers will be zoomed. An + instance of ``GroupBy`` model can be used to tell what other renderers + should be zoomed when a given one is hit. + + .. note:: + This property is experimental and may change at any point + """).accepts(Enum("group_by_name"), lambda _: GroupByName()) \ + .accepts(List(List(Instance(DataRenderer))), lambda groups: GroupByModels(groups=groups)) + maintain_focus = Bool(default=True, help=""" If True, then hitting a range bound in any one dimension will prevent all further zooming all dimensions. If False, zooming can continue @@ -792,6 +855,14 @@ class TapTool(Tap, SelectTool): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) + mode = Enum(SelectionMode, default="toggle", help=""" + Defines what should happen when a new selection is made. + + The default is to toggle the existing selection. Other options are to + replace the selection, append to it, intersect with it, subtract from + it or compute a symmetric difference with it. + """) + behavior = Enum("select", "inspect", default="select", help=""" This tool can be configured to either make selections or inspections on associated data sources. The difference is that selection changes @@ -859,8 +930,6 @@ def __init__(self, *args, **kwargs) -> None: """) - mode = Override(default="xor") - class CrosshairTool(InspectTool): ''' *toolbar icon*: |crosshair_icon| @@ -1382,12 +1451,12 @@ def __init__(self, *args, **kwargs) -> None: :geometry: object containing the coordinates of the hover cursor """) - tooltips = Either(Null, Instance(Template), String, List(Tuple(String, String)), - default=[ - ("index","$index"), - ("data (x, y)","($x, $y)"), - ("screen (x, y)","($sx, $sy)"), - ], help=""" + tooltips = Either(Null, Instance(DOMElement), String, List(Tuple(String, String)), + default=[ + ("index","$index"), + ("data (x, y)","($x, $y)"), + ("screen (x, y)","($sx, $sy)"), + ], help=""" The (name, field) pairs describing what the hover tool should display when there is a hit. @@ -1975,6 +2044,8 @@ def __init__(self, *args, **kwargs) -> None: Tool.register_alias("tap", lambda: TapTool()) Tool.register_alias("doubletap", lambda: TapTool(gesture="doubletap")) Tool.register_alias("crosshair", lambda: CrosshairTool()) +Tool.register_alias("xcrosshair", lambda: CrosshairTool(dimensions="width")) +Tool.register_alias("ycrosshair", lambda: CrosshairTool(dimensions="height")) Tool.register_alias("box_select", lambda: BoxSelectTool()) Tool.register_alias("xbox_select", lambda: BoxSelectTool(dimensions="width")) Tool.register_alias("ybox_select", lambda: BoxSelectTool(dimensions="height")) diff --git a/src/bokeh/models/widgets/inputs.py b/src/bokeh/models/widgets/inputs.py index d0a0ad92087..bfc8faac1e1 100644 --- a/src/bokeh/models/widgets/inputs.py +++ b/src/bokeh/models/widgets/inputs.py @@ -51,6 +51,7 @@ String, Tuple, ) +from ...events import ModelEvent from ...util.deprecation import deprecated from ..dom import HTML from ..formatters import TickFormatter @@ -84,7 +85,6 @@ # Dev API #----------------------------------------------------------------------------- - @abstract class InputWidget(Widget): ''' Abstract base class for input widgets. @@ -107,6 +107,22 @@ def __init__(self, *args, **kwargs) -> None: # General API #----------------------------------------------------------------------------- +# TODO mark this as a one way event from server to client +class ClearInput(ModelEvent): + """ + Notifies the input widget that its input/value needs to be cleared. + + This is specially useful for widgets whose value can't be simply cleared by + assigning to ``value`` (or equivalent) property. + + """ + event_name = "clear_input" + + def __init__(self, model: InputWidget) -> None: + if not isinstance(model, InputWidget): + raise ValueError(f"{self.__class__.__name__} event only applies to input models, i.e. instances of bokeh.models.widgets.InputWidget") + super().__init__(model=model) + class FileInput(InputWidget): ''' Present a file-chooser dialog to users and return the contents of the selected files. @@ -117,7 +133,7 @@ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) value = Readonly(Either(String, List(String)), help=''' - The base64-enconded contents of the file or files that were loaded. + The base64-encoded contents of the file or files that were loaded. If `multiple` is set to False (default), this value is a single string with the contents of the single file that was chosen. @@ -191,6 +207,30 @@ def __init__(self, *args, **kwargs) -> None: selection of more than one file at a time should be possible. """) + directory = Bool(default=False, help=""" + Whether to allow selection of directories instead of files. + + The filename will be relative paths to the uploaded directory. + + .. note:: + When a directory is uploaded it will give add a confirmation pop up. + The confirmation pop up cannot be disabled, as this is a security feature + in the browser. + + .. note:: + The `accept` parameter only works with file extension. + When using `accept` with `directory`, the number of files + reported will be the total amount of files, not the filtered. + """) + + + def clear(self) -> None: + """ Clear the contents of this file input widget. + + """ + doc = self.document + if doc is not None: + doc.callbacks.send_event(ClearInput(self)) class NumericInput(InputWidget): ''' Numeric input widget. diff --git a/src/bokeh/models/widgets/tables.py b/src/bokeh/models/widgets/tables.py index a66ddcb2347..658e0220187 100644 --- a/src/bokeh/models/widgets/tables.py +++ b/src/bokeh/models/widgets/tables.py @@ -47,6 +47,7 @@ ) from ...core.property.singletons import Intrinsic from ...model import Model +from ..comparisons import Comparison from ..sources import CDSView, ColumnDataSource, DataSource from .widget import Widget @@ -716,6 +717,9 @@ def __init__(self, *args, **kwargs) -> None: Whether this column shold be displayed or not. """) + sorter = Nullable(Instance(Comparison), help=""" + """) + @abstract class TableWidget(Widget): ''' Abstract base class for data table (data grid) widgets. diff --git a/src/bokeh/plotting/_tools.py b/src/bokeh/plotting/_tools.py index bcea36b2393..4755bd67a38 100644 --- a/src/bokeh/plotting/_tools.py +++ b/src/bokeh/plotting/_tools.py @@ -79,7 +79,7 @@ def process_active_tools(toolbar: Toolbar, tool_map: dict[str, Tool], Args: toolbar (Toolbar): instance of a Toolbar object - tools_map (dict[str]): tool_map from _process_tools_arg + tool_map (dict[str]): tool_map from _process_tools_arg active_drag (str, None, "auto" or Tool): the tool to set active for drag active_inspect (str, None, "auto", Tool or Tool[]): the tool to set active for inspect active_scroll (str, None, "auto" or Tool): the tool to set active for scroll diff --git a/src/bokeh/protocol/message.py b/src/bokeh/protocol/message.py index f02106b0629..6b12d8e7c9d 100644 --- a/src/bokeh/protocol/message.py +++ b/src/bokeh/protocol/message.py @@ -208,8 +208,7 @@ def add_buffer(self, buffer: Buffer) -> None: ''' Associate a buffer header and payload with this message. Args: - buf_header (``JSON``) : a buffer header - buf_payload (``JSON`` or bytes) : a buffer payload + buffer (Buffer) : a buffer Returns: None diff --git a/src/bokeh/resources.py b/src/bokeh/resources.py index 7f8c116b05a..ca6ac84559f 100644 --- a/src/bokeh/resources.py +++ b/src/bokeh/resources.py @@ -336,7 +336,9 @@ def __init__( del root_dir self.version = settings.cdn_version(version) del version - self.minified = settings.minified(minified if minified is not None else not self.dev) + if minified is None and self.dev: + minified = False + self.minified = settings.minified(minified) del minified self.log_level = settings.log_level(log_level) del log_level diff --git a/src/bokeh/sampledata/__init__.py b/src/bokeh/sampledata/__init__.py index bba466db0ba..94eb1cb3631 100644 --- a/src/bokeh/sampledata/__init__.py +++ b/src/bokeh/sampledata/__init__.py @@ -5,6 +5,13 @@ # The full license is in the file LICENSE.txt, distributed with this software. #----------------------------------------------------------------------------- ''' +The ``bokeh.sampledata`` module exposes datasets that are used in examples and +documentation. Some datasets require separate installation. To install those +using ``pip``, execute the command: + +.. code-block:: sh + + pip install bokeh_sampledata ''' diff --git a/src/bokeh/server/server.py b/src/bokeh/server/server.py index 035705192e9..2df86865924 100644 --- a/src/bokeh/server/server.py +++ b/src/bokeh/server/server.py @@ -157,7 +157,7 @@ def stop(self, wait: bool = True) -> None: as stops the ``HTTPServer`` that this instance was configured with. Args: - fast (bool): + wait (bool): Whether to wait for orderly cleanup (default: True) Returns: diff --git a/src/bokeh/server/views/app_index.html b/src/bokeh/server/views/app_index.html index 52ef3ad570b..a0fe00ebf48 100644 --- a/src/bokeh/server/views/app_index.html +++ b/src/bokeh/server/views/app_index.html @@ -39,7 +39,7 @@