Skip to content

Commit 499a8b2

Browse files
committed
Merge remote-tracking branch 'origin/master' into v4.0
2 parents 635182a + 38a48b7 commit 499a8b2

17 files changed

Lines changed: 413 additions & 51 deletions

.github/workflows/upload-dev-build.yml

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,37 @@ env:
2121
jobs:
2222
upload:
2323
runs-on: ubuntu-latest
24-
# Only run on manual dispatch,
25-
# OR if the parent run succeeded and was triggered by a PR from
26-
# a branch in the main repo (not a fork)
24+
permissions:
25+
statuses: write
26+
# Only run on manual dispatch,
27+
# OR if the parent run succeeded and was triggered by a PR.
28+
# Fork PRs run through so we can post a "skipped" commit status to the PR
29+
# head and log why; the actual upload steps are guarded by fork-check so no
30+
# dev build is published for fork PRs (they lack DEV_DEPLOY_APP access).
2731
if: |
2832
github.event_name == 'workflow_dispatch' ||
2933
(
30-
github.event_name == 'workflow_run' &&
34+
github.event_name == 'workflow_run' &&
3135
github.event.workflow_run.event == 'pull_request' &&
32-
github.event.workflow_run.conclusion == 'success' &&
33-
github.event.workflow_run.head_repository.full_name == github.repository
36+
github.event.workflow_run.conclusion == 'success'
3437
)
3538
steps:
39+
- name: Check if PR is from a fork
40+
id: fork-check
41+
env:
42+
HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }}
43+
REPO: ${{ github.repository }}
44+
run: |
45+
if [ -n "${HEAD_REPO}" ] && [ "${HEAD_REPO}" != "${REPO}" ]; then
46+
echo "::notice::Dev-build upload skipped: PR is from a fork (${HEAD_REPO}). Dev builds are only uploaded for PRs from branches within ${REPO}. A maintainer can trigger a build manually via workflow_dispatch with the PR number as input."
47+
echo "is_fork=true" >> "$GITHUB_OUTPUT"
48+
else
49+
echo "is_fork=false" >> "$GITHUB_OUTPUT"
50+
fi
51+
3652
- name: Get required metadata (PR number, commit SHA, workflow run ID containing artifacts)
3753
id: get-metadata
54+
if: steps.fork-check.outputs.is_fork == 'false'
3855
env:
3956
GH_TOKEN: ${{ github.token }}
4057
GH_EVENT_NAME: ${{ github.event_name }}
@@ -91,6 +108,7 @@ jobs:
91108
92109
- name: Download build artifact
93110
id: download-artifact
111+
if: steps.fork-check.outputs.is_fork == 'false'
94112
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
95113
with:
96114
name: dist # uploaded by publish-dist.yml > publish-dist
@@ -100,6 +118,7 @@ jobs:
100118

101119
- name: Prepare folders
102120
id: setup-metadata
121+
if: steps.fork-check.outputs.is_fork == 'false'
103122
env:
104123
GH_TOKEN: ${{ github.token }}
105124
PR_NUM: ${{ steps.get-metadata.outputs.PR_NUM }}
@@ -124,6 +143,7 @@ jobs:
124143
125144
- name: Generate GitHub App token
126145
id: generate-token
146+
if: steps.fork-check.outputs.is_fork == 'false'
127147
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 #v3.1.1
128148
with:
129149
client-id: ${{ vars.DEV_DEPLOY_APP_ID }}
@@ -132,6 +152,7 @@ jobs:
132152
repositories: plotly.js-dev-builds
133153

134154
- name: Check out plotly.js-dev-builds repo
155+
if: steps.fork-check.outputs.is_fork == 'false'
135156
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
136157
with:
137158
repository: plotly/plotly.js-dev-builds
@@ -140,6 +161,7 @@ jobs:
140161

