Skip to content

Commit c43fcd4

Browse files
authored
Merge pull request #134 from sgratzl/release/v4.4.0
Release v4.4.0
2 parents 9c51b11 + 75decf4 commit c43fcd4

File tree

12 files changed

+137
-30
lines changed

12 files changed

+137
-30
lines changed

.github/dependabot.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ updates:
55
directory: '/'
66
schedule:
77
interval: 'monthly'
8+
target-branch: 'dev'
9+
labels:
10+
- 'dependencies'
11+
- 'chore'
812
- package-ecosystem: 'npm'
913
directory: '/'
1014
schedule:
1115
interval: 'monthly'
16+
target-branch: 'dev'
17+
labels:
18+
- 'dependencies'
19+
- 'chore'

.github/workflows/create_release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
run: |
2929
echo "next_tag=$(npm version --no-git-tag-version ${{ github.event.inputs.versionName }} --preid ${{ github.event.inputs.preid }})" >> $GITHUB_OUTPUT
3030
- name: Create pull request into main
31-
uses: peter-evans/create-pull-request@v4
31+
uses: peter-evans/create-pull-request@v6
3232
with:
3333
branch: release/${{ steps.version.outputs.next_tag }}
3434
commit-message: 'chore: release ${{ steps.version.outputs.next_tag }}'

.github/workflows/deploy_website.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
${{ runner.os }}-yarn2-v5
3535
- run: yarn install
3636
- run: yarn docs:build
37-
- uses: actions/configure-pages@v4
37+
- uses: actions/configure-pages@v5
3838
- uses: actions/upload-pages-artifact@v3
3939
with:
4040
path: docs/.vitepress/dist

.github/workflows/release_helper.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
echo "releasing ${{ steps.extract_version.outputs.version }} with tag ${{ steps.extract_version.outputs.npm_tag }}"
3232
- name: Create Release
3333
id: create_release
34-
uses: release-drafter/release-drafter@v5
34+
uses: release-drafter/release-drafter@v6
3535
env:
3636
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3737
with:
@@ -70,7 +70,7 @@ jobs:
7070
- run: yarn build
7171
- run: yarn pack
7272
- name: Upload Release Asset
73-
uses: AButler/upload-release-assets@v2.0.2
73+
uses: AButler/upload-release-assets@v3.0
7474
with:
7575
files: 'package.tgz'
7676
repo-token: ${{ secrets.GITHUB_TOKEN }}

