From 85123ac0748d92a1637ee97a37e8172fa85d547f Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Sun, 14 Mar 2021 17:27:57 +0200 Subject: [PATCH] Add option to turn off grouping of bar datasets (#8641) * Add option to turn off grouping of bar datasets * Disregard time offset --- docs/docs/charts/bar.mdx | 1 + src/controllers/controller.bar.js | 53 ++++--- .../controller.bar/not-grouped/on-time.js | 134 ++++++++++++++++++ .../controller.bar/not-grouped/on-time.png | Bin 0 -> 17240 bytes 4 files changed, 166 insertions(+), 22 deletions(-) create mode 100644 test/fixtures/controller.bar/not-grouped/on-time.js create mode 100644 test/fixtures/controller.bar/not-grouped/on-time.png diff --git a/docs/docs/charts/bar.mdx b/docs/docs/charts/bar.mdx index 555fe38e5..d3123a21f 100644 --- a/docs/docs/charts/bar.mdx +++ b/docs/docs/charts/bar.mdx @@ -173,6 +173,7 @@ The bar chart accepts the following configuration from the associated dataset op | `categoryPercentage` | `number` | `0.8` | Percent (0-1) of the available width each category should be within the sample width. [more...](#barpercentage-vs-categorypercentage) | `barThickness` | `number`\|`string` | | Manually set width of each bar in pixels. If set to `'flex'`, it computes "optimal" sample widths that globally arrange bars side by side. If not set (default), bars are equally sized based on the smallest interval. [more...](#barthickness) | `base` | `number` | | Base value for the bar in data units along the value axis. If not set, defaults to the value axis base value. +| `grouped` | `boolean` | `true` | Should the bars be grouped on index axis. When `true`, all the datasets at same index value will be placed next to each other centering on that index value. When `false`, each bar is placed on its actual index-axis value. | `maxBarThickness` | `number` | | Set this to ensure that bars are not sized thicker than this. | `minBarLength` | `number` | | Set this to ensure that bars have a minimum length in pixels. diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 54be96a96..0d4176f27 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -255,9 +255,9 @@ export default class BarController extends DatasetController { updateElements(bars, start, count, mode) { const me = this; const reset = mode === 'reset'; - const vscale = me._cachedMeta.vScale; - const base = vscale.getBasePixel(); - const horizontal = vscale.isHorizontal(); + const vScale = me._cachedMeta.vScale; + const base = vScale.getBasePixel(); + const horizontal = vScale.isHorizontal(); const ruler = me._getRuler(); const firstOpts = me.resolveDataElementOptions(start, mode); const sharedOptions = me.getSharedOptions(firstOpts); @@ -266,14 +266,14 @@ export default class BarController extends DatasetController { me.updateSharedOptions(sharedOptions, mode, firstOpts); for (let i = start; i < start + count; i++) { - const vpixels = me._calculateBarValuePixels(i); + const vpixels = reset ? {base, head: base} : me._calculateBarValuePixels(i); const ipixels = me._calculateBarIndexPixels(i, ruler); const properties = { horizontal, - base: reset ? base : vpixels.base, - x: horizontal ? reset ? base : vpixels.head : ipixels.center, - y: horizontal ? ipixels.center : reset ? base : vpixels.head, + base: vpixels.base, + x: horizontal ? vpixels.head : ipixels.center, + y: horizontal ? ipixels.center : vpixels.head, height: horizontal ? ipixels.size : undefined, width: horizontal ? undefined : ipixels.size }; @@ -370,6 +370,7 @@ export default class BarController extends DatasetController { */ _getRuler() { const me = this; + const opts = me.options; const meta = me._cachedMeta; const iScale = meta.iScale; const pixels = []; @@ -379,11 +380,8 @@ export default class BarController extends DatasetController { pixels.push(iScale.getPixelForValue(me.getParsed(i)[iScale.axis], i)); } - // Note: a potential optimization would be to skip computing this - // only if the barThickness option is defined - // Since a scriptable option may return null or undefined that - // means the option would have to be of type number - const min = computeMinSampleSize(iScale); + const barThickness = opts.barThickness; + const min = barThickness || computeMinSampleSize(iScale); return { min, @@ -391,7 +389,10 @@ export default class BarController extends DatasetController { start: iScale._startPixel, end: iScale._endPixel, stackCount: me._getStackCount(), - scale: iScale + scale: iScale, + grouped: opts.grouped, + // bar thickness ratio used for non-grouped bars + ratio: barThickness ? 1 : opts.categoryPercentage * opts.barPercentage }; } @@ -459,17 +460,24 @@ export default class BarController extends DatasetController { */ _calculateBarIndexPixels(index, ruler) { const me = this; + const scale = ruler.scale; const options = me.options; - const stackCount = options.skipNull ? me._getStackCount(index) : ruler.stackCount; - const range = options.barThickness === 'flex' - ? computeFlexCategoryTraits(index, ruler, options, stackCount) - : computeFitCategoryTraits(index, ruler, options, stackCount); + const maxBarThickness = valueOrDefault(options.maxBarThickness, Infinity); + let center, size; + if (ruler.grouped) { + const stackCount = options.skipNull ? me._getStackCount(index) : ruler.stackCount; + const range = options.barThickness === 'flex' + ? computeFlexCategoryTraits(index, ruler, options, stackCount) + : computeFitCategoryTraits(index, ruler, options, stackCount); - const stackIndex = me._getStackIndex(me.index, me._cachedMeta.stack); - const center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); - const size = Math.min( - valueOrDefault(options.maxBarThickness, Infinity), - range.chunk * range.ratio); + const stackIndex = me._getStackIndex(me.index, me._cachedMeta.stack); + center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); + size = Math.min(maxBarThickness, range.chunk * range.ratio); + } else { + // For non-grouped bar charts, exact pixel values are used + center = scale.getPixelForValue(me.getParsed(index)[scale.axis], index); + size = Math.min(maxBarThickness, ruler.min * ruler.ratio); + } return { base: center - size / 2, @@ -512,6 +520,7 @@ BarController.defaults = { categoryPercentage: 0.8, barPercentage: 0.9, + grouped: true, animations: { numbers: { diff --git a/test/fixtures/controller.bar/not-grouped/on-time.js b/test/fixtures/controller.bar/not-grouped/on-time.js new file mode 100644 index 000000000..6c2d44cbb --- /dev/null +++ b/test/fixtures/controller.bar/not-grouped/on-time.js @@ -0,0 +1,134 @@ +const data1 = [ + { + x: '2017-11-02T20:30:00', + y: 27 + }, + { + x: '2017-11-03T20:53:00', + y: 30 + }, + { + x: '2017-11-06T05:46:00', + y: 19 + }, + { + x: '2017-11-06T21:03:00', + y: 28 + }, + { + x: '2017-11-07T20:49:00', + y: 29 + }, + { + x: '2017-11-08T21:52:00', + y: 33 + } +]; + +const data2 = [ + { + x: '2017-11-03T13:07:00', + y: 45 + }, + { + x: '2017-11-04T04:50:00', + y: 40 + }, + { + x: '2017-11-06T12:48:00', + y: 38 + }, + { + x: '2017-11-07T12:28:00', + y: 42 + }, + { + x: '2017-11-08T12:45:00', + y: 51 + }, + { + x: '2017-11-09T05:23:00', + y: 57 + } +]; + +const data3 = [ + { + x: '2017-11-03T16:30:00', + y: 32 + }, + { + x: '2017-11-04T11:50:00', + y: 34 + }, + { + x: '2017-11-06T18:30:00', + y: 28 + }, + { + x: '2017-11-07T15:51:00', + y: 31 + }, + { + x: '2017-11-08T17:27:00', + y: 36 + }, + { + x: '2017-11-09T06:53:00', + y: 31 + } +]; + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/5139', + config: { + type: 'bar', + data: { + datasets: [ + { + data: data1, + backgroundColor: 'rgb(0,0,255)', + }, + { + data: data2, + backgroundColor: 'rgb(255,0,0)', + }, + { + data: data3, + backgroundColor: 'rgb(0,255,0)', + }, + ] + }, + options: { + barThickness: 10, + grouped: false, + scales: { + x: { + bounds: 'ticks', + type: 'time', + offset: false, + position: 'bottom', + display: true, + time: { + isoWeekday: true, + unit: 'day' + }, + grid: { + offset: false + } + }, + y: { + beginAtZero: true, + display: false + } + }, + } + }, + options: { + spriteText: true, + canvas: { + width: 1000, + height: 300 + } + } +}; diff --git a/test/fixtures/controller.bar/not-grouped/on-time.png b/test/fixtures/controller.bar/not-grouped/on-time.png new file mode 100644 index 0000000000000000000000000000000000000000..92506bd3ae7b05f5d7f791ed195efbe3c361ccd3 GIT binary patch literal 17240 zcmeHvd03Ozw!Q@_I3Nyftx!l&5qkt_6~uy&C{{!SR7460SS=z12+9x*hRCr>IVvbJ zD2R!IARSNx3s$Gq%JVgBHs61^+1ROehmLS|SlgcaxDeBqJJEIx zn=N;RInlJegtcZbW^Oq@bKdNkaTKFi%T<=8*JJ!8q$-? z9+ff*slIPtUS_v-#q895=jt~%_K7X<`;s2NbM^F%>1D)P{m*Z_efIygVM^>u-BL1g zP0F($hCPX&oI2z$5Tsduz3qS(x4q4*bmAOQu^)L!)&Gf| zT8tpQtQ;%&W#zeG;sIY~d#j~*;>`3*t#Iyhj`#ca?K_J6;QI$PQ{z~M;orNZ3wVsn z<>h_z&NlvGj5jYalG-lLdv{xdj_VT?l&NQ$|Guy{Yh21AepnK0*#EX&a@_Ch*Ky(5 zUF^0E#`p;j^*`HZhv7Vv^4mY|?rv@I*Z%oW5M)$n7Gp3k#z6ahm`r)d&>*bWAeg@o z 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 G>wf@`++q0u literal 0 HcmV?d00001