Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: heatmap snap domain to interval #1253

Merged
merged 13 commits into from
Jul 30, 2021

Conversation

markov00
Copy link
Member

@markov00 markov00 commented Jul 14, 2021

Summary

Heatmaps with a time scale on the X-axis now adjust the rendered time range to fully cover the edges when a custom domain is used.
We also took this opportunity to clean and abstract the code used for computing and handling date and time.
The library is now able to abstract from the underlying implementation library (moment or luxon at the moment), allowing us to experiment and work with diverse libraries removing some tech debt.

Jul-20-2021.16-14-24.mp4

Details

I've abstracted some time logic as asked by @monfera taking inspiration(copying) from what was done in the timeslip code.
I've also introduced some specific Elasticsearch types like the CalendarInterval and FixedInterval. I've made them ES specific as the concept and the snapping logic applies the same strategy as the one described in https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html

Issues

fix #1165

Checklist

  • The proper chart type label was added (e.g. :xy, :partition) if the PR involves a specific chart type
  • The proper feature label was added (e.g. :interactions, :axis) if the PR involves a specific chart feature
  • Whenever possible, please check if the closing issue is connected to a running GH project
  • Any consumer-facing exports were added to packages/charts/src/index.ts (and stories only import from ../src except for test data & storybook)
  • Proper documentation or storybook story was added for features that require explanation or tutorials
  • Unit tests were updated or added to match the most common scenarios

Copy link
Collaborator

@nickofthyme nickofthyme left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good to me. I left a few comments below.

packages/charts/src/utils/data/date_time.test.ts Outdated Show resolved Hide resolved
packages/charts/src/utils/data/date_time.ts Outdated Show resolved Hide resolved
stories/heatmap/3_time.tsx Outdated Show resolved Hide resolved
stories/heatmap/3_time.tsx Outdated Show resolved Hide resolved
stories/heatmap/3_time.tsx Outdated Show resolved Hide resolved
stories/heatmap/3_time.tsx Outdated Show resolved Hide resolved
@@ -18,3 +19,85 @@ export function getMomentWithTz(date: number | Date, timeZone?: string) {
}
return moment.tz(date, timeZone);
}

/** @internal */
export type UnixTimestamp = number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor

@monfera monfera Jul 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eelsewhere we have export type TimeMs = number; (that's for duration). So a S or Ms postfix may be useful.

It might be worth putting both this and the existing TimeMs into a common time type file (even outside /charts). The latter could perhaps be renamed DurationMs.

@markov00 markov00 marked this pull request as ready for review July 20, 2021 14:05
@markov00 markov00 requested review from nickofthyme and monfera July 20, 2021 14:05
@markov00 markov00 added :data Data/series/scales related issue :heatmap Heatmap/Swimlane chart related issue bug Something isn't working labels Jul 20, 2021
markov00 added 3 commits July 20, 2021 17:32
Chromium doesn't support zone parameter
Comment on lines 301 to 304
const leftIndex =
typeof startValue === 'number' ? bisectLeft(xValues as any, startValue) : xValues.indexOf(startValue);
const rightIndex =
typeof endValue === 'number' ? bisectLeft(xValues as any, endValue) : xValues.indexOf(endValue) + 1;
Copy link
Contributor

@monfera monfera Jul 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type of the 1st and 2nd params of bisectLeft need to match, and we're typeguarding the 2nd param to number, so it's slightly tighter to say as number[]. Though it's not truthful, maybe we could brainstorm so I can learn about whether OrdinalDomain needs to be (number | string)[] and can't be number[] | string[] eg. by coercing numbers to strings if at least one element is a string. Though there's still the lack of type coherence between xValues and startValue - probably they correlate strongly (otherwise we can't do as any or as number[] here) but this seems to be lost on TS

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the type of bisectLeft is incorrect or too restrictive (we can't overrule it as it's beyond what the D3 TS API guarantees, but iirc these are 3rd party post hoc libs, not canonical Bostock contract) then maybe it'd be better to factor it out into our bisectLeft utility with the desired types, so we can easily change from D3 if needed. I think we could already use the also logarithmically bisecting monotonicHillClimb instead of d3.bisectLeft

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one main reason for having (number | string)[] as OrdinalDomain: we like to preserve the original type, coming from the user data, to return the original values on the interaction callbacks (brushing or clicking)
We can probably force it to be number[] | string[] but I don't have strong opinion here

Copy link
Contributor

@monfera monfera Jul 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more info may strengthen the case for string[] | number[]:

D3 bisectLeft, and generally, logarithmic search requires total order, which we likely ensure by sorting the values beforehand(*).

