From 5ff4deb031f6d3ad4dba8f1759e538fada2cbee0 Mon Sep 17 00:00:00 2001 From: Akshat Shukla Date: Sat, 10 Jan 2026 20:06:36 +0530 Subject: [PATCH 1/4] Fix: keep line visible when zooming between data points When zooming into a range with no visible data points, the line controller skipped rendering entirely. This change includes boundary points before and after the visible range so line segments crossing the viewport are still drawn. Fixes #12174 --- src/controllers/controller.line.js | 55 ++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index fddd5ce9889..b4c6104bc0e 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -45,14 +45,16 @@ export default class LineController extends DatasetController { const animationsDisabled = this.chart._animationsDisabled; let {start, count} = _getStartAndCountOfVisiblePoints(meta, points, animationsDisabled); - this._drawStart = start; - this._drawCount = count; - if (_scaleRangesChanged(meta)) { start = 0; count = points.length; } + ({start, count} = this._includeBoundaryPoints(meta, start, count)); + + this._drawStart = start; + this._drawCount = count; + // Update Line line._chart = this.chart; line._datasetIndex = this.index; @@ -73,6 +75,53 @@ export default class LineController extends DatasetController { this.updateElements(points, start, count, mode); } + _includeBoundaryPoints(meta, start, count) { + const {iScale, _parsed} = meta; + const {min, max, axis: iAxis} = iScale; + + if (!_parsed?.length || !isNumber(min) || !isNumber(max)) { + return {start, count}; + } + + let beforeIndex = -1; + let beforeValue = -Infinity; + let afterIndex = -1; + let afterValue = Infinity; + + for (let i = 0; i < _parsed.length; ++i) { + const value = _parsed[i][iAxis]; + if (!isNumber(value)) { + continue; + } + if (value < min && value > beforeValue) { + beforeValue = value; + beforeIndex = i; + } else if (value > max && value < afterValue) { + afterValue = value; + afterIndex = i; + } + } + + let drawStart = start; + let drawEnd = count > 0 ? start + count - 1 : -1; + + if (beforeIndex !== -1) { + drawStart = drawEnd === -1 ? beforeIndex : Math.min(drawStart, beforeIndex); + drawEnd = Math.max(drawEnd, beforeIndex); + } + + if (afterIndex !== -1) { + drawStart = drawEnd === -1 ? afterIndex : Math.min(drawStart, afterIndex); + drawEnd = Math.max(drawEnd, afterIndex); + } + + if (drawEnd >= drawStart && drawEnd !== -1) { + return {start: drawStart, count: drawEnd - drawStart + 1}; + } + + return {start, count}; + } + updateElements(points, start, count, mode) { const reset = mode === 'reset'; const {iScale, vScale, _stacked, _dataset} = this._cachedMeta; From 32d77057150815cba27ed3a44ab166fa38b8c55e Mon Sep 17 00:00:00 2001 From: Akshat Shukla Date: Sat, 10 Jan 2026 20:59:13 +0530 Subject: [PATCH 2/4] Update src/controllers/controller.line.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/controllers/controller.line.js | 125 ++++++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 12 deletions(-) diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index b4c6104bc0e..cc61e052d93 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -83,22 +83,123 @@ export default class LineController extends DatasetController { return {start, count}; } + const length = _parsed.length; + + // Find the largest value < min (beforeIndex) using binary search. let beforeIndex = -1; let beforeValue = -Infinity; + { + let low = 0; + let high = length - 1; + while (low <= high) { + let mid = (low + high) >> 1; + let value = _parsed[mid][iAxis]; + + // If value is not numeric, search outward from mid to find a nearby numeric value. + if (!isNumber(value)) { + let left = mid - 1; + let right = mid + 1; + let found = false; + while (left >= low || right <= high) { + if (left >= low) { + const lv = _parsed[left][iAxis]; + if (isNumber(lv)) { + value = lv; + mid = left; + found = true; + break; + } + left--; + } + if (right <= high) { + const rv = _parsed[right][iAxis]; + if (isNumber(rv)) { + value = rv; + mid = right; + found = true; + break; + } + right++; + } + } + if (!found) { + // No numeric values in current search window. + break; + } + } + + if (!isNumber(value)) { + break; + } + + if (value < min) { + if (value > beforeValue) { + beforeValue = value; + beforeIndex = mid; + } + low = mid + 1; + } else { + high = mid - 1; + } + } + } + + // Find the smallest value > max (afterIndex) using binary search. let afterIndex = -1; let afterValue = Infinity; - - for (let i = 0; i < _parsed.length; ++i) { - const value = _parsed[i][iAxis]; - if (!isNumber(value)) { - continue; - } - if (value < min && value > beforeValue) { - beforeValue = value; - beforeIndex = i; - } else if (value > max && value < afterValue) { - afterValue = value; - afterIndex = i; + { + let low = 0; + let high = length - 1; + while (low <= high) { + let mid = (low + high) >> 1; + let value = _parsed[mid][iAxis]; + + // If value is not numeric, search outward from mid to find a nearby numeric value. + if (!isNumber(value)) { + let left = mid - 1; + let right = mid + 1; + let found = false; + while (left >= low || right <= high) { + if (left >= low) { + const lv = _parsed[left][iAxis]; + if (isNumber(lv)) { + value = lv; + mid = left; + found = true; + break; + } + left--; + } + if (right <= high) { + const rv = _parsed[right][iAxis]; + if (isNumber(rv)) { + value = rv; + mid = right; + found = true; + break; + } + right++; + } + } + if (!found) { + // No numeric values in current search window. + break; + } + } + + if (!isNumber(value)) { + break; + } + + if (value > max) { + if (value < afterValue) { + afterValue = value; + afterIndex = mid; + } + high = mid - 1; + } else { + low = mid + 1; + } } } From 4aa623de574dcd4287ba9ec0cbea2d4101da2cfd Mon Sep 17 00:00:00 2001 From: Akshat Shukla Date: Sat, 10 Jan 2026 20:59:30 +0530 Subject: [PATCH 3/4] Update src/controllers/controller.line.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/controllers/controller.line.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index cc61e052d93..ec2ea1ef5b7 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -50,7 +50,9 @@ export default class LineController extends DatasetController { count = points.length; } - ({start, count} = this._includeBoundaryPoints(meta, start, count)); + if (count !== points.length) { + ({start, count} = this._includeBoundaryPoints(meta, start, count)); + } this._drawStart = start; this._drawCount = count; From ba8417a88650fc9e1bdcdf8c7179e99360bb9632 Mon Sep 17 00:00:00 2001 From: Akshat Shukla Date: Sat, 17 Jan 2026 22:10:04 +0530 Subject: [PATCH 4/4] Fix: detect unsorted data when parsing is disabled --- src/core/core.datasetController.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 9b7126a93fd..dfa09473713 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -459,8 +459,26 @@ export default class DatasetController { if (this._parsing === false) { meta._parsed = data; - meta._sorted = true; parsed = data; + + if (sorted && iScale) { + const axis = iAxis; + + for (i = start; i < start + count; ++i) { + cur = parsed[i]; + const curValue = cur && cur[axis]; + const prevValue = prev && prev[axis]; + + if (curValue === null || curValue === undefined || (prevValue !== undefined && curValue < prevValue)) { + sorted = false; + break; + } + + prev = cur; + } + } + + meta._sorted = sorted; } else { if (isArray(data[start])) { parsed = this.parseArrayData(meta, data, start, count);