diff --git a/.github/workflows/bokeh-ci.yml b/.github/workflows/bokeh-ci.yml index adb0c7f5292..4bcd289cb28 100644 --- a/.github/workflows/bokeh-ci.yml +++ b/.github/workflows/bokeh-ci.yml @@ -27,7 +27,18 @@ jobs: with: miniconda-version: 'latest' activate-environment: bk-test - environment-file: conda/environment-build.yml + run-post: ${{ runner.os != 'Windows' }} + + - name: Install libmamba solver + shell: bash -l {0} + run: | + conda install -q -n base conda-libmamba-solver + conda config --set solver libmamba + + - name: Update bk-test environment + shell: bash -l {0} + run: | + conda env update -q -n bk-test -f conda/environment-build.yml - name: Install node modules run: bash scripts/ci/install_node_modules.sh @@ -91,6 +102,11 @@ jobs: source-tree: 'keep' sampledata: 'cache' + - name: List installed software + run: | + conda info + conda list + - name: Run codebase checks run: pytest --color=yes tests/codebase @@ -113,7 +129,6 @@ jobs: sampledata: 'cache' - name: Install chromium - if: runner.os == 'Linux' run: | if [[ "$(chromium --version | cut -d' ' -f2)" = "$CHROME_VER" ]]; then echo "Using pre-installed version of chromium" @@ -126,6 +141,12 @@ jobs: sudo snap install $CHROME_REV.snap fi + - name: List installed software + run: | + conda info + conda list + chromium --version + - name: Start chrome headless working-directory: ./bokehjs run: node make test:spawn:headless # starts chrome in the background on port 9222 @@ -166,6 +187,11 @@ jobs: source-tree: 'delete' sampledata: 'cache' + - name: List installed software + run: | + conda info + conda list + - name: Run tests run: pytest -v --cov=bokeh --cov-report=xml --tb=short --driver chrome --color=yes tests/integration @@ -202,6 +228,11 @@ jobs: - name: Ensure Python version run: if [[ ! "$(python --version | cut -d' ' -f2)" == "${{ matrix.python-version }}"* ]]; then exit 1; fi + - name: List installed software + run: | + conda info + conda list + - name: Test defaults run: pytest tests/test_defaults.py @@ -238,6 +269,11 @@ jobs: source-tree: 'delete' sampledata: 'none' # no sampledata for minimal tests + - name: List installed software + run: | + conda info + conda list + - name: Run tests run: pytest -m "not sampledata" --cov=bokeh --cov-report=xml --color=yes tests/unit @@ -262,6 +298,11 @@ jobs: source-tree: 'delete' sampledata: 'download' # test at least one real download + - name: List installed software + run: | + conda info + conda list + - name: Build docs run: bash scripts/ci/build_docs.sh @@ -288,10 +329,16 @@ jobs: - name: Install downstream packages run: bash scripts/ci/install_downstream_packages.sh + - name: List installed software + run: | + conda info + conda list + - 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: diff --git a/.github/workflows/bokeh-release-build.yml b/.github/workflows/bokeh-release-build.yml index c20d61f058c..b05e03da8b1 100644 --- a/.github/workflows/bokeh-release-build.yml +++ b/.github/workflows/bokeh-release-build.yml @@ -49,8 +49,8 @@ jobs: - name: Post install status run: | - echo "$(which node) @ $(node --version)" - echo "$(which npm) @ $(npm --version)" + echo "node $(node --version)" + echo "npm $(npm --version)" conda info conda list diff --git a/.github/workflows/bokehjs-ci.yml b/.github/workflows/bokehjs-ci.yml index dbe7d9b5e09..85d8c783224 100644 --- a/.github/workflows/bokehjs-ci.yml +++ b/.github/workflows/bokehjs-ci.yml @@ -34,7 +34,7 @@ jobs: - name: Upgrade npm shell: bash run: | - npm install --location=global npm@8 + npm install --location=global npm - name: Install chromium if: runner.os == 'Linux' @@ -57,6 +57,23 @@ jobs: run: | npm ci --no-progress + - name: List installed software + working-directory: ./bokehjs + shell: bash + run: | + echo "node $(node --version)" + echo "npm $(npm --version)" + if [ "$RUNNER_OS" == "Linux" ]; then + chromium --version + elif [ "$RUNNER_OS" == "Windows" ]; then + # TODO: this hangs; version is reported in tests + # c:/Program\ Files/Google/Chrome/Application/chrome.EXE --version + echo "Chromium ??? (see tests)" + elif [ "$RUNNER_OS" == "macOS" ]; then + /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version + fi + npm list + - name: Build bokehjs working-directory: ./bokehjs shell: bash diff --git a/.github/workflows/composite/test-setup/action.yml b/.github/workflows/composite/test-setup/action.yml index fcd94bad7a1..01057c12168 100644 --- a/.github/workflows/composite/test-setup/action.yml +++ b/.github/workflows/composite/test-setup/action.yml @@ -24,7 +24,18 @@ runs: with: miniconda-version: 'latest' activate-environment: bk-test - environment-file: conda/environment-test-${{ inputs.test-env }}.yml + run-post: ${{ runner.os != 'Windows' }} + + - name: Install libmamba solver + shell: bash -l {0} + run: | + conda install -q -n base conda-libmamba-solver + conda config --set solver libmamba + + - name: Update bk-test environment + shell: bash -l {0} + run: | + conda env update -q -n bk-test -f conda/environment-test-${{ inputs.test-env }}.yml - name: Download wheel package id: download diff --git a/README.md b/README.md index d5f3f50fa67..bc2173eccae 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,6 @@ Non-monetary support can help with development, collaboration, infrastructure, s * [GitGuardian](https://gitguardian.com/) * [GitHub](https://github.com/) * [makepath](https://makepath.com/) -* [Pentest Tools](https://pentest-tools.com) * [Pingdom](https://www.pingdom.com/website-monitoring) * [Slack](https://slack.com) * [QuestionScout](https://www.questionscout.com/) diff --git a/bokehjs/eslint.json b/bokehjs/eslint.json index 43bd5ab267f..f74ba44b83d 100644 --- a/bokehjs/eslint.json +++ b/bokehjs/eslint.json @@ -16,7 +16,7 @@ "tsconfigRootDir": ".", "sourceType": "module" }, - "plugins": ["@typescript-eslint", "deprecation"], + "plugins": ["@typescript-eslint"], "extends": [], "rules": { "@typescript-eslint/ban-types": ["error", { @@ -124,7 +124,6 @@ "anonymous": {"before": false, "after": true}, "method": {"before": true, "after": false} }], - "yield-star-spacing": ["error", {"before": false, "after": true}], - "deprecation/deprecation": ["error"] + "yield-star-spacing": ["error", {"before": false, "after": true}] } } diff --git a/bokehjs/make/tasks/pack.ts b/bokehjs/make/tasks/pack.ts index 5933086d40f..5b72f4b7e04 100644 --- a/bokehjs/make/tasks/pack.ts +++ b/bokehjs/make/tasks/pack.ts @@ -1,5 +1,6 @@ import cp from "child_process" import fs from "fs" +import os from "os" import path from "path" import {task, BuildError} from "../task" @@ -27,7 +28,16 @@ function npm_pack() { fs.unlinkSync(tgz_latest) } - fs.symlinkSync(tgz, tgz_latest) + try { + fs.symlinkSync(tgz, tgz_latest) + } catch (error: unknown) { + if (error instanceof Error) { + const e = error as NodeJS.ErrnoException + if (e.errno === os.constants.errno.EPERM) { + fs.copyFileSync(tgz, tgz_latest) + } + } + } } task("pack", async () => { diff --git a/bokehjs/src/lib/core/util/types.ts b/bokehjs/src/lib/core/util/types.ts index dad80a70173..d6f157397a6 100644 --- a/bokehjs/src/lib/core/util/types.ts +++ b/bokehjs/src/lib/core/util/types.ts @@ -20,6 +20,14 @@ 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 function isBoolean(obj: unknown): obj is boolean { return obj === true || obj === false || toString.call(obj) === "[object Boolean]" } diff --git a/bokehjs/src/lib/core/visuals/patterns.ts b/bokehjs/src/lib/core/visuals/patterns.ts index ad191f2ab4a..db8d2a05963 100644 --- a/bokehjs/src/lib/core/visuals/patterns.ts +++ b/bokehjs/src/lib/core/visuals/patterns.ts @@ -118,7 +118,6 @@ function create_hatch_canvas(ctx: Context2d, ctx.moveTo(3*h4+0.5, h) ctx.lineTo(5*h4+0.5, 0) ctx.stroke() - ctx.stroke() break case "left_diagonal_line": ctx.moveTo(h4+0.5, h) @@ -130,7 +129,6 @@ function create_hatch_canvas(ctx: Context2d, ctx.moveTo(5*h4+0.5, h) ctx.lineTo(3*h4+0.5, 0) ctx.stroke() - ctx.stroke() break case "diagonal_cross": _x(ctx, h) diff --git a/bokehjs/src/lib/models/canvas/canvas.ts b/bokehjs/src/lib/models/canvas/canvas.ts index 49cc244e092..73caa3fb7ed 100644 --- a/bokehjs/src/lib/models/canvas/canvas.ts +++ b/bokehjs/src/lib/models/canvas/canvas.ts @@ -173,9 +173,18 @@ export class CanvasView extends UIElementView { return changed } + override after_resize(): void { + if (this.plot_views.length != 0) { + // Canvas is being managed by a plot, thus it should not attempt + // self-resize, as it would result in inconsistent state and + // possibly invalid layout and/or lack of repaint of a plot. + return + } else { + super.after_resize() + } + } + override _after_resize(): void { - if (this.plot_views.length != 0) - return // XXX temporary hack super._after_resize() const {width, height} = this.bbox this.primary.resize(width, height) @@ -184,9 +193,7 @@ export class CanvasView extends UIElementView { resize(): void { this._update_bbox() - const {width, height} = this.bbox - this.primary.resize(width, height) - this.overlays.resize(width, height) + this._after_resize() } prepare_webgl(frame_box: FrameBox): void { diff --git a/bokehjs/src/lib/models/glyphs/webgl/base_line.ts b/bokehjs/src/lib/models/glyphs/webgl/base_line.ts index e6aec2688d2..c0ddd0f2d74 100644 --- a/bokehjs/src/lib/models/glyphs/webgl/base_line.ts +++ b/bokehjs/src/lib/models/glyphs/webgl/base_line.ts @@ -36,7 +36,7 @@ export abstract class BaseLineGL extends BaseGLGlyph { super(regl_wrapper, glyph) this._antialias = 1.5 // Make this larger to test antialiasing at edges. - this._miter_limit = 5.0 // Threshold for miters to be replaced by bevels. + this._miter_limit = 10.0 // Threshold for miters to be replaced by bevels. } abstract override draw(_indices: number[], main_glyph: GlyphView, transform: Transform): void diff --git a/bokehjs/src/lib/models/glyphs/webgl/regl_line.frag b/bokehjs/src/lib/models/glyphs/webgl/regl_line.frag index e7c7c249520..0b1c894255b 100644 --- a/bokehjs/src/lib/models/glyphs/webgl/regl_line.frag +++ b/bokehjs/src/lib/models/glyphs/webgl/regl_line.frag @@ -23,50 +23,39 @@ uniform float u_dash_offset; varying float v_segment_length; varying vec2 v_coords; varying float v_flags; -varying float v_cos_theta_turn_right_start; -varying float v_cos_theta_turn_right_end; +varying float v_cos_turn_angle_start; +varying float v_cos_turn_angle_end; #ifdef DASHED varying float v_length_so_far; #endif +#define ONE_MINUS_SMALL (1.0 - 1e-6) + float cross_z(in vec2 v0, in vec2 v1) { return v0.x*v1.y - v0.y*v1.x; } -float point_line_side(in vec2 point, in vec2 start, in vec2 end) -{ - // +ve if point to right of line. - // Alternatively could do dot product with right_vector. - return cross_z(point - start, end - start); -} - -float point_line_distance(in vec2 point, in vec2 start, in vec2 end) -{ - return point_line_side(point, start, end) / distance(start, end); -} - vec2 right_vector(in vec2 v) { return vec2(v.y, -v.x); } -float bevel_join_distance(in float sign_start, in float halfwidth) +float bevel_join_distance(in vec2 coords, in vec2 other_right, in float sign_turn_right) { - float cos_theta_turn_right = sign_start > 0.0 ? v_cos_theta_turn_right_start - : v_cos_theta_turn_right_end; - float cos_theta = abs(cos_theta_turn_right); - float turn_right = sign(cos_theta_turn_right); - float distance_along = sign_start > 0.0 ? 0.0 : v_segment_length; - - // In v_coords reference frame (x is along segment, y across). - vec2 line_start = vec2(distance_along, halfwidth*turn_right); - float sin_alpha = cos_theta; - float cos_alpha = sqrt(1.0 - sin_alpha*sin_alpha); - vec2 line_along = vec2(-sign_start*turn_right*sin_alpha, -cos_alpha); - - return halfwidth + sign_start*point_line_distance( - v_coords, line_start, line_start+line_along); + // other_right is unit vector facing right of the other (previous or next) segment, in coord reference frame + float hw = 0.5*u_linewidth; // Not including antialiasing + if (other_right.y >= ONE_MINUS_SMALL) { // other_right.y is -cos(turn_angle) + // 180 degree turn. + return abs(hw - v_coords.x); + } + else { + const vec2 segment_right = vec2(0.0, -1.0); + // corner_right is unit vector bisecting corner facing right, in coord reference frame + vec2 corner_right = normalize(other_right + segment_right); + vec2 outside_point = (-hw*sign_turn_right)*segment_right; + return hw + sign_turn_right*dot(outside_point - coords, corner_right); + } } float cap(in int cap_type, in float x, in float y) @@ -87,6 +76,12 @@ float distance_to_alpha(in float dist) 0.5*(u_linewidth + u_antialias), dist); } +vec2 turn_angle_to_right_vector(in float cos_turn_angle, in float sign_turn_right) +{ + float sin_turn_angle = sign_turn_right*sqrt(1.0 - cos_turn_angle*cos_turn_angle); + return vec2(sin_turn_angle, -cos_turn_angle); +} + #ifdef DASHED float dash_distance(in float x) { @@ -109,38 +104,11 @@ float dash_distance(in float x) return u_dash_scale*dist; } -float clip_dash_distance(in float x, in float offset, in float sign_along) +mat2 rotation_matrix(in vec2 other_right) { - // Return clipped dash distance, sign_along is +1.0 if looking forward - // into next segment and -1.0 if looking backward into previous segment. - float half_antialias = 0.5*u_antialias; - - if (sign_along*x > half_antialias) { - // Outside antialias region, use usual dash distance. - return dash_distance(offset + x); - } - else { - // Inside antialias region. - // Dash distance at edge of antialias region clipped to half_antialias. - float edge_dist = min(dash_distance(offset + sign_along*half_antialias), half_antialias); - - // Physical distance from dash distance at edge of antialias region. - return edge_dist + sign_along*x - half_antialias; - } -} - -mat2 rotation_matrix(in float sign_start) -{ - // Rotation matrix for v_coords from this segment to prev or next segment. - float cos_theta_turn_right = sign_start > 0.0 ? v_cos_theta_turn_right_start - : v_cos_theta_turn_right_end; - float cos_theta = abs(cos_theta_turn_right); - float turn_right = sign(cos_theta_turn_right); - - float sin_theta = sqrt(1.0 - cos_theta*cos_theta)*sign_start*turn_right; - float cos_2theta = 2.0*cos_theta*cos_theta - 1.0; - float sin_2theta = 2.0*sin_theta*cos_theta; - return mat2(cos_2theta, -sin_2theta, sin_2theta, cos_2theta); + float sin_angle = other_right.x; + float cos_angle = -other_right.y; + return mat2(cos_angle, -sin_angle, sin_angle, cos_angle); } #endif @@ -153,6 +121,12 @@ void main() // Extract flags. int flags = int(v_flags + 0.5); + bool turn_right_end = (flags / 32 > 0); + float sign_turn_right_end = turn_right_end ? 1.0 : -1.0; + flags -= 32*int(turn_right_end); + bool turn_right_start = (flags / 16 > 0); + float sign_turn_right_start = turn_right_start ? 1.0 : -1.0; + flags -= 16*int(turn_right_start); bool miter_too_large_end = (flags / 8 > 0); flags -= 8*int(miter_too_large_end); bool miter_too_large_start = (flags / 4 > 0); @@ -161,33 +135,39 @@ void main() flags -= 2*int(has_end_cap); bool has_start_cap = flags > 0; + // Unit vectors to right of previous and next segments in coord reference frame + vec2 prev_right = turn_angle_to_right_vector(v_cos_turn_angle_start, sign_turn_right_start); + vec2 next_right = turn_angle_to_right_vector(v_cos_turn_angle_end, sign_turn_right_end); + float dist = v_coords.y; // For straight segment, and miter join. - // Along-segment coords with respect to end of segment, +ve inside segment - // so equivalent to v_coords.x at start of segment. - float end_coords_x = v_segment_length - v_coords.x; + // Along-segment coords with respect to end of segment, facing inwards + vec2 end_coords = vec2(v_segment_length, 0.0) - v_coords; if (v_coords.x <= half_antialias) { // At start of segment, either cap or join. if (has_start_cap) dist = cap(cap_type, v_coords.x, v_coords.y); - else if (join_type == round_join) - dist = distance(v_coords, vec2(0.0, 0.0)); - else if (join_type == bevel_join || - (join_type == miter_join && miter_too_large_start)) - dist = max(abs(dist), bevel_join_distance(1.0, halfwidth)); - // else a miter join which uses the default dist calculation. + else if (join_type == round_join) { + if (v_coords.x <= 0.0) + dist = distance(v_coords, vec2(0.0, 0.0)); + } + else { // bevel or miter join + if (join_type == bevel_join || miter_too_large_start) + dist = max(abs(dist), bevel_join_distance(v_coords, prev_right, sign_turn_right_start)); + float prev_sideways_dist = -sign_turn_right_start*dot(v_coords, prev_right); + dist = max(abs(dist), prev_sideways_dist); + } } - else if (end_coords_x <= half_antialias) { - // At end of segment, either cap or join. - if (has_end_cap) - dist = cap(cap_type, end_coords_x, v_coords.y); - else if (join_type == round_join) - dist = distance(v_coords, vec2(v_segment_length, 0)); - else if ((join_type == bevel_join || - (join_type == miter_join && miter_too_large_end))) - dist = max(abs(dist), bevel_join_distance(-1.0, halfwidth)); - // else a miter join which uses the default dist calculation. + + if (end_coords.x <= half_antialias) { + if (has_end_cap) { + dist = max(abs(dist), cap(cap_type, end_coords.x, v_coords.y)); + } + else if (join_type == bevel_join || miter_too_large_end) { + // Bevel join at end impacts half antialias distance + dist = max(abs(dist), bevel_join_distance(end_coords, next_right, sign_turn_right_end)); + } } float alpha = distance_to_alpha(abs(dist)); @@ -197,68 +177,48 @@ void main() // Dashes in straight segments (outside of joins) are easily calculated. dist = dash_distance(v_coords.x); - if (!has_start_cap && cap_type == butt_cap) { - if (v_coords.x < half_antialias) { - // Outer of start join rendered solid color or not at all - // depending on whether corner point is in dash or gap, with - // antialiased ends. - if (dash_distance(0.0) > 0.0) { - // Corner is solid color. - dist = max(dist, min(half_antialias, -v_coords.x)); - // Avoid visible antialiasing band between corner and dash. - dist = max(dist, dash_distance(half_antialias)); - } - else { - // Use large negative value so corner not colored. - dist = -halfwidth; - - if (v_coords.x > -half_antialias) { - // Consider antialias region of dash after start region. - float edge_dist = min(dash_distance(half_antialias), half_antialias); - dist = max(dist, edge_dist + v_coords.x - half_antialias); - } - } - } - - vec2 prev_coords = rotation_matrix(1.0)*v_coords; + vec2 prev_coords = rotation_matrix(prev_right)*v_coords; + float start_dash_distance = dash_distance(0.0); - if (abs(prev_coords.y) < halfwidth && prev_coords.x < half_antialias) { - // Extend dashes across from end of previous segment, with antialiased end. - float new_dist = clip_dash_distance(prev_coords.x, 0.0, -1.0); - new_dist = min(new_dist, 0.5*u_linewidth - abs(prev_coords.y)); - dist = max(dist, new_dist); + if (!has_start_cap && cap_type == butt_cap) { + // Outer of start join rendered solid color or not at all depending on whether corner + // point is in dash or gap, with antialiased ends. + bool outer_solid = start_dash_distance >= 0.0 && v_coords.x < half_antialias && prev_coords.x > -half_antialias; + if (outer_solid) { + // Within solid outer region, antialiased at ends + float half_aa_dist = dash_distance(half_antialias); + if (half_aa_dist > 0.0) // Next dash near, do not want antialiased gap + dist = half_aa_dist - v_coords.x + half_antialias; + else + dist = start_dash_distance - v_coords.x; + + half_aa_dist = dash_distance(-half_antialias); + if (half_aa_dist > 0.0) // Prev dash nearm do not want antialiased gap + dist = min(dist, half_aa_dist + prev_coords.x + half_antialias); + else + dist = min(dist, start_dash_distance + prev_coords.x); } - } - - if (!has_end_cap && cap_type == butt_cap) { - if (end_coords_x < half_antialias) { - // Similar for end join. - if (dash_distance(v_segment_length) > 0.0) { - // Corner is solid color. - dist = max(dist, min(half_antialias, -end_coords_x)); - // Avoid visible antialiasing band between corner and dash. - dist = max(dist, dash_distance(v_segment_length - half_antialias)); - } - else { - // Use large negative value so corner not colored. - dist = -halfwidth; - - if (end_coords_x > -half_antialias) { - // Consider antialias region of dash before end region. - float edge_dist = min(dash_distance(v_segment_length - half_antialias), - half_antialias); - dist = max(dist, edge_dist + end_coords_x - half_antialias); - } + else { + // Outer not rendered, antialias ends. + if (v_coords.x < half_antialias) + dist = min(0.0, dash_distance(half_antialias) - half_antialias) + v_coords.x; + + if (prev_coords.x > -half_antialias && prev_coords.x <= half_antialias) { + // Antialias from end of previous segment into join + float prev_dist = min(0.0, dash_distance(-half_antialias) - half_antialias) - prev_coords.x; + // Consider width of previous segment + prev_dist = min(prev_dist, 0.5*u_linewidth - abs(prev_coords.y)); + dist = max(dist, prev_dist); } } + } - vec2 next_coords = rotation_matrix(-1.0)*(v_coords - vec2(v_segment_length, 0.0)); - - if (abs(next_coords.y) < halfwidth && next_coords.x > -half_antialias) { - // Extend dashes across from next segment, with antialiased end. - float new_dist = clip_dash_distance(next_coords.x, v_segment_length, 1.0); - new_dist = min(new_dist, 0.5*u_linewidth - abs(next_coords.y)); - dist = max(dist, new_dist); + if (!has_end_cap && cap_type == butt_cap && end_coords.x < half_antialias) { + float end_dash_distance = dash_distance(v_segment_length); + bool increasing = end_dash_distance >= 0.0 && sign_turn_right_end*v_coords.y < 0.0; + if (!increasing) { + float half_aa_dist = dash_distance(v_segment_length - half_antialias); + dist = min(0.0, half_aa_dist - half_antialias) + end_coords.x; } } diff --git a/bokehjs/src/lib/models/glyphs/webgl/regl_line.vert b/bokehjs/src/lib/models/glyphs/webgl/regl_line.vert index 8e910902e22..937544a1544 100644 --- a/bokehjs/src/lib/models/glyphs/webgl/regl_line.vert +++ b/bokehjs/src/lib/models/glyphs/webgl/regl_line.vert @@ -30,13 +30,15 @@ uniform float u_miter_limit; varying float v_segment_length; varying vec2 v_coords; -varying float v_flags; // Booleans for start/end caps and miters too long. -varying float v_cos_theta_turn_right_start; // Sign gives turn_right, abs gives -varying float v_cos_theta_turn_right_end; // cos(theta). +varying float v_flags; // Boolean flags +varying float v_cos_turn_angle_start; +varying float v_cos_turn_angle_end; #ifdef DASHED varying float v_length_so_far; #endif +#define SMALL 1e-6 + float cross_z(in vec2 v0, in vec2 v1) { return v0.x*v1.y - v0.y*v1.x; @@ -47,18 +49,43 @@ vec2 right_vector(in vec2 v) return vec2(v.y, -v.x); } -vec2 line_intersection(in vec2 point0, in vec2 dir0, - in vec2 point1, in vec2 dir1) +// Calculate cos/sin turn angle with adjacent segment, and unit normal vector to right +float calc_turn_angle(in bool has_cap, in vec2 segment_right, in vec2 other_right, out vec2 point_right, out float sin_turn_angle) +{ + float cos_turn_angle; + vec2 diff = segment_right + other_right; + float len = length(diff); + if (has_cap || len < SMALL) { + point_right = segment_right; + cos_turn_angle = -1.0; // Turns back on itself. + sin_turn_angle = 0.0; + } + else { + point_right = diff / len; + cos_turn_angle = dot(segment_right, other_right); // cos zero at +/-pi/2, +ve angle is turn right + sin_turn_angle = cross_z(segment_right, other_right); + } + return cos_turn_angle; +} + +// If miter too large use bevel join instead +bool miter_too_large(in int join_type, in float cos_turn_angle) { - // Line-line intersection: point0 + lambda0 dir0 = point1 + lambda1 dir1. - // Not checking if lines are parallel! - float lambda0 = cross_z(point1 - point0, dir1) / cross_z(dir0, dir1); - return point0 + lambda0*dir0; + float cos_half_angle_sqr = 0.5*(1.0 + cos_turn_angle); // Trig identity + return join_type == miter_join && cos_half_angle_sqr < 1.0 / (u_miter_limit*u_miter_limit); } -float sign_no_zero(in float x) +vec2 normalize_check_len(in vec2 vec, in float len) { - return x >= 0.0 ? 1.0 : -1.0; + if (abs(len) < SMALL) + return vec2(1.0, 0.0); + else + return vec / len; +} + +vec2 normalize_check(in vec2 vec) +{ + return normalize_check_len(vec, length(vec)); } void main() @@ -69,61 +96,39 @@ void main() return; } - const float min_miter_factor_round_join_mesh = sqrt(2.0); - int join_type = int(u_line_join + 0.5); int cap_type = int(u_line_cap + 0.5); float halfwidth = 0.5*(u_linewidth + u_antialias); - vec2 segment_along = normalize(a_point_end - a_point_start); // unit vector. + + vec2 segment_along = a_point_end - a_point_start; v_segment_length = length(a_point_end - a_point_start); + segment_along = normalize_check_len(segment_along, v_segment_length); // unit vector. vec2 segment_right = right_vector(segment_along); // unit vector. vec2 xy; - bool miter_too_large_start = false; - bool miter_too_large_end = false; + // in screen coords + vec2 prev_along = normalize_check(a_point_start - a_point_prev); + vec2 prev_right = right_vector(prev_along); + vec2 next_right = right_vector(normalize_check(a_point_next - a_point_end)); - v_coords.y = a_position.y*halfwidth; // Overwritten later for end points. + v_coords.y = a_position.y*halfwidth; // Overwritten later for join points. + // Start and end cap properties bool has_start_cap = a_show_prev < 0.5; bool has_end_cap = a_show_next < 0.5; - vec2 point_normal_start; - float cos_theta_start; - float turn_right_start; - if (has_start_cap) - point_normal_start = segment_right; - else { - vec2 prev_right = right_vector(normalize(a_point_start - a_point_prev)); - point_normal_start = normalize(segment_right + prev_right); - cos_theta_start = dot(segment_right, point_normal_start); // Always +ve - turn_right_start = sign_no_zero(dot(segment_right, a_point_prev - a_point_start)); - } + // Start and end join properties + vec2 point_right_start, point_right_end; + float sin_turn_angle_start, sin_turn_angle_end; + v_cos_turn_angle_start = calc_turn_angle(has_start_cap, segment_right, prev_right, point_right_start, sin_turn_angle_start); + v_cos_turn_angle_end = calc_turn_angle(has_end_cap, segment_right, next_right, point_right_end, sin_turn_angle_end); + float sign_turn_right_start = sin_turn_angle_start >= 0.0 ? 1.0 : -1.0; - vec2 point_normal_end; - float cos_theta_end; - float turn_right_end; - if (has_end_cap) - point_normal_end = segment_right; - else { - vec2 next_right = right_vector(normalize(a_point_next - a_point_end)); - point_normal_end = normalize(segment_right + next_right); - cos_theta_end = dot(segment_right, point_normal_end); // Always +ve - turn_right_end = sign_no_zero(dot(segment_right, a_point_next - a_point_end)); - } - - float miter_factor_start = 1.0 / dot(segment_right, point_normal_start); - float miter_factor_end = 1.0 / dot(segment_right, point_normal_end); - if (join_type == miter_join) { - // If miter too large, use bevel join instead. - miter_too_large_start = (miter_factor_start > u_miter_limit); - miter_too_large_end = (miter_factor_end > u_miter_limit); - } + bool miter_too_large_start = !has_start_cap && miter_too_large(join_type, v_cos_turn_angle_start); + bool miter_too_large_end = !has_end_cap && miter_too_large(join_type, v_cos_turn_angle_end); float sign_at_start = -sign(a_position.x); // +ve at segment start, -ve end. vec2 point = sign_at_start > 0.0 ? a_point_start : a_point_end; - vec2 adjacent_point = - sign_at_start > 0.0 ? (has_start_cap ? a_point_start : a_point_prev) - : (has_end_cap ? a_point_end : a_point_next); if ( (has_start_cap && sign_at_start > 0.0) || (has_end_cap && sign_at_start < 0.0) ) { @@ -134,67 +139,62 @@ void main() else xy -= sign_at_start*halfwidth*segment_along; } - else { // Join. - // +ve if turning to right, -ve if to left. - float turn_sign = sign_at_start > 0.0 ? turn_right_start : turn_right_end; - - vec2 adjacent_right = sign_at_start*normalize(right_vector(point - adjacent_point)); - vec2 point_right = normalize(segment_right + adjacent_right); - float miter_factor = sign_at_start > 0.0 ? miter_factor_start : miter_factor_end; - bool miter_too_large = sign_at_start > 0.0 ? miter_too_large_start : miter_too_large_end; - - if (abs(a_position.x) > 1.5) { - // Outer point, meets prev/next segment. - float factor; // multiplied by halfwidth... - - if (join_type == bevel_join || (join_type == miter_join && miter_too_large)) - factor = 1.0 / miter_factor; // cos_theta. - else if (join_type == round_join && - miter_factor > min_miter_factor_round_join_mesh) - factor = 1.0; - else // miter, or round (small angle only). - factor = miter_factor; - - xy = point - point_right*(halfwidth*turn_sign*factor); - v_coords.y = turn_sign*halfwidth*factor / miter_factor; - } - else if (turn_sign*a_position.y < 0.0) { - // Inner point, meets prev/next segment. - float len = halfwidth*miter_factor; - float segment_len = v_segment_length; - float adjacent_len = distance(point, adjacent_point); - - if (len <= min(segment_len, adjacent_len)) - // Normal behaviour. - xy = point - point_right*(len*a_position.y); - else - // For short wide line segments the inner point using the above - // calculation can be outside of the line. Here clipping it. - xy = point + segment_right*(halfwidth*turn_sign); + else if (sign_at_start > 0.0) { + vec2 inside_point = a_point_start + segment_right*(sign_turn_right_start*halfwidth); + vec2 prev_outside_point = a_point_start - prev_right*(sign_turn_right_start*halfwidth); + + // join at start. + if (join_type == round_join || join_type == bevel_join || miter_too_large_start) { + if (v_cos_turn_angle_start <= 0.0) { // |turn_angle| > 90 degrees + xy = a_point_start - segment_right*(halfwidth*a_position.y) - halfwidth*segment_along; + } + else { + if (a_position.x < -1.5) { + xy = prev_outside_point; + v_coords.y = -dot(xy - a_point_start, segment_right); + } + else if (a_position.y*sign_turn_right_start > 0.0) { // outside corner of turn + float d = halfwidth*abs(sin_turn_angle_start); + xy = a_point_start - segment_right*(halfwidth*a_position.y) - d*segment_along; + } + else { // inside corner of turn + xy = inside_point; + } + } } - else { - // Point along outside edge. - xy = point - segment_right*(halfwidth*a_position.y); - if (join_type == round_join && - miter_factor > min_miter_factor_round_join_mesh) { - xy = line_intersection(xy, segment_along, - point - turn_sign*point_right*halfwidth, - right_vector(point_right)); + else { // miter join + if (a_position.x < -1.5) { + xy = prev_outside_point; + v_coords.y = -dot(xy - a_point_start, segment_right); + } + else if (a_position.y*sign_turn_right_start > 0.0) { // outside corner of turn + float tan_half_turn_angle = (1.0-v_cos_turn_angle_start) / sin_turn_angle_start; // Trig identity + float d = sign_turn_right_start*halfwidth*tan_half_turn_angle; + xy = a_point_start - segment_right*(halfwidth*a_position.y) - d*segment_along; + } + else { // inside corner if turn + xy = inside_point; } } } + else { + xy = point - segment_right*(halfwidth*a_position.y); + } vec2 pos = xy + 0.5; // Bokeh's offset. pos /= u_canvas_size / u_pixel_ratio; // in 0..1 gl_Position = vec4(2.0*pos.x - 1.0, 1.0 - 2.0*pos.y, 0.0, 1.0); + bool turn_right_start = sin_turn_angle_start >= 0.0; + bool turn_right_end = sin_turn_angle_end >= 0.0; + v_coords.x = dot(xy - a_point_start, segment_along); v_flags = float(int(has_start_cap) + 2*int(has_end_cap) + 4*int(miter_too_large_start) + - 8*int(miter_too_large_end)); - v_cos_theta_turn_right_start = cos_theta_start*turn_right_start; - v_cos_theta_turn_right_end = cos_theta_end*turn_right_end; + 8*int(miter_too_large_end) + + 16*int(turn_right_start) + + 32*int(turn_right_end)); #ifdef DASHED v_length_so_far = a_length_so_far; diff --git a/bokehjs/src/lib/models/glyphs/webgl/regl_wrap.ts b/bokehjs/src/lib/models/glyphs/webgl/regl_wrap.ts index e145b2270a6..5def63e9587 100644 --- a/bokehjs/src/lib/models/glyphs/webgl/regl_wrap.ts +++ b/bokehjs/src/lib/models/glyphs/webgl/regl_wrap.ts @@ -56,13 +56,13 @@ export class ReglWrapper { this._line_geometry = this._regl.buffer({ usage: "static", type: "float", - data: [[-2, 0], [-1, -1], [1, -1], [2, 0], [1, 1], [-1, 1]], + data: [[-2, 0], [-1, -1], [1, -1], [1, 1], [-1, 1]], }) this._line_triangles = this._regl.elements({ usage: "static", - primitive: "triangles", - data: [[0, 1, 5], [1, 2, 5], [5, 2, 4], [2, 3, 4]], + primitive: "triangle fan", + data: [0, 1, 2, 3, 4], }) } catch (err) { this._regl_available = false @@ -143,15 +143,15 @@ export class ReglWrapper { // Mesh for line rendering (solid and dashed). // -// 1 5-----4 -// /|\ |\ -// / | \ | \ -// y 0 0 | \ | 3 -// \ | \ | / -// \| \|/ +// 1 4-----3 +// / | +// / | +// y 0 0 | +// \ | +// \ | // -1 1-----2 // -// -2 -1 1 2 +// -2 -1 1 // x function regl_solid_line(regl: Regl, line_geometry: Buffer, line_triangles: Elements): ReglRenderFunction { type Props = t.LineGlyphProps diff --git a/bokehjs/src/lib/models/layouts/layout_dom.ts b/bokehjs/src/lib/models/layouts/layout_dom.ts index 7587bbceb2d..5e9568a3996 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 {remove, px, CSSOurStyles} from "core/dom" import {Display} from "core/css" -import {isNumber, isArray} from "core/util/types" +import {isNumber, isArray, isNotNull} from "core/util/types" import * as p from "core/properties" import {build_views, ViewStorage, IterViews} from "core/build_views" @@ -117,7 +117,10 @@ export abstract class LayoutDOMView extends UIElementView { abstract get child_models(): UIElement[] get child_views(): UIElementView[] { - return this.child_models.map((child) => this._child_views.get(child)!) + // 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) } get layoutable_views(): LayoutDOMView[] { @@ -366,6 +369,8 @@ export abstract class LayoutDOMView extends UIElementView { this._measure_layout() } + private _layout_computed: boolean = false + compute_layout(): void { if (this.parent instanceof LayoutDOMView) { // TODO: this.is_managed this.parent.compute_layout() @@ -375,6 +380,7 @@ export abstract class LayoutDOMView extends UIElementView { this._compute_layout() this.after_layout() } + this._layout_computed = true } protected _compute_layout(): void { @@ -495,12 +501,18 @@ export abstract class LayoutDOMView extends UIElementView { } override has_finished(): boolean { - if (!super.has_finished()) + if (!super.has_finished()) { + return false + } + + if (this.is_layout_root && !this._layout_computed) { return false + } for (const child_view of this.child_views) { - if (!child_view.has_finished()) + if (!child_view.has_finished()) { return false + } } return true diff --git a/bokehjs/src/lib/models/tools/actions/help_tool.ts b/bokehjs/src/lib/models/tools/actions/help_tool.ts index e3bcf3abadd..d566a7a3019 100644 --- a/bokehjs/src/lib/models/tools/actions/help_tool.ts +++ b/bokehjs/src/lib/models/tools/actions/help_tool.ts @@ -32,7 +32,7 @@ export class HelpTool extends ActionTool { this.prototype.default_view = HelpToolView this.define(({String}) => ({ - redirect: [ String, "https://docs.bokeh.org/en/latest/docs/user_guide/tools.html"], + redirect: [ String, "https://docs.bokeh.org/en/latest/docs/user_guide/interaction/tools.html"], })) this.override({ diff --git a/bokehjs/src/lib/models/widgets/abstract_slider.ts b/bokehjs/src/lib/models/widgets/abstract_slider.ts index 221c6119e52..32d3677ab9c 100644 --- a/bokehjs/src/lib/models/widgets/abstract_slider.ts +++ b/bokehjs/src/lib/models/widgets/abstract_slider.ts @@ -144,6 +144,9 @@ abstract class AbstractBaseSliderView extends OrientedControlView { tooltip.style.display = show ? "block" : "" } + this._noUiSlider.on("start", () => this._toggle_user_select(false)) + this._noUiSlider.on("end", () => this._toggle_user_select(true)) + this._noUiSlider.on("start", (_, i) => toggle_tooltip(i, true)) this._noUiSlider.on("end", (_, i) => toggle_tooltip(i, false)) } else { @@ -169,6 +172,13 @@ abstract class AbstractBaseSliderView extends OrientedControlView { this._has_finished = true } + protected _toggle_user_select(enable: boolean): void { + const {style} = document.body + const value = enable ? "" : "none" + style.userSelect = value + style.webkitUserSelect = value + } + protected _slide(values: number[]): void { this.model.value = this._calc_from(values) } diff --git a/bokehjs/src/lib/models/widgets/tables/data_table.ts b/bokehjs/src/lib/models/widgets/tables/data_table.ts index d56629f8ec1..f44c2b549f3 100644 --- a/bokehjs/src/lib/models/widgets/tables/data_table.ts +++ b/bokehjs/src/lib/models/widgets/tables/data_table.ts @@ -120,7 +120,11 @@ export class TableDataProvider implements DataProvider { /* eslint-disable @typescript-eslint/strict-boolean-expressions */ return sign*(v0 - v1 || +isNaN(v0) - +isNaN(v1)) } else { - return `${v0}` > `${v1}` ? sign : -sign + const result = `${v0}`.localeCompare(`${v1}`) + if (result == 0) + continue + else + return sign*result } } return 0 diff --git a/bokehjs/src/lib/models/widgets/text_like_input.ts b/bokehjs/src/lib/models/widgets/text_like_input.ts index 387ccddb00a..160b7524e42 100644 --- a/bokehjs/src/lib/models/widgets/text_like_input.ts +++ b/bokehjs/src/lib/models/widgets/text_like_input.ts @@ -8,7 +8,6 @@ export abstract class TextLikeInputView extends InputWidgetView { override connect_signals(): void { super.connect_signals() - this.connect(this.model.properties.name.change, () => this.input_el.name = this.model.name ?? "") this.connect(this.model.properties.value.change, () => this.input_el.value = this.model.value) this.connect(this.model.properties.value_input.change, () => this.input_el.value = this.model.value_input) this.connect(this.model.properties.disabled.change, () => this.input_el.disabled = this.model.disabled) @@ -31,7 +30,6 @@ export abstract class TextLikeInputView extends InputWidgetView { this.group_el.appendChild(el) const {input_el} = this - input_el.name = this.model.name ?? "" input_el.value = this.model.value input_el.disabled = this.model.disabled input_el.placeholder = this.model.placeholder diff --git a/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_rounded_corners_(border_radius_property).png b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_rounded_corners_(border_radius_property).png index 302924f2e35..6f1e5b023fb 100644 Binary files a/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_rounded_corners_(border_radius_property).png and b/bokehjs/test/baselines/linux/BoxAnnotation_annotation__should_support_rounded_corners_(border_radius_property).png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#10856__makes_GlyphRenderer_ignore_changes_to_secondary_glyphs.png b/bokehjs/test/baselines/linux/Bug__in_issue_#10856__makes_GlyphRenderer_ignore_changes_to_secondary_glyphs.png index 7f81cd358a8..69f34bd0eb9 100644 Binary files a/bokehjs/test/baselines/linux/Bug__in_issue_#10856__makes_GlyphRenderer_ignore_changes_to_secondary_glyphs.png and b/bokehjs/test/baselines/linux/Bug__in_issue_#10856__makes_GlyphRenderer_ignore_changes_to_secondary_glyphs.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#12058__renders_gaps_in_straight_bevel-joined_webgl_lines.png b/bokehjs/test/baselines/linux/Bug__in_issue_#12058__renders_gaps_in_straight_bevel-joined_webgl_lines.png index a12a10a1529..42177b13b44 100644 Binary files a/bokehjs/test/baselines/linux/Bug__in_issue_#12058__renders_gaps_in_straight_bevel-joined_webgl_lines.png and b/bokehjs/test/baselines/linux/Bug__in_issue_#12058__renders_gaps_in_straight_bevel-joined_webgl_lines.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#12357__and_#12429_prevents_selection_of_line_segments_using_indices.png b/bokehjs/test/baselines/linux/Bug__in_issue_#12357__and_#12429_prevents_selection_of_line_segments_using_indices.png index b1d3981b5b9..2d906fad823 100644 Binary files a/bokehjs/test/baselines/linux/Bug__in_issue_#12357__and_#12429_prevents_selection_of_line_segments_using_indices.png and b/bokehjs/test/baselines/linux/Bug__in_issue_#12357__and_#12429_prevents_selection_of_line_segments_using_indices.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#12412__displays_canvas_step_glyph_with_incorrect_alpha.png b/bokehjs/test/baselines/linux/Bug__in_issue_#12412__displays_canvas_step_glyph_with_incorrect_alpha.png index 6307b0f6afb..dd3ab5f20c9 100644 Binary files a/bokehjs/test/baselines/linux/Bug__in_issue_#12412__displays_canvas_step_glyph_with_incorrect_alpha.png and b/bokehjs/test/baselines/linux/Bug__in_issue_#12412__displays_canvas_step_glyph_with_incorrect_alpha.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#12913__incorrectly_renders_webgl_lines_with_very_small_angular_separation.blf b/bokehjs/test/baselines/linux/Bug__in_issue_#12913__incorrectly_renders_webgl_lines_with_very_small_angular_separation.blf new file mode 100644 index 00000000000..2d90113f745 --- /dev/null +++ b/bokehjs/test/baselines/linux/Bug__in_issue_#12913__incorrectly_renders_webgl_lines_with_very_small_angular_separation.blf @@ -0,0 +1,11 @@ +Row bbox=[0, 0, 300, 150] + Plot bbox=[0, 0, 150, 150] + CartesianFrame bbox=[36, 27, 109, 101] + LinearAxis bbox=[36, 128, 109, 22] + LinearAxis bbox=[0, 27, 36, 101] + Title bbox=[36, 0, 109, 27] + Plot bbox=[150, 0, 150, 150] + CartesianFrame bbox=[36, 27, 109, 101] + LinearAxis bbox=[36, 128, 109, 22] + LinearAxis bbox=[0, 27, 36, 101] + Title bbox=[36, 0, 109, 27] diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#12913__incorrectly_renders_webgl_lines_with_very_small_angular_separation.png b/bokehjs/test/baselines/linux/Bug__in_issue_#12913__incorrectly_renders_webgl_lines_with_very_small_angular_separation.png new file mode 100644 index 00000000000..fd990a0d78d Binary files /dev/null and b/bokehjs/test/baselines/linux/Bug__in_issue_#12913__incorrectly_renders_webgl_lines_with_very_small_angular_separation.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13104__results_in_a_race_condition_in_the_layout_if_lazy_initialize()_takes_time.blf b/bokehjs/test/baselines/linux/Bug__in_issue_#13104__results_in_a_race_condition_in_the_layout_if_lazy_initialize()_takes_time.blf new file mode 100644 index 00000000000..3ed00c6cb4a --- /dev/null +++ b/bokehjs/test/baselines/linux/Bug__in_issue_#13104__results_in_a_race_condition_in_the_layout_if_lazy_initialize()_takes_time.blf @@ -0,0 +1,25 @@ +GridBox bbox=[0, 0, 1200, 1200] + Plot bbox=[0, 0, 600, 600] + CartesianFrame bbox=[29, 5, 541, 573] + LinearAxis bbox=[29, 578, 541, 22] + LinearAxis bbox=[0, 5, 29, 573] + Title bbox=[29, 5, 541, 0] + ToolbarPanel bbox=[570, 5, 30, 573] + Plot bbox=[600, 0, 600, 600] + CartesianFrame bbox=[29, 5, 541, 573] + LinearAxis bbox=[29, 578, 541, 22] + LinearAxis bbox=[0, 5, 29, 573] + Title bbox=[29, 5, 541, 0] + ToolbarPanel bbox=[570, 5, 30, 573] + Plot bbox=[0, 600, 600, 600] + CartesianFrame bbox=[29, 5, 541, 573] + LinearAxis bbox=[29, 578, 541, 22] + LinearAxis bbox=[0, 5, 29, 573] + Title bbox=[29, 5, 541, 0] + ToolbarPanel bbox=[570, 5, 30, 573] + Plot bbox=[600, 600, 600, 600] + CartesianFrame bbox=[29, 5, 541, 573] + LinearAxis bbox=[29, 578, 541, 22] + LinearAxis bbox=[0, 5, 29, 573] + Title bbox=[29, 5, 541, 0] + ToolbarPanel bbox=[570, 5, 30, 573] diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#13104__results_in_a_race_condition_in_the_layout_if_lazy_initialize()_takes_time.png b/bokehjs/test/baselines/linux/Bug__in_issue_#13104__results_in_a_race_condition_in_the_layout_if_lazy_initialize()_takes_time.png new file mode 100644 index 00000000000..f416a96aead Binary files /dev/null and b/bokehjs/test/baselines/linux/Bug__in_issue_#13104__results_in_a_race_condition_in_the_layout_if_lazy_initialize()_takes_time.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#4888__doesn't_allow_to_render_many_(N=50)_webgl_glyphs_efficiently.png b/bokehjs/test/baselines/linux/Bug__in_issue_#4888__doesn't_allow_to_render_many_(N=50)_webgl_glyphs_efficiently.png index 5b011feddac..1a2fd9086ad 100644 Binary files a/bokehjs/test/baselines/linux/Bug__in_issue_#4888__doesn't_allow_to_render_many_(N=50)_webgl_glyphs_efficiently.png and b/bokehjs/test/baselines/linux/Bug__in_issue_#4888__doesn't_allow_to_render_many_(N=50)_webgl_glyphs_efficiently.png differ diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#8346__should_support_updating_line.png b/bokehjs/test/baselines/linux/Bug__in_issue_#8346__should_support_updating_line.png index f752a51d869..fa4a9031f18 100644 Binary files a/bokehjs/test/baselines/linux/Bug__in_issue_#8346__should_support_updating_line.png and b/bokehjs/test/baselines/linux/Bug__in_issue_#8346__should_support_updating_line.png differ diff --git a/bokehjs/test/baselines/linux/ContourRenderer__should_support_line_fill_and_hatch_properties.png b/bokehjs/test/baselines/linux/ContourRenderer__should_support_line_fill_and_hatch_properties.png index 09204951b80..a5977bbc71b 100644 Binary files a/bokehjs/test/baselines/linux/ContourRenderer__should_support_line_fill_and_hatch_properties.png and b/bokehjs/test/baselines/linux/ContourRenderer__should_support_line_fill_and_hatch_properties.png differ diff --git a/bokehjs/test/baselines/linux/Examples__should_support_GridBandPattern.blf b/bokehjs/test/baselines/linux/Examples__should_support_GridBandPattern.blf new file mode 100644 index 00000000000..2e3e894a247 --- /dev/null +++ b/bokehjs/test/baselines/linux/Examples__should_support_GridBandPattern.blf @@ -0,0 +1,5 @@ +Plot bbox=[0, 0, 600, 300] + CartesianFrame bbox=[36, 5, 559, 268] + LinearAxis bbox=[36, 273, 559, 27] + LinearAxis bbox=[0, 5, 36, 268] + Title bbox=[36, 5, 559, 0] diff --git a/bokehjs/test/baselines/linux/Examples__should_support_GridBandPattern.png b/bokehjs/test/baselines/linux/Examples__should_support_GridBandPattern.png new file mode 100644 index 00000000000..83c88346e23 Binary files /dev/null and b/bokehjs/test/baselines/linux/Examples__should_support_GridBandPattern.png differ diff --git a/bokehjs/test/baselines/linux/Glyph_models__should_support_AnnularWedge.png b/bokehjs/test/baselines/linux/Glyph_models__should_support_AnnularWedge.png index f0e27779280..0c55c6e9f83 100644 Binary files a/bokehjs/test/baselines/linux/Glyph_models__should_support_AnnularWedge.png and b/bokehjs/test/baselines/linux/Glyph_models__should_support_AnnularWedge.png differ diff --git a/bokehjs/test/baselines/linux/Glyph_models__should_support_Annulus.png b/bokehjs/test/baselines/linux/Glyph_models__should_support_Annulus.png index 5251805de53..c2891d63740 100644 Binary files a/bokehjs/test/baselines/linux/Glyph_models__should_support_Annulus.png and b/bokehjs/test/baselines/linux/Glyph_models__should_support_Annulus.png differ diff --git a/bokehjs/test/baselines/linux/Glyph_models__should_support_Block.png b/bokehjs/test/baselines/linux/Glyph_models__should_support_Block.png index ad9f3166f1a..3a8d3f32897 100644 Binary files a/bokehjs/test/baselines/linux/Glyph_models__should_support_Block.png and b/bokehjs/test/baselines/linux/Glyph_models__should_support_Block.png differ diff --git a/bokehjs/test/baselines/linux/Glyph_models__should_support_Circle.png b/bokehjs/test/baselines/linux/Glyph_models__should_support_Circle.png index 53fa4332360..58d678d7534 100644 Binary files a/bokehjs/test/baselines/linux/Glyph_models__should_support_Circle.png and b/bokehjs/test/baselines/linux/Glyph_models__should_support_Circle.png differ diff --git a/bokehjs/test/baselines/linux/Glyph_models__should_support_Ellipse.png b/bokehjs/test/baselines/linux/Glyph_models__should_support_Ellipse.png index a63876a4324..2e2c1db3141 100644 Binary files a/bokehjs/test/baselines/linux/Glyph_models__should_support_Ellipse.png and b/bokehjs/test/baselines/linux/Glyph_models__should_support_Ellipse.png differ diff --git a/bokehjs/test/baselines/linux/Glyph_models__should_support_Patch.png b/bokehjs/test/baselines/linux/Glyph_models__should_support_Patch.png index ccb7e3dc5ec..24215961c36 100644 Binary files a/bokehjs/test/baselines/linux/Glyph_models__should_support_Patch.png and b/bokehjs/test/baselines/linux/Glyph_models__should_support_Patch.png differ diff --git a/bokehjs/test/baselines/linux/Glyph_models__should_support_Patches.png b/bokehjs/test/baselines/linux/Glyph_models__should_support_Patches.png index ccb7e3dc5ec..24215961c36 100644 Binary files a/bokehjs/test/baselines/linux/Glyph_models__should_support_Patches.png and b/bokehjs/test/baselines/linux/Glyph_models__should_support_Patches.png differ diff --git a/bokehjs/test/baselines/linux/Glyph_models__should_support_Step.png b/bokehjs/test/baselines/linux/Glyph_models__should_support_Step.png index 53c56e1a608..697d346d1d7 100644 Binary files a/bokehjs/test/baselines/linux/Glyph_models__should_support_Step.png and b/bokehjs/test/baselines/linux/Glyph_models__should_support_Step.png differ diff --git a/bokehjs/test/baselines/linux/Glyph_models__should_support_Step_with_NaN.png b/bokehjs/test/baselines/linux/Glyph_models__should_support_Step_with_NaN.png index 8611ef928fb..5c31ee924b0 100644 Binary files a/bokehjs/test/baselines/linux/Glyph_models__should_support_Step_with_NaN.png and b/bokehjs/test/baselines/linux/Glyph_models__should_support_Step_with_NaN.png differ diff --git a/bokehjs/test/baselines/linux/Glyph_models__should_support_Wedge.png b/bokehjs/test/baselines/linux/Glyph_models__should_support_Wedge.png index d2adb38c347..62f1e5594ec 100644 Binary files a/bokehjs/test/baselines/linux/Glyph_models__should_support_Wedge.png and b/bokehjs/test/baselines/linux/Glyph_models__should_support_Wedge.png differ diff --git a/bokehjs/test/baselines/linux/Glyph_models__should_support_fill_with_hatch_patterns.png b/bokehjs/test/baselines/linux/Glyph_models__should_support_fill_with_hatch_patterns.png index d90f9332095..060530d13d5 100644 Binary files a/bokehjs/test/baselines/linux/Glyph_models__should_support_fill_with_hatch_patterns.png and b/bokehjs/test/baselines/linux/Glyph_models__should_support_fill_with_hatch_patterns.png differ diff --git a/bokehjs/test/baselines/linux/HexTile_glyph__should_support_'flattop'_orientation_with_hatch_patterns.png b/bokehjs/test/baselines/linux/HexTile_glyph__should_support_'flattop'_orientation_with_hatch_patterns.png index d22017cd92d..71be6f089d6 100644 Binary files a/bokehjs/test/baselines/linux/HexTile_glyph__should_support_'flattop'_orientation_with_hatch_patterns.png and b/bokehjs/test/baselines/linux/HexTile_glyph__should_support_'flattop'_orientation_with_hatch_patterns.png differ diff --git a/bokehjs/test/baselines/linux/HexTile_glyph__should_support_'pointytop'_orientation_with_hatch_patterns.png b/bokehjs/test/baselines/linux/HexTile_glyph__should_support_'pointytop'_orientation_with_hatch_patterns.png index f4694375929..8da71ef9530 100644 Binary files a/bokehjs/test/baselines/linux/HexTile_glyph__should_support_'pointytop'_orientation_with_hatch_patterns.png and b/bokehjs/test/baselines/linux/HexTile_glyph__should_support_'pointytop'_orientation_with_hatch_patterns.png differ diff --git a/bokehjs/test/baselines/linux/Line_glyph__should_support_180_degree_turns.blf b/bokehjs/test/baselines/linux/Line_glyph__should_support_180_degree_turns.blf new file mode 100644 index 00000000000..a0f9cd41dcb --- /dev/null +++ b/bokehjs/test/baselines/linux/Line_glyph__should_support_180_degree_turns.blf @@ -0,0 +1,16 @@ +Row bbox=[0, 0, 900, 300] + Plot bbox=[0, 0, 300, 300] + CartesianFrame bbox=[26, 27, 269, 251] + LinearAxis bbox=[26, 278, 269, 22] + LinearAxis bbox=[0, 27, 26, 251] + Title bbox=[26, 0, 269, 27] + Plot bbox=[300, 0, 300, 300] + CartesianFrame bbox=[26, 27, 269, 251] + LinearAxis bbox=[26, 278, 269, 22] + LinearAxis bbox=[0, 27, 26, 251] + Title bbox=[26, 0, 269, 27] + Plot bbox=[600, 0, 300, 300] + CartesianFrame bbox=[26, 27, 269, 251] + LinearAxis bbox=[26, 278, 269, 22] + LinearAxis bbox=[0, 27, 26, 251] + Title bbox=[26, 0, 269, 27] diff --git a/bokehjs/test/baselines/linux/Line_glyph__should_support_180_degree_turns.png b/bokehjs/test/baselines/linux/Line_glyph__should_support_180_degree_turns.png new file mode 100644 index 00000000000..af150eb3c84 Binary files /dev/null and b/bokehjs/test/baselines/linux/Line_glyph__should_support_180_degree_turns.png differ diff --git a/bokehjs/test/baselines/linux/Line_glyph__should_support_NaN_in_coords.png b/bokehjs/test/baselines/linux/Line_glyph__should_support_NaN_in_coords.png index 6d44d1956e4..9899af99fa2 100644 Binary files a/bokehjs/test/baselines/linux/Line_glyph__should_support_NaN_in_coords.png and b/bokehjs/test/baselines/linux/Line_glyph__should_support_NaN_in_coords.png differ diff --git a/bokehjs/test/baselines/linux/Line_glyph__should_support_closed_line_loops.png b/bokehjs/test/baselines/linux/Line_glyph__should_support_closed_line_loops.png index 7650e38f304..2afe5446108 100644 Binary files a/bokehjs/test/baselines/linux/Line_glyph__should_support_closed_line_loops.png and b/bokehjs/test/baselines/linux/Line_glyph__should_support_closed_line_loops.png differ diff --git a/bokehjs/test/baselines/linux/Line_glyph__should_support_crossing_line.png b/bokehjs/test/baselines/linux/Line_glyph__should_support_crossing_line.png index 6193b6e9f86..6eceffe4867 100644 Binary files a/bokehjs/test/baselines/linux/Line_glyph__should_support_crossing_line.png and b/bokehjs/test/baselines/linux/Line_glyph__should_support_crossing_line.png differ diff --git a/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_named_patterns.png b/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_named_patterns.png index f8abdecc1d9..edb7b4803b9 100644 Binary files a/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_named_patterns.png and b/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_named_patterns.png differ diff --git a/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_numerical_patterns.png b/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_numerical_patterns.png index 7824fec328c..73c570875d6 100644 Binary files a/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_numerical_patterns.png and b/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_numerical_patterns.png differ diff --git a/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_offsets.png b/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_offsets.png index 3b60a273694..62a6ecc7b79 100644 Binary files a/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_offsets.png and b/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_offsets.png differ diff --git a/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_round_caps.png b/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_round_caps.png index 8314a26b7cc..0eafca85368 100644 Binary files a/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_round_caps.png and b/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_round_caps.png differ diff --git a/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_square_caps.png b/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_square_caps.png index 141ddc4c4db..1fa9a77da5c 100644 Binary files a/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_square_caps.png and b/bokehjs/test/baselines/linux/Line_glyph__should_support_dashed_lines_with_square_caps.png differ diff --git a/bokehjs/test/baselines/linux/Line_glyph__should_support_solid_lines_with_caps_and_joins.png b/bokehjs/test/baselines/linux/Line_glyph__should_support_solid_lines_with_caps_and_joins.png index aaa19921064..37f6e986308 100644 Binary files a/bokehjs/test/baselines/linux/Line_glyph__should_support_solid_lines_with_caps_and_joins.png and b/bokehjs/test/baselines/linux/Line_glyph__should_support_solid_lines_with_caps_and_joins.png differ diff --git a/bokehjs/test/baselines/linux/Marker_glyph__should_support_hatch.png b/bokehjs/test/baselines/linux/Marker_glyph__should_support_hatch.png index a93a208f9e2..dbbd4b915b6 100644 Binary files a/bokehjs/test/baselines/linux/Marker_glyph__should_support_hatch.png and b/bokehjs/test/baselines/linux/Marker_glyph__should_support_hatch.png differ diff --git a/bokehjs/test/baselines/linux/Plot__with_webgl_backend__should_allow_empty_line_glyphs.png b/bokehjs/test/baselines/linux/Plot__with_webgl_backend__should_allow_empty_line_glyphs.png index f74b6b4fe4e..8a894d7573c 100644 Binary files a/bokehjs/test/baselines/linux/Plot__with_webgl_backend__should_allow_empty_line_glyphs.png and b/bokehjs/test/baselines/linux/Plot__with_webgl_backend__should_allow_empty_line_glyphs.png differ diff --git a/bokehjs/test/baselines/linux/Rect_glyph__should_support_hatch_patterns_and_line_joins.png b/bokehjs/test/baselines/linux/Rect_glyph__should_support_hatch_patterns_and_line_joins.png index 404cd72bd3e..7cf41da41a7 100644 Binary files a/bokehjs/test/baselines/linux/Rect_glyph__should_support_hatch_patterns_and_line_joins.png and b/bokehjs/test/baselines/linux/Rect_glyph__should_support_hatch_patterns_and_line_joins.png differ diff --git a/bokehjs/test/baselines/linux/webgl__should_render_overlapping_near_parallel_lines.png b/bokehjs/test/baselines/linux/webgl__should_render_overlapping_near_parallel_lines.png index 5dbc35b25de..2f30de5c507 100644 Binary files a/bokehjs/test/baselines/linux/webgl__should_render_overlapping_near_parallel_lines.png and b/bokehjs/test/baselines/linux/webgl__should_render_overlapping_near_parallel_lines.png differ diff --git a/bokehjs/test/baselines/linux/webgl__should_support_nan_in_line.png b/bokehjs/test/baselines/linux/webgl__should_support_nan_in_line.png index 980781084a5..370ddb61f29 100644 Binary files a/bokehjs/test/baselines/linux/webgl__should_support_nan_in_line.png and b/bokehjs/test/baselines/linux/webgl__should_support_nan_in_line.png differ diff --git a/bokehjs/test/baselines/linux/webgl__should_support_zoom_without_NaN_problems.png b/bokehjs/test/baselines/linux/webgl__should_support_zoom_without_NaN_problems.png index 1ac031cf7e0..a8f26b63df1 100644 Binary files a/bokehjs/test/baselines/linux/webgl__should_support_zoom_without_NaN_problems.png and b/bokehjs/test/baselines/linux/webgl__should_support_zoom_without_NaN_problems.png differ diff --git a/bokehjs/test/integration/examples/grid_band_pattern.ts b/bokehjs/test/integration/examples/grid_band_pattern.ts new file mode 100644 index 00000000000..5f77e6707cd --- /dev/null +++ b/bokehjs/test/integration/examples/grid_band_pattern.ts @@ -0,0 +1,38 @@ +import {display} from "../_util" + +import {figure} from "@bokehjs/api/plotting" +import {f} from "@bokehjs/api/expr" +import {linspace} from "@bokehjs/core/util/array" +import {SingleIntervalTicker} from "@bokehjs/models" + +const {PI} = Math + +describe("Examples", () => { + it("should support GridBandPattern", async () => { + const p = figure({width: 600, height: 300, toolbar_location: null}) + + const x = linspace(0, 4*PI) + const y = f`np.sin(${x})` + p.line(x, y, {line_color: "#3577b3"}) + + p.xaxis.ticker = new SingleIntervalTicker({interval: PI/2}) + p.xgrid.ticker = new SingleIntervalTicker({interval: PI}) + + p.xaxis.major_label_overrides = new Map([ + [ PI/2, "π/2"], + [ PI, "π"], + [3*PI/2, "3π/2"], + [2*PI, "2π"], + [5*PI/2, "5π/2"], + [3*PI, "3π"], + [7*PI/2, "7π/2"], + [4*PI, "4π"], + ]) + + p.xgrid.band_hatch_pattern = "/" + p.xgrid.band_hatch_color = "#e4e4e4" + p.ygrid.grid_line_color = null + + await display(p) + }) +}) diff --git a/bokehjs/test/integration/glyphs/line.ts b/bokehjs/test/integration/glyphs/line.ts index 80aeb68f438..8b85aab2230 100644 --- a/bokehjs/test/integration/glyphs/line.ts +++ b/bokehjs/test/integration/glyphs/line.ts @@ -300,6 +300,38 @@ describe("Line glyph", () => { await display(row([p0, p1, p2])) }) + + it("should support 180 degree turns", async () => { + function make_plot(output_backend: OutputBackend) { + const p = fig([300, 300], {output_backend, title: output_backend}) + + const x = [1.1, 1.9, 1.5] + let y = 0.5 + const Y = () => [y+=1, y, y] + + const dashes = [[], [30, 10], [40, 20]] + + for (let i = 0; i < colors.length; i++) { + for (let j = 0; j < linewidths.length; j++) { + p.line(x, Y(), { + line_width: linewidths[j], + line_color: colors[i], + line_alpha: alphas[i], + line_join: joins[i], + line_dash: dashes[i], + }) + } + } + + return p + } + + const p0 = make_plot("canvas") + const p1 = make_plot("svg") + const p2 = make_plot("webgl") + + await display(row([p0, p1, p2])) + }) }) // webgl vs canvas comparison diff --git a/bokehjs/test/integration/regressions.ts b/bokehjs/test/integration/regressions.ts index 6ce08ce70b0..4ea3441dfe0 100644 --- a/bokehjs/test/integration/regressions.ts +++ b/bokehjs/test/integration/regressions.ts @@ -49,7 +49,7 @@ import {range, linspace} from "@bokehjs/core/util/array" import {ndarray} from "@bokehjs/core/util/ndarray" import {Random} from "@bokehjs/core/util/random" import {Matrix} from "@bokehjs/core/util/matrix" -import {paint} from "@bokehjs/core/util/defer" +import {paint, delay} from "@bokehjs/core/util/defer" import {encode_rgba} from "@bokehjs/core/util/color" import {Figure, figure, show} from "@bokehjs/api/plotting" import {MarkerArgs} from "@bokehjs/api/glyph_api" @@ -59,6 +59,7 @@ import {XY, LRTB} from "@bokehjs/core/util/bbox" import {MathTextView} from "@bokehjs/models/text/math_text" import {PlotView} from "@bokehjs/models/plots/plot" +import {FigureView} from "@bokehjs/models/plots/figure" import {gridplot} from "@bokehjs/api/gridplot" import {f} from "@bokehjs/api/expr" @@ -3203,4 +3204,66 @@ describe("Bug", () => { await display(gp) }) }) + + describe("in issue #12913", () => { + it("incorrectly renders webgl lines with very small angular separation", async () => { + const x = [0, 0.5, 0.5+1e-8, 0.5, 1] + const y = [0.4, 0, 1, 0.4, 0] + const line_join = "round" + const line_width = 20 + const line_alpha = 0.8 + const x_range = new Range1d({start: -0.2, end: 1.2}) + const y_range = new Range1d({start: -0.2, end: 1.2}) + + function make_plot(output_backend: OutputBackend) { + const p = fig([150, 150], {output_backend, title: output_backend, x_range, y_range}) + p.line(x, y, {line_width, line_join, line_alpha}) + return p + } + + const p0 = make_plot("canvas") + const p1 = make_plot("webgl") + + await display(row([p0, p1])) + }) + }) + + describe("in issue #13104", () => { + it("results in a race condition in the layout if lazy_initialize() takes time", async () => { + class CustomFigureView extends FigureView { + declare model: CustomFigure + + override async lazy_initialize(): Promise { + await super.lazy_initialize() + await delay(5) + } + } + + class CustomFigure extends Figure { + declare __view_type__: CustomFigureView + + static { + this.prototype.default_view = CustomFigureView + } + } + + const p00 = new CustomFigure() + p00.circle([1, 2, 3], [1, 2, 3], {fill_color: "red"}) + const p01 = new CustomFigure() + p01.circle([1, 2, 3], [1, 2, 3], {fill_color: "green"}) + const p10 = new CustomFigure() + p10.circle([1, 2, 3], [1, 2, 3], {fill_color: "blue"}) + const p11 = new CustomFigure() + p11.circle([1, 2, 3], [1, 2, 3], {fill_color: "yellow"}) + + const gp = new GridBox({ + children: [ + [p00, 0, 0], [p01, 0, 1], + [p10, 1, 0], [p11, 1, 1], + ], + }) + + await display(gp) + }) + }) }) diff --git a/bokehjs/test/unit/core/util/arrayable.ts b/bokehjs/test/unit/core/util/arrayable.ts index 6c8f9d7c56d..3f793fc3a77 100644 --- a/bokehjs/test/unit/core/util/arrayable.ts +++ b/bokehjs/test/unit/core/util/arrayable.ts @@ -1,5 +1,5 @@ -import {expect} from "assertions" import * as arrayable from "@bokehjs/core/util/arrayable" +import {expect} from "assertions" describe("core/util/arrayable module", () => { @@ -159,4 +159,60 @@ describe("core/util/arrayable module", () => { expect(arrayable.interpolate([], [], [])).to.be.equal([]) expect(arrayable.interpolate([1], [], [])).to.be.equal([NaN]) }) + + it("should support subselect() function", () => { + const a = [1, 2, 3, 4, 5, 6, undefined] + const b = [1, 4, 5] + expect(arrayable.subselect(a, b)).to.be.equal([2, 5, 6]) + const c = [0, 100] + const d = [10, NaN] + expect(arrayable.subselect(a, c)).to.be.equal([1, undefined]) + expect(arrayable.subselect(a, d)).to.be.equal([undefined, undefined]) + }) + + it("should support insert() function", () => { + expect(arrayable.insert([1, 2, 3], 4, 1)).to.be.equal([1, 4, 2, 3]) + }) + + it("should support append() function", () => { + expect(arrayable.append([1, 2, 3], 4)).to.be.equal([1, 2, 3, 4]) + }) + + it("should support prepend() function", () => { + expect(arrayable.prepend([1, 2, 3], 4)).to.be.equal([4, 1, 2, 3]) + }) + + it("should support mul() function", () => { + const a = [1, 2, 3] + const b = [2, 4, 6] + expect(arrayable.mul(a, 2)).to.be.equal(b) + }) + + it("should support map() function", () => { + const a = [1, 2, 3] + const b = [2, 4, 6] + expect(arrayable.map(a, (num) => num * 2)).to.be.equal(b) + }) + + it("should support map() function with Float32Array", () => { + const arr = Float32Array.of(1, 2, 3) + const ret = arrayable.map(arr, (num) => num * 2) + expect(ret).to.be.instanceof(Float32Array) + expect(ret).to.be.equal(Float32Array.of(2, 4, 6)) + }) + + it("should support sum() function", () => { + expect(arrayable.sum([1, 2, 3, 4])).to.be.equal(10) + expect(arrayable.sum([1, 2, 3, NaN])).to.be.equal(NaN) + }) + + it("should support some() function", () => { + expect(arrayable.some([-1, 2, 6, 11], (num) => num % 2 === 0)).to.be.true + expect(arrayable.some([1, 2, 3, 4], (num) => num > 5)).to.be.false + }) + + it("should support every() function", () => { + expect(arrayable.every([1, 2, 3, 4], (num) => num > 0)).to.be.true + expect(arrayable.every([-2, 1, 2, 3, 4], (num) => num > 0)).to.be.false + }) }) diff --git a/bokehjs/test/unit/regressions.ts b/bokehjs/test/unit/regressions.ts index 464211be37e..7ead6f6ed7f 100644 --- a/bokehjs/test/unit/regressions.ts +++ b/bokehjs/test/unit/regressions.ts @@ -13,6 +13,7 @@ import {assert} from "@bokehjs/core/util/assert" import {is_equal} from "@bokehjs/core/util/eq" import {linspace} from "@bokehjs/core/util/array" import {ndarray} from "@bokehjs/core/util/ndarray" +import {BitSet} from "@bokehjs/core/util/bitset" import {base64_to_buffer} from "@bokehjs/core/util/buffer" import {offset_bbox} from "@bokehjs/core/dom" import {Color, Arrayable} from "@bokehjs/core/types" @@ -25,6 +26,9 @@ import {defer, paint} from "@bokehjs/core/util/defer" import {UIElement, UIElementView} from "@bokehjs/models/ui/ui_element" import {ImageURLView} from "@bokehjs/models/glyphs/image_url" import {CopyToolView} from "@bokehjs/models/tools/actions/copy_tool" +import {TableDataProvider} from "@bokehjs/models/widgets/tables/data_table" +import {TableColumn} from "@bokehjs/models/widgets/tables/table_column" +import {DTINDEX_NAME} from "@bokehjs/models/widgets/tables/definitions" class QualifiedModelView extends UIElementView { declare model: QualifiedModel @@ -642,4 +646,40 @@ describe("Bug", () => { expect(view.el.classList.contains(cls)).to.be.true }) }) + + describe("in issue #6683", () => { + it("doesn't allow TableDataProvider to correctly sort strings with accents", async () => { + const source = new ColumnDataSource({ + data: { + words: ["met", "no", "mute", "méteo", "mill", "mole"], + }, + }) + const indices = BitSet.from_indices(6, [0, 1, 2, 3, 4, 5]) + const view = new CDSView({indices}) + const provider = new TableDataProvider(source, view) + const column = new TableColumn({field: "words"}).toColumn() + + provider.sort([{sortCol: column, sortAsc: true}]) + const records_asc = provider.getRecords() + expect(records_asc).to.be.equal([ + {words: "met", [DTINDEX_NAME]: 0}, + {words: "méteo", [DTINDEX_NAME]: 3}, + {words: "mill", [DTINDEX_NAME]: 4}, + {words: "mole", [DTINDEX_NAME]: 5}, + {words: "mute", [DTINDEX_NAME]: 2}, + {words: "no", [DTINDEX_NAME]: 1}, + ]) + + provider.sort([{sortCol: column, sortAsc: false}]) + const records_dsc = provider.getRecords() + expect(records_dsc).to.be.equal([ + {words: "no", [DTINDEX_NAME]: 1}, + {words: "mute", [DTINDEX_NAME]: 2}, + {words: "mole", [DTINDEX_NAME]: 5}, + {words: "mill", [DTINDEX_NAME]: 4}, + {words: "méteo", [DTINDEX_NAME]: 3}, + {words: "met", [DTINDEX_NAME]: 0}, + ]) + }) + }) }) diff --git a/conda/environment-release-build.yml b/conda/environment-release-build.yml index 9bdf9754918..396ca39939d 100644 --- a/conda/environment-release-build.yml +++ b/conda/environment-release-build.yml @@ -39,7 +39,7 @@ dependencies: # docs - autoclasstoc <=1.4 - pydata_sphinx_theme ==0.9 - - sphinx >5.1 + - sphinx ==5.3 - sphinx-copybutton - sphinx-design - sphinxext-opengraph diff --git a/conda/environment-test-3.10.yml b/conda/environment-test-3.10.yml index 597782d25bb..cc4099f9e80 100644 --- a/conda/environment-test-3.10.yml +++ b/conda/environment-test-3.10.yml @@ -67,7 +67,7 @@ dependencies: - autoclasstoc <=1.4 - pydata_sphinx_theme ==0.9 - requests-unixsocket >= 0.3.0 - - sphinx >5.1 + - sphinx ==5.3 - sphinx-copybutton - sphinx-design - sphinxext-opengraph diff --git a/conda/environment-test-3.11.yml b/conda/environment-test-3.11.yml index 2036bf5fcef..77fa9bc571b 100644 --- a/conda/environment-test-3.11.yml +++ b/conda/environment-test-3.11.yml @@ -67,7 +67,7 @@ dependencies: - autoclasstoc <=1.4 - pydata_sphinx_theme ==0.9 - requests-unixsocket >= 0.3.0 - - sphinx >5.1 + - sphinx ==5.3 - sphinx-copybutton - sphinx-design - sphinxext-opengraph diff --git a/conda/environment-test-3.8.yml b/conda/environment-test-3.8.yml index 72372119405..b5fd96737a7 100644 --- a/conda/environment-test-3.8.yml +++ b/conda/environment-test-3.8.yml @@ -67,7 +67,7 @@ dependencies: - autoclasstoc <=1.4 - pydata_sphinx_theme ==0.9 - requests-unixsocket >= 0.3.0 - - sphinx >5.1 + - sphinx ==5.3 - sphinx-copybutton - sphinx-design - sphinxext-opengraph diff --git a/conda/environment-test-3.9.yml b/conda/environment-test-3.9.yml index e805ce314a3..3384933edf1 100644 --- a/conda/environment-test-3.9.yml +++ b/conda/environment-test-3.9.yml @@ -67,7 +67,7 @@ dependencies: - autoclasstoc <=1.4 - pydata_sphinx_theme ==0.9 - requests-unixsocket >= 0.3.0 - - sphinx >5.1 + - sphinx ==5.3 - sphinx-copybutton - sphinx-design - sphinxext-opengraph diff --git a/conda/environment-test-minimal-deps.yml b/conda/environment-test-minimal-deps.yml index b514ab0fbe8..bfde8bcc320 100644 --- a/conda/environment-test-minimal-deps.yml +++ b/conda/environment-test-minimal-deps.yml @@ -57,7 +57,7 @@ dependencies: - autoclasstoc <=1.4 - pydata_sphinx_theme ==0.9 - requests-unixsocket >= 0.3.0 - - sphinx >5.1 + - sphinx ==5.3 - sphinx-copybutton - sphinx-design - sphinxext-opengraph diff --git a/docs/bokeh/source/docs/dev_guide/setup.rst b/docs/bokeh/source/docs/dev_guide/setup.rst index 4cc949e1950..a303031fa7d 100644 --- a/docs/bokeh/source/docs/dev_guide/setup.rst +++ b/docs/bokeh/source/docs/dev_guide/setup.rst @@ -91,7 +91,7 @@ upstream with the following commands: .. tab-item:: HTTPS - ..code-block:: sh + .. code-block:: sh git remote add upstream https://github.com/bokeh/bokeh.git git fetch upstream @@ -156,7 +156,7 @@ directory, and run the following commands: .. code-block:: sh cd bokehjs - npm install --location=global npm@8 + npm install --location=global npm If you do not want to install npm globally, leave out the ``--location=global`` flag. In this case, you need to adjust all subsequent ``npm`` commands to use @@ -569,7 +569,7 @@ checkout* directory: .. code-block:: sh - BOKEH_DEV=false python -m bokeh serve --show examples/app/sliders.py + BOKEH_DEV=false python -m bokeh serve --show examples/server/app/sliders.py .. tab-item:: Windows (PS) :sync: ps @@ -577,7 +577,7 @@ checkout* directory: .. code-block:: powershell $Env:BOKEH_DEV = "False" - python.exe -m bokeh serve --show .\examples\app\sliders.py + python.exe -m bokeh serve --show .\examples\server\app\sliders.py .. tab-item:: Windows (CMD) :sync: cmd @@ -585,7 +585,7 @@ checkout* directory: .. code-block:: doscon set BOKEH_DEV=false - python -m bokeh serve --show examples\app\sliders.py + python -m bokeh serve --show examples\server\app\sliders.py This should open up a browser with an interactive figure: @@ -601,44 +601,70 @@ Troubleshooting --------------- Updating an existing development environment does not always work as -expected. Make sure your +expected. As a general rule, make sure your :ref:`conda environment `, :ref:`Node packages `, and -:ref:`local build ` are up to date. +:ref:`local build ` are always up to date. -Sometimes you may run into issues if the tags of the Bokeh repository have not -been cloned to your local directory. To check if the necessary tags are present, -run the following command: +The following list contains solutions to common issue that you might encounter when +setting up a development environment: -.. tab-set:: +.. dropdown:: Git tags missing (``KeyError: '0.0.1'``) - .. tab-item:: Linux/macOS - :sync: sh + Sometimes you may run into issues if the tags of the Bokeh repository have not + been cloned to your local directory. You might see a ``KeyError: '0.0.1'`` on your + console output, for example. - .. code-block:: sh + To check if the necessary tags are present, run the following command: - git tag -l | tail + .. tab-set:: - .. tab-item:: Windows (PS) - :sync: ps + .. tab-item:: Linux/macOS + :sync: sh - .. code-block:: powershell + .. code-block:: sh - git tag -l + git tag -l | tail - .. tab-item:: Windows (CMD) - :sync: cmd + .. tab-item:: Windows (PS) + :sync: ps - .. code-block:: doscon + .. code-block:: powershell + + git tag -l + + .. tab-item:: Windows (CMD) + :sync: cmd + + .. code-block:: doscon + + git tag -l + + If there are no tags present, make sure that you follow the steps of + :ref:`setting the Bokeh repository as an additional upstream `. + +.. dropdown:: Git commit fails due to line endings (``test_code_quality.py``, ``File + contains carriage returns``) + + On Windows systems, you may get a ``File contains carriage returns at end of line: + `` error while trying to push your local branch to your remote branch on + GitHub. This is because Bokeh only allows LF line endings, while some Windows-based + tools may add CR LF line endings. + + If you see this error, try running the following command: + ``git config --global core.autocrlf false``. After running this command, delete and + re-clone your forked repository (see :ref:`contributor_guide_setup_cloning`) + + This command configures git to always preserves the original LF-only newlines. + See the `GitHub documentation`_ or `Git config documentation`_ for other options. - git tag -l -If there are no tags present, make sure that you follow the steps of :ref:`setting the Bokeh repository as an additional upstream`. +.. dropdown:: Errors after updating from an older version -If you keep getting errors after updating an older environment, use -``conda remove --name bkdev --all``, delete your local ``bokeh`` folder, -and reinstall your development environment, following the steps in this guide -from :ref:`the beginning `. + If you keep getting errors after updating an older environment, use + ``conda remove --name bkdev --all``, delete your local ``bokeh`` folder, + and reinstall your development environment, following the steps in this guide + from :ref:`the beginning `. For more information on running and installing Bokeh, check the :ref:`additional resources available to contributors `. @@ -673,3 +699,5 @@ Slack`_. .. _pip: https://pip.pypa.io/ .. _merge conflicts: https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging#_basic_merge_conflicts .. _source maps: https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Use_a_source_map +.. _GitHub documentation: https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings +.. _Git config documentation: https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreautocrlf diff --git a/docs/bokeh/source/docs/first_steps/first_steps_1.rst b/docs/bokeh/source/docs/first_steps/first_steps_1.rst index a12469d8b57..71c20aaa39f 100644 --- a/docs/bokeh/source/docs/first_steps/first_steps_1.rst +++ b/docs/bokeh/source/docs/first_steps/first_steps_1.rst @@ -29,6 +29,11 @@ find links to both those resources. .. _first_steps_1_line_chart: +.. note:: + + All the code in "First Steps" sections can be be run as standard Python scripts. + When run, these scripts will create HTML outputs that are visible in a web browser. + Creating a simple line chart ---------------------------- @@ -54,13 +59,19 @@ right of the plot to explore: Follow these steps to recreate this simple line chart: -1. Import the necessary functions from the |bokeh.plotting| module: +1. Create a new Python file on your machine (e.g. ``simple_line_chart.py``) and open +it in a code editor of your choice (such as `Sublime Text`_,`Visual Studio Code`_ , etc.). + +.. _Sublime Text: https://www.sublimetext.com/ +.. _Visual Studio Code: https://code.visualstudio.com/ + +2. As the first line of your new Python script, import the necessary functions from the |bokeh.plotting| module: .. code-block:: python from bokeh.plotting import figure, show -2. Define two lists containing the data for your line chart: +3. Define two lists containing the data for your line chart: .. code-block:: python @@ -68,7 +79,7 @@ Follow these steps to recreate this simple line chart: x = [1, 2, 3, 4, 5] y = [6, 7, 2, 4, 5] -3. Use the |figure| function to create your plot. Pass the following arguments: +4. Use the |figure| function to create your plot. Pass the following arguments: * ``title``: the title of your line chart (optional) * ``x_axis_label``: a text label to put on the chart's x-axis (optional) @@ -79,7 +90,7 @@ Follow these steps to recreate this simple line chart: # create a new plot with a title and axis labels p = figure(title="Simple line example", x_axis_label='x', y_axis_label='y') -4. Add a line graph to the plot you just created, using the +5. Add a line graph to the plot you just created, using the :func:`~bokeh.plotting.figure.line` function. Pass the following arguments: * your lists ``x`` and ``y`` containing the data @@ -91,7 +102,7 @@ Follow these steps to recreate this simple line chart: # add a line renderer with legend and line thickness to the plot p.line(x, y, legend_label="Temp.", line_width=2) -5. Finally, use the |show| function to generate your graph and +6. Finally, use the |show| function to generate your graph and open a web browser to display the generated HTML file. .. code-block:: python @@ -99,6 +110,12 @@ Follow these steps to recreate this simple line chart: # show the results show(p) +7. From the command line, run the Python script you just created. For example: + + .. code-block:: sh + + python simple_line_chart.py + When you execute these lines of code, Bokeh creates an output file ``"lines.html"``. Bokeh also opens a browser to display it. diff --git a/examples/README.md b/examples/README.md index 955047b1348..9af2fbfdf80 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,46 +6,65 @@ This directory contains many examples of different ways to use Bokeh. As Bokeh h fast, it is important that you **ensure that the version of an example you're looking at matches the version of Bokeh you are running**. -### [`app`](app/) +### [`advanced`](advanced/) +This folder contains examples of more advanced or complex Bokeh applications and use +cases, beyond the basic plotting and visualization examples that are included in the +examples folder. -This directory contains examples of Bokeh Apps, which are simple and easy to create web applications for data visualization or exploration. +### [`basic`](basic/) -### [`custom`](custom/) +Each example in the basic folder provides a clear and concise demonstration of how to +create a specific type of visualization using Bokeh. These examples are intended for users +who want to learn how to create basic visualizations using the library. -The content of this directory demonstrate how to add custom extensions to Bokeh. Check project's documentation [page](https://docs.bokeh.org/en/latest/docs/user_guide/extensions.html) +### [`interaction`](interaction/) -### [`embed`](embed/) - -This directory includes examples that show how to embed Bokeh plots and widget in HTML documents. - -### [`howto`](howto/) - -The examples in this directory are mini-tutorials that demonstrate and explain -some particular aspect of Bokeh capability (such as [linking and -brushing](http://www.infovis-wiki.net/index.php?title=Linking_and_Brushing)), +The examples in this directory are mini-tutorials that +demonstrate and explain some particular aspect of Bokeh capability +(such as [linking and brushing](http://www.infovis-wiki.net/index.php?title=Linking_and_Brushing)), or walk through a particular example in additional detail. - ### [`models`](models/) This directory contains examples that use the lowest-level [`bokeh.models`](https://docs.bokeh.org/en/latest/docs/reference/models.html) -interface. For more information about Bokeh models see [the concepts section of -the user_guide](https://docs.bokeh.org/en/latest/docs/user_guide/concepts.html#interfaces) +interface. For more information about Bokeh models see +[the concepts section of the user_guide](https://docs.bokeh.org/en/latest/docs/user_guide/concepts.html#interfaces) + + +### [`output`](output/) + +This folder contains examples of how to use Bokeh to generate various types of output, +including static image files, standalone HTML files, and embedded Bokeh plots in Jupyter +notebooks. ### [`plotting`](plotting/) This directory contains example using the [`bokeh.plotting`](https://docs.bokeh.org/en/latest/docs/reference/plotting.html) -interface. For more information about Bokeh plotting see [the concepts section of -the user_guide](https://docs.bokeh.org/en/latest/docs/user_guide/concepts.html#interfaces) +interface. For more information about Bokeh plotting see +[the concepts section of the user_guide](https://docs.bokeh.org/en/latest/docs/user_guide/concepts.html#interfaces) + +### [`reference`](reference/) + +This folder contains a collection of examples that demonstrate the usage and +functionality of various Bokeh objects and properties, including Bokeh models, properties, +and tools. + +### [`server`](server/) + +This folder contains a collection of examples that demonstrate +how to create interactive web applications using Bokeh server. + +### [`styling`](styling/) + +This folder contains a collection of examples that demonstrate +various ways to style Bokeh plots using CSS styles and themes. -### [`webgl`](webgl/) +### [`topics`](topics/) -This directory contains examples that demonstrate the various glyphs that have -support for WebGL rendering. Most of these examples have a testing purpose, e.g. -to compare the appearance of the WebGL glyph with its regular appearance, or to -test another aspect of WebGL (e.g. blending of transparent glyphs). +This folder contains a collection of examples that cover a +wide range of topics related to creating interactive visualizations with Bokeh. ## Other sources for examples diff --git a/examples/interaction/widgets/autocompleteinput.py b/examples/interaction/widgets/autocompleteinput.py index cc0b8c3c49e..fd42aa9580c 100644 --- a/examples/interaction/widgets/autocompleteinput.py +++ b/examples/interaction/widgets/autocompleteinput.py @@ -2,7 +2,7 @@ from bokeh.models import AutocompleteInput from bokeh.sampledata.world_cities import data -completion_list = data["name"].tolist() +completion_list = data.dropna()["name"].tolist() auto_complete_input = AutocompleteInput(title="Enter a city:", completions=completion_list) diff --git a/examples/models/tile_source.py b/examples/models/tile_source.py index 06faddbac2c..e033d32e1a9 100644 --- a/examples/models/tile_source.py +++ b/examples/models/tile_source.py @@ -1,3 +1,12 @@ +''' Example to demonstrate creating map-based visualizations and +working with geographical data using WMTSTileSource in Bokeh. + +.. bokeh-example-metadata:: + :apis: bokeh.models.WMTSTileSource, bokeh.models.Range1d, bokeh.models.BoxZoomTool, bokeh.models.PanTool, bokeh.models.WheelZoomTool + :refs: :ref:`ug_topics_geo_tile_provider_maps` + :keywords: tile source + +''' from bokeh.document import Document from bokeh.embed import file_html from bokeh.models import (BoxZoomTool, PanTool, Plot, Range1d, diff --git a/examples/plotting/line_on_off.py b/examples/plotting/line_on_off.py index 3277b0ad10d..2693091621e 100644 --- a/examples/plotting/line_on_off.py +++ b/examples/plotting/line_on_off.py @@ -1,5 +1,11 @@ -""" Example demonstrating turning lines on and off - with JS only - +""" Example demonstrating turning lines on and off - with JS only. +It involves checking and unchecking the checkboxes representing +the plotted lines to turn the lines on/off. + +.. bokeh-example-metadata:: + :apis: bokeh.plotting.figure.line, bokeh.layouts.row, bokeh.models.CustomJS + :refs: :ref:`ug_basic_lines_multi`,:ref:`_ug_interaction_widgets_examples_checkboxgroup`,:ref:`ug_interaction_js_callbacks` + :keywords: line, checkbox, CustomJS """ import numpy as np @@ -10,7 +16,7 @@ from bokeh.plotting import figure, show p = figure() -props = dict(line_width=4, line_alpha=0.7) +props = {"line_width": 4, "line_alpha": 0.7} x = np.linspace(0, 4 * np.pi, 100) l0 = p.line(x, np.sin(x), color=Viridis3[0], legend_label="Line 0", **props) l1 = p.line(x, 4 * np.cos(x), color=Viridis3[1], legend_label="Line 1", **props) diff --git a/release/deploy.py b/release/deploy.py index 69db2604a21..ceb18d5cffe 100644 --- a/release/deploy.py +++ b/release/deploy.py @@ -49,16 +49,20 @@ def publish_conda_package(config: Config, system: System) -> ActionReturn: def publish_documentation(config: Config, system: System) -> ActionReturn: version, release_level = config.version, config.release_level path = f"deployment-{version}/docs/bokeh/build/html" - flags = "--acl bucket-owner-full-control --cache-control max-age=31536000,public" + flags = "--only-show-errors --acl bucket-owner-full-control" + WEEK = 3600 * 24 * 7 + YEAR = 3600 * 24 * 365 + def cache(max_age: int) -> str: + return f"--cache-control max-age={max_age},public" try: if config.prerelease: - system.run(f"aws s3 sync {path} s3://docs.bokeh.org/en/dev-{release_level}/ {flags} {REGION}") + system.run(f"aws s3 sync {path} s3://docs.bokeh.org/en/dev-{release_level}/ {flags} {cache(YEAR)} {REGION}") system.run(f'aws cloudfront create-invalidation --distribution-id {CLOUDFRONT_ID} --paths "/en/dev-{release_level}*" {REGION}') else: - system.run(f"aws s3 sync {path} s3://docs.bokeh.org/en/latest/ {flags} {REGION}") - system.run(f"aws s3 sync {path} s3://docs.bokeh.org/en/{version}/ {flags} {REGION}") + system.run(f"aws s3 sync {path} s3://docs.bokeh.org/en/{version}/ {flags} {cache(YEAR)} {REGION}") + system.run(f"aws s3 sync {path} s3://docs.bokeh.org/en/latest/ --delete {flags} {cache(WEEK)} {REGION}") switcher = f"deployment-{version}/docs/bokeh/switcher.json" - system.run(f"aws s3 cp {switcher} s3://docs.bokeh.org/ {flags} {REGION}") + system.run(f"aws s3 cp {switcher} s3://docs.bokeh.org/ {flags} {cache(WEEK)} {REGION}") system.run(f'aws cloudfront create-invalidation --distribution-id {CLOUDFRONT_ID} --paths "/en/latest*" "/en/{version}*" "/switcher.json" {REGION}') return PASSED("Publish to documentation site succeeded") except RuntimeError as e: diff --git a/scripts/ci/install_node_modules.sh b/scripts/ci/install_node_modules.sh index e670769ae77..f4f65fb6070 100755 --- a/scripts/ci/install_node_modules.sh +++ b/scripts/ci/install_node_modules.sh @@ -3,7 +3,7 @@ set -x #echo on cd bokehjs -npm install --location=global npm@8 +npm install --location=global npm npm ci --no-progress node make examples --no-build git restore package*.json # avoid .dirty in the package version diff --git a/setup.py b/setup.py index 17317f33954..ed9dab4fbf9 100644 --- a/setup.py +++ b/setup.py @@ -83,16 +83,18 @@ def install_js(packages: list[str]) -> None: if missing: die(BOKEHJS_INSTALL_FAIL.format(missing=", ".join(missing))) - if PKG_JS.exists(): - rmtree(PKG_JS) - copytree(BUILD_JS, PKG_JS) - - if PKG_TSLIB.exists(): - rmtree(PKG_TSLIB) - if BUILD_TSLIB.exists(): - PKG_TSLIB.mkdir() - for lib_file in BUILD_TSLIB.glob("lib.*.d.ts"): - copy(lib_file, PKG_TSLIB) + if not PKG_JS.is_symlink(): + if PKG_JS.exists(): + rmtree(PKG_JS) + copytree(BUILD_JS, PKG_JS) + + if not PKG_TSLIB.is_symlink(): + if PKG_TSLIB.exists(): + rmtree(PKG_TSLIB) + if BUILD_TSLIB.exists(): + PKG_TSLIB.mkdir() + for lib_file in BUILD_TSLIB.glob("lib.*.d.ts"): + copy(lib_file, PKG_TSLIB) new = set( ".".join([*Path(parent).relative_to(SRC_ROOT).parts, d]) @@ -118,10 +120,23 @@ def build_or_install_bokehjs(packages: list[str]) -> None: raise ValueError(f"Unrecognized action {action!r}") print(f"Used {bright(yellow(kind))} BokehJS from {loc}\n") +def check_tags() -> None: + try: + tags = subprocess.check_output(("git", "tag", "-l")).split() + if len(tags) < 5: # arbitrary "too-low" cutoff, there will always be more than 5 tags, if there are any + die(bright(red(MISSING_TAGS))) + except Exception: + print(bright(yellow("!!! Could not check repo tags. Please ensure full tag history"))) + def die(x: str) -> NoReturn: print(f"{x}\n") sys.exit(1) +MISSING_TAGS = """ +!!! This repository is missing git tags! Full tag history is required to build Bokeh. +!!! +!!! See https://docs.bokeh.org/en/latest/docs/dev_guide/setup.html#troubleshooting +""" SUCCESS = f"{bright(green('Success!'))}\n" FAILED = f"{bright(red('Failed.'))}\n" BUILD_SUCCESS_MSG =f"{SUCCESS}\nBuild output:\n\n{{msg}}" @@ -141,11 +156,13 @@ def die(x: str) -> NoReturn: class Build(build): # type: ignore def run(self) -> None: + check_tags() build_or_install_bokehjs(self.distribution.packages) super().run() class EditableWheel(editable_wheel): # type: ignore def run(self) -> None: + check_tags() build_or_install_bokehjs(self.distribution.packages) super().run() diff --git a/src/bokeh/core/serialization.py b/src/bokeh/core/serialization.py index 4c9c97921c5..7bebf2b876f 100644 --- a/src/bokeh/core/serialization.py +++ b/src/bokeh/core/serialization.py @@ -199,8 +199,10 @@ def to_serializable(self, serializer: Serializer) -> AnyRep: ObjID = int class Serializer: - """ Convert built-in and custom types into serializable representations. """ - + """ Convert built-in and custom types into serializable representations. + Not all built-in types are supported (e.g., decimal.Decimal due to + lacking support for fixed point arithmetic in JavaScript). + """ _encoders: ClassVar[dict[type[Any], Encoder]] = {} @classmethod diff --git a/src/bokeh/embed/bundle.py b/src/bokeh/embed/bundle.py index 1c93fee362c..7eaedf37c55 100644 --- a/src/bokeh/embed/bundle.py +++ b/src/bokeh/embed/bundle.py @@ -32,6 +32,7 @@ join, normpath, ) +from pathlib import Path from typing import ( TYPE_CHECKING, Callable, @@ -44,10 +45,9 @@ from ..core.templates import CSS_RESOURCES, JS_RESOURCES from ..document.document import Document from ..model import Model -from ..resources import BaseResources, Resources +from ..resources import Resources from ..settings import settings from ..util.compiler import bundle_models -from ..util.warnings import warn from .util import contains_tex_string, is_tex_string if TYPE_CHECKING: @@ -145,46 +145,28 @@ def add(self, artifact: Artifact) -> None: elif isinstance(artifact, Style): self.css_raw.append(artifact.content) -def bundle_for_objs_and_resources(objs: Sequence[Model | Document] | None, - resources: BaseResources | tuple[BaseResources | None, BaseResources | None] | None) -> Bundle: +def bundle_for_objs_and_resources(objs: Sequence[Model | Document] | None, resources: Resources | None) -> Bundle: ''' Generate rendered CSS and JS resources suitable for the given collection of Bokeh objects Args: objs (seq[Model or Document]) : - resources (BaseResources or tuple[BaseResources]) + resources (Resources) Returns: Bundle ''' - # Any env vars will overide a local default passed in - resources = settings.resources(default=resources) - if isinstance(resources, str): - resources = Resources(mode=resources) - - if resources is None or isinstance(resources, BaseResources): - js_resources = css_resources = resources - elif isinstance(resources, tuple) and len(resources) == 2 and all(r is None or isinstance(r, BaseResources) for r in resources): - js_resources, css_resources = resources - - if js_resources and not css_resources: - warn('No Bokeh CSS Resources provided to template. If required you will need to provide them manually.') - - if css_resources and not js_resources: - warn('No Bokeh JS Resources provided to template. If required you will need to provide them manually.') - else: - raise ValueError(f"expected Resources or a pair of optional Resources, got {resources!r}") - if objs is not None: - all_objs = _all_objs(objs) + all_objs = _all_objs(objs) use_widgets = _use_widgets(all_objs) use_tables = _use_tables(all_objs) use_gl = _use_gl(all_objs) use_mathjax = _use_mathjax(all_objs) else: # XXX: force all components on server and in notebook, because we don't know in advance what will be used + all_objs = None use_widgets = True use_tables = True use_gl = True @@ -195,30 +177,23 @@ def bundle_for_objs_and_resources(objs: Sequence[Model | Document] | None, css_files: list[str] = [] css_raw: list[str] = [] - from copy import deepcopy - - if js_resources: - js_resources = deepcopy(js_resources) - if not use_widgets and "bokeh-widgets" in js_resources.js_components: - js_resources.js_components.remove("bokeh-widgets") - if not use_tables and "bokeh-tables" in js_resources.js_components: - js_resources.js_components.remove("bokeh-tables") - if not use_gl and "bokeh-gl" in js_resources.js_components: - js_resources.js_components.remove("bokeh-gl") - if not use_mathjax and "bokeh-mathjax" in js_resources.js_components: - js_resources.js_components.remove("bokeh-mathjax") - - js_files.extend(js_resources.js_files) - js_raw.extend(js_resources.js_raw) - - if css_resources: - css_resources = deepcopy(css_resources) - css_files.extend(css_resources.css_files) - css_raw.extend(css_resources.css_raw) - - if js_resources: - extensions = _bundle_extensions(all_objs if objs else None, js_resources) - mode = js_resources.mode if resources is not None else "inline" + if resources is not None: + components = list(resources.components) + if not use_widgets: components.remove("bokeh-widgets") + if not use_tables: components.remove("bokeh-tables") + if not use_gl: components.remove("bokeh-gl") + if not use_mathjax: components.remove("bokeh-mathjax") + + resources = resources.clone(components=components) + + js_files.extend(resources.js_files) + js_raw.extend(resources.js_raw) + + css_files.extend(resources.css_files) + css_raw.extend(resources.css_raw) + + extensions = _bundle_extensions(all_objs if objs else None, resources) + mode = resources.mode if mode == "inline": js_raw.extend([ Resources._inline(bundle.artifact_path) for bundle in extensions ]) elif mode == "server": @@ -232,12 +207,12 @@ def bundle_for_objs_and_resources(objs: Sequence[Model | Document] | None, else: js_files.extend([ bundle.artifact_path for bundle in extensions ]) - models = [ obj.__class__ for obj in all_objs ] if objs else None + models = [ obj.__class__ for obj in all_objs ] if all_objs else None ext = bundle_models(models) if ext is not None: js_raw.append(ext) - return Bundle(js_files, js_raw, css_files, css_raw, js_resources.hashes if js_resources else {}) + return Bundle(js_files, js_raw, css_files, css_raw, resources.hashes if resources else {}) #----------------------------------------------------------------------------- # Private API @@ -277,15 +252,17 @@ class Pkg(TypedDict, total=False): module: str main: str -extension_dirs: dict[str, str] = {} # name -> path +extension_dirs: dict[str, Path] = {} -def _bundle_extensions(all_objs: set[Model], resources: Resources) -> list[ExtensionEmbed]: +def _bundle_extensions(objs: set[Model] | None, resources: Resources) -> list[ExtensionEmbed]: names: set[str] = set() bundles: list[ExtensionEmbed] = [] extensions = [".min.js", ".js"] if resources.minified else [".js"] - for obj in all_objs if all_objs is not None else Model.model_class_reverse_map.values(): + all_objs = objs if objs is not None else Model.model_class_reverse_map.values() + + for obj in all_objs: if hasattr(obj, "__implementation__"): continue name = obj.__view_module__.split(".")[0] @@ -347,7 +324,7 @@ def _bundle_extensions(all_objs: set[Model], resources: Resources) -> list[Exten else: raise ValueError(f"can't resolve artifact path for '{name}' extension") - extension_dirs[name] = artifacts_dir + extension_dirs[name] = Path(artifacts_dir) server_url = f"{server_prefix}/{server_path}" embed = ExtensionEmbed(artifact_path, server_url, cdn_url) bundles.append(embed) diff --git a/src/bokeh/embed/standalone.py b/src/bokeh/embed/standalone.py index 661d01f4af6..22ff4fc0c2e 100644 --- a/src/bokeh/embed/standalone.py +++ b/src/bokeh/embed/standalone.py @@ -45,7 +45,7 @@ ) from ..document.document import DEFAULT_TITLE, Document from ..model import Model -from ..resources import CSSResources, JSResources, Resources +from ..resources import Resources from ..themes import Theme from .bundle import Script, bundle_for_objs_and_resources from .elements import html_page_for_render_items, script_for_render_items @@ -294,7 +294,7 @@ def div_for_root(root: RenderRoot) -> str: return script, result def file_html(models: Model | Document | Sequence[Model], - resources: Resources | tuple[JSResources | None, CSSResources | None] | None, + resources: Resources | None, title: str | None = None, template: Template | str = FILE, template_variables: dict[str, Any] = {}, @@ -311,7 +311,7 @@ def file_html(models: Model | Document | Sequence[Model], models (Model or Document or seq[Model]) : Bokeh object or objects to render typically a Model or Document - resources (Resources or tuple(JSResources or None, CSSResources or None)) : + resources (Resources) : A resource configuration for Bokeh JS & CSS assets. title (str, optional) : diff --git a/src/bokeh/embed/util.py b/src/bokeh/embed/util.py index 6c5a079f8bf..3180ed32531 100644 --- a/src/bokeh/embed/util.py +++ b/src/bokeh/embed/util.py @@ -38,7 +38,7 @@ from ..settings import settings from ..themes.theme import Theme from ..util.dataclasses import dataclass, field -from ..util.serialization import make_globally_unique_id +from ..util.serialization import make_globally_unique_css_safe_id, make_globally_unique_id if TYPE_CHECKING: from ..document.document import DocJson @@ -320,10 +320,10 @@ def standalone_docs_json_and_render_items(models: Model | Document | Sequence[Mo (docid, roots) = docs[doc] if model is not None: - roots[model] = make_globally_unique_id() + roots[model] = make_globally_unique_css_safe_id() else: for model in doc.roots: - roots[model] = make_globally_unique_id() + roots[model] = make_globally_unique_css_safe_id() docs_json: dict[ID, DocJson] = {} for doc, (docid, _) in docs.items(): diff --git a/src/bokeh/io/notebook.py b/src/bokeh/io/notebook.py index f34e9c3bac6..2450174b0aa 100644 --- a/src/bokeh/io/notebook.py +++ b/src/bokeh/io/notebook.py @@ -442,11 +442,14 @@ def load_notebook(resources: Resources | None = None, verbose: bool = False, from ..embed.bundle import bundle_for_objs_and_resources from ..resources import Resources from ..settings import settings - from ..util.serialization import make_id + from ..util.serialization import make_globally_unique_css_safe_id if resources is None: resources = Resources(mode=settings.resources()) + element_id: ID | None + html: str | None + if not hide_banner: if resources.mode == 'inline': js_info: str | list[str] = 'inline' @@ -459,7 +462,7 @@ def load_notebook(resources: Resources | None = None, verbose: bool = False, if _NOTEBOOK_LOADED and verbose: warnings.append('Warning: BokehJS previously loaded') - element_id: ID | None = make_id() + element_id = make_globally_unique_css_safe_id() html = NOTEBOOK_LOAD.render( element_id = element_id, @@ -482,9 +485,10 @@ def load_notebook(resources: Resources | None = None, verbose: bool = False, if html is not None: publish_display_data({'text/html': html}) + publish_display_data({ - JS_MIME_TYPE : nb_js, - LOAD_MIME_TYPE : jl_js, + JS_MIME_TYPE: nb_js, + LOAD_MIME_TYPE: jl_js, }) def publish_display_data(data: dict[str, Any], metadata: dict[Any, Any] | None = None, *, transient: dict[str, Any] | None = None, **kwargs: Any) -> None: diff --git a/src/bokeh/models/annotations/legends.py b/src/bokeh/models/annotations/legends.py index d35eb9f0dd7..2b264f15ad7 100644 --- a/src/bokeh/models/annotations/legends.py +++ b/src/bokeh/models/annotations/legends.py @@ -344,7 +344,7 @@ def __init__(self, *args, **kwargs) -> None: location = Either(Enum(LegendLocation), Tuple(Float, Float), default="top_right", help=""" The location where the legend should draw itself. It's either one of - ``bokeh.core.enums.LegendLocation``'s enumerated values, or a ``(x, y)`` + :class:`~bokeh.core.enums.LegendLocation`'s enumerated values, or a ``(x, y)`` tuple indicating an absolute location absolute location in screen coordinates (pixels from the bottom-left corner). """) diff --git a/src/bokeh/models/tools.py b/src/bokeh/models/tools.py index c0f708bffa7..0a51aeda600 100644 --- a/src/bokeh/models/tools.py +++ b/src/bokeh/models/tools.py @@ -1364,7 +1364,7 @@ def __init__(self, *args, **kwargs) -> None: """) DEFAULT_HELP_TIP = "Click the question mark to learn more about Bokeh plot tools." -DEFAULT_HELP_URL = "https://docs.bokeh.org/en/latest/docs/user_guide/tools.html" +DEFAULT_HELP_URL = "https://docs.bokeh.org/en/latest/docs/user_guide/interaction/tools.html" class HelpTool(ActionTool): ''' A button tool to provide a "help" link to users. diff --git a/src/bokeh/resources.py b/src/bokeh/resources.py index 7f87c108253..eb0b289abbb 100644 --- a/src/bokeh/resources.py +++ b/src/bokeh/resources.py @@ -45,6 +45,7 @@ Dict, Literal, Protocol, + TypedDict, Union, cast, get_args, @@ -70,13 +71,24 @@ DEFAULT_SERVER_HOST = "localhost" DEFAULT_SERVER_PORT = 5006 -DEFAULT_SERVER_HTTP_URL = f"http://{DEFAULT_SERVER_HOST}:{DEFAULT_SERVER_PORT}/" + +def server_url(host: str | None = None, port: int | None = None, ssl: bool = False) -> str: + protocol = "https" if ssl else "http" + return f"{protocol}://{host or DEFAULT_SERVER_HOST}:{port or DEFAULT_SERVER_PORT}/" + +DEFAULT_SERVER_HTTP_URL = server_url() BaseMode: TypeAlias = Literal["inline", "cdn", "server", "relative", "absolute"] DevMode: TypeAlias = Literal["server-dev", "relative-dev", "absolute-dev"] ResourcesMode: TypeAlias = Union[BaseMode, DevMode] +Component = Literal["bokeh", "bokeh-gl", "bokeh-widgets", "bokeh-tables", "bokeh-mathjax", "bokeh-api"] + +class ComponentDefs(TypedDict): + js: list[Component] + css: list[Component] + # __all__ defined at the bottom on the class module # ----------------------------------------------------------------------------- @@ -250,7 +262,55 @@ class Urls: ResourceAttr = Literal["__css__", "__javascript__"] -class BaseResources: +class Resources: + """ + The Resources class encapsulates information relating to loading or + embedding Bokeh Javascript and CSS. + + Args: + mode (str) : how should Bokeh JS and CSS be included in output + + See below for descriptions of available modes + + version (str, optional) : what version of Bokeh JS and CSS to load + + Only valid with the ``'cdn'`` mode + + root_dir (str, optional) : root directory for loading Bokeh JS and CSS assets + + Only valid with ``'relative'`` and ``'relative-dev'`` modes + + minified (bool, optional) : whether JavaScript and CSS should be minified or not (default: True) + + root_url (str, optional) : URL and port of Bokeh Server to load resources from + + Only valid with ``'server'`` and ``'server-dev'`` modes + + The following **mode** values are available for configuring a Resource object: + + * ``'inline'`` configure to provide entire Bokeh JS and CSS inline + * ``'cdn'`` configure to load Bokeh JS and CSS from ``https://cdn.bokeh.org`` + * ``'server'`` configure to load from a Bokeh Server + * ``'server-dev'`` same as ``server`` but supports non-minified assets + * ``'relative'`` configure to load relative to the given directory + * ``'relative-dev'`` same as ``relative`` but supports non-minified assets + * ``'absolute'`` configure to load from the installed Bokeh library static directory + * ``'absolute-dev'`` same as ``absolute`` but supports non-minified assets + + Once configured, a Resource object exposes the following public attributes: + + Attributes: + js_raw : any raw JS that needs to be placed inside ``