Out of the properties of total order, transitivity is most important: if a <= b and b <= c then a <= c must be true (it's not necessarily true for partial orders, eg. when a number is defined non-comparable to a string).

(*) The mechanism doesn't matter, eg. [].sort also relies on a predicate plus array which together guarantee a total order, otherwise the order is ill defined: EcmaScript requires transitivity for predictable results: If a <CF b and b <CF c, then a <CF c (transitivity of <CF).

Sorting (number | string)[] with a < predicate doesn't lead to total order. Example:

a = '2'
b = 2.5
c = '10'

a < b: true
b < c: true
a < c: false

So a sensible option is to string-compare all elements if at least one element in the array is not a number, which is the default sort predicate (and number-compare for all-numeric arrays, which is not the default sort predicate). Instead of checking for this at the place of sorting, we may as well come clean and convert upfront. As you suggested Marco, the original value can still be retained. The effect of the conversion though is that a user-supplied [5, '5'] will map to ['5', '5'] so an equally good method is, documenting in the API, perhaps even enforcing via TS, that the domain values be homogeneous.

There can of course be other sorting rules, eg. all strings are moved to the end (tiebreaker rule for when a string and a number are compared), and within both the strings and numbers, there's full order

@monfera monfera self-requested a review July 27, 2021 13:02
Copy link
Contributor

@monfera monfera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM in general, there's an as yet unexpected mock change, and maybe we could do the bisecting differently. Glad to add a bisectLeft-like thin wrapper of the lower level monotonicHillClimb function we already have for logarithmic search in an array

Comment on lines 94 to 115
switch (interval.unit) {
case 'minute':
case 'm':
return 'minutes';
case 'hour':
case 'h':
return 'hour';
case 'day':
case 'd':
return 'day';
case 'week':
case 'w':
return 'week';
case 'month':
case 'M':
return 'month';
case 'quarter':
case 'q':
return 'quarter';
case 'year':
case 'y':
return 'year';
Copy link
Contributor

@monfera monfera Jul 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it's quite a long switch, requiring up to 14 comparisons, it'd be a bit nicer if these were in a map, eg.

const esCalendarIntervalsToMoment = { 
  minute: 'minutes',
  m: 'minutes',
  hour: 'hour',
  h: 'hour',
  ...
}

...

const whatever = esCalendarIntervalsToMoment[interval.unit];

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in f471fd0

Copy link
Collaborator

@nickofthyme nickofthyme left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, still not entirely sure what the end game is having multiple time libraries.

Comment on lines 116 to 118
default:
return 'hour';
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
default:
return 'hour';
}
case 'hour':
case 'h':
default:
return 'hour';
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've update the logic to use a map instead of a switch as proposed by robert

Comment on lines +9 to +20
// NOTE: to switch implementation just change the imported file (moment,luxon)
import {
addTimeToObj,
timeObjToUnixTimestamp,
startTimeOfObj,
endTimeOfObj,
timeObjFromAny,
timeObjToUTCOffset,
subtractTimeToObj,
formatTimeObj,
diffTimeObjs,
} from './moment';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really not sure why this is needed as this is not configurable from outside of charts.

As it is we use moment across the charts code so are you thinking of being able to swap time libraries entirely? In that case we could have optional dependencies on moment and luxon so the user can switch between them.

If not is this to only be used as a utility to handle different time types?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was done in the hope we can switch soon and easily to luxon or to Temporal https://tc39.es/proposal-temporal/docs/index.html
It really depends on Kibana right now, so extracting everything like that can help us migrating easily

@markov00 markov00 merged commit b439182 into elastic:master Jul 30, 2021
@markov00 markov00 deleted the 2021_07_14-heatmap_snap_time branch July 30, 2021 08:58
github-actions bot pushed a commit that referenced this pull request Aug 6, 2021
# [33.2.0](v33.1.0...v33.2.0) (2021-08-06)

### Bug Fixes

* heatmap snap domain to interval ([#1253](#1253)) ([b439182](b439182)), closes [#1165](#1165)
* hex colors to allow alpha channel ([#1274](#1274)) ([03b4f42](03b4f42))

### Features

* **bullet:** the tooltip shows up around the drawn part of the chart only ([#1278](#1278)) ([a96cbb4](a96cbb4))
* **legend:** multiline labels with maxLines option ([#1285](#1285)) ([e0eb096](e0eb096))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working :data Data/series/scales related issue :heatmap Heatmap/Swimlane chart related issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Heatmap time buckets should use value from timeScale
3 participants