@internetarchive/histogram-date-range 1.2.1-alpha.1 → 1.2.2-alpha-webdev7377.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.editorconfig +29 -29
  2. package/.eslintrc.js +14 -14
  3. package/.github/workflows/ci.yml +30 -26
  4. package/LICENSE +661 -661
  5. package/README.md +113 -113
  6. package/demo/index.css +22 -22
  7. package/demo/index.html +159 -159
  8. package/dist/demo/js/app-root.d.ts +19 -19
  9. package/dist/demo/js/app-root.js +46 -46
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/src/histogram-date-range.d.ts +166 -159
  14. package/dist/src/histogram-date-range.js +856 -846
  15. package/dist/src/histogram-date-range.js.map +1 -1
  16. package/dist/test/histogram-date-range.test.d.ts +1 -1
  17. package/dist/test/histogram-date-range.test.js +488 -488
  18. package/dist/test/histogram-date-range.test.js.map +1 -1
  19. package/docs/demo/index.css +22 -22
  20. package/docs/demo/index.html +159 -159
  21. package/docs/dist/src/histogram-date-range.js +17 -12
  22. package/index.ts +1 -1
  23. package/package.json +85 -85
  24. package/snowpack.config.js +10 -10
  25. package/src/histogram-date-range.ts +923 -915
  26. package/test/histogram-date-range.test.ts +684 -684
  27. package/tsconfig.json +21 -21
  28. package/web-dev-server.config.mjs +28 -28
  29. package/web-test-runner.config.mjs +29 -29
  30. package/dist/demo/app-root.d.ts +0 -19
  31. package/dist/demo/app-root.js +0 -58
  32. package/dist/demo/app-root.js.map +0 -1
  33. package/dist/docs/_snowpack/pkg/@internetarchive/ia-activity-indicator/ia-activity-indicator.d.ts +0 -1
  34. package/dist/docs/_snowpack/pkg/@internetarchive/ia-activity-indicator/ia-activity-indicator.js +0 -2
  35. package/dist/docs/_snowpack/pkg/@internetarchive/ia-activity-indicator/ia-activity-indicator.js.map +0 -1
