diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c56a672..dda67f9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,15 @@ updates: directory: '/' schedule: interval: 'monthly' + target-branch: 'dev' + labels: + - 'dependencies' + - 'chore' - package-ecosystem: 'npm' directory: '/' schedule: interval: 'monthly' + target-branch: 'dev' + labels: + - 'dependencies' + - 'chore' diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 060db06..e9294f4 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -28,7 +28,7 @@ jobs: run: | echo "next_tag=$(npm version --no-git-tag-version ${{ github.event.inputs.versionName }} --preid ${{ github.event.inputs.preid }})" >> $GITHUB_OUTPUT - name: Create pull request into main - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@v6 with: branch: release/${{ steps.version.outputs.next_tag }} commit-message: 'chore: release ${{ steps.version.outputs.next_tag }}' diff --git a/.github/workflows/deploy_website.yml b/.github/workflows/deploy_website.yml index 3cc2762..17ffb86 100644 --- a/.github/workflows/deploy_website.yml +++ b/.github/workflows/deploy_website.yml @@ -34,7 +34,7 @@ jobs: ${{ runner.os }}-yarn2-v5 - run: yarn install - run: yarn docs:build - - uses: actions/configure-pages@v4 + - uses: actions/configure-pages@v5 - uses: actions/upload-pages-artifact@v3 with: path: docs/.vitepress/dist diff --git a/.github/workflows/release_helper.yml b/.github/workflows/release_helper.yml index 25c09b3..5c17b89 100644 --- a/.github/workflows/release_helper.yml +++ b/.github/workflows/release_helper.yml @@ -31,7 +31,7 @@ jobs: echo "releasing ${{ steps.extract_version.outputs.version }} with tag ${{ steps.extract_version.outputs.npm_tag }}" - name: Create Release id: create_release - uses: release-drafter/release-drafter@v5 + uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -70,7 +70,7 @@ jobs: - run: yarn build - run: yarn pack - name: Upload Release Asset - uses: AButler/upload-release-assets@v2.0.2 + uses: AButler/upload-release-assets@v3.0 with: files: 'package.tgz' repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/docs/examples/items.ts b/docs/examples/items.ts index c21a204..428e50b 100644 --- a/docs/examples/items.ts +++ b/docs/examples/items.ts @@ -8,8 +8,9 @@ export const config: ChartConfiguration<'boxplot'> = { data, options: { elements: { - boxplot: { + boxandwhiskers: { itemRadius: 2, + itemHitRadius: 4, }, }, }, diff --git a/package.json b/package.json index 9ea66d2..36c34cf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@sgratzl/chartjs-chart-boxplot", "description": "Chart.js module for charting boxplots and violin charts", - "version": "4.3.3", + "version": "4.4.0", "publishConfig": { "access": "public" }, diff --git a/src/controllers/BoxPlotController.ts b/src/controllers/BoxPlotController.ts index f0cc08c..09a7455 100644 --- a/src/controllers/BoxPlotController.ts +++ b/src/controllers/BoxPlotController.ts @@ -11,6 +11,7 @@ import { AnimationOptions, ScriptableContext, CartesianScaleTypeRegistry, + BarControllerDatasetOptions, } from 'chart.js'; import { merge } from 'chart.js/helpers'; import { asBoxPlotStats, IBoxPlot, IBoxplotOptions } from '../data'; @@ -70,6 +71,10 @@ export class BoxPlotController extends StatsBase, IBoxplotOptions, ScriptableAndArrayOptions>, ScriptableAndArrayOptions>, diff --git a/src/controllers/StatsBase.ts b/src/controllers/StatsBase.ts index 0a63b7f..2351fc0 100644 --- a/src/controllers/StatsBase.ts +++ b/src/controllers/StatsBase.ts @@ -147,7 +147,10 @@ export abstract class StatsBase vScale.getLabelForValue(v)); const s = this._toStringStats(r.value.raw); @@ -166,6 +170,10 @@ export abstract class StatsBase= 0) { + // TODO formatter + return `(item: ${this.items[this.hoveredItemIndex]})`; + } return s; }; return r; diff --git a/src/controllers/ViolinController.ts b/src/controllers/ViolinController.ts index 1433863..35e5bc0 100644 --- a/src/controllers/ViolinController.ts +++ b/src/controllers/ViolinController.ts @@ -11,6 +11,7 @@ import { AnimationOptions, ScriptableContext, CartesianScaleTypeRegistry, + BarControllerDatasetOptions, } from 'chart.js'; import { merge } from 'chart.js/helpers'; import { asViolinStats, IViolin, IViolinOptions } from '../data'; @@ -79,6 +80,10 @@ export type ViolinDataPoint = number[] | (Partial & Pick, IViolinOptions, ScriptableAndArrayOptions>, ScriptableAndArrayOptions>, diff --git a/src/elements/BoxAndWiskers.ts b/src/elements/BoxAndWiskers.ts index c8b32c2..6a44535 100644 --- a/src/elements/BoxAndWiskers.ts +++ b/src/elements/BoxAndWiskers.ts @@ -259,6 +259,6 @@ export class BoxAndWiskers extends StatsBase { - boxplot: ScriptableAndArrayOptions>; + boxandwhiskers: ScriptableAndArrayOptions>; } } diff --git a/src/elements/base.ts b/src/elements/base.ts index 72dcec4..a8e343f 100644 --- a/src/elements/base.ts +++ b/src/elements/base.ts @@ -116,6 +116,13 @@ export interface IStatsBaseOptions { * @indexable */ itemBorderWidth: number; + /** + * hit radius for hit test of items + * @default 0 + * @scriptable + * @indexable + */ + itemHitRadius: number; /** * padding that is added around the bounding box when computing a mouse hit @@ -195,6 +202,7 @@ export const baseDefaults = { itemStyle: 'circle', itemRadius: 0, itemBorderWidth: 0, + itemHitRadius: 0, meanStyle: 'circle', meanRadius: 3, @@ -399,7 +407,8 @@ export class StatsBase= 0 + this._outlierIndexInRange(mouseX, mouseY, useFinalPosition) != null || + this._itemIndexInRange(mouseX, mouseY, useFinalPosition) != null ); } @@ -422,7 +431,11 @@ export class StatsBase hitRadius) || (!vertical && Math.abs(mouseY - props.y) > hitRadius)) { - return -1; + return null; } const toCompare = vertical ? mouseY : mouseX; for (let i = 0; i < outliers.length; i += 1) { if (Math.abs(outliers[i] - toCompare) <= hitRadius) { - return i; + return vertical ? { index: i, x: props.x, y: outliers[i] } : { index: i, x: outliers[i], y: props.y }; + } + } + return null; + } + + /** + * @hidden + */ + protected _itemIndexInRange( + mouseX: number, + mouseY: number, + useFinalPosition?: boolean + ): { index: number; x: number; y: number } | null { + const hitRadius = this.options.itemHitRadius; + if (hitRadius <= 0) { + return null; + } + const props = this.getProps(['x', 'y', 'items', 'width', 'height', 'outliers'], useFinalPosition); + const vert = this.isVertical(); + const { options } = this; + + if (options.itemRadius <= 0 || !props.items || props.items.length <= 0) { + return null; + } + // jitter based on random data + // use the dataset index and index to initialize the random number generator + const random = rnd(this._datasetIndex * 1000 + this._index); + const outliers = new Set(props.outliers || []); + + if (vert) { + for (let i = 0; i < props.items.length; i++) { + const y = props.items[i]; + if (!outliers.has(y)) { + const x = props.x - props.width / 2 + random() * props.width; + if (Math.abs(x - mouseX) <= hitRadius && Math.abs(y - mouseY) <= hitRadius) { + return { index: i, x, y }; + } + } + } + } else { + for (let i = 0; i < props.items.length; i++) { + const x = props.items[i]; + if (!outliers.has(x)) { + const y = props.y - props.height / 2 + random() * props.height; + if (Math.abs(x - mouseX) <= hitRadius && Math.abs(y - mouseY) <= hitRadius) { + return { index: i, x, y }; + } + } } } - return -1; + return null; } /** @@ -482,28 +543,40 @@ export class StatsBase { index: number; datasetIndex: number; }; + _tooltipItem?: { + index: number; + datasetIndex: number; + }; } /** @@ -19,6 +23,9 @@ export function patchInHoveredOutlier( if (value && that._tooltipOutlier != null && item.datasetIndex === that._tooltipOutlier.datasetIndex) { value.hoveredOutlierIndex = that._tooltipOutlier.index; } + if (value && that._tooltipItem != null && item.datasetIndex === that._tooltipItem.datasetIndex) { + value.hoveredItemIndex = that._tooltipItem.index; + } } /**