From cf2ac87ef31110f09b6e6d22edb41ddaa9eebfb9 Mon Sep 17 00:00:00 2001 From: Daniel Cohen Gindi Date: Wed, 22 Apr 2026 14:19:53 +0300 Subject: [PATCH] pointStyleYOffset --- docs/configuration/legend.md | 1 + docs/samples/legend/point-style.md | 13 +++-- src/plugins/plugin.legend.js | 2 +- src/types/index.d.ts | 4 ++ .../legend-pointStyle-y-offset.json | 53 ++++++++++++++++++ .../legend-pointStyle-y-offset.png | Bin 0 -> 2818 bytes test/specs/plugin.legend.tests.js | 49 ++++++++++++++++ 7 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/plugin.legend/pointStyle-y-offset/legend-pointStyle-y-offset.json create mode 100644 test/fixtures/plugin.legend/pointStyle-y-offset/legend-pointStyle-y-offset.png diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index 1621f5a87d1..7da82739e9b 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -71,6 +71,7 @@ Namespace: `options.plugins.legend.labels` | `textAlign` | `string` | `'center'` | Horizontal alignment of the label text. Options are: `'left'`, `'right'` or `'center'`. | `usePointStyle` | `boolean` | `false` | Label style will match corresponding point style (size is based on pointStyleWidth or the minimum value between boxWidth and font.size). | `pointStyleWidth` | `number` | `null` | If `usePointStyle` is true, the width of the point style used for the legend. +| `pointStyleYOffset` | `number` | `0` | If `usePointStyle` is true, the vertical offset in pixels applied to the point style drawn for the legend item. Useful for visually centering asymmetric point styles such as `line` or `dash`, or for problematic fonts. | `useBorderRadius` | `boolean` | `false` | Label borderRadius will match corresponding borderRadius. | `borderRadius` | `number` | `undefined` | Override the borderRadius to use. diff --git a/docs/samples/legend/point-style.md b/docs/samples/legend/point-style.md index 355045c676e..4b720d827c3 100644 --- a/docs/samples/legend/point-style.md +++ b/docs/samples/legend/point-style.md @@ -58,12 +58,15 @@ module.exports = { }; ``` -## Docs +For asymmetric point styles such as `line` or `dash`, you can nudge the symbol vertically using the `pointStyleYOffset` option set to a small positive number (e.g., `4` pixels). + +## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Legend](../../configuration/legend.md) - * [Legend Label Configuration](../../configuration/legend.md#legend-label-configuration) - * `usePointStyle` + * [Legend Label Configuration](../../configuration/legend.md#legend-label-configuration) + * `usePointStyle` + * `pointStyleYOffset` * [Elements](../../configuration/elements.md) - * [Point Configuration](../../configuration/elements.md#point-configuration) - * [Point Styles](../../configuration/elements.md#point-styles) + * [Point Configuration](../../configuration/elements.md#point-configuration) + * [Point Styles](../../configuration/elements.md#point-styles) diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 6ed99413536..bb77a2df545 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -417,7 +417,7 @@ export class Legend extends Element { const realX = rtlHelper.x(x); - drawLegendBox(realX, y, legendItem); + drawLegendBox(realX, y + (labelOpts.pointStyleYOffset || 0), legendItem); x = _textX(textAlign, x + boxWidth + halfFontSize, isHorizontal ? x + width : this.right, opts.rtl); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 32831adc88c..1479cd0db9f 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -2496,6 +2496,10 @@ export interface LegendOptions { * If usePointStyle is true, the width of the point style used for the legend. */ pointStyleWidth: number; + /** + * If you are using a font that causes incorrect alignment, adjust this value to ensure proper alignment. + */ + datasetRadiusBuffer: number; /** * Generates legend items for each thing in the legend. Default implementation returns the text + styling for the color box. See Legend Item for details. */ diff --git a/test/fixtures/plugin.legend/pointStyle-y-offset/legend-pointStyle-y-offset.json b/test/fixtures/plugin.legend/pointStyle-y-offset/legend-pointStyle-y-offset.json new file mode 100644 index 00000000000..f68b3b8bafa --- /dev/null +++ b/test/fixtures/plugin.legend/pointStyle-y-offset/legend-pointStyle-y-offset.json @@ -0,0 +1,53 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["A"], + "datasets": [ + { + "label": "Line", + "data": [10], + "backgroundColor": "#4bc0c0", + "borderColor": "#4bc0c0", + "borderWidth": 2, + "pointStyle": "line" + }, + { + "label": "Dash", + "data": [20], + "backgroundColor": "#ff6384", + "borderColor": "#ff6384", + "borderWidth": 2, + "pointStyle": "dash" + } + ] + }, + "options": { + "plugins": { + "legend": { + "display": true, + "labels": { + "usePointStyle": true, + "pointStyleWidth": 30, + "pointStyleYOffset": 4 + } + } + }, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + } + } + }, + "options": { + "canvas": { + "width": 512, + "height": 128 + } + } +} + diff --git a/test/fixtures/plugin.legend/pointStyle-y-offset/legend-pointStyle-y-offset.png b/test/fixtures/plugin.legend/pointStyle-y-offset/legend-pointStyle-y-offset.png new file mode 100644 index 0000000000000000000000000000000000000000..f66bc5884b3e7df7b2b16b9cf1c8191f6b770423 GIT binary patch literal 2818 zcmXw*dpwlcAIHy(p^>pomXuBAr=pNsq+F+%+7v74mw3+J`Hy<{hYU~8?78P$frr@x(OKRqlcz`EBEszTkLp$ z$p6&tgG55!<)jOw2jhLUq|Fwx>0T1@C-fTkHf2-lw!En6_ItJW@{&`T;mclkla~u& zGbR)C-y6e2@_Dp36NKxYBb8IX#_@?HG6P_+Q#c$W!o1Q8i^CI1RrRW1*;)7;C$k<8 zB-gn=MxJN8v7NMlthf{gdF_Y_3GDbObX*3mK#ok*nwAGG5wiv;+ere&3TGaQSj-xh z`^(=~p$-neLxu=sr0X0dp)#q7>%1TZJWmWL_gv%tw%6c3mRb&>>I_BqFllPfs3v101y zEZ^NMuToaJudXp&ms;ohw$|j;c-{@u@x%D&`T40{76rc?TfNJC!L}>sYfG)=TCb{V zbwS+fjan@Yfd=pWa*hk9w&zR^SpkC~by3x8HzrOW?cnKb$?Pm0%Oku^mHfoEO|4$z z<&ijYJkQjVT8n~v6>hGT?oEr{ec>{9TWVdx=TaBn9_ICJuS?jtzUGn(tEBj z;lQFI#Vw|AR=ZBHdB_7NgTKD#DA+&!PBHOMDdS8lKEEO-f%VstkI%WCbq+pj-nOw> zK8|rlDy`ExQ~C?Cq(Ec(NK@2I!qI8wNq2>!k6E27gF7>0h)=p)wq2r!4YyY9o-n0_m~-YheVo^OKHYQeduoh>&7 z8Q*yLbtU|VT6~kt@LZDvS6HX)*xrf%mPmpLyISI;jhC6qEkV2AKEcP8F;$0xbH10m zFG=dCw6mT)%L?yQ9h+%a9W7I$brkdS?^*WEpXiHrG#%_%^!UV!(ijhmzcOH!K^LSh zuDSeBxK=vHw;Ugi7FY#w6gSzKNT)U4#|P~ofuOdD4+`QQ2Tb`7$~!u8DsvlVUI?Ce zs?6rG7k#hrSxbYlg2~BAvucwy6Hw#ZvT3ID#}l^aXWAmaogIp}$ZBR4C=KPC@L8+N z&I#96KlJD5=g#cm8OZ9}D!sGJ=)Kz6SlgAAwbc9e590Kl1hd68&AL+4>UiP|^WdO! z{8rt&S#^6)g$-YOzW93e>MNY9T*FOG%&4+lFmj^F>@awM<}ka`Nv;Y25Y%E@yl<2rVMuI2i;)2FM*o>}OD1 zv)mIj*mjtraW`SyBv*}z#{J^K0ipM@n$aCwC6-{k=9Xd&PTVU*QecoVaxlqK*gEyBWPObFs-LlwezC1O5!o2YOa|(g^On3@gRdxtSegd@1I2NSz!duqk-e+H+FYAFkSnZ>lNt#)8HP66p)KDBTR!mg z*&y0hYdDCV-o2+02{=ZlVZm=Cb)zZD!AnT)hpU=Mci~mj1ykH-5Fw84CLiu*BzsC5 z-Oa#Z7`RW?JsDMmca+mUuSOeGu@1>!(b5@d4hDM+w(*#BZnx-?3Vx{tf^1&U(Be>F zaVW_p<)Y%m!90d)B6gyZ8Bnr|I<-qAc?yx#cNb18-boos>QWo6M3Qb$(%r#%J1Pl# zb&QZ>|6LMJN03|J0xBsECB>7AnIcIZB56RQ2qm?nF&M8|^WtO?l!vg1O0z>@D1@V8 z#c?8NfWR=?F%$+s*wb1RE`rE~+jNoUjzTX8xyePpieMB1YiK7?=mKGtd6BOO<|43< z7K}nFglw@QHxaBy-~x??!Xpr7w-){^g8c~G6iG)R8A3*Kp^XSGAF|GEXpR{hBT; zEti4AF)-4R<`E}_GwLTlSp-+z zKwH`&oDmZ#JTTe77!nP8JlyL_Sd=pw2tBJuopVjZS`c=bLte`RSQ2!q?gI@UY598X(RE-GSaO5QiVI0>K~5*J|*O1o8Ul+No0?SW5#6~*47@{m{3f6tXuW&i*H literal 0 HcmV?d00001 diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index e0bed42c263..df785174a04 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -877,6 +877,55 @@ describe('Legend block tests', function() { }]); }); + it('should apply pointStyleYOffset to the legend point symbol only', function() { + function drawWithOffset(offset) { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: ['A'], + datasets: [{ + label: 'dataset1', + data: [1], + pointStyle: 'circle' + }] + }, + options: { + plugins: { + legend: { + labels: { + usePointStyle: true, + pointStyleYOffset: offset + } + } + } + } + }); + + var mockContext = window.createMockContext(); + chart.legend.ctx = mockContext; + chart.legend.draw(); + + var calls = mockContext.getCalls(); + var arcCall = calls.filter(function(call) { + return call.name === 'arc'; + })[0]; + var fillTextCall = calls.filter(function(call) { + return call.name === 'fillText'; + })[0]; + + return { + arcY: arcCall.args[1], + textY: fillTextCall.args[2] + }; + } + + var base = drawWithOffset(0); + var shifted = drawWithOffset(6); + + expect(shifted.arcY - base.arcY).toBeCloseTo(6, 6); + expect(shifted.textY).toBeCloseTo(base.textY, 6); + }); + it('should not crash when the legend defaults are false', function() { const oldDefaults = Chart.defaults.plugins.legend;