141162
- name: Commit and push files
142163
id: commit-and-push
164+
if: steps.fork-check.outputs.is_fork == 'false'
143165
env:
144166
PR_NUM: ${{ steps.get-metadata.outputs.PR_NUM }}
145167
SHORT_SHA: ${{ steps.get-metadata.outputs.SHORT_SHA }}
@@ -172,6 +194,7 @@ jobs:
172194
fi
173195
174196
- name: Generate summary
197+
if: steps.fork-check.outputs.is_fork == 'false'
175198
env:
176199
PR_NUM: ${{ steps.get-metadata.outputs.PR_NUM }}
177200
SHORT_SHA: ${{ steps.get-metadata.outputs.SHORT_SHA }}
@@ -182,3 +205,46 @@ jobs:
182205
echo "- Latest build for this PR: [${BASE_URL}/latest/plotly.min.js](${BASE_URL}/latest/plotly.min.js)" >> $GITHUB_STEP_SUMMARY
183206
echo "- Build for this commit: [${BASE_URL}/${SHORT_SHA}/plotly.min.js](${BASE_URL}/${SHORT_SHA}/plotly.min.js)" >> $GITHUB_STEP_SUMMARY
184207
echo "The above links should start working a minute or two after this job completes." >> $GITHUB_STEP_SUMMARY
208+
209+
- name: Report dev-build outcome to PR head
210+
# Post a commit status on the PR head SHA so a row appears in the PR
211+
# merge-box widget. Statuses are keyed by (sha, context) with
212+
# last-write-wins semantics, so a maintainer-triggered dispatch that
213+
# follows an earlier "Skipped" status updates the same row in place.
214+
if: |
215+
always() && (
216+
(github.event_name == 'workflow_run' && github.event.workflow_run.head_sha != '') ||
217+
(github.event_name == 'workflow_dispatch' && steps.get-metadata.outputs.SHA != '')
218+
)
219+
env:
220+
GH_TOKEN: ${{ github.token }}
221+
REPO: ${{ github.repository }}
222+
SHA: ${{ github.event.workflow_run.head_sha || steps.get-metadata.outputs.SHA }}
223+
IS_FORK: ${{ steps.fork-check.outputs.is_fork }}
224+
PR_NUM: ${{ steps.get-metadata.outputs.PR_NUM }}
225+
UPLOAD_OUTCOME: ${{ steps.commit-and-push.outcome }}
226+
RUN_ID: ${{ github.run_id }}
227+
run: |
228+
RUN_URL="https://github.com/${REPO}/actions/runs/${RUN_ID}"
229+
CONTRIBUTING_URL="https://github.com/${REPO}/blob/master/CONTRIBUTING.md#live-links-to-dev-builds"
230+
231+
if [ "${IS_FORK}" == "true" ]; then
232+
STATE="success"
233+
DESCRIPTION="Skipped — PR is from a fork. Ask a maintainer to trigger a build."
234+
TARGET_URL="${CONTRIBUTING_URL}"
235+
elif [ "${UPLOAD_OUTCOME}" == "success" ]; then
236+
STATE="success"
237+
DESCRIPTION="Latest dev builds for PR #${PR_NUM}"
238+
TARGET_URL="https://plotly.github.io/plotly.js-dev-builds/${UPLOAD_DIR_NAME}/pr-${PR_NUM}/latest"
239+
else
240+
STATE="failure"
241+
DESCRIPTION="Upload failed — see workflow run for details."
242+
TARGET_URL="${RUN_URL}"
243+
fi
244+
245+
jq -n \
246+
--arg state "${STATE}" \
247+
--arg description "${DESCRIPTION}" \
248+
--arg target_url "${TARGET_URL}" \
249+
'{state: $state, target_url: $target_url, description: $description, context: "Upload Dev Build"}' \
250+
| gh api --method POST --input - "repos/${REPO}/statuses/${SHA}"

