From adaeffa935d3ef99f8337e11d319d1ebe3c13416 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Tue, 15 Aug 2017 11:18:37 +0800 Subject: [PATCH 1/3] Improve offset calculation for scale.offset option - Offset is calulated based on the intervals between the first two data points and the last two data points (barThickness: 'flex') or the minimum interval of all data (barThickness: undefined). - Add test for both cases of barThickness: 'flex' and undefined. --- src/controllers/controller.bar.js | 2 +- src/scales/scale.time.js | 65 +++++++++++++----- .../controller.bar/bar-thickness-offset.png | Bin 6577 -> 3186 bytes test/specs/scale.time.tests.js | 29 ++++++-- 4 files changed, 71 insertions(+), 25 deletions(-) diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index cadae3fdb6c..6040c5ddf9e 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -164,7 +164,7 @@ function computeFlexCategoryTraits(index, ruler, options) { if (prev === null) { // first data: its size is double based on the next point or, // if it's also the last data, we use the scale end extremity. - prev = curr - (next === null ? ruler.end - curr : next - curr); + prev = curr - (next === null ? Math.max(curr - ruler.start, ruler.end - curr) * 2 : next - curr); } if (next === null) { diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index dfd708b2345..6c3cdc7abb5 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -361,27 +361,54 @@ function generate(min, max, capacity, options) { * Returns the right and left offsets from edges in the form of {left, right}. * Offsets are added when the `offset` option is true. */ -function computeOffsets(table, ticks, min, max, options) { +function computeOffsets(table, ticks, data, min, max, options) { + var pos = []; + var minInterval = 1; + var timeOpts = options.time; + var barThickness = options.barThickness; var left = 0; var right = 0; - var upper, lower; - - if (options.offset && ticks.length) { - if (!options.time.min) { - upper = ticks.length > 1 ? ticks[1] : max; - lower = ticks[0]; - left = ( - interpolate(table, 'time', upper, 'pos') - - interpolate(table, 'time', lower, 'pos') - ) / 2; + var i, ilen, curr, prev, length, width; + + if (options.offset) { + data.forEach(function(timestamp) { + if (timestamp >= min && timestamp <= max) { + pos.push(interpolate(table, 'time', timestamp, 'pos')); + } + }); + + if (!barThickness) { + [data, ticks].forEach(function(timestamps) { + for (i = 0, ilen = timestamps.length; i < ilen; ++i) { + curr = interpolate(table, 'time', timestamps[i], 'pos'); + minInterval = i > 0 ? Math.min(minInterval, curr - prev) : minInterval; + prev = curr; + } + }); } - if (!options.time.max) { - upper = ticks[ticks.length - 1]; - lower = ticks.length > 1 ? ticks[ticks.length - 2] : min; - right = ( - interpolate(table, 'time', upper, 'pos') - - interpolate(table, 'time', lower, 'pos') - ) / 2; + + length = pos.length; + if (length) { + if (!timeOpts.min) { + if (length === 1) { + width = (1 - pos[0]) * 2; + } else if (barThickness) { + width = pos[1] - pos[0]; + } else { + width = minInterval; + } + left = Math.max(width / 2 - pos[0], 0); + } + if (!timeOpts.max) { + if (length === 1) { + width = pos[0] * 2; + } else if (barThickness) { + width = pos[length - 1] - pos[length - 2]; + } else { + width = minInterval; + } + right = Math.max(width / 2 - (1 - pos[length - 1]), 0); + } } } @@ -643,7 +670,7 @@ module.exports = function() { me._unit = timeOpts.unit || determineUnitForFormatting(ticks, timeOpts.minUnit, me.min, me.max); me._majorUnit = determineMajorUnit(me._unit); me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution); - me._offsets = computeOffsets(me._table, ticks, min, max, options); + me._offsets = computeOffsets(me._table, ticks, me._timestamps.data, min, max, options); me._labelFormat = determineLabelFormat(me._timestamps.data, timeOpts); return ticksFromTimestamps(ticks, me._majorUnit); diff --git a/test/fixtures/controller.bar/bar-thickness-offset.png b/test/fixtures/controller.bar/bar-thickness-offset.png index 8dcecac88a409dff7867684f0987662f1cc4f46e..74b2b33161dae67dc8779c5aec9ea12f89bc7cb2 100644 GIT binary patch literal 3186 zcmeH}Ye-XJ7{{M;I<`$Smjk8ihUB8q1`$m|+l)z>mqk}mazl_2lnwI3aHfYYl%MRA zP)ho#K^O4?Dx{r7iqOa@Ax?3gqo!Tmam%&oE^p@qreA{~sPle#UwGf=`S5>!{QqzL z>7o3JRYN`P?wRWvQ=e7 zO?Y|6scM@MDZaLU+@!HE9ebwJGYBe<;w)z6jJOb!V z1)Q*FV_?n)LQGn5U>ZRzg#ZEebWExMj|9k4T%m<289)!AC82j8K&6BP%&Z5PN(4-3 z^j9@{CbBzh33?R@M&0uvPcmA00nFswn28;xzZhzKQ*;b!XoMgwC)qaI?_8N3jKh|E zpI*Q72jUT9XLWO!t955*CEIa z^hsg2(ELA;MbU}WbZFF{|CQFQ;nCh#CtG8}?u)Kd<_M{`t)Z313I&2nzdYhM&{){S zcvnBjFHv&z-C#(U^L|vq1z;-9xb?@-#890Djn0Q=RTR3cgk9uy+-OBgE&}Tz=r$Q@ z-99cexqs7Qq=}DB5HZc*nx^XcxHQACM7R(n`Li%gR6{uV>CWYpPP1PL+{()>(zP8o G-uwX%S*J+= literal 6577 zcmeI1dr(tX9>>qU1PCCT2rdYLP1YAwQ1A(uLZWCXB4|sisJwiEyU1H4kPw2dB4Y(B zAdN(VrE80{pdvvGk|-h~V0<P+BsAGF;?NZln|8v(uX9xG*@5C3hE3~{-J6a3a+92_J`hGf# z*|}EDqvA<5x~XMNw^ke;J~Cd^TwKr9Pz1{30{S7nKz1scEzii3Ue=t?%ECbKTey2V zVO^tGWyCcSSwusRYy5n!VAJ<5^Yx*jfc4VQe+(u}XPpN{Ml_mXZ0~yDH5d5gJ2bRk zHD4#*+y*AcR?z0bs09#&&6I1d0Po<_!29;@2nPQe1H>#D>avDw)pOulRQfUq?uFq* zW`?UPn(<^6n&H&mfs-^# zL(jg16E|O@2nuca()x>K@BRknpFP}=g zLIj3olwgSJG_x6&47>0+8aqbH+gpVdg}b14b%uQN^gw|Xw!IFZuXvocMv=E9)h_x_ z=$$FnWU$T+2otoo3TNMLHB9y2o^2&gG=UR@Y9X3&sbjc=pJpY&hv>@&m6QlobFA)1 z9vs+Ghcs)RSKC^XM{}03T!8S7Pl6|@4%}ELdK>_56K8D*{H)b3Sg?Xwbe4he1Jm?J z0R8Y^<1YoUZ`eMBUFB}xb_kIS?{-00hCKuHK8_)SyZdc6!w9-a?b`vW7PfS2nFLEF z{Ljl__%FaCfj8s8%ic4Q9L%C>q$S)VIM6gS(=QjK85>I2$^?@IsE~ESq1L5v!)O*)fitVLD+)`m?FUb zm_d#m}9b--exEpVikI*fd)V33#&Lc6MfZI86^41(R3Pl#svzh^VwKbWs7_9V# zuvE9P{}={L(5!sqj0M5^o_?6{XdZyhf%{0r$pf`s1dy*OueK)FeuNMfIFAUI7jaa!}IN}1L}K+@gk zc-FYj9@^?rLswtD$c+t^EQ4Xm#LRIlB8w1AAkSkG%<>3oHj1>nf6f5^`m#8fuOsx|NhUb2S{7!XxV@MZQuUgqy)Yc6Rhkh zaWEVE92ITaD!TsF-MGOcG~+8d+syBaE3U$L2}B=fBKl??iDqo|3oaaVRXn)Vm@#`L zI6E*`YWkXcB)>;#C@(uS_`!;5?@GtR@VJ}3Yfz#E{%$}(dp(qHdMp@z{{)4oKa;*N zUzTHDp%Uv;iK4@Sc~j3z1T8}1A3l6aiz>LmRpe2|mO|L|9a*w~)~2CZ<-t+=;)H*S zwH-B*081*8O!ktB zZDP_Wj?sreB0fs{E3GwxhmvAUa}zBJ%A!#%qWb^LwkTEujvN6PZZ-(xhmWecmG^2T zbmk-a%xro>zKP=}s*H@2s`pSwGp6ns7v{>3muivi5LQ3c7t0 z8N_<>urPrVhFCG34WU!z_-oG;VC9~>jaFI;s=lMN{bXYa+%2S%E)2eSMeDKsmdZxBxZRYclMZED0)lfrdm0Wc` zg@C*O@~6T7q90hf2|AN!$LQ-F|Ce7JZNY-7ps9{GQu2tr42drlgYVK`A0>|JEQ zI{#Opt)DIb#f~7Z;n==uNAfI=(!$#}p-j7=b>wpwrwT6fU$b#qv5qPKJC1aH_G=MK fy92n%3ZcHs(Z24L4=Kp67!bZGYGdPu#H0TJN7H47 diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 964e25e080a..cefe60737c2 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -1276,16 +1276,35 @@ describe('Time scale tests', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.xAxes[0]; + var minInterval; options.offset = true; chart.update(); - var numTicks = scale.ticks.length; - var firstTickInterval = scale.getPixelForTick(1) - scale.getPixelForTick(0); - var lastTickInterval = scale.getPixelForTick(numTicks - 1) - scale.getPixelForTick(numTicks - 2); + if (source === 'auto' && distribution === 'series') { + minInterval = scale.getPixelForTick(5) - scale.getPixelForTick(4); + } else { + minInterval = scale.getPixelForValue('2020') - scale.getPixelForValue('2019'); + } + + expect(scale.getPixelForValue('2017')).toBeCloseToPixel(scale.left + minInterval / 2); + expect(scale.getPixelForValue('2042')).toBeCloseToPixel(scale.left + scale.width - minInterval / 2); + }); + + it ('should add offset from the edges if offset is true and barThickness is "flex"', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + + options.offset = true; + options.barThickness = 'flex'; + chart.update(); + + var firstInterval = scale.getPixelForValue('2019') - scale.getPixelForValue('2017'); + var lastInterval = scale.getPixelForValue('2042') - scale.getPixelForValue('2025'); - expect(scale.getPixelForValue('2017')).toBeCloseToPixel(scale.left + firstTickInterval / 2); - expect(scale.getPixelForValue('2042')).toBeCloseToPixel(scale.left + scale.width - lastTickInterval / 2); + expect(scale.getPixelForValue('2017')).toBeCloseToPixel(scale.left + firstInterval / 2); + expect(scale.getPixelForValue('2042')).toBeCloseToPixel(scale.left + scale.width - lastInterval / 2); }); it ('should not add offset if min and max extend the labels range', function() { From 839a1d84554103f62e000d49887e5a85be9fc6be Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Thu, 9 Aug 2018 22:18:14 +0800 Subject: [PATCH 2/3] Calculate minInterval only when there is one or more data points in the range --- src/scales/scale.time.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 6c3cdc7abb5..c23eadcdcf1 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -377,18 +377,19 @@ function computeOffsets(table, ticks, data, min, max, options) { } }); - if (!barThickness) { - [data, ticks].forEach(function(timestamps) { - for (i = 0, ilen = timestamps.length; i < ilen; ++i) { - curr = interpolate(table, 'time', timestamps[i], 'pos'); - minInterval = i > 0 ? Math.min(minInterval, curr - prev) : minInterval; - prev = curr; - } - }); - } - length = pos.length; if (length) { + // Calculate minInterval + if (!barThickness) { + [data, ticks].forEach(function(timestamps) { + for (i = 0, ilen = timestamps.length; i < ilen; ++i) { + curr = interpolate(table, 'time', timestamps[i], 'pos'); + minInterval = i > 0 ? Math.min(minInterval, curr - prev) : minInterval; + prev = curr; + } + }); + } + if (!timeOpts.min) { if (length === 1) { width = (1 - pos[0]) * 2; From dc9edbad2d58275fd06a9425a8ff1a3d5aee25b7 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Sun, 12 Aug 2018 20:43:38 +0800 Subject: [PATCH 3/3] Improve readability and add more comments --- src/scales/scale.time.js | 84 ++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index c23eadcdcf1..00e07e2d5c3 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -358,7 +358,11 @@ function generate(min, max, capacity, options) { } /** - * Returns the right and left offsets from edges in the form of {left, right}. + * Returns the right and left offsets from the edges of the chart in the form of {left, right} + * where each values is a relative width to the scale and ranges between 0 and 1. + * They add extra margins on the both sides by scaling down the original scale. + * Offsets are typically used for bar charts. They are calculated based on intervals + * between the data points to keep the leftmost and rightmost bars from being cut. * Offsets are added when the `offset` option is true. */ function computeOffsets(table, ticks, data, min, max, options) { @@ -370,47 +374,51 @@ function computeOffsets(table, ticks, data, min, max, options) { var right = 0; var i, ilen, curr, prev, length, width; - if (options.offset) { - data.forEach(function(timestamp) { - if (timestamp >= min && timestamp <= max) { - pos.push(interpolate(table, 'time', timestamp, 'pos')); - } - }); + if (!options.offset) { + return {left: 0, right: 0}; + } - length = pos.length; - if (length) { - // Calculate minInterval - if (!barThickness) { - [data, ticks].forEach(function(timestamps) { - for (i = 0, ilen = timestamps.length; i < ilen; ++i) { - curr = interpolate(table, 'time', timestamps[i], 'pos'); - minInterval = i > 0 ? Math.min(minInterval, curr - prev) : minInterval; - prev = curr; - } - }); - } + data.forEach(function(timestamp) { + if (timestamp >= min && timestamp <= max) { + pos.push(interpolate(table, 'time', timestamp, 'pos')); + } + }); - if (!timeOpts.min) { - if (length === 1) { - width = (1 - pos[0]) * 2; - } else if (barThickness) { - width = pos[1] - pos[0]; - } else { - width = minInterval; - } - left = Math.max(width / 2 - pos[0], 0); - } - if (!timeOpts.max) { - if (length === 1) { - width = pos[0] * 2; - } else if (barThickness) { - width = pos[length - 1] - pos[length - 2]; - } else { - width = minInterval; - } - right = Math.max(width / 2 - (1 - pos[length - 1]), 0); + length = pos.length; + if (!length) { + return {left: 0, right: 0}; + } + + if (!barThickness) { + // Calculate minInterval + [data, ticks].forEach(function(timestamps) { + for (i = 0, ilen = timestamps.length; i < ilen; ++i) { + curr = interpolate(table, 'time', timestamps[i], 'pos'); + minInterval = i > 0 ? Math.min(minInterval, curr - prev) : minInterval; + prev = curr; } + }); + } + + if (!timeOpts.min) { + if (length === 1) { + width = (1 - pos[0]) * 2; + } else if (barThickness) { + width = pos[1] - pos[0]; + } else { + width = minInterval; + } + left = Math.max(width / 2 - pos[0], 0); + } + if (!timeOpts.max) { + if (length === 1) { + width = pos[0] * 2; + } else if (barThickness) { + width = pos[length - 1] - pos[length - 2]; + } else { + width = minInterval; } + right = Math.max(width / 2 - (1 - pos[length - 1]), 0); } return {left: left, right: right};