Load only the newly selected days when a chart's time range changes - #2433
Load only the newly selected days when a chart's time range changes#2433Flix6x wants to merge 30 commits into
Conversation
Context: - getAssetKPIs() advances the end date it is given by one day, to make the KPI window inclusive. - On the asset page, it was handed the very Date objects that storeEndDate and previousResult.end hold, so after one date selection both were a day late. The exact-range short-circuit in fetchGraphDataAndKPIs then stopped matching, and embedAndLoad sliced a day too much off the cached data. - The initial call additionally read storeStartDate/storeEndDate from the module body, which is evaluated before the DOMContentLoaded handler that assigns them (verified in headless Chrome), so on an asset with KPIs it threw on undefined and aborted the rest of the module. Change: - Pass copies to getAssetKPIs at both call sites. - Derive the initial window from the template values rather than from the not-yet-assigned globals. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - Four call sites in graphs.html (initial load, sensors-to-show reload, replay, and date selection) each built the same /chart_data query string by hand and repeated the same response handling. - That duplication is what makes a reuse cache awkward to add: it would have to be threaded through every call site separately. Change: - Add chart-data-source.js with buildChartDataUrl, fetchChartData and fetchChartAnnotations, and route all five fetches through it. - Drop the now-unused decompressChartData import, the dead module-level queryStartDate/queryEndDate, and two parameters of fetchGraphDataAndKPIs that the caller no longer needs. - No behaviour change: the generated URLs are unchanged. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - Issue #101: changing the selected time window re-queried the entire window, even when almost all of it was already loaded. Only an exactly identical selection was served from memory. - Profiling a 5-sensor asset holding a year of 15-minute data: a request costs ~105 ms regardless of size, plus ~24 us per record. Re-fetching a year takes ~4.4 s and 14 MiB, of which ~93% is server time. Change: - Add chart-data-cache.js, which works out what a newly selected window adds to the loaded one and fetches only that. - The date picker only yields whole nominal days, so the loaded window is always one contiguous range; interval arithmetic stays on Date objects so a 23- or 25-hour day across a DST transition stays correct. - Merged records are de-duplicated, since an event straddling the seam is returned by both halves. - A window fully inside the loaded one now needs no request at all. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - The UI now fetches only the part of a newly selected window that it does not already hold, which assumes that [a, b) plus [b, c) equals [a, c). Change: - Assert that splitting a window loses no events and changes no values, for resolutions that divide a day and for one (7 minutes) that does not. - Assert that the event straddling the seam is repeated rather than dropped, and that both halves agree on its value, which is what makes de-duplicating in the front end sufficient. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - Both changes are user-visible: charts respond faster to a new date selection, and the KPI-related date corruption is a bugfix. Change: - Add one entry under New features and one under Bugfixes for v1.1.0. - PR numbers are placeholders (XXXX) until the PR exists. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - The reuse introduced in the previous commit trimmed its records to the window on display, so narrowing the selection discarded everything outside it. Zooming into 7 days of a loaded 31-day window and back out then re-fetched the other 24 days in two requests. Change: - Hold the widest contiguous span fetched so far and clip only what is handed to the chart, so a narrowed selection and a return to the earlier span both cost nothing. - The cache is bounded by the widest contiguous span browsed, which is what selecting that span in one go would have loaded anyway. Selecting a window that does not touch the cached span replaces it, so the cached span stays contiguous. - Reset the cache in reloadChartData, where the sensors shown or the data itself may have changed and nothing held is still trustworthy. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - The entries were added before the PR existed, with XXXX placeholders. Change: - Point both entries at PR #2433. Signed-off-by: F.N. Claessen <felix@seita.nl>
Documentation build overview
10 files changed ·
|
Context: - Review feedback: "source" is reserved for belief sourcing in this repo, so naming a module after it is misleading. Change: - Rename the module and update its two importers. Signed-off-by: F.N. Claessen <felix@seita.nl>
There was a problem hiding this comment.
Pull request overview
This PR improves asset/sensor chart navigation performance by introducing a front-end cache that reuses already-fetched chart data when the selected time window changes, so only newly needed days are requested. It also centralizes chart-data fetching into a single module, adds a backend contract test for window-splitting, and includes a small KPI-related date-mutation fix plus a changelog entry.
Changes:
- Refactor chart data and annotation fetching to go through
chart-data-source.js, and integrate a newchart-data-cache.jsintographs.html. - Add
test_chart_data_window_splitting.pyto pin down the backend API contract needed for safely fetching adjacent/overlapping windows. - Add changelog entries describing the chart range-fetch optimization and the KPI/chart initial-load bugfix.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| flexmeasures/ui/templates/includes/graphs.html | Switch chart data/annotation requests to the new data-source module and use the new cache when the date range changes; fixes KPI/date mutation and initial window derivation. |
| flexmeasures/ui/static/js/chart-data-source.js | New module to build /chart_data URLs and fetch/decompress chart data (plus annotations) in one place. |
| flexmeasures/ui/static/js/chart-data-cache.js | New module to compute missing sub-ranges, fetch only what’s new, merge/dedupe, and return data for the selected window. |
| flexmeasures/data/tests/test_chart_data_window_splitting.py | New backend test module asserting that fetching a window in parts matches fetching it in one request. |
| documentation/changelog.rst | Adds user-facing entries for the performance improvement and KPI/chart load bugfix. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Suppressed comments (6)
flexmeasures/ui/templates/includes/graphs.html:514
- chartDataCache is never populated with the initially fetched data (initialData uses fetchChartData directly, and previousResult is set, but the cache remains empty). As a result, the first user range change after page load will still fetch the entire newly selected window, even if it largely overlaps what was already loaded, which undermines the “load only newly selected days” behavior. Consider adding a way to prime the cache with {start,end,data} right after the initial fetch so subsequent changes can request only the missing slivers.
// The module body is evaluated before the DOMContentLoaded handler that assigns storeStartDate/storeEndDate, so derive the initial window here.
const initialStartDate = new Date('{{ event_starts_after }}');
const initialEndDate = new Date('{{ event_ends_before }}');
{% if active_subpage == "asset_graph" and has_kpis %}
// Pass copies, because getAssetKPIs advances the end date it is given by a day.
getAssetKPIs(new Date(initialStartDate), new Date(initialEndDate));
{% endif %}
const initialData = fetchChartData(dataPath, { start: initialStartDate, end: initialEndDate, signal: signal });
let fetchedInitialData = await Promise.all([
initialData,
embedAndLoad(chartSpecsPath + 'event_starts_after=' + '{{ event_starts_after }}' + '&event_ends_before=' + '{{ event_ends_before }}' + '&', elementId, datasetName, previousResult, sessionStart, sessionEnd),
]).then(function (result) { return result[0] }).catch(console.error);
$("#spinner").hide();
if (!fastRenderActive() && vegaView) {
vegaView.change(datasetName, vega.changeset().remove(vega.truthy).insert(fetchedInitialData)).resize().run();
}
var sessionStart = new Date('{{ event_starts_after }}');
var sessionEnd = new Date('{{ event_ends_before }}');
previousResult = {
start: sessionStart,
end: sessionEnd,
data: fetchedInitialData
};
flexmeasures/ui/templates/includes/graphs.html:462
- reloadChartData resets chartDataCache and then reloads the current window via fetchChartData, but the cache is not repopulated with the freshly fetched data. This means the next time-range change after a “sensorsToShowUpdated”/data-refresh event will again fetch the full window instead of only the new days. Consider priming the cache with the reloaded {start,end,data} after newData is received.
$("#spinner").show();
// The sensors shown, or the data itself, may have changed, so nothing held is still trustworthy.
chartDataCache.reset();
let newData = fetchChartData(dataPath, { start: storeStartDate, end: storeEndDate, signal: signal });
newData = await Promise.all([
newData,
embedAndLoad(chartSpecsPath + 'event_starts_after=' + storeStartDate.toISOString() + '&event_ends_before=' + storeEndDate.toISOString() + '&', elementId, datasetName, previousResult, storeStartDate, storeEndDate),
]).then(function (result) { return result[0] }).catch(console.error);
$("#spinner").hide();
if (!fastRenderActive() && vegaView) {
vegaView.change(datasetName, vega.changeset().remove(vega.truthy).insert(newData)).resize().run();
}
previousResult = {
start: storeStartDate,
end: storeEndDate,
data: newData
};
flexmeasures/data/tests/test_chart_data_window_splitting.py:7
- Module docstring wraps mid-phrase (e.g. line breaks after “time” and “asking”), which conflicts with the repo’s docstring wrapping convention (break lines only after punctuation). Please reflow the docstring so each physical line ends with punctuation (or keep the sentences on single lines).
"""Tests that a chart data window can be fetched in parts.
The asset and sensor pages reuse the data they already hold when the selected time
window changes, and fetch only the parts that are new (see chart-data-cache.js).
That is only sound if asking for [a, b) and [b, c) yields the same beliefs as asking
for [a, c) in one go, which is what these tests pin down.
"""
flexmeasures/ui/static/js/chart-data-cache.js:45
- This JSDoc comment wraps mid-phrase (line break after “after”), which conflicts with the repo’s comment wrapping convention (break lines only after punctuation). Please reflow so each physical line ends with punctuation, or keep the sentence on one line.
/**
* Keep only the records for events that start inside the given window.
*
* This matches the API's own filtering, which returns events starting at or after
* `event_starts_after`, so trimming here yields what a fetch would have returned.
*
flexmeasures/ui/static/js/chart-data-cache.js:66
- This JSDoc block wraps mid-phrase in multiple places (e.g. line breaks after “the” and “in”), which conflicts with the repo’s comment wrapping convention (break lines only after punctuation). Please reflow the sentences so each physical line ends with punctuation.
/**
* Drop records that describe the same belief twice.
*
* Splitting a window can hand back one event on both sides of the seam, when the
* sensor's resolution does not divide the window evenly (verified in
* test_chart_data_window_splitting.py).
* Records are never lost that way, only repeated, so de-duplicating is enough.
*
flexmeasures/ui/static/js/chart-data-cache.js:95
- This JSDoc block wraps mid-phrase in several places (e.g. line breaks after “on”, “browsed,” “the”, “series”), which conflicts with the repo’s comment wrapping convention (break lines only after punctuation). Please reflow the comment so line breaks occur only after punctuation.
/**
* Create a cache that serves chart data for a window, fetching only what it lacks.
*
* The cache holds the widest contiguous span loaded so far, not just the span on
* display, so narrowing the selection and widening it again costs nothing.
* Its size is therefore bounded by the widest contiguous span the user has browsed,
* which is what selecting that span in one go would have loaded anyway.
*
* Selecting a window that does not touch the cached span replaces it, keeping the
* cached span contiguous.
*
* The merged records are deliberately not re-sorted: the fast chart sorts each series
* by time itself, and Vega-Lite sorts line and area marks by their x channel.
*
| // Holds the widest contiguous span fetched so far, so that re-selecting a span we | ||
| // just moved away from costs no request. Reset whenever the data itself may have changed. | ||
| const chartDataCache = createChartDataCache(); |
Context: - Review feedback: clipping cached records by event_start does not match the API, which selects events *overlapping* the window (event_ends_after / event_starts_before). A 50-minute sensor lost its leading event whenever the window was narrowed, since that event starts before midnight and runs into the day. - Investigating that surfaced a second, larger mismatch: the API anchors its resampling at the start of the window asked for, so a coarser sensor shown next to a 7-minute one comes back on shifted timestamps for a window offset by a fraction of the resolution. Those records cannot be reconciled with the ones already held. Change: - Clip on overlap rather than on event_start, and match instantaneous events inclusively on both edges, as the API does. - Derive the resolution the events are actually spaced on: every sensor with a resolution is resampled to the finest one requested, while the response still reports each sensor's own. - Refuse to reuse records for a window that is not on the same resampling grid, falling back to fetching the window whole. Whole-day selections of sensors whose resolution divides a day are always aligned, so this only bites the cases that were wrong before. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - Review feedback: the cache was only ever filled by a date selection, so the first selection after a page load found it empty and re-fetched the whole newly selected range, even though the initial window was already in memory. The same applied after a reload triggered by a data change. Change: - Route the initial load and the post-reset reload through the cache, so the optimisation applies from the first date change onwards. Neither changes what is requested: an empty cache fetches the whole window. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - The front end reproduces a fetch from records it already holds, so it depends on three behaviours that were not covered. Change: - Assert that a window selects events overlapping it, not only events starting inside it. - Assert that instantaneous events are included on both window edges. - Assert that resampling is anchored at the requested window start, which is why the front end refuses to reuse records across such a shift. - Reflow two docstrings to break only after punctuation, per the repo convention. Signed-off-by: F.N. Claessen <felix@seita.nl>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
flexmeasures/ui/templates/includes/graphs.html:1008
getAssetKPIsmutates itsendDateargument (adds a day). The call sites here now defensively pass copies, but this side effect is easy to reintroduce elsewhere. Consider makinggetAssetKPIsnon-mutating by cloningendDateinside the function before adjusting it, so callers can safely pass their real window dates.
// Pass copies: getAssetKPIs advances the end date it is given by a day,
// and this endDate is the very object held by storeEndDate and previousResult.end.
getAssetKPIs(new Date(startDate), new Date(endDate));
{% endif %}
flexmeasures/ui/static/js/chart-data-cache.js:180
reusablecurrently requires strict overlap (end > cached.start && start < cached.end). For adjacent windows (e.g. shifting a 1‑day selection forward by 1 day sostart === cached.end), this is false, so the cache is replaced instead of being extended to the union. That contradicts the “widest contiguous span fetched so far” goal and makes stepping back re-fetch. Consider allowing boundary equality here (treat adjacency as reusable/contiguous), soloadwill fetch the new window and then widencachedto include both.
// Reuse only what a direct fetch would have returned identically.
const reusable =
cached !== null &&
end > cached.start &&
start < cached.end &&
onSameResamplingGrid(cached.start, start, end, resolutionMs);
const ranges = missingRanges(start, end, reusable ? cached : null);
| * Both windows are half-open, [start, end), matching how the API treats | ||
| * `event_starts_after` and `event_ends_before`. |
There was a problem hiding this comment.
Fixed in 9e3f654. The missingRanges doc now states that half-open holds for sensors with a resolution, and that instantaneous sensors are the exception, matched inclusively on both edges — with the reason it is harmless here: an instant on a boundary is fetched by the windows on either side and then de-duplicated.
Context: - Review feedback: reuse required a strict overlap, so a selection stepped on by exactly its own width (start === cached.end) counted as disjoint and replaced the cache. Stepping back then re-fetched a window that had just been thrown away, which is the opposite of holding the widest span. Change: - Allow the boundaries to touch, so such a selection extends the cached span instead of replacing it. What gets fetched is unchanged: the new window is still entirely missing and is still fetched whole. - Note in missingRanges that the half-open convention has one exception, instantaneous sensors, which the API matches inclusively on both edges. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - Review feedback: passing defensive copies at both call sites left the trap in place, since the next caller has to know to do the same. That is what caused the date corruption fixed in 04d6b1b. Change: - Advance a copy of the end date inside getAssetKPIs, and drop the copies at the call sites, which can now pass the chart's own window dates. Signed-off-by: F.N. Claessen <felix@seita.nl>
|
Reflowed the JavaScript comments too (2ad1f0c-range: see I had previously left these on the grounds that 108 Python tests and 72 browser checks still pass. |
Context: - The repo's line-break convention was written for Python, but it exists for stable review comments and text search, which applies just as much to the JavaScript comments added here. Change: - Reflow the JSDoc blocks and inline comments in chart-data-cache.js, chart-data-fetch.js and the graphs.html module, so that every physical line ends at a comma, semicolon, colon or period. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - The convention only claimed to apply to Python, so JavaScript comments were outside it, even though the reasons for it (stable review comments and text search) do not depend on the language. Change: - Widen applyTo to .js and .html, retitle to "Docstrings and comments", and scope the RST/Click/doctest guidance as Python-specific. - Add a JavaScript section covering JSDoc blocks and // comments, in .js files and in the script blocks of Jinja templates, with an example, and note that @param/@returns lines stay on one line however long. - State that automated reviewers should report JavaScript comments that wrap mid-phrase, as they already do for Python docstrings. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - Twice now I claimed this block was not inside the DOMContentLoaded handler: first that it ran in the module body, then that the handler's extent varied per page because its closing brace sat in a Jinja conditional. Both were inferred from indentation and both were wrong. - Parsing the rendered pages settles it. The handler spans the same region on the asset page and on the sensor page, closing unconditionally at the end of the timezone check, and the initial window is inside it and after the assignment of storeStartDate/storeEndDate in every case. - The brace that misled me closes the sensor upload click listener. It is written at four spaces where the listener is opened at eight, so it reads as the end of the handler. Change: - Use storeStartDate/storeEndDate, as main did, dropping the second copy of the window that was only introduced to work around a problem that does not exist. - Indent the upload listener's closing brace to match its opening, so the next reader is not misled the same way. Signed-off-by: F.N. Claessen <felix@seita.nl>
|
Second correction on the same point, and this one closes it out. Felix asked me to investigate the "conditional handler brace" I reported. It is not a bug, and my description of it was wrong too — the same inference-from-indentation mistake, made twice. What is actually true, from parsing both rendered pages:
The handler closes unconditionally, at the end of the timezone-check block just before What misled me: the sensor upload block's Origin: it arrived with the upload block in 0e42c11 ("Move sensor data ingestion to job queue", #2101). The intended structure is what is there; only the indentation is off. Consequences, in a1938ec:
168 Python tests pass. |
Context: - The KPI day-counting fix was split out into PR #2434, since it changes user-visible numbers while this PR is a performance change. Both branches carry the same code change and merge cleanly either way, but the changelog would have gained the line twice. Change: - Drop the entry here. The date-corruption entry stays, as that fix is needed by the cache in this PR. Signed-off-by: F.N. Claessen <felix@seita.nl>
|
Split the KPI day-counting fix out into #2434, per @Flix6x's request, since it changes user-visible numbers while this PR is a performance change. I tested separability rather than assuming it. On Worth being clear about what did not get split. This PR keeps the fix that stops |
Context: - Date selections are not serialised and their requests are not aborted, so two can be in flight at once. The merge read what was held *after* awaiting, by which time another selection may have replaced it. - Selecting [1, 7) and then [20, 25), with the second answered first, left the cache claiming [1, 25) while holding only the two ends. Selecting [10, 12) afterwards was then served from that gap: an empty chart, with no request made. The widening selection itself also returned 2 of its 6 days, since it merged against the unrelated window. Change: - Snapshot what is held on entry and merge against that snapshot, so a result's span and its records always describe the same thing. - Return the window this call assembled, rather than whatever is held by the time it finishes. Whichever selection finishes last decides what is kept; both spans are self-consistent, so the worst case is a later selection re-fetching. Signed-off-by: F.N. Claessen <felix@seita.nl>
|
A JS test suite would have caught a bug in this PR — it just did. While investigating what JS testing would cost (@Flix6x's question), I probed an area none of the existing checks covered: two date selections in flight at once. Date selections are not serialised and their requests are not aborted — The merge read what was held after awaiting, by which time another selection could have replaced it. Reproduced with controlled response ordering:
The cache then claimed Fixed in 9928c43 by snapshotting what is held on entry and merging against that snapshot, so a result's span and its records always describe the same thing, and by returning the window the call actually assembled rather than whatever is held when it finishes. Whichever selection finishes last decides what is kept; both spans are self-consistent, so the worst case is a later selection re-fetching. The scenario is now a regression check. 74 browser checks and 100 UI tests pass. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
flexmeasures/ui/templates/includes/graphs.html:693
- The date-picker "selected" handler starts new fetches without aborting any in-flight requests. If a user changes the range quickly, an older Promise.all can resolve after a newer one and overwrite the chart/previousResult with stale data. Consider aborting the previous controller (or using a monotonically increasing request id and ignoring out-of-date responses) when a new selection starts, and pass the new signal to all three async branches (chart data, annotations, embedAndLoad).
checkDSTTransitions(startDate, endDate)
Promise.all([
fetchGraphDataAndKPIs(previousResult, startDate, endDate),
// Fetch annotations
fetchChartAnnotations(dataPath, { start: startDate, end: endDate, signal: signal }),
Context: - Review feedback: the sentence wrapped after "daylight saving", against the repo convention, which PR #2433 has just extended to cover JavaScript as well as Python. Change: - Reflow so each line ends at a comma or a period. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - Review feedback: the picker's selected handler starts its requests without aborting any still in flight, and whichever settles last draws itself and becomes previousResult. A slow earlier selection answering after a quick later one therefore put the older range on screen, while the picker and the URL named the newer one. - The cache fix in 9928c43 addressed a different race, inside the cache. It kept what is held self-consistent, but said nothing about which answer the page draws. Change: - Count how often the shown window changes, claim that count when a selection starts, and drop the result if the count has moved on. - Bump the same count when reloadChartData runs, since a reload for changed data supersedes any selection still in flight. Signed-off-by: F.N. Claessen <felix@seita.nl>
|
@Flix6x was right that the last review left something unaddressed, and it was a real bug. Fixed in 1b95e9c. The picker's This is not the race fixed in 9928c43. That one was inside the cache, and kept what is held self-consistent; it said nothing about which answer the page draws. Two separate races in the same feature, one layer apart. Fixed with a generation counter rather than by aborting: the shared Verified the guard lands in the right scope by parsing the rendered page — the claim is a On why I missed it, since it matters for how I read these: the review's headline said "generated no new comments", and the review-comments API returned nothing. The finding was in a collapsed Suppressed comments block in the review body, labelled "previously missed — in code that hasn't changed since the last review". Those are demoted by diff recency, not by confidence, and they do not appear in the comments API at all. Across this session's reviews, 5 said "no new comments" and 2 of those carried suppressed findings. I now read the full review body every time. |
Context: - Review feedback: 1b95e9c had reloadChartData bump the shown-window count so that a selection in flight could not overwrite it, but the reload never checked the count itself. A range selected while a reload was running could therefore render first, and the reload could then finish and draw the window the user had already moved away from. - The guard was written in one direction only. Change: - Have the reload claim a count as the selection handler does, and drop its result if a window has been shown since. Signed-off-by: F.N. Claessen <felix@seita.nl>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
flexmeasures/ui/templates/includes/graphs.html:515
- The initial page-load fetch (
initialData/fetchedInitialData) is not protected byshownWindowGeneration, so a user selection made while that initialPromise.allis still in flight can render first and then be overwritten when this initial load resolves (chart and URL/picker can desync). Consider claiming a generation for the initial load and returning early if it’s stale before updating the chart/previousResult, consistent with the selection and reload paths.
{% if event_starts_after and event_ends_before %}
$("#spinner").show();
{% if active_subpage == "asset_graph" and has_kpis %}
getAssetKPIs(storeStartDate, storeEndDate);
{% endif %}
// Through the cache, so that the very first date change already only fetches the days it adds.
const initialData = chartDataCache.load(dataPath, { start: storeStartDate, end: storeEndDate, signal: signal });
let fetchedInitialData = await Promise.all([
initialData,
embedAndLoad(chartSpecsPath + 'event_starts_after=' + '{{ event_starts_after }}' + '&event_ends_before=' + '{{ event_ends_before }}' + '&', elementId, datasetName, previousResult, sessionStart, sessionEnd),
]).then(function (result) { return result[0] }).catch(console.error);
$("#spinner").hide();
if (!fastRenderActive() && vegaView) {
vegaView.change(datasetName, vega.changeset().remove(vega.truthy).insert(fetchedInitialData)).resize().run();
}
Context: - The initial load declared `var sessionStart` and `var sessionEnd` inside the DOMContentLoaded handler, far below the picker's creation. Being `var`, they were hoisted to the top of that handler and shadowed the module-level values assigned earlier. - So createPicker(sessionStart, sessionEnd, ...) and, on simulation servers, computeSimulationRanges(...) both received `undefined` rather than the session's window. Confirmed by parsing the rendered page, which puts the declaration at line 341 and the use at line 217 within one handler spanning 170 to 434, and by running the same shape in a browser. Change: - Drop the two shadowing declarations and build the dates where they are used, so the module-level values reach the picker. Signed-off-by: F.N. Claessen <felix@seita.nl>
Context: - Review feedback: the picker is created before the initial load starts, and that load can take seconds on a wide window, so a range selected while it runs would render first and then be overwritten when the first load resolved. - That is the third async path onto the chart, after the selection handler and the reload, and the only one still unguarded. Change: - Claim a count for the initial load and draw its result only if no window has been shown since. Signed-off-by: F.N. Claessen <felix@seita.nl>
| New features | ||
| ------------- | ||
|
|
||
| * Changing the selected time range on an asset or sensor chart now only loads the data that is actually new, instead of reloading the whole range, which makes stepping through or extending a long period much faster [see `PR #2433 <https://www.github.com/FlexMeasures/flexmeasures/pull/2433>`_ and `issue #101 <https://github.com/FlexMeasures/flexmeasures/issues/101>`_] |
…gument Context: - Found while adversarially reviewing PR #2434: this branch corrected the inline comment about the fastChartWindow snapshot but left the block comment above the declaration still giving the old reason, that getAssetKPIs bumps storeEndDate by a day. It does not, since bc6d5c1. Change: - Give the snapshot its real reason, matching the wording on PR #2434 so the two branches stay identical here. Signed-off-by: F.N. Claessen <felix@seita.nl>
Description
Closes #101.
Changing the selected time range re-queried the whole range, even when nearly all of it was already loaded. Only a byte-identical selection was served from memory. This PR loads just the days that are actually new.
ui/graphs: fetch all chart data through one module (chart-data-fetch.js), replacing four hand-built/chart_dataquery strings. No behaviour change; the generated URLs are unchanged.ui/graphs: addchart-data-cache.js, which works out what a newly selected window adds to the loaded one and fetches only that.ui/graphs: hold the widest contiguous span fetched so far and clip only what is handed to the chart, so narrowing the selection and widening it again costs nothing.ui/graphs: two KPI bugfixes, unrelated to Intelligent chart updating #101 but found on the way.getAssetKPIsmutated the veryDateobjects held bystoreEndDateandpreviousResult.end, so on a KPI-enabled asset page both ran a day late after any selection. It also advanced an end date that both callers already pass as exclusive, while the endpoint ends its window beforeendas well, so every KPI covered one day more than the chart beside it (a three-day selection totalled four days).documentation/changelog.rstDesign notes
The date picker only ever yields whole (nominal) days, so a selection is always one contiguous range and the cached span can be a single interval — no interval set, no gap solver. This follows the original prototype in
empras/templates/index.html(updateData()), removed from main in ba8f900 shortly before #101 was filed.Interval arithmetic stays on
Dateobjects rather than millisecond counts, because a nominal day is 23 or 25 hours across a DST transition. There is a test for the 23-hour day.A window that does not touch the cached span replaces it, so the cached span stays contiguous and cannot fragment. Memory is therefore bounded by the widest contiguous span browsed, which is what selecting that span in one go would have loaded anyway.
Merged records are de-duplicated rather than fetched as strictly non-overlapping ranges, because an event straddling the seam is returned by both halves (see the tests below). They are deliberately not re-sorted:
fast-chart.jssorts each series by time itself, and Vega-Lite sorts line and area marks by their x channel.Reproducing a fetch from records already held turns out to depend on three API behaviours, each now pinned by a test:
event_ends_after/event_starts_before), not events that start inside it. So the cache clips on overlap; filtering onevent_startwould drop the leading event of, say, a 50-minute sensor, whose event runs across midnight.The resolution used for that arithmetic is derived, not read off a record: every sensor with a resolution is resampled to the finest one requested, while the response still reports each sensor's own (an hourly sensor next to a 15-minute one reports 3600 while its data arrives at 900).
Look & Feel
Profiled on a seeded asset with 5 sensors of 15-minute data over a year (175,200 beliefs).
Cost of one
chart_datarequest, by window width:Where that time goes at 365 days: DB search 3,757 ms, serialisation 804 ms, transfer 142 ms (localhost),
JSON.parse99 ms,decompressChartData74 ms. Server time is ~93% of the total, so this is worth fixing on the fetch side.Two things worth recording. A request costs ~105 ms regardless of size (a 20-record window still costs 105 ms), plus ~24 µs per record. And that fixed floor is why splitting a range into one request per day would be a bad idea: 31 days would be 31 × 105 ms ≈ 3.3 s of pure overhead against 460 ms for a single request.
Cost of a navigation step, before and after this PR:
The both-sides figure is the sum of two sequential measurements; in the browser the two slivers are requested concurrently, so wall-clock is ~120 ms.
How to test
flexmeasures/data/tests/test_chart_data_window_splitting.pypins down the API contract the cache relies on: fetching[a, b)and[b, c)yields exactly the same beliefs, with the same values, as fetching[a, c). It covers resolutions that divide a day and one (7 minutes) that does not — in the latter case the event straddling the seam is returned on both sides, never dropped, and both sides agree on its value, which is what makes de-duplicating in the front end sufficient. It also pins the three behaviours listed under Design notes: overlap-based selection, inclusive edges for instantaneous sensors, and resampling anchored at the requested window start.Manually, on an asset page with a lot of data, with the browser's network tab open:
chart_datarequest.Not covered by automated tests
The repo has no JavaScript test runner, so the cache logic was verified with a standalone harness driving the real modules in headless Chrome (58 checks: interval arithmetic, the 23-hour DST day, overlap clipping, the resampling-grid guard, de-duplication, request counts per navigation step, and cache invalidation). Those checks are not in CI. Adding a JS test runner feels like a separate decision, so I left it out — happy to follow up if wanted.
The live page was not driven end to end: the logic is covered as above, and both pages were verified to render with their emitted module parsing cleanly, but the wiring itself is only exercised by hand.