@@ -1,847 +1,857 @@
1
- import { __decorate } from "tslib";
2
- import { css, html, nothing, LitElement, svg, } from 'lit';
3
- import { property, state, customElement } from 'lit/decorators.js';
4
- import { live } from 'lit/directives/live.js';
5
- import '@internetarchive/ia-activity-indicator';
6
- import dayjs from 'dayjs/esm';
7
- import customParseFormat from 'dayjs/esm/plugin/customParseFormat';
8
- dayjs.extend(customParseFormat);
9
- // these values can be overridden via the component's HTML (camelCased) attributes
10
- const WIDTH = 180;
11
- const HEIGHT = 40;
12
- const SLIDER_WIDTH = 10;
13
- const TOOLTIP_WIDTH = 125;
14
- const TOOLTIP_HEIGHT = 30;
15
- const DATE_FORMAT = 'YYYY';
16
- const MISSING_DATA = 'no data';
17
- const UPDATE_DEBOUNCE_DELAY_MS = 0;
18
- // this constant is not set up to be overridden
19
- const SLIDER_CORNER_SIZE = 4;
20
- // these CSS custom props can be overridden from the HTML that is invoking this component
21
- const sliderColor = css `var(--histogramDateRangeSliderColor, #4B65FE)`;
22
- const selectedRangeColor = css `var(--histogramDateRangeSelectedRangeColor, #DBE0FF)`;
23
- const barIncludedFill = css `var(--histogramDateRangeBarIncludedFill, #2C2C2C)`;
24
- const activityIndicatorColor = css `var(--histogramDateRangeActivityIndicator, #2C2C2C)`;
25
- const barExcludedFill = css `var(--histogramDateRangeBarExcludedFill, #CCCCCC)`;
26
- const inputRowMargin = css `var(--histogramDateRangeInputRowMargin, 0)`;
27
- const inputBorder = css `var(--histogramDateRangeInputBorder, 0.5px solid #2C2C2C)`;
28
- const inputWidth = css `var(--histogramDateRangeInputWidth, 35px)`;
29
- const inputFontSize = css `var(--histogramDateRangeInputFontSize, 1.2rem)`;
30
- const inputFontFamily = css `var(--histogramDateRangeInputFontFamily, sans-serif)`;
31
- const tooltipBackgroundColor = css `var(--histogramDateRangeTooltipBackgroundColor, #2C2C2C)`;
32
- const tooltipTextColor = css `var(--histogramDateRangeTooltipTextColor, #FFFFFF)`;
33
- const tooltipFontSize = css `var(--histogramDateRangeTooltipFontSize, 1.1rem)`;
34
- const tooltipFontFamily = css `var(--histogramDateRangeTooltipFontFamily, sans-serif)`;
35
- let HistogramDateRange = class HistogramDateRange extends LitElement {
36
- constructor() {
37
- /* eslint-disable lines-between-class-members */
38
- super(...arguments);
39
- // public reactive properties that can be set via HTML attributes
40
- this.width = WIDTH;
41
- this.height = HEIGHT;
42
- this.sliderWidth = SLIDER_WIDTH;
43
- this.tooltipWidth = TOOLTIP_WIDTH;
44
- this.tooltipHeight = TOOLTIP_HEIGHT;
45
- this.updateDelay = UPDATE_DEBOUNCE_DELAY_MS;
46
- this.dateFormat = DATE_FORMAT;
47
- this.missingDataMessage = MISSING_DATA;
48
- this.minDate = '';
49
- this.maxDate = '';
50
- this.disabled = false;
51
- this.bins = [];
52
- /** If true, update events will not be canceled by the date inputs receiving focus */
53
- this.updateWhileFocused = false;
54
- // internal reactive properties not exposed as attributes
55
- this._tooltipOffset = 0;
56
- this._tooltipVisible = false;
57
- this._isDragging = false;
58
- this._isLoading = false;
59
- // non-reactive properties (changes don't auto-trigger re-rendering)
60
- this._minSelectedDate = '';
61
- this._maxSelectedDate = '';
62
- this._minDateMS = 0;
63
- this._maxDateMS = 0;
64
- this._dragOffset = 0;
65
- this._histWidth = 0;
66
- this._binWidth = 0;
67
- this._histData = [];
68
- this._previousDateRange = '';
69
- // use arrow functions (rather than standard JS class instance methods) so
70
- // that `this` is bound to the histogramDateRange object and not the event
71
- // target. for more info see
72
- // https://lit-element.polymer-project.org/guide/events#using-this-in-event-listeners
73
- this.drag = (e) => {
74
- // prevent selecting text or other ranges while dragging, especially in Safari
75
- e.preventDefault();
76
- if (this.disabled) {
77
- return;
78
- }
79
- this.setDragOffset(e);
80
- this._isDragging = true;
81
- this.addListeners();
82
- this.cancelPendingUpdateEvent();
83
- };
84
- this.drop = () => {
85
- if (this._isDragging) {
86
- this.removeListeners();
87
- this.beginEmitUpdateProcess();
88
- }
89
- this._isDragging = false;
90
- };
91
- /**
92
- * Adjust the date range based on slider movement
93
- *
94
- * @param e PointerEvent from the slider being moved
95
- */
96
- this.move = (e) => {
97
- const histogramClientX = this.getBoundingClientRect().x;
98
- const newX = e.clientX - histogramClientX - this._dragOffset;
99
- const slider = this._currentSlider;
100
- if (slider.id === 'slider-min') {
101
- this.minSelectedDate = this.translatePositionToDate(this.validMinSliderX(newX));
102
- }
103
- else {
104
- this.maxSelectedDate = this.translatePositionToDate(this.validMaxSliderX(newX));
105
- }
106
- };
107
- }
108
- /* eslint-enable lines-between-class-members */
109
- disconnectedCallback() {
110
- this.removeListeners();
111
- super.disconnectedCallback();
112
- }
113
- updated(changedProps) {
114
- // check for changes that would affect bin data calculations
115
- if (changedProps.has('bins') ||
116
- changedProps.has('minDate') ||
117
- changedProps.has('maxDate') ||
118
- changedProps.has('minSelectedDate') ||
119
- changedProps.has('maxSelectedDate') ||
120
- changedProps.has('width') ||
121
- changedProps.has('height')) {
122
- this.handleDataUpdate();
123
- }
124
- }
125
- /**
126
- * Set private properties that depend on the attribute bin data
127
- *
128
- * We're caching these values and not using getters to avoid recalculating all
129
- * of the hist data every time the user drags a slider or hovers over a bar
130
- * creating a tooltip.
131
- */
132
- handleDataUpdate() {
133
- if (!this.hasBinData) {
134
- return;
135
- }
136
- this._histWidth = this.width - this.sliderWidth * 2;
137
- this._minDateMS = this.getMSFromString(this.minDate);
138
- this._maxDateMS = this.getMSFromString(this.maxDate);
139
- this._binWidth = this._histWidth / this._numBins;
140
- this._previousDateRange = this.currentDateRangeString;
141
- this._histData = this.calculateHistData();
142
- this.minSelectedDate = this.minSelectedDate
143
- ? this.minSelectedDate
144
- : this.minDate;
145
- this.maxSelectedDate = this.maxSelectedDate
146
- ? this.maxSelectedDate
147
- : this.maxDate;
148
- this.requestUpdate();
149
- }
150
- calculateHistData() {
151
- const minValue = Math.min(...this.bins);
152
- const maxValue = Math.max(...this.bins);
153
- // if there is no difference between the min and max values, use a range of
154
- // 1 because log scaling will fail if the range is 0
155
- const valueRange = minValue === maxValue ? 1 : Math.log1p(maxValue);
156
- const valueScale = this.height / valueRange;
157
- const dateScale = this.dateRangeMS / this._numBins;
158
- return this.bins.map((v, i) => {
159
- return {
160
- value: v,
161
- // use log scaling for the height of the bar to prevent tall bars from
162
- // making the smaller ones too small to see
163
- height: Math.floor(Math.log1p(v) * valueScale),
164
- binStart: `${this.formatDate(i * dateScale + this._minDateMS)}`,
165
- binEnd: `${this.formatDate((i + 1) * dateScale + this._minDateMS)}`,
166
- };
167
- });
168
- }
169
- get hasBinData() {
170
- return this._numBins > 0;
171
- }
172
- get _numBins() {
173
- if (!this.bins || !this.bins.length) {
174
- return 0;
175
- }
176
- return this.bins.length;
177
- }
178
- get histogramLeftEdgeX() {
179
- return this.sliderWidth;
180
- }
181
- get histogramRightEdgeX() {
182
- return this.width - this.sliderWidth;
183
- }
184
- /** component's loading (and disabled) state */
185
- get loading() {
186
- return this._isLoading;
187
- }
188
- set loading(value) {
189
- this.disabled = value;
190
- this._isLoading = value;
191
- }
192
- /** formatted minimum date of selected date range */
193
- get minSelectedDate() {
194
- return this.formatDate(this.getMSFromString(this._minSelectedDate));
195
- }
196
- /** updates minSelectedDate if new date is valid */
197
- set minSelectedDate(rawDate) {
198
- if (!this._minSelectedDate) {
199
- // because the values needed to calculate valid max/min values are not
200
- // available during the lit init when it's populating properties from
201
- // attributes, fall back to just the raw date if nothing is already set
202
- this._minSelectedDate = rawDate;
203
- return;
204
- }
205
- const proposedDateMS = this.getMSFromString(rawDate);
206
- const isValidDate = !Number.isNaN(proposedDateMS);
207
- const isNotTooRecent = proposedDateMS <= this.getMSFromString(this.maxSelectedDate);
208
- if (isValidDate && isNotTooRecent) {
209
- this._minSelectedDate = this.formatDate(proposedDateMS);
210
- }
211
- this.requestUpdate();
212
- }
213
- /** formatted maximum date of selected date range */
214
- get maxSelectedDate() {
215
- return this.formatDate(this.getMSFromString(this._maxSelectedDate));
216
- }
217
- /** updates maxSelectedDate if new date is valid */
218
- set maxSelectedDate(rawDate) {
219
- if (!this._maxSelectedDate) {
220
- // because the values needed to calculate valid max/min values are not
221
- // available during the lit init when it's populating properties from
222
- // attributes, fall back to just the raw date if nothing is already set
223
- this._maxSelectedDate = rawDate;
224
- return;
225
- }
226
- const proposedDateMS = this.getMSFromString(rawDate);
227
- const isValidDate = !Number.isNaN(proposedDateMS);
228
- const isNotTooOld = proposedDateMS >= this.getMSFromString(this.minSelectedDate);
229
- if (isValidDate && isNotTooOld) {
230
- this._maxSelectedDate = this.formatDate(proposedDateMS);
231
- }
232
- this.requestUpdate();
233
- }
234
- /** horizontal position of min date slider */
235
- get minSliderX() {
236
- const x = this.translateDateToPosition(this.minSelectedDate);
237
- return this.validMinSliderX(x);
238
- }
239
- /** horizontal position of max date slider */
240
- get maxSliderX() {
241
- const x = this.translateDateToPosition(this.maxSelectedDate);
242
- return this.validMaxSliderX(x);
243
- }
244
- get dateRangeMS() {
245
- return this._maxDateMS - this._minDateMS;
246
- }
247
- showTooltip(e) {
248
- if (this._isDragging || this.disabled) {
249
- return;
250
- }
251
- const target = e.currentTarget;
252
- const x = target.x.baseVal.value + this.sliderWidth / 2;
253
- const dataset = target.dataset;
254
- const itemsText = `item${dataset.numItems !== '1' ? 's' : ''}`;
255
- const formattedNumItems = Number(dataset.numItems).toLocaleString();
256
- this._tooltipOffset =
257
- x + (this._binWidth - this.sliderWidth - this.tooltipWidth) / 2;
258
- this._tooltipContent = html `
259
- ${formattedNumItems} ${itemsText}<br />
260
- ${dataset.binStart} - ${dataset.binEnd}
261
- `;
262
- this._tooltipVisible = true;
263
- }
264
- hideTooltip() {
265
- this._tooltipContent = undefined;
266
- this._tooltipVisible = false;
267
- }
268
- /**
269
- * Constrain a proposed value for the minimum (left) slider
270
- *
271
- * If the value is less than the leftmost valid position, then set it to the
272
- * left edge of the histogram (ie the slider width). If the value is greater
273
- * than the rightmost valid position (the position of the max slider), then
274
- * set it to the position of the max slider
275
- */
276
- validMinSliderX(newX) {
277
- // allow the left slider to go right only to the right slider, even if the
278
- // max selected date is out of range
279
- const rightLimit = Math.min(this.translateDateToPosition(this.maxSelectedDate), this.histogramRightEdgeX);
280
- newX = this.clamp(newX, this.histogramLeftEdgeX, rightLimit);
281
- const isInvalid = Number.isNaN(newX) || rightLimit < this.histogramLeftEdgeX;
282
- return isInvalid ? this.histogramLeftEdgeX : newX;
283
- }
284
- /**
285
- * Constrain a proposed value for the maximum (right) slider
286
- *
287
- * If the value is greater than the rightmost valid position, then set it to
288
- * the right edge of the histogram (ie histogram width - slider width). If the
289
- * value is less than the leftmost valid position (the position of the min
290
- * slider), then set it to the position of the min slider
291
- */
292
- validMaxSliderX(newX) {
293
- // allow the right slider to go left only to the left slider, even if the
294
- // min selected date is out of range
295
- const leftLimit = Math.max(this.histogramLeftEdgeX, this.translateDateToPosition(this.minSelectedDate));
296
- newX = this.clamp(newX, leftLimit, this.histogramRightEdgeX);
297
- const isInvalid = Number.isNaN(newX) || leftLimit > this.histogramRightEdgeX;
298
- return isInvalid ? this.histogramRightEdgeX : newX;
299
- }
300
- addListeners() {
301
- window.addEventListener('pointermove', this.move);
302
- window.addEventListener('pointerup', this.drop);
303
- window.addEventListener('pointercancel', this.drop);
304
- }
305
- removeListeners() {
306
- window.removeEventListener('pointermove', this.move);
307
- window.removeEventListener('pointerup', this.drop);
308
- window.removeEventListener('pointercancel', this.drop);
309
- }
310
- /**
311
- * start a timer to emit an update event. this timer can be canceled (and the
312
- * event not emitted) if user drags a slider or focuses a date input within
313
- * the update delay
314
- */
315
- beginEmitUpdateProcess() {
316
- this.cancelPendingUpdateEvent();
317
- this._emitUpdatedEventTimer = setTimeout(() => {
318
- if (this.currentDateRangeString === this._previousDateRange) {
319
- // don't emit duplicate event if no change since last emitted event
320
- return;
321
- }
322
- this._previousDateRange = this.currentDateRangeString;
323
- const options = {
324
- detail: {
325
- minDate: this.minSelectedDate,
326
- maxDate: this.maxSelectedDate,
327
- },
328
- bubbles: true,
329
- composed: true,
330
- };
331
- this.dispatchEvent(new CustomEvent('histogramDateRangeUpdated', options));
332
- }, this.updateDelay);
333
- }
334
- cancelPendingUpdateEvent() {
335
- if (this._emitUpdatedEventTimer === undefined) {
336
- return;
337
- }
338
- clearTimeout(this._emitUpdatedEventTimer);
339
- this._emitUpdatedEventTimer = undefined;
340
- }
341
- /**
342
- * find position of pointer in relation to the current slider
343
- */
344
- setDragOffset(e) {
345
- this._currentSlider = e.currentTarget;
346
- const sliderX = this._currentSlider.id === 'slider-min'
347
- ? this.minSliderX
348
- : this.maxSliderX;
349
- const histogramClientX = this.getBoundingClientRect().x;
350
- this._dragOffset = e.clientX - histogramClientX - sliderX;
351
- }
352
- /**
353
- * @param x horizontal position of slider
354
- * @returns string representation of date
355
- */
356
- translatePositionToDate(x) {
357
- // use Math.ceil to round up to fix case where input like 1/1/2010 would get
358
- // translated to 12/31/2009
359
- const milliseconds = Math.ceil(((x - this.sliderWidth) * this.dateRangeMS) / this._histWidth);
360
- return this.formatDate(this._minDateMS + milliseconds);
361
- }
362
- /**
363
- * Returns slider x-position corresponding to given date
364
- *
365
- * @param date
366
- * @returns x-position of slider
367
- */
368
- translateDateToPosition(date) {
369
- const milliseconds = this.getMSFromString(date);
370
- return (this.sliderWidth +
371
- ((milliseconds - this._minDateMS) * this._histWidth) / this.dateRangeMS);
372
- }
373
- /** ensure that the returned value is between minValue and maxValue */
374
- clamp(x, minValue, maxValue) {
375
- return Math.min(Math.max(x, minValue), maxValue);
376
- }
377
- handleInputFocus() {
378
- if (!this.updateWhileFocused) {
379
- this.cancelPendingUpdateEvent();
380
- }
381
- }
382
- handleMinDateInput(e) {
383
- const target = e.currentTarget;
384
- if (target.value !== this.minSelectedDate) {
385
- this.minSelectedDate = target.value;
386
- this.beginEmitUpdateProcess();
387
- }
388
- }
389
- handleMaxDateInput(e) {
390
- const target = e.currentTarget;
391
- if (target.value !== this.maxSelectedDate) {
392
- this.maxSelectedDate = target.value;
393
- this.beginEmitUpdateProcess();
394
- }
395
- }
396
- handleKeyUp(e) {
397
- if (e.key === 'Enter') {
398
- const target = e.currentTarget;
399
- target.blur();
400
- if (target.id === 'date-min') {
401
- this.handleMinDateInput(e);
402
- }
403
- else if (target.id === 'date-max') {
404
- this.handleMaxDateInput(e);
405
- }
406
- }
407
- }
408
- get currentDateRangeString() {
409
- return `${this.minSelectedDate}:${this.maxSelectedDate}`;
410
- }
411
- getMSFromString(date) {
412
- // It's possible that `date` is not a string in certain situations.
413
- // For instance if you use LitElement bindings and the date is `2000`,
414
- // it will be treated as a number instead of a string. This just makes sure
415
- // we're dealing with a string.
416
- const stringified = typeof date === 'string' ? date : String(date);
417
- const digitGroupCount = (stringified.split(/(\d+)/).length - 1) / 2;
418
- if (digitGroupCount === 1) {
419
- // if there's just a single set of digits, assume it's a year
420
- const dateObj = new Date(0, 0); // start at January 1, 1900
421
- dateObj.setFullYear(Number(stringified)); // override year
422
- return dateObj.getTime(); // get time in milliseconds
423
- }
424
- return dayjs(stringified, [this.dateFormat, DATE_FORMAT]).valueOf();
425
- }
426
- /**
427
- * expand or narrow the selected range by moving the slider nearest the
428
- * clicked bar to the outer edge of the clicked bar
429
- *
430
- * @param e Event click event from a histogram bar
431
- */
432
- handleBarClick(e) {
433
- const dataset = e.currentTarget.dataset;
434
- // use the midpoint of the width of the clicked bar to determine which is
435
- // the nearest slider
436
- const clickPosition = (this.getMSFromString(dataset.binStart) +
437
- this.getMSFromString(dataset.binEnd)) /
438
- 2;
439
- const distanceFromMinSlider = Math.abs(clickPosition - this.getMSFromString(this.minSelectedDate));
440
- const distanceFromMaxSlider = Math.abs(clickPosition - this.getMSFromString(this.maxSelectedDate));
441
- // update the selected range by moving the nearer slider
442
- if (distanceFromMinSlider < distanceFromMaxSlider) {
443
- this.minSelectedDate = dataset.binStart;
444
- }
445
- else {
446
- this.maxSelectedDate = dataset.binEnd;
447
- }
448
- this.beginEmitUpdateProcess();
449
- }
450
- get minSliderTemplate() {
451
- // width/height in pixels of curved part of the sliders (like
452
- // border-radius); used as part of a SVG quadratic curve. see
453
- // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#curve_commands
454
- const cs = SLIDER_CORNER_SIZE;
455
- const sliderShape = `
456
- M${this.minSliderX},0
457
- h-${this.sliderWidth - cs}
458
- q-${cs},0 -${cs},${cs}
459
- v${this.height - cs * 2}
460
- q0,${cs} ${cs},${cs}
461
- h${this.sliderWidth - cs}
462
- `;
463
- return this.generateSliderSVG(this.minSliderX, 'slider-min', sliderShape);
464
- }
465
- get maxSliderTemplate() {
466
- const cs = SLIDER_CORNER_SIZE;
467
- const sliderShape = `
468
- M${this.maxSliderX},0
469
- h${this.sliderWidth - cs}
470
- q${cs},0 ${cs},${cs}
471
- v${this.height - cs * 2}
472
- q0,${cs} -${cs},${cs}
473
- h-${this.sliderWidth - cs}
474
- `;
475
- return this.generateSliderSVG(this.maxSliderX, 'slider-max', sliderShape);
476
- }
477
- generateSliderSVG(sliderPositionX, id, sliderShape) {
478
- // whether the curved part of the slider is facing towards the left (1), ie
479
- // minimum, or facing towards the right (-1), ie maximum
480
- const k = id === 'slider-min' ? 1 : -1;
481
- return svg `
482
- <svg
483
- id="${id}"
484
- class="
485
- ${this.disabled ? '' : 'draggable'}
486
- ${this._isDragging ? 'dragging' : ''}"
487
- @pointerdown="${this.drag}"
488
- >
489
- <path d="${sliderShape} z" fill="${sliderColor}" />
490
- <rect
491
- x="${sliderPositionX - this.sliderWidth * k + this.sliderWidth * 0.4 * k}"
492
- y="${this.height / 3}"
493
- width="1"
494
- height="${this.height / 3}"
495
- fill="white"
496
- />
497
- <rect
498
- x="${sliderPositionX - this.sliderWidth * k + this.sliderWidth * 0.6 * k}"
499
- y="${this.height / 3}"
500
- width="1"
501
- height="${this.height / 3}"
502
- fill="white"
503
- />
504
- </svg>
505
- `;
506
- }
507
- get selectedRangeTemplate() {
508
- return svg `
509
- <rect
510
- x="${this.minSliderX}"
511
- y="0"
512
- width="${this.maxSliderX - this.minSliderX}"
513
- height="${this.height}"
514
- fill="${selectedRangeColor}"
515
- />`;
516
- }
517
- get histogramTemplate() {
518
- const xScale = this._histWidth / this._numBins;
519
- const barWidth = xScale - 1;
520
- let x = this.sliderWidth; // start at the left edge of the histogram
521
- // the stroke-dasharray style below creates a transparent border around the
522
- // right edge of the bar, which prevents user from encountering a gap
523
- // between adjacent bars (eg when viewing the tooltips or when trying to
524
- // extend the range by clicking on a bar)
525
- return this._histData.map(data => {
526
- const bar = svg `
527
- <rect
528
- class="bar"
529
- style='stroke-dasharray: 0 ${barWidth} ${data.height} ${barWidth} 0 ${data.height};'
530
- x="${x}"
531
- y="${this.height - data.height}"
532
- width="${barWidth}"
533
- height="${data.height}"
534
- @pointerenter="${this.showTooltip}"
535
- @pointerleave="${this.hideTooltip}"
536
- @click="${this.handleBarClick}"
537
- fill="${x + barWidth >= this.minSliderX && x <= this.maxSliderX
538
- ? barIncludedFill
539
- : barExcludedFill}"
540
- data-num-items="${data.value}"
541
- data-bin-start="${data.binStart}"
542
- data-bin-end="${data.binEnd}"
543
- />`;
544
- x += xScale;
545
- return bar;
546
- });
547
- }
548
- formatDate(dateMS) {
549
- if (Number.isNaN(dateMS)) {
550
- return '';
551
- }
552
- const date = dayjs(dateMS);
553
- if (date.year() < 1000) {
554
- // years before 1000 don't play well with dayjs custom formatting, so fall
555
- // back to displaying only the year
556
- return String(date.year());
557
- }
558
- return date.format(this.dateFormat);
559
- }
560
- /**
561
- * NOTE: we are relying on the lit `live` directive in the template to
562
- * ensure that the change to minSelectedDate is noticed and the input value
563
- * gets properly re-rendered. see
564
- * https://lit.dev/docs/templates/directives/#live
565
- */
566
- get minInputTemplate() {
567
- return html `
568
- <input
569
- id="date-min"
570
- placeholder="${this.dateFormat}"
571
- type="text"
572
- @focus="${this.handleInputFocus}"
573
- @blur="${this.handleMinDateInput}"
574
- @keyup="${this.handleKeyUp}"
575
- .value="${live(this.minSelectedDate)}"
576
- ?disabled="${this.disabled}"
577
- />
578
- `;
579
- }
580
- get maxInputTemplate() {
581
- return html `
582
- <input
583
- id="date-max"
584
- placeholder="${this.dateFormat}"
585
- type="text"
586
- @focus="${this.handleInputFocus}"
587
- @blur="${this.handleMaxDateInput}"
588
- @keyup="${this.handleKeyUp}"
589
- .value="${live(this.maxSelectedDate)}"
590
- ?disabled="${this.disabled}"
591
- />
592
- `;
593
- }
594
- get minLabelTemplate() {
595
- return html `<label for="date-min" class="sr-only">Minimum date:</label>`;
596
- }
597
- get maxLabelTemplate() {
598
- return html `<label for="date-max" class="sr-only">Maximum date:</label>`;
599
- }
600
- get tooltipTemplate() {
601
- return html `
602
- <style>
603
- #tooltip {
604
- width: ${this.tooltipWidth}px;
605
- height: ${this.tooltipHeight}px;
606
- top: ${-9 - this.tooltipHeight}px;
607
- left: ${this._tooltipOffset}px;
608
- display: ${this._tooltipVisible ? 'block' : 'none'};
609
- }
610
- #tooltip:after {
611
- left: ${this.tooltipWidth / 2}px;
612
- }
613
- </style>
614
- <div id="tooltip">${this._tooltipContent}</div>
615
- `;
616
- }
617
- get noDataTemplate() {
618
- return html `
619
- <div class="missing-data-message">${this.missingDataMessage}</div>
620
- `;
621
- }
622
- get activityIndicatorTemplate() {
623
- if (!this.loading) {
624
- return nothing;
625
- }
626
- return html `
627
- <ia-activity-indicator mode="processing"> </ia-activity-indicator>
628
- `;
629
- }
630
- render() {
631
- if (!this.hasBinData) {
632
- return this.noDataTemplate;
633
- }
634
- return html `
635
- <div
636
- id="container"
637
- class="
638
- noselect
639
- ${this._isDragging ? 'dragging' : ''}
640
- "
641
- style="width: ${this.width}px"
642
- >
643
- ${this.activityIndicatorTemplate} ${this.tooltipTemplate}
644
- <div
645
- class="inner-container
646
- ${this.disabled ? 'disabled' : ''}"
647
- >
648
- <svg
649
- width="${this.width}"
650
- height="${this.height}"
651
- @pointerleave="${this.drop}"
652
- >
653
- ${this.selectedRangeTemplate}
654
- <svg id="histogram">${this.histogramTemplate}</svg>
655
- ${this.minSliderTemplate} ${this.maxSliderTemplate}
656
- </svg>
657
- <div id="inputs">
658
- ${this.minLabelTemplate} ${this.minInputTemplate}
659
- <div class="dash">-</div>
660
- ${this.maxLabelTemplate} ${this.maxInputTemplate}
661
- <slot name="inputs-right-side"></slot>
662
- </div>
663
- </div>
664
- </div>
665
- `;
666
- }
667
- };
668
- HistogramDateRange.styles = css `
669
- .missing-data-message {
670
- text-align: center;
671
- }
672
- #container {
673
- margin: 0;
674
- touch-action: none;
675
- position: relative;
676
- }
677
- .disabled {
678
- opacity: 0.3;
679
- }
680
- ia-activity-indicator {
681
- position: absolute;
682
- left: calc(50% - 10px);
683
- top: 10px;
684
- width: 20px;
685
- height: 20px;
686
- --activityIndicatorLoadingDotColor: rgba(0, 0, 0, 0);
687
- --activityIndicatorLoadingRingColor: ${activityIndicatorColor};
688
- }
689
-
690
- /* prevent selection from interfering with tooltip, especially on mobile */
691
- /* https://stackoverflow.com/a/4407335/1163042 */
692
- .noselect {
693
- -webkit-touch-callout: none; /* iOS Safari */
694
- -webkit-user-select: none; /* Safari */
695
- -moz-user-select: none; /* Old versions of Firefox */
696
- -ms-user-select: none; /* Internet Explorer/Edge */
697
- user-select: none; /* current Chrome, Edge, Opera and Firefox */
698
- }
699
- .bar {
700
- /* create a transparent border around the hist bars to prevent "gaps" and
701
- flickering when moving around between bars. this also helps with handling
702
- clicks on the bars, preventing users from being able to click in between
703
- bars */
704
- stroke: rgba(0, 0, 0, 0);
705
- /* ensure transparent stroke wide enough to cover gap between bars */
706
- stroke-width: 2px;
707
- }
708
- .bar:hover {
709
- /* highlight currently hovered bar */
710
- fill-opacity: 0.7;
711
- }
712
- .disabled .bar:hover {
713
- /* ensure no visual hover interaction when disabled */
714
- fill-opacity: 1;
715
- }
716
- /****** histogram ********/
717
- #tooltip {
718
- position: absolute;
719
- background: ${tooltipBackgroundColor};
720
- color: ${tooltipTextColor};
721
- text-align: center;
722
- border-radius: 3px;
723
- padding: 2px;
724
- font-size: ${tooltipFontSize};
725
- font-family: ${tooltipFontFamily};
726
- touch-action: none;
727
- pointer-events: none;
728
- }
729
- #tooltip:after {
730
- content: '';
731
- position: absolute;
732
- margin-left: -5px;
733
- top: 100%;
734
- /* arrow */
735
- border: 5px solid ${tooltipTextColor};
736
- border-color: ${tooltipBackgroundColor} transparent transparent
737
- transparent;
738
- }
739
- /****** slider ********/
740
- .draggable:hover {
741
- cursor: grab;
742
- }
743
- .dragging {
744
- cursor: grabbing !important;
745
- }
746
- /****** inputs ********/
747
- #inputs {
748
- display: flex;
749
- justify-content: center;
750
- margin: ${inputRowMargin};
751
- }
752
- #inputs .dash {
753
- position: relative;
754
- bottom: -1px;
755
- align-self: center; /* Otherwise the dash sticks to the top while the inputs grow */
756
- }
757
- input {
758
- width: ${inputWidth};
759
- margin: 0 3px;
760
- border: ${inputBorder};
761
- border-radius: 2px !important;
762
- text-align: center;
763
- font-size: ${inputFontSize};
764
- font-family: ${inputFontFamily};
765
- }
766
- .sr-only {
767
- position: absolute !important;
768
- width: 1px !important;
769
- height: 1px !important;
770
- margin: 0 !important;
771
- padding: 0 !important;
772
- border: 0 !important;
773
- overflow: hidden !important;
774
- white-space: nowrap !important;
775
- clip: rect(1px, 1px, 1px, 1px) !important;
776
- -webkit-clip-path: inset(50%) !important;
777
- clip-path: inset(50%) !important;
778
- }
779
- `;
780
- __decorate([
781
- property({ type: Number })
782
- ], HistogramDateRange.prototype, "width", void 0);
783
- __decorate([
784
- property({ type: Number })
785
- ], HistogramDateRange.prototype, "height", void 0);
786
- __decorate([
787
- property({ type: Number })
788
- ], HistogramDateRange.prototype, "sliderWidth", void 0);
789
- __decorate([
790
- property({ type: Number })
791
- ], HistogramDateRange.prototype, "tooltipWidth", void 0);
792
- __decorate([
793
- property({ type: Number })
794
- ], HistogramDateRange.prototype, "tooltipHeight", void 0);
795
- __decorate([
796
- property({ type: Number })
797
- ], HistogramDateRange.prototype, "updateDelay", void 0);
798
- __decorate([
799
- property({ type: String })
800
- ], HistogramDateRange.prototype, "dateFormat", void 0);
801
- __decorate([
802
- property({ type: String })
803
- ], HistogramDateRange.prototype, "missingDataMessage", void 0);
804
- __decorate([
805
- property({ type: String })
806
- ], HistogramDateRange.prototype, "minDate", void 0);
807
- __decorate([
808
- property({ type: String })
809
- ], HistogramDateRange.prototype, "maxDate", void 0);
810
- __decorate([
811
- property({ type: Boolean })
812
- ], HistogramDateRange.prototype, "disabled", void 0);
813
- __decorate([
814
- property({ type: Object })
815
- ], HistogramDateRange.prototype, "bins", void 0);
816
- __decorate([
817
- property({ type: Boolean })
818
- ], HistogramDateRange.prototype, "updateWhileFocused", void 0);
819
- __decorate([
820
- state()
821
- ], HistogramDateRange.prototype, "_tooltipOffset", void 0);
822
- __decorate([
823
- state()
824
- ], HistogramDateRange.prototype, "_tooltipContent", void 0);
825
- __decorate([
826
- state()
827
- ], HistogramDateRange.prototype, "_tooltipVisible", void 0);
828
- __decorate([
829
- state()
830
- ], HistogramDateRange.prototype, "_isDragging", void 0);
831
- __decorate([
832
- state()
833
- ], HistogramDateRange.prototype, "_isLoading", void 0);
834
- __decorate([
835
- property({ type: Boolean })
836
- ], HistogramDateRange.prototype, "loading", null);
837
- __decorate([
838
- property()
839
- ], HistogramDateRange.prototype, "minSelectedDate", null);
840
- __decorate([
841
- property()
842
- ], HistogramDateRange.prototype, "maxSelectedDate", null);
843
- HistogramDateRange = __decorate([
844
- customElement('histogram-date-range')
845
- ], HistogramDateRange);
846
- export { HistogramDateRange };
1
+ import { __decorate } from "tslib";
2
+ import '@internetarchive/ia-activity-indicator';
3
+ import dayjs from 'dayjs/esm';
4
+ import customParseFormat from 'dayjs/esm/plugin/customParseFormat';
5
+ import { css, html, LitElement, nothing, svg, } from 'lit';
6
+ import { customElement, property, state } from 'lit/decorators.js';
7
+ import { live } from 'lit/directives/live.js';
8
+ dayjs.extend(customParseFormat);
9
+ // these values can be overridden via the component's HTML (camelCased) attributes
10
+ const WIDTH = 180;
11
+ const HEIGHT = 40;
12
+ const SLIDER_WIDTH = 10;
13
+ const TOOLTIP_WIDTH = 125;
14
+ const TOOLTIP_HEIGHT = 30;
15
+ const DATE_FORMAT = 'YYYY';
16
+ const MISSING_DATA = 'no data';
17
+ const UPDATE_DEBOUNCE_DELAY_MS = 0;
18
+ // this constant is not set up to be overridden
19
+ const SLIDER_CORNER_SIZE = 4;
20
+ // these CSS custom props can be overridden from the HTML that is invoking this component
21
+ const sliderColor = css `var(--histogramDateRangeSliderColor, #4B65FE)`;
22
+ const selectedRangeColor = css `var(--histogramDateRangeSelectedRangeColor, #DBE0FF)`;
23
+ const barIncludedFill = css `var(--histogramDateRangeBarIncludedFill, #2C2C2C)`;
24
+ const activityIndicatorColor = css `var(--histogramDateRangeActivityIndicator, #2C2C2C)`;
25
+ const barExcludedFill = css `var(--histogramDateRangeBarExcludedFill, #CCCCCC)`;
26
+ const inputRowMargin = css `var(--histogramDateRangeInputRowMargin, 0)`;
27
+ const inputBorder = css `var(--histogramDateRangeInputBorder, 0.5px solid #2C2C2C)`;
28
+ const inputWidth = css `var(--histogramDateRangeInputWidth, 35px)`;
29
+ const inputFontSize = css `var(--histogramDateRangeInputFontSize, 1.2rem)`;
30
+ const inputFontFamily = css `var(--histogramDateRangeInputFontFamily, sans-serif)`;
31
+ const tooltipBackgroundColor = css `var(--histogramDateRangeTooltipBackgroundColor, #2C2C2C)`;
32
+ const tooltipTextColor = css `var(--histogramDateRangeTooltipTextColor, #FFFFFF)`;
33
+ const tooltipFontSize = css `var(--histogramDateRangeTooltipFontSize, 1.1rem)`;
34
+ const tooltipFontFamily = css `var(--histogramDateRangeTooltipFontFamily, sans-serif)`;
35
+ let HistogramDateRange = class HistogramDateRange extends LitElement {
36
+ constructor() {
37
+ /* eslint-disable lines-between-class-members */
38
+ super(...arguments);
39
+ // public reactive properties that can be set via HTML attributes
40
+ this.width = WIDTH;
41
+ this.height = HEIGHT;
42
+ this.sliderWidth = SLIDER_WIDTH;
43
+ this.tooltipWidth = TOOLTIP_WIDTH;
44
+ this.tooltipHeight = TOOLTIP_HEIGHT;
45
+ this.updateDelay = UPDATE_DEBOUNCE_DELAY_MS;
46
+ this.dateFormat = DATE_FORMAT;
47
+ this.missingDataMessage = MISSING_DATA;
48
+ this.minDate = '';
49
+ this.maxDate = '';
50
+ this.disabled = false;
51
+ this.bins = [];
52
+ /** If true, update events will not be canceled by the date inputs receiving focus */
53
+ this.updateWhileFocused = false;
54
+ // internal reactive properties not exposed as attributes
55
+ this._tooltipOffset = 0;
56
+ this._tooltipVisible = false;
57
+ this._isDragging = false;
58
+ this._isLoading = false;
59
+ // non-reactive properties (changes don't auto-trigger re-rendering)
60
+ this._minSelectedDate = '';
61
+ this._maxSelectedDate = '';
62
+ this._minDateMS = 0;
63
+ this._maxDateMS = 0;
64
+ this._dragOffset = 0;
65
+ this._histWidth = 0;
66
+ this._binWidth = 0;
67
+ this._histData = [];
68
+ this._previousDateRange = '';
69
+ // use arrow functions (rather than standard JS class instance methods) so
70
+ // that `this` is bound to the histogramDateRange object and not the event
71
+ // target. for more info see
72
+ // https://lit-element.polymer-project.org/guide/events#using-this-in-event-listeners
73
+ this.drag = (e) => {
74
+ // prevent selecting text or other ranges while dragging, especially in Safari
75
+ e.preventDefault();
76
+ if (this.disabled) {
77
+ return;
78
+ }
79
+ this.setDragOffset(e);
80
+ this._isDragging = true;
81
+ this.addListeners();
82
+ this.cancelPendingUpdateEvent();
83
+ };
84
+ this.drop = () => {
85
+ if (this._isDragging) {
86
+ this.removeListeners();
87
+ this.beginEmitUpdateProcess();
88
+ }
89
+ this._isDragging = false;
90
+ };
91
+ /**
92
+ * Adjust the date range based on slider movement
93
+ *
94
+ * @param e PointerEvent from the slider being moved
95
+ */
96
+ this.move = (e) => {
97
+ const histogramClientX = this.getBoundingClientRect().x;
98
+ const newX = e.clientX - histogramClientX - this._dragOffset;
99
+ const slider = this._currentSlider;
100
+ if (slider.id === 'slider-min') {
101
+ this.minSelectedDate = this.translatePositionToDate(this.validMinSliderX(newX));
102
+ }
103
+ else {
104
+ this.maxSelectedDate = this.translatePositionToDate(this.validMaxSliderX(newX));
105
+ }
106
+ };
107
+ }
108
+ /* eslint-enable lines-between-class-members */
109
+ disconnectedCallback() {
110
+ this.removeListeners();
111
+ super.disconnectedCallback();
112
+ }
113
+ willUpdate(changedProps) {
114
+ // check for changes that would affect bin data calculations
115
+ if (changedProps.has('bins') ||
116
+ changedProps.has('minDate') ||
117
+ changedProps.has('maxDate') ||
118
+ changedProps.has('minSelectedDate') ||
119
+ changedProps.has('maxSelectedDate') ||
120
+ changedProps.has('width') ||
121
+ changedProps.has('height')) {
122
+ this.handleDataUpdate();
123
+ }
124
+ }
125
+ /**
126
+ * Set private properties that depend on the attribute bin data
127
+ *
128
+ * We're caching these values and not using getters to avoid recalculating all
129
+ * of the hist data every time the user drags a slider or hovers over a bar
130
+ * creating a tooltip.
131
+ */
132
+ handleDataUpdate() {
133
+ if (!this.hasBinData) {
134
+ return;
135
+ }
136
+ this._histWidth = this.width - this.sliderWidth * 2;
137
+ this._minDateMS = this.getMSFromString(this.minDate);
138
+ this._maxDateMS = this.getMSFromString(this.maxDate);
139
+ this._binWidth = this._histWidth / this._numBins;
140
+ this._previousDateRange = this.currentDateRangeString;
141
+ this._histData = this.calculateHistData();
142
+ this.minSelectedDate = this.minSelectedDate
143
+ ? this.minSelectedDate
144
+ : this.minDate;
145
+ this.maxSelectedDate = this.maxSelectedDate
146
+ ? this.maxSelectedDate
147
+ : this.maxDate;
148
+ }
149
+ calculateHistData() {
150
+ const minValue = Math.min(...this.bins);
151
+ const maxValue = Math.max(...this.bins);
152
+ // if there is no difference between the min and max values, use a range of
153
+ // 1 because log scaling will fail if the range is 0
154
+ const valueRange = minValue === maxValue ? 1 : Math.log1p(maxValue);
155
+ const valueScale = this.height / valueRange;
156
+ const dateScale = this.dateRangeMS / this._numBins;
157
+ return this.bins.map((v, i) => {
158
+ return {
159
+ value: v,
160
+ // use log scaling for the height of the bar to prevent tall bars from
161
+ // making the smaller ones too small to see
162
+ height: Math.floor(Math.log1p(v) * valueScale),
163
+ binStart: `${this.formatDate(i * dateScale + this._minDateMS, this.resolvedTooltipDateFormat)}`,
164
+ binEnd: `${this.formatDate((i + 1) * dateScale + this._minDateMS, this.resolvedTooltipDateFormat)}`,
165
+ };
166
+ });
167
+ }
168
+ get hasBinData() {
169
+ return this._numBins > 0;
170
+ }
171
+ get _numBins() {
172
+ if (!this.bins || !this.bins.length) {
173
+ return 0;
174
+ }
175
+ return this.bins.length;
176
+ }
177
+ get histogramLeftEdgeX() {
178
+ return this.sliderWidth;
179
+ }
180
+ get histogramRightEdgeX() {
181
+ return this.width - this.sliderWidth;
182
+ }
183
+ /**
184
+ * Returns the consumer-specified tooltip date format if set, falling back to
185
+ * the general `dateFormat` property and then the default format if none is set.
186
+ */
187
+ get resolvedTooltipDateFormat() {
188
+ var _a, _b;
189
+ return (_b = (_a = this.tooltipDateFormat) !== null && _a !== void 0 ? _a : this.dateFormat) !== null && _b !== void 0 ? _b : DATE_FORMAT;
190
+ }
191
+ /** component's loading (and disabled) state */
192
+ get loading() {
193
+ return this._isLoading;
194
+ }
195
+ set loading(value) {
196
+ this.disabled = value;
197
+ this._isLoading = value;
198
+ }
199
+ /** formatted minimum date of selected date range */
200
+ get minSelectedDate() {
201
+ return this.formatDate(this.getMSFromString(this._minSelectedDate));
202
+ }
203
+ /** updates minSelectedDate if new date is valid */
204
+ set minSelectedDate(rawDate) {
205
+ if (!this._minSelectedDate) {
206
+ // because the values needed to calculate valid max/min values are not
207
+ // available during the lit init when it's populating properties from
208
+ // attributes, fall back to just the raw date if nothing is already set
209
+ this._minSelectedDate = rawDate;
210
+ return;
211
+ }
212
+ const proposedDateMS = this.getMSFromString(rawDate);
213
+ const isValidDate = !Number.isNaN(proposedDateMS);
214
+ const isNotTooRecent = proposedDateMS <= this.getMSFromString(this.maxSelectedDate);
215
+ if (isValidDate && isNotTooRecent) {
216
+ this._minSelectedDate = this.formatDate(proposedDateMS);
217
+ }
218
+ this.requestUpdate();
219
+ }
220
+ /** formatted maximum date of selected date range */
221
+ get maxSelectedDate() {
222
+ return this.formatDate(this.getMSFromString(this._maxSelectedDate));
223
+ }
224
+ /** updates maxSelectedDate if new date is valid */
225
+ set maxSelectedDate(rawDate) {
226
+ if (!this._maxSelectedDate) {
227
+ // because the values needed to calculate valid max/min values are not
228
+ // available during the lit init when it's populating properties from
229
+ // attributes, fall back to just the raw date if nothing is already set
230
+ this._maxSelectedDate = rawDate;
231
+ return;
232
+ }
233
+ const proposedDateMS = this.getMSFromString(rawDate);
234
+ const isValidDate = !Number.isNaN(proposedDateMS);
235
+ const isNotTooOld = proposedDateMS >= this.getMSFromString(this.minSelectedDate);
236
+ if (isValidDate && isNotTooOld) {
237
+ this._maxSelectedDate = this.formatDate(proposedDateMS);
238
+ }
239
+ this.requestUpdate();
240
+ }
241
+ /** horizontal position of min date slider */
242
+ get minSliderX() {
243
+ const x = this.translateDateToPosition(this.minSelectedDate);
244
+ return this.validMinSliderX(x);
245
+ }
246
+ /** horizontal position of max date slider */
247
+ get maxSliderX() {
248
+ const x = this.translateDateToPosition(this.maxSelectedDate);
249
+ return this.validMaxSliderX(x);
250
+ }
251
+ get dateRangeMS() {
252
+ return this._maxDateMS - this._minDateMS;
253
+ }
254
+ showTooltip(e) {
255
+ if (this._isDragging || this.disabled) {
256
+ return;
257
+ }
258
+ const target = e.currentTarget;
259
+ const x = target.x.baseVal.value + this.sliderWidth / 2;
260
+ const dataset = target.dataset;
261
+ const itemsText = `item${dataset.numItems !== '1' ? 's' : ''}`;
262
+ const formattedNumItems = Number(dataset.numItems).toLocaleString();
263
+ this._tooltipOffset =
264
+ x + (this._binWidth - this.sliderWidth - this.tooltipWidth) / 2;
265
+ this._tooltipContent = html `
266
+ ${formattedNumItems} ${itemsText}<br />
267
+ ${dataset.binStart} - ${dataset.binEnd}
268
+ `;
269
+ this._tooltipVisible = true;
270
+ }
271
+ hideTooltip() {
272
+ this._tooltipContent = undefined;
273
+ this._tooltipVisible = false;
274
+ }
275
+ /**
276
+ * Constrain a proposed value for the minimum (left) slider
277
+ *
278
+ * If the value is less than the leftmost valid position, then set it to the
279
+ * left edge of the histogram (ie the slider width). If the value is greater
280
+ * than the rightmost valid position (the position of the max slider), then
281
+ * set it to the position of the max slider
282
+ */
283
+ validMinSliderX(newX) {
284
+ // allow the left slider to go right only to the right slider, even if the
285
+ // max selected date is out of range
286
+ const rightLimit = Math.min(this.translateDateToPosition(this.maxSelectedDate), this.histogramRightEdgeX);
287
+ newX = this.clamp(newX, this.histogramLeftEdgeX, rightLimit);
288
+ const isInvalid = Number.isNaN(newX) || rightLimit < this.histogramLeftEdgeX;
289
+ return isInvalid ? this.histogramLeftEdgeX : newX;
290
+ }
291
+ /**
292
+ * Constrain a proposed value for the maximum (right) slider
293
+ *
294
+ * If the value is greater than the rightmost valid position, then set it to
295
+ * the right edge of the histogram (ie histogram width - slider width). If the
296
+ * value is less than the leftmost valid position (the position of the min
297
+ * slider), then set it to the position of the min slider
298
+ */
299
+ validMaxSliderX(newX) {
300
+ // allow the right slider to go left only to the left slider, even if the
301
+ // min selected date is out of range
302
+ const leftLimit = Math.max(this.histogramLeftEdgeX, this.translateDateToPosition(this.minSelectedDate));
303
+ newX = this.clamp(newX, leftLimit, this.histogramRightEdgeX);
304
+ const isInvalid = Number.isNaN(newX) || leftLimit > this.histogramRightEdgeX;
305
+ return isInvalid ? this.histogramRightEdgeX : newX;
306
+ }
307
+ addListeners() {
308
+ window.addEventListener('pointermove', this.move);
309
+ window.addEventListener('pointerup', this.drop);
310
+ window.addEventListener('pointercancel', this.drop);
311
+ }
312
+ removeListeners() {
313
+ window.removeEventListener('pointermove', this.move);
314
+ window.removeEventListener('pointerup', this.drop);
315
+ window.removeEventListener('pointercancel', this.drop);
316
+ }
317
+ /**
318
+ * start a timer to emit an update event. this timer can be canceled (and the
319
+ * event not emitted) if user drags a slider or focuses a date input within
320
+ * the update delay
321
+ */
322
+ beginEmitUpdateProcess() {
323
+ this.cancelPendingUpdateEvent();
324
+ this._emitUpdatedEventTimer = setTimeout(() => {
325
+ if (this.currentDateRangeString === this._previousDateRange) {
326
+ // don't emit duplicate event if no change since last emitted event
327
+ return;
328
+ }
329
+ this._previousDateRange = this.currentDateRangeString;
330
+ const options = {
331
+ detail: {
332
+ minDate: this.minSelectedDate,
333
+ maxDate: this.maxSelectedDate,
334
+ },
335
+ bubbles: true,
336
+ composed: true,
337
+ };
338
+ this.dispatchEvent(new CustomEvent('histogramDateRangeUpdated', options));
339
+ }, this.updateDelay);
340
+ }
341
+ cancelPendingUpdateEvent() {
342
+ if (this._emitUpdatedEventTimer === undefined) {
343
+ return;
344
+ }
345
+ clearTimeout(this._emitUpdatedEventTimer);
346
+ this._emitUpdatedEventTimer = undefined;
347
+ }
348
+ /**
349
+ * find position of pointer in relation to the current slider
350
+ */
351
+ setDragOffset(e) {
352
+ this._currentSlider = e.currentTarget;
353
+ const sliderX = this._currentSlider.id === 'slider-min'
354
+ ? this.minSliderX
355
+ : this.maxSliderX;
356
+ const histogramClientX = this.getBoundingClientRect().x;
357
+ this._dragOffset = e.clientX - histogramClientX - sliderX;
358
+ }
359
+ /**
360
+ * @param x horizontal position of slider
361
+ * @returns string representation of date
362
+ */
363
+ translatePositionToDate(x) {
364
+ // use Math.ceil to round up to fix case where input like 1/1/2010 would get
365
+ // translated to 12/31/2009
366
+ const milliseconds = Math.ceil(((x - this.sliderWidth) * this.dateRangeMS) / this._histWidth);
367
+ return this.formatDate(this._minDateMS + milliseconds);
368
+ }
369
+ /**
370
+ * Returns slider x-position corresponding to given date
371
+ *
372
+ * @param date
373
+ * @returns x-position of slider
374
+ */
375
+ translateDateToPosition(date) {
376
+ const milliseconds = this.getMSFromString(date);
377
+ return (this.sliderWidth +
378
+ ((milliseconds - this._minDateMS) * this._histWidth) / this.dateRangeMS);
379
+ }
380
+ /** ensure that the returned value is between minValue and maxValue */
381
+ clamp(x, minValue, maxValue) {
382
+ return Math.min(Math.max(x, minValue), maxValue);
383
+ }
384
+ handleInputFocus() {
385
+ if (!this.updateWhileFocused) {
386
+ this.cancelPendingUpdateEvent();
387
+ }
388
+ }
389
+ handleMinDateInput(e) {
390
+ const target = e.currentTarget;
391
+ if (target.value !== this.minSelectedDate) {
392
+ this.minSelectedDate = target.value;
393
+ this.beginEmitUpdateProcess();
394
+ }
395
+ }
396
+ handleMaxDateInput(e) {
397
+ const target = e.currentTarget;
398
+ if (target.value !== this.maxSelectedDate) {
399
+ this.maxSelectedDate = target.value;
400
+ this.beginEmitUpdateProcess();
401
+ }
402
+ }
403
+ handleKeyUp(e) {
404
+ if (e.key === 'Enter') {
405
+ const target = e.currentTarget;
406
+ target.blur();
407
+ if (target.id === 'date-min') {
408
+ this.handleMinDateInput(e);
409
+ }
410
+ else if (target.id === 'date-max') {
411
+ this.handleMaxDateInput(e);
412
+ }
413
+ }
414
+ }
415
+ get currentDateRangeString() {
416
+ return `${this.minSelectedDate}:${this.maxSelectedDate}`;
417
+ }
418
+ getMSFromString(date) {
419
+ // It's possible that `date` is not a string in certain situations.
420
+ // For instance if you use LitElement bindings and the date is `2000`,
421
+ // it will be treated as a number instead of a string. This just makes sure
422
+ // we're dealing with a string.
423
+ const stringified = typeof date === 'string' ? date : String(date);
424
+ const digitGroupCount = (stringified.split(/(\d+)/).length - 1) / 2;
425
+ if (digitGroupCount === 1) {
426
+ // if there's just a single set of digits, assume it's a year
427
+ const dateObj = new Date(0, 0); // start at January 1, 1900
428
+ dateObj.setFullYear(Number(stringified)); // override year
429
+ return dateObj.getTime(); // get time in milliseconds
430
+ }
431
+ return dayjs(stringified, [this.dateFormat, DATE_FORMAT]).valueOf();
432
+ }
433
+ /**
434
+ * expand or narrow the selected range by moving the slider nearest the
435
+ * clicked bar to the outer edge of the clicked bar
436
+ *
437
+ * @param e Event click event from a histogram bar
438
+ */
439
+ handleBarClick(e) {
440
+ const dataset = e.currentTarget.dataset;
441
+ // use the midpoint of the width of the clicked bar to determine which is
442
+ // the nearest slider
443
+ const clickPosition = (this.getMSFromString(dataset.binStart) +
444
+ this.getMSFromString(dataset.binEnd)) /
445
+ 2;
446
+ const distanceFromMinSlider = Math.abs(clickPosition - this.getMSFromString(this.minSelectedDate));
447
+ const distanceFromMaxSlider = Math.abs(clickPosition - this.getMSFromString(this.maxSelectedDate));
448
+ // update the selected range by moving the nearer slider
449
+ if (distanceFromMinSlider < distanceFromMaxSlider) {
450
+ this.minSelectedDate = dataset.binStart;
451
+ }
452
+ else {
453
+ this.maxSelectedDate = dataset.binEnd;
454
+ }
455
+ this.beginEmitUpdateProcess();
456
+ }
457
+ get minSliderTemplate() {
458
+ // width/height in pixels of curved part of the sliders (like
459
+ // border-radius); used as part of a SVG quadratic curve. see
460
+ // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#curve_commands
461
+ const cs = SLIDER_CORNER_SIZE;
462
+ const sliderShape = `
463
+ M${this.minSliderX},0
464
+ h-${this.sliderWidth - cs}
465
+ q-${cs},0 -${cs},${cs}
466
+ v${this.height - cs * 2}
467
+ q0,${cs} ${cs},${cs}
468
+ h${this.sliderWidth - cs}
469
+ `;
470
+ return this.generateSliderSVG(this.minSliderX, 'slider-min', sliderShape);
471
+ }
472
+ get maxSliderTemplate() {
473
+ const cs = SLIDER_CORNER_SIZE;
474
+ const sliderShape = `
475
+ M${this.maxSliderX},0
476
+ h${this.sliderWidth - cs}
477
+ q${cs},0 ${cs},${cs}
478
+ v${this.height - cs * 2}
479
+ q0,${cs} -${cs},${cs}
480
+ h-${this.sliderWidth - cs}
481
+ `;
482
+ return this.generateSliderSVG(this.maxSliderX, 'slider-max', sliderShape);
483
+ }
484
+ generateSliderSVG(sliderPositionX, id, sliderShape) {
485
+ // whether the curved part of the slider is facing towards the left (1), ie
486
+ // minimum, or facing towards the right (-1), ie maximum
487
+ const k = id === 'slider-min' ? 1 : -1;
488
+ return svg `
489
+ <svg
490
+ id="${id}"
491
+ class="
492
+ ${this.disabled ? '' : 'draggable'}
493
+ ${this._isDragging ? 'dragging' : ''}"
494
+ @pointerdown="${this.drag}"
495
+ >
496
+ <path d="${sliderShape} z" fill="${sliderColor}" />
497
+ <rect
498
+ x="${sliderPositionX - this.sliderWidth * k + this.sliderWidth * 0.4 * k}"
499
+ y="${this.height / 3}"
500
+ width="1"
501
+ height="${this.height / 3}"
502
+ fill="white"
503
+ />
504
+ <rect
505
+ x="${sliderPositionX - this.sliderWidth * k + this.sliderWidth * 0.6 * k}"
506
+ y="${this.height / 3}"
507
+ width="1"
508
+ height="${this.height / 3}"
509
+ fill="white"
510
+ />
511
+ </svg>
512
+ `;
513
+ }
514
+ get selectedRangeTemplate() {
515
+ return svg `
516
+ <rect
517
+ x="${this.minSliderX}"
518
+ y="0"
519
+ width="${this.maxSliderX - this.minSliderX}"
520
+ height="${this.height}"
521
+ fill="${selectedRangeColor}"
522
+ />`;
523
+ }
524
+ get histogramTemplate() {
525
+ const xScale = this._histWidth / this._numBins;
526
+ const barWidth = xScale - 1;
527
+ let x = this.sliderWidth; // start at the left edge of the histogram
528
+ // the stroke-dasharray style below creates a transparent border around the
529
+ // right edge of the bar, which prevents user from encountering a gap
530
+ // between adjacent bars (eg when viewing the tooltips or when trying to
531
+ // extend the range by clicking on a bar)
532
+ return this._histData.map(data => {
533
+ const bar = svg `
534
+ <rect
535
+ class="bar"
536
+ style='stroke-dasharray: 0 ${barWidth} ${data.height} ${barWidth} 0 ${data.height};'
537
+ x="${x}"
538
+ y="${this.height - data.height}"
539
+ width="${barWidth}"
540
+ height="${data.height}"
541
+ @pointerenter="${this.showTooltip}"
542
+ @pointerleave="${this.hideTooltip}"
543
+ @click="${this.handleBarClick}"
544
+ fill="${x + barWidth >= this.minSliderX && x <= this.maxSliderX
545
+ ? barIncludedFill
546
+ : barExcludedFill}"
547
+ data-num-items="${data.value}"
548
+ data-bin-start="${data.binStart}"
549
+ data-bin-end="${data.binEnd}"
550
+ />`;
551
+ x += xScale;
552
+ return bar;
553
+ });
554
+ }
555
+ formatDate(dateMS, format = this.dateFormat) {
556
+ if (Number.isNaN(dateMS)) {
557
+ return '';
558
+ }
559
+ const date = dayjs(dateMS);
560
+ if (date.year() < 1000) {
561
+ // years before 1000 don't play well with dayjs custom formatting, so fall
562
+ // back to displaying only the year
563
+ return String(date.year());
564
+ }
565
+ return date.format(format);
566
+ }
567
+ /**
568
+ * NOTE: we are relying on the lit `live` directive in the template to
569
+ * ensure that the change to minSelectedDate is noticed and the input value
570
+ * gets properly re-rendered. see
571
+ * https://lit.dev/docs/templates/directives/#live
572
+ */
573
+ get minInputTemplate() {
574
+ return html `
575
+ <input
576
+ id="date-min"
577
+ placeholder="${this.dateFormat}"
578
+ type="text"
579
+ @focus="${this.handleInputFocus}"
580
+ @blur="${this.handleMinDateInput}"
581
+ @keyup="${this.handleKeyUp}"
582
+ .value="${live(this.minSelectedDate)}"
583
+ ?disabled="${this.disabled}"
584
+ />
585
+ `;
586
+ }
587
+ get maxInputTemplate() {
588
+ return html `
589
+ <input
590
+ id="date-max"
591
+ placeholder="${this.dateFormat}"
592
+ type="text"
593
+ @focus="${this.handleInputFocus}"
594
+ @blur="${this.handleMaxDateInput}"
595
+ @keyup="${this.handleKeyUp}"
596
+ .value="${live(this.maxSelectedDate)}"
597
+ ?disabled="${this.disabled}"
598
+ />
599
+ `;
600
+ }
601
+ get minLabelTemplate() {
602
+ return html `<label for="date-min" class="sr-only">Minimum date:</label>`;
603
+ }
604
+ get maxLabelTemplate() {
605
+ return html `<label for="date-max" class="sr-only">Maximum date:</label>`;
606
+ }
607
+ get tooltipTemplate() {
608
+ return html `
609
+ <style>
610
+ #tooltip {
611
+ width: ${this.tooltipWidth}px;
612
+ height: ${this.tooltipHeight}px;
613
+ top: ${-9 - this.tooltipHeight}px;
614
+ left: ${this._tooltipOffset}px;
615
+ display: ${this._tooltipVisible ? 'block' : 'none'};
616
+ }
617
+ #tooltip:after {
618
+ left: ${this.tooltipWidth / 2}px;
619
+ }
620
+ </style>
621
+ <div id="tooltip">${this._tooltipContent}</div>
622
+ `;
623
+ }
624
+ get noDataTemplate() {
625
+ return html `
626
+ <div class="missing-data-message">${this.missingDataMessage}</div>
627
+ `;
628
+ }
629
+ get activityIndicatorTemplate() {
630
+ if (!this.loading) {
631
+ return nothing;
632
+ }
633
+ return html `
634
+ <ia-activity-indicator mode="processing"> </ia-activity-indicator>
635
+ `;
636
+ }
637
+ render() {
638
+ if (!this.hasBinData) {
639
+ return this.noDataTemplate;
640
+ }
641
+ return html `
642
+ <div
643
+ id="container"
644
+ class="
645
+ noselect
646
+ ${this._isDragging ? 'dragging' : ''}
647
+ "
648
+ style="width: ${this.width}px"
649
+ >
650
+ ${this.activityIndicatorTemplate} ${this.tooltipTemplate}
651
+ <div
652
+ class="inner-container
653
+ ${this.disabled ? 'disabled' : ''}"
654
+ >
655
+ <svg
656
+ width="${this.width}"
657
+ height="${this.height}"
658
+ @pointerleave="${this.drop}"
659
+ >
660
+ ${this.selectedRangeTemplate}
661
+ <svg id="histogram">${this.histogramTemplate}</svg>
662
+ ${this.minSliderTemplate} ${this.maxSliderTemplate}
663
+ </svg>
664
+ <div id="inputs">
665
+ ${this.minLabelTemplate} ${this.minInputTemplate}
666
+ <div class="dash">-</div>
667
+ ${this.maxLabelTemplate} ${this.maxInputTemplate}
668
+ <slot name="inputs-right-side"></slot>
669
+ </div>
670
+ </div>
671
+ </div>
672
+ `;
673
+ }
674
+ };
675
+ HistogramDateRange.styles = css `
676
+ .missing-data-message {
677
+ text-align: center;
678
+ }
679
+ #container {
680
+ margin: 0;
681
+ touch-action: none;
682
+ position: relative;
683
+ }
684
+ .disabled {
685
+ opacity: 0.3;
686
+ }
687
+ ia-activity-indicator {
688
+ position: absolute;
689
+ left: calc(50% - 10px);
690
+ top: 10px;
691
+ width: 20px;
692
+ height: 20px;
693
+ --activityIndicatorLoadingDotColor: rgba(0, 0, 0, 0);
694
+ --activityIndicatorLoadingRingColor: ${activityIndicatorColor};
695
+ }
696
+
697
+ /* prevent selection from interfering with tooltip, especially on mobile */
698
+ /* https://stackoverflow.com/a/4407335/1163042 */
699
+ .noselect {
700
+ -webkit-touch-callout: none; /* iOS Safari */
701
+ -webkit-user-select: none; /* Safari */
702
+ -moz-user-select: none; /* Old versions of Firefox */
703
+ -ms-user-select: none; /* Internet Explorer/Edge */
704
+ user-select: none; /* current Chrome, Edge, Opera and Firefox */
705
+ }
706
+ .bar {
707
+ /* create a transparent border around the hist bars to prevent "gaps" and
708
+ flickering when moving around between bars. this also helps with handling
709
+ clicks on the bars, preventing users from being able to click in between
710
+ bars */
711
+ stroke: rgba(0, 0, 0, 0);
712
+ /* ensure transparent stroke wide enough to cover gap between bars */
713
+ stroke-width: 2px;
714
+ }
715
+ .bar:hover {
716
+ /* highlight currently hovered bar */
717
+ fill-opacity: 0.7;
718
+ }
719
+ .disabled .bar:hover {
720
+ /* ensure no visual hover interaction when disabled */
721
+ fill-opacity: 1;
722
+ }
723
+ /****** histogram ********/
724
+ #tooltip {
725
+ position: absolute;
726
+ background: ${tooltipBackgroundColor};
727
+ color: ${tooltipTextColor};
728
+ text-align: center;
729
+ border-radius: 3px;
730
+ padding: 2px;
731
+ font-size: ${tooltipFontSize};
732
+ font-family: ${tooltipFontFamily};
733
+ touch-action: none;
734
+ pointer-events: none;
735
+ }
736
+ #tooltip:after {
737
+ content: '';
738
+ position: absolute;
739
+ margin-left: -5px;
740
+ top: 100%;
741
+ /* arrow */
742
+ border: 5px solid ${tooltipTextColor};
743
+ border-color: ${tooltipBackgroundColor} transparent transparent
744
+ transparent;
745
+ }
746
+ /****** slider ********/
747
+ .draggable:hover {
748
+ cursor: grab;
749
+ }
750
+ .dragging {
751
+ cursor: grabbing !important;
752
+ }
753
+ /****** inputs ********/
754
+ #inputs {
755
+ display: flex;
756
+ justify-content: center;
757
+ margin: ${inputRowMargin};
758
+ }
759
+ #inputs .dash {
760
+ position: relative;
761
+ bottom: -1px;
762
+ align-self: center; /* Otherwise the dash sticks to the top while the inputs grow */
763
+ }
764
+ input {
765
+ width: ${inputWidth};
766
+ margin: 0 3px;
767
+ border: ${inputBorder};
768
+ border-radius: 2px !important;
769
+ text-align: center;
770
+ font-size: ${inputFontSize};
771
+ font-family: ${inputFontFamily};
772
+ }
773
+ .sr-only {
774
+ position: absolute !important;
775
+ width: 1px !important;
776
+ height: 1px !important;
777
+ margin: 0 !important;
778
+ padding: 0 !important;
779
+ border: 0 !important;
780
+ overflow: hidden !important;
781
+ white-space: nowrap !important;
782
+ clip: rect(1px, 1px, 1px, 1px) !important;
783
+ -webkit-clip-path: inset(50%) !important;
784
+ clip-path: inset(50%) !important;
785
+ }
786
+ `;
787
+ __decorate([
788
+ property({ type: Number })
789
+ ], HistogramDateRange.prototype, "width", void 0);
790
+ __decorate([
791
+ property({ type: Number })
792
+ ], HistogramDateRange.prototype, "height", void 0);
793
+ __decorate([
794
+ property({ type: Number })
795
+ ], HistogramDateRange.prototype, "sliderWidth", void 0);
796
+ __decorate([
797
+ property({ type: Number })
798
+ ], HistogramDateRange.prototype, "tooltipWidth", void 0);
799
+ __decorate([
800
+ property({ type: Number })
801
+ ], HistogramDateRange.prototype, "tooltipHeight", void 0);
802
+ __decorate([
803
+ property({ type: Number })
804
+ ], HistogramDateRange.prototype, "updateDelay", void 0);
805
+ __decorate([
806
+ property({ type: String })
807
+ ], HistogramDateRange.prototype, "dateFormat", void 0);
808
+ __decorate([
809
+ property({ type: String })
810
+ ], HistogramDateRange.prototype, "tooltipDateFormat", void 0);
811
+ __decorate([
812
+ property({ type: String })
813
+ ], HistogramDateRange.prototype, "missingDataMessage", void 0);
814
+ __decorate([
815
+ property({ type: String })
816
+ ], HistogramDateRange.prototype, "minDate", void 0);
817
+ __decorate([
818
+ property({ type: String })
819
+ ], HistogramDateRange.prototype, "maxDate", void 0);
820
+ __decorate([
821
+ property({ type: Boolean })
822
+ ], HistogramDateRange.prototype, "disabled", void 0);
823
+ __decorate([
824
+ property({ type: Array })
825
+ ], HistogramDateRange.prototype, "bins", void 0);
826
+ __decorate([
827
+ property({ type: Boolean })
828
+ ], HistogramDateRange.prototype, "updateWhileFocused", void 0);
829
+ __decorate([
830
+ state()
831
+ ], HistogramDateRange.prototype, "_tooltipOffset", void 0);
832
+ __decorate([
833
+ state()
834
+ ], HistogramDateRange.prototype, "_tooltipContent", void 0);
835
+ __decorate([
836
+ state()
837
+ ], HistogramDateRange.prototype, "_tooltipVisible", void 0);
838
+ __decorate([
839
+ state()
840
+ ], HistogramDateRange.prototype, "_isDragging", void 0);
841
+ __decorate([
842
+ state()
843
+ ], HistogramDateRange.prototype, "_isLoading", void 0);
844
+ __decorate([
845
+ property({ type: Boolean })
846
+ ], HistogramDateRange.prototype, "loading", null);
847
+ __decorate([
848
+ property()
849
+ ], HistogramDateRange.prototype, "minSelectedDate", null);
850
+ __decorate([
851
+ property()
852
+ ], HistogramDateRange.prototype, "maxSelectedDate", null);
853
+ HistogramDateRange = __decorate([
854
+ customElement('histogram-date-range')
855
+ ], HistogramDateRange);
856
+ export { HistogramDateRange };
847
857
  //# sourceMappingURL=histogram-date-range.js.map