docs/examples/items.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ export const config: ChartConfiguration<'boxplot'> = {
88
data,
99
options: {
1010
elements: {
11-
boxplot: {
11+
boxandwhiskers: {
1212
itemRadius: 2,
13+
itemHitRadius: 4,
1314
},
1415
},
1516
},

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@sgratzl/chartjs-chart-boxplot",
33
"description": "Chart.js module for charting boxplots and violin charts",
4-
"version": "4.3.3",
4+
"version": "4.4.0",
55
"publishConfig": {
66
"access": "public"
77
},

src/controllers/BoxPlotController.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
AnimationOptions,
1212
ScriptableContext,
1313
CartesianScaleTypeRegistry,
14+
BarControllerDatasetOptions,
1415
} from 'chart.js';
1516
import { merge } from 'chart.js/helpers';
1617
import { asBoxPlotStats, IBoxPlot, IBoxplotOptions } from '../data';
@@ -70,6 +71,10 @@ export class BoxPlotController extends StatsBase<IBoxPlot, Required<IBoxplotOpti
7071

7172
export interface BoxPlotControllerDatasetOptions
7273
extends ControllerDatasetOptions,
74+
Pick<
75+
BarControllerDatasetOptions,
76+
'barPercentage' | 'barThickness' | 'categoryPercentage' | 'maxBarThickness' | 'minBarLength'
77+
>,
7378
IBoxplotOptions,
7479
ScriptableAndArrayOptions<IBoxAndWhiskersOptions, ScriptableContext<'boxplot'>>,
7580
ScriptableAndArrayOptions<CommonHoverOptions, ScriptableContext<'boxplot'>>,

src/controllers/StatsBase.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,10 @@ export abstract class StatsBase<S extends IBaseStats, C extends Required<IBaseOp
147147
/**
148148
* @hidden
149149
*/
150-
getLabelAndValue(index: number): { label: string; value: string & { raw: S; hoveredOutlierIndex: number } & S } {
150+
getLabelAndValue(index: number): {
151+
label: string;
152+
value: string & { raw: S; hoveredOutlierIndex: number; hoveredItemIndex: number } & S;
153+
} {
151154
const r = super.getLabelAndValue(index) as any;
152155
const { vScale } = this._cachedMeta;
153156
const parsed = this.getParsed(index) as unknown as S;
@@ -157,6 +160,7 @@ export abstract class StatsBase<S extends IBaseStats, C extends Required<IBaseOp
157160
r.value = {
158161
raw: parsed,
159162
hoveredOutlierIndex: -1,
163+
hoveredItemIndex: -1,
160164
};
161165
this._transformStats(r.value, parsed, (v) => vScale.getLabelForValue(v));
162166
const s = this._toStringStats(r.value.raw);
@@ -166,6 +170,10 @@ export abstract class StatsBase<S extends IBaseStats, C extends Required<IBaseOp
166170
// TODO formatter
167171
return `(outlier: ${this.outliers[this.hoveredOutlierIndex]})`;
168172
}
173+
if (this.hoveredItemIndex >= 0) {
174+
// TODO formatter
175+
return `(item: ${this.items[this.hoveredItemIndex]})`;
176+
}
169177
return s;
170178
};
171179
return r;

src/controllers/ViolinController.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
AnimationOptions,
1212
ScriptableContext,
1313
CartesianScaleTypeRegistry,
14+
BarControllerDatasetOptions,
1415
} from 'chart.js';
1516
import { merge } from 'chart.js/helpers';
1617
import { asViolinStats, IViolin, IViolinOptions } from '../data';
@@ -79,6 +80,10 @@ export type ViolinDataPoint = number[] | (Partial<IViolin> & Pick<IViolin, 'medi
7980

8081
export interface ViolinControllerDatasetOptions
8182
extends ControllerDatasetOptions,
83+
Pick<
84+
BarControllerDatasetOptions,
85+
'barPercentage' | 'barThickness' | 'categoryPercentage' | 'maxBarThickness' | 'minBarLength'
86+
>,
8287
IViolinOptions,
8388
ScriptableAndArrayOptions<IViolinElementOptions, ScriptableContext<'violin'>>,
8489
ScriptableAndArrayOptions<CommonHoverOptions, ScriptableContext<'violin'>>,

src/elements/BoxAndWiskers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,6 @@ export class BoxAndWiskers extends StatsBase<IBoxAndWhiskerProps, IBoxAndWhisker
259259

260260
declare module 'chart.js' {
261261
export interface ElementOptionsByType<TType extends ChartType> {
262-
boxplot: ScriptableAndArrayOptions<IBoxAndWhiskersOptions & CommonHoverOptions, ScriptableContext<TType>>;
262+
boxandwhiskers: ScriptableAndArrayOptions<IBoxAndWhiskersOptions & CommonHoverOptions, ScriptableContext<TType>>;
263263
}
264264
}

src/elements/base.ts

Lines changed: 95 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ export interface IStatsBaseOptions {
116116
* @indexable
117117
*/
118118
itemBorderWidth: number;
119+
/**
120+
* hit radius for hit test of items
121+
* @default 0
122+
* @scriptable
123+
* @indexable
124+
*/
125+
itemHitRadius: number;
119126

120127
/**
121128
* padding that is added around the bounding box when computing a mouse hit
@@ -195,6 +202,7 @@ export const baseDefaults = {
195202
itemStyle: 'circle',
196203
itemRadius: 0,
197204
itemBorderWidth: 0,
205+
itemHitRadius: 0,
198206

199207
meanStyle: 'circle',
200208
meanRadius: 3,
@@ -399,7 +407,8 @@ export class StatsBase<T extends IStatsBaseProps & { mean?: number }, O extends
399407
}
400408
return (
401409
this._boxInRange(mouseX, mouseY, useFinalPosition) ||
402-
this._outlierIndexInRange(mouseX, mouseY, useFinalPosition) >= 0
410+
this._outlierIndexInRange(mouseX, mouseY, useFinalPosition) != null ||
411+
this._itemIndexInRange(mouseX, mouseY, useFinalPosition) != null
403412
);
404413
}
405414

@@ -422,23 +431,75 @@ export class StatsBase<T extends IStatsBaseProps & { mean?: number }, O extends
422431
/**
423432
* @hidden
424433
*/
425-
protected _outlierIndexInRange(mouseX: number, mouseY: number, useFinalPosition?: boolean): number {
434+
protected _outlierIndexInRange(
435+
mouseX: number,
436+
mouseY: number,
437+
useFinalPosition?: boolean
438+
): { index: number; x: number; y: number } | null {
426439
const props = this.getProps(['x', 'y'], useFinalPosition);
427440
const hitRadius = this.options.outlierHitRadius;
428441
const outliers = this._getOutliers(useFinalPosition);
429442
const vertical = this.isVertical();
430443

431444
// check if along the outlier line
432445
if ((vertical && Math.abs(mouseX - props.x) > hitRadius) || (!vertical && Math.abs(mouseY - props.y) > hitRadius)) {
433-
return -1;
446+
return null;
434447
}
435448
const toCompare = vertical ? mouseY : mouseX;
436449
for (let i = 0; i < outliers.length; i += 1) {
437450
if (Math.abs(outliers[i] - toCompare) <= hitRadius) {
438-
return i;
451+
return vertical ? { index: i, x: props.x, y: outliers[i] } : { index: i, x: outliers[i], y: props.y };
452+
}
453+
}
454+
return null;
455+
}
456+
457+
/**
458+
* @hidden
459+
*/
460+
protected _itemIndexInRange(
461+
mouseX: number,
462+
mouseY: number,
463+
useFinalPosition?: boolean
464+
): { index: number; x: number; y: number } | null {
465+
const hitRadius = this.options.itemHitRadius;
466+
if (hitRadius <= 0) {
467+
return null;
468+
}
469+
const props = this.getProps(['x', 'y', 'items', 'width', 'height', 'outliers'], useFinalPosition);
470+
const vert = this.isVertical();
471+
const { options } = this;
472+
473+
if (options.itemRadius <= 0 || !props.items || props.items.length <= 0) {
474+
return null;
475+
}
476+
// jitter based on random data
477+
// use the dataset index and index to initialize the random number generator
478+
const random = rnd(this._datasetIndex * 1000 + this._index);
479+
const outliers = new Set(props.outliers || []);
480+
481+
if (vert) {
482+
for (let i = 0; i < props.items.length; i++) {
483+
const y = props.items[i];
484+
if (!outliers.has(y)) {
485+
const x = props.x - props.width / 2 + random() * props.width;
486+
if (Math.abs(x - mouseX) <= hitRadius && Math.abs(y - mouseY) <= hitRadius) {
487+
return { index: i, x, y };
488+
}
489+
}
490+
}
491+
} else {
492+
for (let i = 0; i < props.items.length; i++) {
493+
const x = props.items[i];
494+
if (!outliers.has(x)) {
495+
const y = props.y - props.height / 2 + random() * props.height;
496+
if (Math.abs(x - mouseX) <= hitRadius && Math.abs(y - mouseY) <= hitRadius) {
497+
return { index: i, x, y };
498+
}
499+
}
439500
}
440501
}
441-
return -1;
502+
return null;
442503
}
443504

444505
/**
@@ -482,28 +543,40 @@ export class StatsBase<T extends IStatsBaseProps & { mean?: number }, O extends
482543
if (tooltip) {
483544
// eslint-disable-next-line no-param-reassign
484545
delete tooltip._tooltipOutlier;
546+
// eslint-disable-next-line no-param-reassign
547+
delete tooltip._tooltipItem;
485548
}
486549

487-
const props = this.getProps(['x', 'y']);
488-
const index = this._outlierIndexInRange(eventPosition.x, eventPosition.y);
489-
if (index < 0 || !tooltip) {
490-
return this.getCenterPoint();
550+
//outlier
551+
const info = this._outlierIndexInRange(eventPosition.x, eventPosition.y);
552+
if (info != null && tooltip) {
553+
// hack in the data of the hovered outlier
554+
// eslint-disable-next-line no-param-reassign
555+
tooltip._tooltipOutlier = {
556+
index: info.index,
557+
datasetIndex: this._datasetIndex,
558+
};
559+
return {
560+
x: info.x,
561+
y: info.y,
562+
};
491563
}
492-
// hack in the data of the hovered outlier
493-
// eslint-disable-next-line no-param-reassign
494-
tooltip._tooltipOutlier = {
495-
index,
496-
datasetIndex: this._datasetIndex,
497-
};
498-
if (this.isVertical()) {
564+
// items
565+
const itemInfo = this._itemIndexInRange(eventPosition.x, eventPosition.y);
566+
if (itemInfo != null && tooltip) {
567+
// hack in the data of the hovered outlier
568+
// eslint-disable-next-line no-param-reassign
569+
tooltip._tooltipItem = {
570+
index: itemInfo.index,
571+
datasetIndex: this._datasetIndex,
572+
};
499573
return {
500-
x: props.x as number,
501-
y: this._getOutliers()[index],
574+
x: itemInfo.x,
575+
y: itemInfo.y,
502576
};
503577
}
504-
return {
505-
x: this._getOutliers()[index],
506-
y: props.y as number,
507-
};
578+
579+
// fallback
580+
return this.getCenterPoint();
508581
}
509582
}

src/tooltip.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export interface ExtendedTooltip extends TooltipModel<'boxplot' | 'violin'> {
55
index: number;
66
datasetIndex: number;
77
};
8+
_tooltipItem?: {
9+
index: number;
10+
datasetIndex: number;
11+
};
812
}
913

1014
/**
@@ -19,6 +23,9 @@ export function patchInHoveredOutlier(
1923
if (value && that._tooltipOutlier != null && item.datasetIndex === that._tooltipOutlier.datasetIndex) {
2024
value.hoveredOutlierIndex = that._tooltipOutlier.index;
2125
}
26+
if (value && that._tooltipItem != null && item.datasetIndex === that._tooltipItem.datasetIndex) {
27+
value.hoveredItemIndex = that._tooltipItem.index;
28+
}
2229
}
2330

2431
/**

0 commit comments

Comments
 (0)