@internetarchive/histogram-date-range 0.1.0 → 0.1.1-alpha
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.
- package/demo/index.css +23 -0
- package/demo/index.html +37 -31
- package/dist/src/histogram-date-range.d.ts +7 -5
- package/dist/src/histogram-date-range.js +86 -47
- package/dist/src/histogram-date-range.js.map +1 -1
- package/dist/test/histogram-date-range.test.js +27 -9
- package/dist/test/histogram-date-range.test.js.map +1 -1
- package/docs/_snowpack/pkg/common/lit-html-bb3fcd20.js +8 -0
- package/docs/_snowpack/pkg/dayjs/esm/plugin/customParseFormat.js +327 -0
- package/docs/_snowpack/pkg/import-map.json +1 -0
- package/docs/_snowpack/pkg/lit/decorators.js +8 -2
- package/docs/_snowpack/pkg/lit/directives/live.js +4 -4
- package/docs/_snowpack/pkg/lit.js +6 -6
- package/docs/demo/index.css +23 -0
- package/docs/demo/index.html +37 -31
- package/docs/dist/src/histogram-date-range.js +69 -40
- package/package.json +3 -3
- package/src/histogram-date-range.ts +108 -53
- package/test/histogram-date-range.test.ts +33 -9
- package/docs/_snowpack/pkg/common/lit-html-e67c9f49.js +0 -8
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
import {property, state, customElement} from "../../_snowpack/pkg/lit/decorators.js";
|
|
20
20
|
import {live} from "../../_snowpack/pkg/lit/directives/live.js";
|
|
21
21
|
import dayjs from "../../_snowpack/pkg/dayjs/esm/index.js";
|
|
22
|
+
import customParseFormat from "../../_snowpack/pkg/dayjs/esm/plugin/customParseFormat.js";
|
|
23
|
+
dayjs.extend(customParseFormat);
|
|
22
24
|
import "../../_snowpack/pkg/@internetarchive/ia-activity-indicator/ia-activity-indicator.js";
|
|
23
25
|
const WIDTH = 180;
|
|
24
26
|
const HEIGHT = 40;
|
|
@@ -90,11 +92,10 @@ export let HistogramDateRange = class extends LitElement {
|
|
|
90
92
|
this.move = (e) => {
|
|
91
93
|
const newX = e.offsetX - this._dragOffset;
|
|
92
94
|
const slider = this._currentSlider;
|
|
93
|
-
const date = this.translatePositionToDate(newX);
|
|
94
95
|
if (slider.id === "slider-min") {
|
|
95
|
-
this.minSelectedDate =
|
|
96
|
+
this.minSelectedDate = this.translatePositionToDate(this.validMinSliderX(newX));
|
|
96
97
|
} else {
|
|
97
|
-
this.maxSelectedDate =
|
|
98
|
+
this.maxSelectedDate = this.translatePositionToDate(this.validMaxSliderX(newX));
|
|
98
99
|
}
|
|
99
100
|
};
|
|
100
101
|
}
|
|
@@ -112,8 +113,8 @@ export let HistogramDateRange = class extends LitElement {
|
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
115
|
this._histWidth = this.width - this.sliderWidth * 2;
|
|
115
|
-
this._minDateMS =
|
|
116
|
-
this._maxDateMS =
|
|
116
|
+
this._minDateMS = this.getMSFromString(this.minDate);
|
|
117
|
+
this._maxDateMS = this.getMSFromString(this.maxDate);
|
|
117
118
|
this._binWidth = this._histWidth / this._numBins;
|
|
118
119
|
this._previousDateRange = this.currentDateRangeString;
|
|
119
120
|
this._histData = this.calculateHistData();
|
|
@@ -124,7 +125,8 @@ export let HistogramDateRange = class extends LitElement {
|
|
|
124
125
|
calculateHistData() {
|
|
125
126
|
const minValue = Math.min(...this.bins);
|
|
126
127
|
const maxValue = Math.max(...this.bins);
|
|
127
|
-
const
|
|
128
|
+
const valueRange = minValue === maxValue ? 1 : Math.log1p(maxValue - minValue);
|
|
129
|
+
const valueScale = this.height / valueRange;
|
|
128
130
|
const dateScale = this.dateRangeMS / this._numBins;
|
|
129
131
|
return this.bins.map((v, i) => {
|
|
130
132
|
return {
|
|
@@ -152,38 +154,42 @@ export let HistogramDateRange = class extends LitElement {
|
|
|
152
154
|
this._isLoading = value;
|
|
153
155
|
}
|
|
154
156
|
get minSelectedDate() {
|
|
155
|
-
return this.formatDate(this._minSelectedDate);
|
|
157
|
+
return this.formatDate(this.getMSFromString(this._minSelectedDate));
|
|
156
158
|
}
|
|
157
159
|
set minSelectedDate(rawDate) {
|
|
158
160
|
if (!this._minSelectedDate) {
|
|
159
161
|
this._minSelectedDate = rawDate;
|
|
162
|
+
return;
|
|
160
163
|
}
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
this._minSelectedDate = this.
|
|
164
|
+
let ms = this.getMSFromString(rawDate);
|
|
165
|
+
if (!Number.isNaN(ms)) {
|
|
166
|
+
ms = Math.min(ms, this.getMSFromString(this._maxSelectedDate));
|
|
167
|
+
this._minSelectedDate = this.formatDate(ms);
|
|
165
168
|
}
|
|
166
169
|
this.requestUpdate();
|
|
167
170
|
}
|
|
168
171
|
get maxSelectedDate() {
|
|
169
|
-
return this.formatDate(this._maxSelectedDate);
|
|
172
|
+
return this.formatDate(this.getMSFromString(this._maxSelectedDate));
|
|
170
173
|
}
|
|
171
174
|
set maxSelectedDate(rawDate) {
|
|
172
175
|
if (!this._maxSelectedDate) {
|
|
173
176
|
this._maxSelectedDate = rawDate;
|
|
177
|
+
return;
|
|
174
178
|
}
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
this._maxSelectedDate = this.
|
|
179
|
+
let ms = this.getMSFromString(rawDate);
|
|
180
|
+
if (!Number.isNaN(ms)) {
|
|
181
|
+
ms = Math.max(this.getMSFromString(this._minSelectedDate), ms);
|
|
182
|
+
this._maxSelectedDate = this.formatDate(ms);
|
|
179
183
|
}
|
|
180
184
|
this.requestUpdate();
|
|
181
185
|
}
|
|
182
186
|
get minSliderX() {
|
|
183
|
-
|
|
187
|
+
const x = this.translateDateToPosition(this.minSelectedDate);
|
|
188
|
+
return this.validMinSliderX(x);
|
|
184
189
|
}
|
|
185
190
|
get maxSliderX() {
|
|
186
|
-
|
|
191
|
+
const x = this.translateDateToPosition(this.maxSelectedDate);
|
|
192
|
+
return this.validMaxSliderX(x);
|
|
187
193
|
}
|
|
188
194
|
get dateRangeMS() {
|
|
189
195
|
return this._maxDateMS - this._minDateMS;
|
|
@@ -196,9 +202,10 @@ export let HistogramDateRange = class extends LitElement {
|
|
|
196
202
|
const x = target.x.baseVal.value + this.sliderWidth / 2;
|
|
197
203
|
const dataset = target.dataset;
|
|
198
204
|
const itemsText = `item${dataset.numItems !== "1" ? "s" : ""}`;
|
|
205
|
+
const formattedNumItems = Number(dataset.numItems).toLocaleString();
|
|
199
206
|
this._tooltipOffset = x + (this._binWidth - this.sliderWidth - this.tooltipWidth) / 2;
|
|
200
207
|
this._tooltipContent = html`
|
|
201
|
-
${
|
|
208
|
+
${formattedNumItems} ${itemsText}<br />
|
|
202
209
|
${dataset.binStart} - ${dataset.binEnd}
|
|
203
210
|
`;
|
|
204
211
|
this._tooltipVisible = true;
|
|
@@ -208,12 +215,16 @@ export let HistogramDateRange = class extends LitElement {
|
|
|
208
215
|
this._tooltipVisible = false;
|
|
209
216
|
}
|
|
210
217
|
validMinSliderX(newX) {
|
|
211
|
-
|
|
212
|
-
|
|
218
|
+
if (Number.isNaN(newX)) {
|
|
219
|
+
return this.sliderWidth;
|
|
220
|
+
}
|
|
221
|
+
return this.clamp(newX, this.sliderWidth, this.translateDateToPosition(this.maxSelectedDate));
|
|
213
222
|
}
|
|
214
223
|
validMaxSliderX(newX) {
|
|
215
|
-
|
|
216
|
-
|
|
224
|
+
if (Number.isNaN(newX)) {
|
|
225
|
+
return this.width - this.sliderWidth;
|
|
226
|
+
}
|
|
227
|
+
return this.clamp(newX, this.translateDateToPosition(this.minSelectedDate), this.width - this.sliderWidth);
|
|
217
228
|
}
|
|
218
229
|
addListeners() {
|
|
219
230
|
window.addEventListener("pointermove", this.move);
|
|
@@ -263,9 +274,11 @@ export let HistogramDateRange = class extends LitElement {
|
|
|
263
274
|
return this.formatDate(this._minDateMS + milliseconds);
|
|
264
275
|
}
|
|
265
276
|
translateDateToPosition(date) {
|
|
266
|
-
const milliseconds =
|
|
267
|
-
|
|
268
|
-
|
|
277
|
+
const milliseconds = this.getMSFromString(date);
|
|
278
|
+
return this.sliderWidth + (milliseconds - this._minDateMS) * this._histWidth / this.dateRangeMS;
|
|
279
|
+
}
|
|
280
|
+
clamp(x, minValue, maxValue) {
|
|
281
|
+
return Math.min(Math.max(x, minValue), maxValue);
|
|
269
282
|
}
|
|
270
283
|
handleMinDateInput(e) {
|
|
271
284
|
const target = e.currentTarget;
|
|
@@ -277,23 +290,31 @@ export let HistogramDateRange = class extends LitElement {
|
|
|
277
290
|
this.maxSelectedDate = target.value;
|
|
278
291
|
this.beginEmitUpdateProcess();
|
|
279
292
|
}
|
|
293
|
+
handleKeyUp(e) {
|
|
294
|
+
if (e.key === "Enter") {
|
|
295
|
+
const target = e.currentTarget;
|
|
296
|
+
target.blur();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
280
299
|
get currentDateRangeString() {
|
|
281
300
|
return `${this.minSelectedDate}:${this.maxSelectedDate}`;
|
|
282
301
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
302
|
+
getMSFromString(date) {
|
|
303
|
+
const digitGroupCount = (date.split(/(\d+)/).length - 1) / 2;
|
|
304
|
+
if (digitGroupCount === 1) {
|
|
305
|
+
const dateObj = new Date(0, 0);
|
|
306
|
+
dateObj.setFullYear(Number(date));
|
|
307
|
+
return dateObj.getTime();
|
|
308
|
+
}
|
|
309
|
+
return dayjs(date, [this.dateFormat, DATE_FORMAT]).valueOf();
|
|
288
310
|
}
|
|
289
311
|
handleBarClick(e) {
|
|
290
312
|
const dataset = e.currentTarget.dataset;
|
|
291
|
-
const
|
|
292
|
-
|
|
313
|
+
const distanceFromMinSlider = this.getMSFromString(dataset.binStart) - this.getMSFromString(this.minSelectedDate);
|
|
314
|
+
const distanceFromMaxSlider = this.getMSFromString(this.maxSelectedDate) - this.getMSFromString(dataset.binEnd);
|
|
315
|
+
if (distanceFromMinSlider < distanceFromMaxSlider) {
|
|
293
316
|
this.minSelectedDate = dataset.binStart;
|
|
294
|
-
}
|
|
295
|
-
const binEndDateMS = dayjs(dataset.binEnd).valueOf();
|
|
296
|
-
if (binEndDateMS > this.maxSelectedDateMS) {
|
|
317
|
+
} else {
|
|
297
318
|
this.maxSelectedDate = dataset.binEnd;
|
|
298
319
|
}
|
|
299
320
|
this.beginEmitUpdateProcess();
|
|
@@ -376,7 +397,7 @@ export let HistogramDateRange = class extends LitElement {
|
|
|
376
397
|
@pointerenter="${this.showTooltip}"
|
|
377
398
|
@pointerleave="${this.hideTooltip}"
|
|
378
399
|
@click="${this.handleBarClick}"
|
|
379
|
-
fill="${x >= this.minSliderX && x <= this.maxSliderX ? barIncludedFill : barExcludedFill}"
|
|
400
|
+
fill="${x + barWidth >= this.minSliderX && x <= this.maxSliderX ? barIncludedFill : barExcludedFill}"
|
|
380
401
|
data-num-items="${data.value}"
|
|
381
402
|
data-bin-start="${data.binStart}"
|
|
382
403
|
data-bin-end="${data.binEnd}"
|
|
@@ -385,9 +406,15 @@ export let HistogramDateRange = class extends LitElement {
|
|
|
385
406
|
return bar;
|
|
386
407
|
});
|
|
387
408
|
}
|
|
388
|
-
formatDate(
|
|
389
|
-
|
|
390
|
-
|
|
409
|
+
formatDate(dateMS) {
|
|
410
|
+
if (Number.isNaN(dateMS)) {
|
|
411
|
+
return "";
|
|
412
|
+
}
|
|
413
|
+
const date = dayjs(dateMS);
|
|
414
|
+
if (date.year() < 1e3) {
|
|
415
|
+
return String(date.year());
|
|
416
|
+
}
|
|
417
|
+
return date.format(this.dateFormat);
|
|
391
418
|
}
|
|
392
419
|
get minInputTemplate() {
|
|
393
420
|
return html`
|
|
@@ -397,6 +424,7 @@ export let HistogramDateRange = class extends LitElement {
|
|
|
397
424
|
type="text"
|
|
398
425
|
@focus="${this.cancelPendingUpdateEvent}"
|
|
399
426
|
@blur="${this.handleMinDateInput}"
|
|
427
|
+
@keyup="${this.handleKeyUp}"
|
|
400
428
|
.value="${live(this.minSelectedDate)}"
|
|
401
429
|
?disabled="${this.disabled}"
|
|
402
430
|
/>
|
|
@@ -410,6 +438,7 @@ export let HistogramDateRange = class extends LitElement {
|
|
|
410
438
|
type="text"
|
|
411
439
|
@focus="${this.cancelPendingUpdateEvent}"
|
|
412
440
|
@blur="${this.handleMaxDateInput}"
|
|
441
|
+
@keyup="${this.handleKeyUp}"
|
|
413
442
|
.value="${live(this.maxSelectedDate)}"
|
|
414
443
|
?disabled="${this.disabled}"
|
|
415
444
|
/>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@internetarchive/histogram-date-range",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1-alpha",
|
|
4
4
|
"description": "Internet Archive histogram date range picker",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
"prepare": "npm run build",
|
|
17
17
|
"lint": "eslint --ext .ts,.html . --ignore-path .gitignore && prettier \"**/*.ts\" --check --ignore-path .gitignore",
|
|
18
18
|
"format": "eslint --ext .ts,.html . --fix --ignore-path .gitignore && prettier \"**/*.ts\" --write --ignore-path .gitignore",
|
|
19
|
-
"test": "tsc && wtr
|
|
19
|
+
"test": "tsc && wtr",
|
|
20
20
|
"test:watch": "tsc && concurrently -k -r \"tsc --watch --preserveWatchOutput\" \"wtr --watch\""
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"dayjs": "^1.9.7",
|
|
24
|
-
"lit": "^2.
|
|
24
|
+
"lit": "^2.1.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@internetarchive/ia-activity-indicator": "^0.0.1",
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
import { property, state, customElement } from 'lit/decorators.js';
|
|
12
12
|
import { live } from 'lit/directives/live.js';
|
|
13
13
|
import dayjs from 'dayjs/esm/index.js';
|
|
14
|
+
import customParseFormat from 'dayjs/esm/plugin/customParseFormat';
|
|
15
|
+
dayjs.extend(customParseFormat);
|
|
14
16
|
import '@internetarchive/ia-activity-indicator/ia-activity-indicator';
|
|
15
17
|
|
|
16
18
|
// these values can be overridden via the component's HTML (camelCased) attributes
|
|
@@ -126,8 +128,8 @@ export class HistogramDateRange extends LitElement {
|
|
|
126
128
|
return;
|
|
127
129
|
}
|
|
128
130
|
this._histWidth = this.width - this.sliderWidth * 2;
|
|
129
|
-
this._minDateMS =
|
|
130
|
-
this._maxDateMS =
|
|
131
|
+
this._minDateMS = this.getMSFromString(this.minDate);
|
|
132
|
+
this._maxDateMS = this.getMSFromString(this.maxDate);
|
|
131
133
|
this._binWidth = this._histWidth / this._numBins;
|
|
132
134
|
this._previousDateRange = this.currentDateRangeString;
|
|
133
135
|
this._histData = this.calculateHistData();
|
|
@@ -143,11 +145,17 @@ export class HistogramDateRange extends LitElement {
|
|
|
143
145
|
private calculateHistData(): HistogramItem[] {
|
|
144
146
|
const minValue = Math.min(...this.bins);
|
|
145
147
|
const maxValue = Math.max(...this.bins);
|
|
146
|
-
|
|
148
|
+
// if there is no difference between the min and max values, use a range of
|
|
149
|
+
// 1 because log scaling will fail if the range is 0
|
|
150
|
+
const valueRange =
|
|
151
|
+
minValue === maxValue ? 1 : Math.log1p(maxValue - minValue);
|
|
152
|
+
const valueScale = this.height / valueRange;
|
|
147
153
|
const dateScale = this.dateRangeMS / this._numBins;
|
|
148
154
|
return this.bins.map((v: number, i: number) => {
|
|
149
155
|
return {
|
|
150
156
|
value: v,
|
|
157
|
+
// use log scaling for the height of the bar to prevent tall bars from
|
|
158
|
+
// making the smaller ones too small to see
|
|
151
159
|
height: Math.floor(Math.log1p(v) * valueScale),
|
|
152
160
|
binStart: `${this.formatDate(i * dateScale + this._minDateMS)}`,
|
|
153
161
|
binEnd: `${this.formatDate((i + 1) * dateScale + this._minDateMS)}`,
|
|
@@ -178,56 +186,60 @@ export class HistogramDateRange extends LitElement {
|
|
|
178
186
|
|
|
179
187
|
/** formatted minimum date of selected date range */
|
|
180
188
|
@property() get minSelectedDate(): string {
|
|
181
|
-
return this.formatDate(this._minSelectedDate);
|
|
189
|
+
return this.formatDate(this.getMSFromString(this._minSelectedDate));
|
|
182
190
|
}
|
|
183
191
|
|
|
192
|
+
/** updates minSelectedDate if new date is valid */
|
|
184
193
|
set minSelectedDate(rawDate: string) {
|
|
185
194
|
if (!this._minSelectedDate) {
|
|
186
195
|
// because the values needed to calculate valid max/min values are not
|
|
187
196
|
// available during the lit init when it's populating properties from
|
|
188
197
|
// attributes, fall back to just the raw date if nothing is already set
|
|
189
198
|
this._minSelectedDate = rawDate;
|
|
199
|
+
return;
|
|
190
200
|
}
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
this._minSelectedDate = this.
|
|
201
|
+
let ms = this.getMSFromString(rawDate);
|
|
202
|
+
if (!Number.isNaN(ms)) {
|
|
203
|
+
ms = Math.min(ms, this.getMSFromString(this._maxSelectedDate));
|
|
204
|
+
this._minSelectedDate = this.formatDate(ms);
|
|
195
205
|
}
|
|
196
206
|
this.requestUpdate();
|
|
197
207
|
}
|
|
198
208
|
|
|
199
209
|
/** formatted maximum date of selected date range */
|
|
200
210
|
@property() get maxSelectedDate(): string {
|
|
201
|
-
return this.formatDate(this._maxSelectedDate);
|
|
211
|
+
return this.formatDate(this.getMSFromString(this._maxSelectedDate));
|
|
202
212
|
}
|
|
203
213
|
|
|
214
|
+
/** updates maxSelectedDate if new date is valid */
|
|
204
215
|
set maxSelectedDate(rawDate: string) {
|
|
205
216
|
if (!this._maxSelectedDate) {
|
|
206
|
-
//
|
|
217
|
+
// because the values needed to calculate valid max/min values are not
|
|
218
|
+
// available during the lit init when it's populating properties from
|
|
219
|
+
// attributes, fall back to just the raw date if nothing is already set
|
|
207
220
|
this._maxSelectedDate = rawDate;
|
|
221
|
+
return;
|
|
208
222
|
}
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
this._maxSelectedDate = this.
|
|
223
|
+
let ms = this.getMSFromString(rawDate);
|
|
224
|
+
if (!Number.isNaN(ms)) {
|
|
225
|
+
ms = Math.max(this.getMSFromString(this._minSelectedDate), ms);
|
|
226
|
+
this._maxSelectedDate = this.formatDate(ms);
|
|
213
227
|
}
|
|
214
228
|
this.requestUpdate();
|
|
215
229
|
}
|
|
230
|
+
|
|
216
231
|
/** horizontal position of min date slider */
|
|
217
232
|
get minSliderX(): number {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
);
|
|
233
|
+
// default to leftmost position if missing or invalid min position
|
|
234
|
+
const x = this.translateDateToPosition(this.minSelectedDate);
|
|
235
|
+
return this.validMinSliderX(x);
|
|
222
236
|
}
|
|
223
237
|
|
|
224
238
|
/** horizontal position of max date slider */
|
|
225
239
|
get maxSliderX(): number {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
this.width - this.sliderWidth
|
|
230
|
-
);
|
|
240
|
+
// default to rightmost position if missing or invalid max position
|
|
241
|
+
const x = this.translateDateToPosition(this.maxSelectedDate);
|
|
242
|
+
return this.validMaxSliderX(x);
|
|
231
243
|
}
|
|
232
244
|
|
|
233
245
|
private get dateRangeMS(): number {
|
|
@@ -242,12 +254,13 @@ export class HistogramDateRange extends LitElement {
|
|
|
242
254
|
const x = target.x.baseVal.value + this.sliderWidth / 2;
|
|
243
255
|
const dataset = target.dataset as BarDataset;
|
|
244
256
|
const itemsText = `item${dataset.numItems !== '1' ? 's' : ''}`;
|
|
257
|
+
const formattedNumItems = Number(dataset.numItems).toLocaleString();
|
|
245
258
|
|
|
246
259
|
this._tooltipOffset =
|
|
247
260
|
x + (this._binWidth - this.sliderWidth - this.tooltipWidth) / 2;
|
|
248
261
|
|
|
249
262
|
this._tooltipContent = html`
|
|
250
|
-
${
|
|
263
|
+
${formattedNumItems} ${itemsText}<br />
|
|
251
264
|
${dataset.binStart} - ${dataset.binEnd}
|
|
252
265
|
`;
|
|
253
266
|
this._tooltipVisible = true;
|
|
@@ -290,11 +303,14 @@ export class HistogramDateRange extends LitElement {
|
|
|
290
303
|
private move = (e: PointerEvent): void => {
|
|
291
304
|
const newX = e.offsetX - this._dragOffset;
|
|
292
305
|
const slider = this._currentSlider as SVGRectElement;
|
|
293
|
-
const date = this.translatePositionToDate(newX);
|
|
294
306
|
if ((slider.id as SliderId) === 'slider-min') {
|
|
295
|
-
this.minSelectedDate =
|
|
307
|
+
this.minSelectedDate = this.translatePositionToDate(
|
|
308
|
+
this.validMinSliderX(newX)
|
|
309
|
+
);
|
|
296
310
|
} else {
|
|
297
|
-
this.maxSelectedDate =
|
|
311
|
+
this.maxSelectedDate = this.translatePositionToDate(
|
|
312
|
+
this.validMaxSliderX(newX)
|
|
313
|
+
);
|
|
298
314
|
}
|
|
299
315
|
};
|
|
300
316
|
|
|
@@ -307,8 +323,14 @@ export class HistogramDateRange extends LitElement {
|
|
|
307
323
|
* to the position of the max slider
|
|
308
324
|
*/
|
|
309
325
|
private validMinSliderX(newX: number): number {
|
|
310
|
-
|
|
311
|
-
|
|
326
|
+
if (Number.isNaN(newX)) {
|
|
327
|
+
return this.sliderWidth;
|
|
328
|
+
}
|
|
329
|
+
return this.clamp(
|
|
330
|
+
newX,
|
|
331
|
+
this.sliderWidth,
|
|
332
|
+
this.translateDateToPosition(this.maxSelectedDate)
|
|
333
|
+
);
|
|
312
334
|
}
|
|
313
335
|
|
|
314
336
|
/**
|
|
@@ -320,8 +342,14 @@ export class HistogramDateRange extends LitElement {
|
|
|
320
342
|
* then set it to the position of the min slider
|
|
321
343
|
*/
|
|
322
344
|
private validMaxSliderX(newX: number): number {
|
|
323
|
-
|
|
324
|
-
|
|
345
|
+
if (Number.isNaN(newX)) {
|
|
346
|
+
return this.width - this.sliderWidth;
|
|
347
|
+
}
|
|
348
|
+
return this.clamp(
|
|
349
|
+
newX,
|
|
350
|
+
this.translateDateToPosition(this.minSelectedDate),
|
|
351
|
+
this.width - this.sliderWidth
|
|
352
|
+
);
|
|
325
353
|
}
|
|
326
354
|
|
|
327
355
|
private addListeners(): void {
|
|
@@ -409,12 +437,17 @@ export class HistogramDateRange extends LitElement {
|
|
|
409
437
|
* @param date
|
|
410
438
|
* @returns x-position of slider
|
|
411
439
|
*/
|
|
412
|
-
private translateDateToPosition(date: string): number
|
|
413
|
-
const milliseconds =
|
|
414
|
-
|
|
440
|
+
private translateDateToPosition(date: string): number {
|
|
441
|
+
const milliseconds = this.getMSFromString(date);
|
|
442
|
+
return (
|
|
415
443
|
this.sliderWidth +
|
|
416
|
-
((milliseconds - this._minDateMS) * this._histWidth) / this.dateRangeMS
|
|
417
|
-
|
|
444
|
+
((milliseconds - this._minDateMS) * this._histWidth) / this.dateRangeMS
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** ensure that the returned value is between minValue and maxValue */
|
|
449
|
+
private clamp(x: number, minValue: number, maxValue: number): number {
|
|
450
|
+
return Math.min(Math.max(x, minValue), maxValue);
|
|
418
451
|
}
|
|
419
452
|
|
|
420
453
|
private handleMinDateInput(e: InputEvent): void {
|
|
@@ -429,28 +462,40 @@ export class HistogramDateRange extends LitElement {
|
|
|
429
462
|
this.beginEmitUpdateProcess();
|
|
430
463
|
}
|
|
431
464
|
|
|
432
|
-
private
|
|
433
|
-
|
|
465
|
+
private handleKeyUp(e: KeyboardEvent): void {
|
|
466
|
+
if (e.key === 'Enter') {
|
|
467
|
+
const target = e.currentTarget as HTMLInputElement;
|
|
468
|
+
target.blur();
|
|
469
|
+
}
|
|
434
470
|
}
|
|
435
471
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
return dayjs(this.minSelectedDate).valueOf();
|
|
472
|
+
private get currentDateRangeString(): string {
|
|
473
|
+
return `${this.minSelectedDate}:${this.maxSelectedDate}`;
|
|
439
474
|
}
|
|
440
475
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
476
|
+
private getMSFromString(date: string): number {
|
|
477
|
+
const digitGroupCount = (date.split(/(\d+)/).length - 1) / 2;
|
|
478
|
+
if (digitGroupCount === 1) {
|
|
479
|
+
// if there's just a single set of digits, assume it's a year
|
|
480
|
+
const dateObj = new Date(0, 0); // start at January 1, 1900
|
|
481
|
+
dateObj.setFullYear(Number(date)); // override year (=> 0099-01-01) = 99 CE
|
|
482
|
+
return dateObj.getTime(); // get time in milliseconds
|
|
483
|
+
}
|
|
484
|
+
return dayjs(date, [this.dateFormat, DATE_FORMAT]).valueOf();
|
|
444
485
|
}
|
|
445
486
|
|
|
446
487
|
private handleBarClick(e: InputEvent): void {
|
|
447
488
|
const dataset = (e.currentTarget as SVGRectElement).dataset as BarDataset;
|
|
448
|
-
const
|
|
449
|
-
|
|
489
|
+
const distanceFromMinSlider =
|
|
490
|
+
this.getMSFromString(dataset.binStart) -
|
|
491
|
+
this.getMSFromString(this.minSelectedDate);
|
|
492
|
+
const distanceFromMaxSlider =
|
|
493
|
+
this.getMSFromString(this.maxSelectedDate) -
|
|
494
|
+
this.getMSFromString(dataset.binEnd);
|
|
495
|
+
// update the selection by moving the nearer slider
|
|
496
|
+
if (distanceFromMinSlider < distanceFromMaxSlider) {
|
|
450
497
|
this.minSelectedDate = dataset.binStart;
|
|
451
|
-
}
|
|
452
|
-
const binEndDateMS = dayjs(dataset.binEnd).valueOf();
|
|
453
|
-
if (binEndDateMS > this.maxSelectedDateMS) {
|
|
498
|
+
} else {
|
|
454
499
|
this.maxSelectedDate = dataset.binEnd;
|
|
455
500
|
}
|
|
456
501
|
this.beginEmitUpdateProcess();
|
|
@@ -561,7 +606,7 @@ export class HistogramDateRange extends LitElement {
|
|
|
561
606
|
@pointerleave="${this.hideTooltip}"
|
|
562
607
|
@click="${this.handleBarClick}"
|
|
563
608
|
fill="${
|
|
564
|
-
x >= this.minSliderX && x <= this.maxSliderX
|
|
609
|
+
x + barWidth >= this.minSliderX && x <= this.maxSliderX
|
|
565
610
|
? barIncludedFill
|
|
566
611
|
: barExcludedFill
|
|
567
612
|
}"
|
|
@@ -574,9 +619,17 @@ export class HistogramDateRange extends LitElement {
|
|
|
574
619
|
});
|
|
575
620
|
}
|
|
576
621
|
|
|
577
|
-
private formatDate(
|
|
578
|
-
|
|
579
|
-
|
|
622
|
+
private formatDate(dateMS: number): string {
|
|
623
|
+
if (Number.isNaN(dateMS)) {
|
|
624
|
+
return '';
|
|
625
|
+
}
|
|
626
|
+
const date = dayjs(dateMS);
|
|
627
|
+
if (date.year() < 1000) {
|
|
628
|
+
// years before 1000 don't play well with dayjs custom formatting, so fall
|
|
629
|
+
// back to displaying only the year
|
|
630
|
+
return String(date.year());
|
|
631
|
+
}
|
|
632
|
+
return date.format(this.dateFormat);
|
|
580
633
|
}
|
|
581
634
|
|
|
582
635
|
/**
|
|
@@ -593,6 +646,7 @@ export class HistogramDateRange extends LitElement {
|
|
|
593
646
|
type="text"
|
|
594
647
|
@focus="${this.cancelPendingUpdateEvent}"
|
|
595
648
|
@blur="${this.handleMinDateInput}"
|
|
649
|
+
@keyup="${this.handleKeyUp}"
|
|
596
650
|
.value="${live(this.minSelectedDate)}"
|
|
597
651
|
?disabled="${this.disabled}"
|
|
598
652
|
/>
|
|
@@ -607,6 +661,7 @@ export class HistogramDateRange extends LitElement {
|
|
|
607
661
|
type="text"
|
|
608
662
|
@focus="${this.cancelPendingUpdateEvent}"
|
|
609
663
|
@blur="${this.handleMaxDateInput}"
|
|
664
|
+
@keyup="${this.handleKeyUp}"
|
|
610
665
|
.value="${live(this.maxSelectedDate)}"
|
|
611
666
|
?disabled="${this.disabled}"
|
|
612
667
|
/>
|