draftlogs/7872_fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Prevent outside bar text for zero-length bars from overlapping axis tick labels [[#7872](https://github.com/plotly/plotly.js/pull/7872)], with thanks to @vizansh for the contribution!

draftlogs/7900_fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Fix `hovertemplate`/`texttemplate`/`tickformat`/`hoverformat` improperly handling d3-format specs that start with a sign flag such as `+.2f` [[#7900](https://github.com/plotly/plotly.js/pull/7900)], with thanks to @TemRevil for the contribution!

draftlogs/7908_fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Fix crash ("Something went wrong with axis scaling") when a colorbar's title or padding leaves it with a negative domain length [[#7908](https://github.com/plotly/plotly.js/pull/7908)], with thanks to @zeehio for the contribution!

draftlogs/7921_fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Prevent MultiPolygon features with no positive-area polygon from aborting choropleth rendering [[#7921](https://github.com/plotly/plotly.js/pull/7921)], with thanks to @swjturay for the contribution!

src/components/colorbar/draw.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ function makeColorBarData(gd) {
166166
return out;
167167
}
168168

169+
// Move domain[0] toward domain[1] by delta, without crossing it
170+
const insetDomainStart = (domain, delta) => [Math.min(domain[0] + delta, domain[1]), domain[1]];
171+
172+
// Move domain[1] toward domain[0] by delta, without crossing it
173+
const insetDomainEnd = (domain, delta) => [domain[0], Math.max(domain[1] - delta, domain[0])];
174+
169175
function drawColorBar(g, opts, gd) {
170176
var isVertical = opts.orientation === 'v';
171177
var len = opts.len;
@@ -299,13 +305,8 @@ function drawColorBar(g, opts, gd) {
299305

300306
// set domain after init, because we may want to
301307
// allow it outside [0,1]
302-
ax.domain = isVertical ? [
303-
vFrac + ypad / gs.h,
304-
vFrac + lenFrac - ypad / gs.h
305-
] : [
306-
vFrac + xpad / gs.w,
307-
vFrac + lenFrac - xpad / gs.w
308-
];
308+
var padFrac = isVertical ? ypad / gs.h : xpad / gs.w;
309+
ax.domain = insetDomainEnd(insetDomainStart([vFrac, vFrac + lenFrac], padFrac), padFrac);
309310

310311
ax.setScale();
311312

@@ -472,10 +473,10 @@ function drawColorBar(g, opts, gd) {
472473
titleHeight += 5;
473474

474475
if(titleSide === 'top') {
475-
ax.domain[1] -= titleHeight / gs.h;
476+
ax.domain = insetDomainEnd(ax.domain, titleHeight / gs.h);
476477
titleTrans[1] *= -1;
477478
} else {
478-
ax.domain[0] += titleHeight / gs.h;
479+
ax.domain = insetDomainStart(ax.domain, titleHeight / gs.h);
479480
var nlines = svgTextUtils.lineCount(titleText);
480481
titleTrans[1] += (1 - nlines) * lineSize;
481482
}
@@ -486,7 +487,7 @@ function drawColorBar(g, opts, gd) {
486487
} else { // horizontal colorbars
487488
if(titleWidth) {
488489
if(titleSide === 'right') {
489-
ax.domain[0] += (titleWidth + titleFontSize / 2) / gs.w;
490+
ax.domain = insetDomainStart(ax.domain, (titleWidth + titleFontSize / 2) / gs.w);
490491
}
491492

492493
titleGroup.attr('transform', strTranslate(titleTrans[0], titleTrans[1]));

src/lib/geo_location_utils.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,11 +238,18 @@ function extractTraceFeature(calcTrace) {
238238
properties: {}
239239
};
240240

241-
// Compute centroid, add it to the properties
242-
if (fOut.geometry.coordinates.length > 0) {
243-
fOut.properties.ct = findCentroid(fOut);
244-
} else {
245-
fOut.properties.ct = [NaN, NaN];
241+
fOut.properties.ct = findCentroid(fOut);
242+
243+
if (isNaN(fOut.properties.ct[0])) {
244+
loggers.log(
245+
[
246+
'Location',
247+
cdi.loc,
248+
'has no polygon with positive area.',
249+
'Its centroid could not be computed,',
250+
'so hover and selection will not work for it.'
251+
].join(' ')
252+
);
246253
}
247254

248255
// Mutate in in/out features into calcdata
@@ -327,6 +334,11 @@ function findCentroid(feature) {
327334
poly = geometry;
328335
}
329336

337+
// Guard against MultiPolygons that don't contain a positive-area polygon
338+
// (collapsed rings measure zero, malformed ring ordering measures negative)
339+
// and when either geometry type has rings holding no points at all.
340+
if (!poly || !poly.coordinates.some((ring) => ring.length > 0)) return [NaN, NaN];
341+
330342
return turfCentroid(poly).geometry.coordinates;
331343
}
332344

src/lib/index.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,16 @@ lib.adjustFormat = function adjustFormat(formatStr) {
1919
if (/^\d%/.test(formatStr)) return '~%';
2020
if (/^\ds/.test(formatStr)) return '~s';
2121

22-
// try adding tilde to the start of format in order to trim
23-
if (!/^[~,.0$]/.test(formatStr) && /[&fps]/.test(formatStr)) return '~' + formatStr;
22+
// A d3-format spec may begin with a sign flag (+, -, (, space). Look past
23+
// that prefix before deciding whether to trim, and reattach it: prepending
24+
// the tilde to the whole string (e.g. "~+.2f") is an invalid spec that
25+
// d3Format rejects, so "+.2f" used to be silently dropped.
26+
var prefix = (formatStr.match(/^[+\-( ]?/) || [''])[0];
27+
var rest = formatStr.slice(prefix.length);
28+
29+
// try adding tilde to trim trailing zeros; leave symbol-led specs ($, #)
30+
// untrimmed, since the symbol isn't part of the prefix we stripped above
31+
if (!/^[~,.0$#]/.test(rest) && /[&fps]/.test(rest)) return prefix + '~' + rest;
2432

2533
return formatStr;
2634
};

src/traces/bar/plot.js

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, r, overhead, op
554554
// get trace attributes
555555
var trace = cd[0].trace;
556556
var isHorizontal = trace.orientation === 'h';
557+
var zeroBarDir = getZeroBarDir(cd, isHorizontal, xa, ya);
557558

558559
var text = getText(fullLayout, cd, i, xa, ya);
559560

@@ -707,7 +708,8 @@ function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, r, overhead, op
707708
transform = toMoveOutsideBar(x0, x1, y0, y1, textBB, {
708709
isHorizontal: isHorizontal,
709710
constrained: constrained,
710-
angle: angle
711+
angle: angle,
712+
zeroBarDir: zeroBarDir
711713
});
712714
} else {
713715
constrained = trace.constraintext === 'both' || trace.constraintext === 'inside';
@@ -957,7 +959,12 @@ function toMoveOutsideBar(x0, x1, y0, y1, textBB, opts) {
957959
var anchorX = 0;
958960
var anchorY = 0;
959961

960-
var dir = isHorizontal ? dirSign(x1, x0) : dirSign(y0, y1);
962+
var dir;
963+
if ((isHorizontal ? x0 === x1 : y0 === y1) && opts.zeroBarDir) {
964+
dir = opts.zeroBarDir;
965+
} else {
966+
dir = isHorizontal ? dirSign(x1, x0) : dirSign(y0, y1);
967+
}
961968
if (isHorizontal) {
962969
targetX = x1 - dir * textpad;
963970
anchorX = dir * extrapad;
@@ -999,6 +1006,38 @@ function getTextPosition(trace, index) {
9991006
return helpers.coerceEnumerated(attributeTextPosition, value);
10001007
}
10011008

1009+
function getZeroBarDir(cd, isHorizontal, xa, ya) {
1010+
var hasPositive = false;
1011+
var hasNegative = false;
1012+
1013+
for (var i = 0; i < cd.length; i++) {
1014+
var s = cd[i].s;
1015+
1016+
if (s > 0) {
1017+
hasPositive = true;
1018+
} else if (s < 0) {
1019+
hasNegative = true;
1020+
}
1021+
1022+
if (hasPositive && hasNegative) {
1023+
return 0;
1024+
}
1025+
}
1026+
1027+
var axis = isHorizontal ? xa : ya;
1028+
var positiveDir = -dirSign(axis.range[0], axis.range[1]);
1029+
1030+
if (!hasNegative) {
1031+
return positiveDir;
1032+
}
1033+
1034+
if (!hasPositive) {
1035+
return -positiveDir;
1036+
}
1037+
1038+
return 0;
1039+
}
1040+
10021041
function calcTexttemplate(fullLayout, cd, index, xa, ya) {
10031042
var trace = cd[0].trace;
10041043
var texttemplate = Lib.castOption(trace, index, 'texttemplate');

test/image/mocks/axes_breaks.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,35 +74,35 @@
7474
"shapes": [
7575
{
7676
"type": "rect",
77-
"y0": 11,
78-
"y1": 89,
77+
"y0": "1970-01-01 00:00:00.011",
78+
"y1": "1970-01-01 00:00:00.089",
7979
"yref": "y2",
8080
"x0": 0.56,
8181
"x1": 1,
8282
"xref": "paper"
8383
},
8484
{
8585
"type": "rect",
86-
"y0": 101,
87-
"y1": 189,
86+
"y0": "1970-01-01 00:00:00.101",
87+
"y1": "1970-01-01 00:00:00.189",
8888
"yref": "y2",
8989
"x0": 0.56,
9090
"x1": 1,
9191
"xref": "paper"
9292
},
9393
{
9494
"type": "rect",
95-
"x0": 11,
96-
"x1": 89,
95+
"x0": "1970-01-01 00:00:00.011",
96+
"x1": "1970-01-01 00:00:00.089",
9797
"xref": "x4",
9898
"y0": 0,
9999
"y1": 0.5,
100100
"yref": "paper"
101101
},
102102
{
103103
"type": "rect",
104-
"x0": 101,
105-
"x1": 189,
104+
"x0": "1970-01-01 00:00:00.101",
105+
"x1": "1970-01-01 00:00:00.189",
106106
"xref": "x4",
107107
"y0": 0,
108108
"y1": 0.5,

0 commit comments

Comments
 (0)