@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 ADDED
@@ -0,0 +1,23 @@
1
+ html {
2
+ font-size: 10px;
3
+ font-family: sans-serif;
4
+ }
5
+ body {
6
+ background: white;
7
+ }
8
+ .container {
9
+ margin-top: 20px;
10
+ display: grid;
11
+ justify-content: center;
12
+ }
13
+ .description {
14
+ margin: 10px auto;
15
+ }
16
+ .received-events {
17
+ position: absolute;
18
+ top: 0;
19
+ }
20
+ button {
21
+ font-size: 100%;
22
+ margin: 10px auto;
23
+ }
package/demo/index.html CHANGED
@@ -3,40 +3,16 @@
3
3
  <head>
4
4
  <meta name="viewport" content="width=device-width, initial-scale=1" />
5
5
  <meta charset="utf-8" />
6
- <style>
7
- html {
8
- font-size: 10px;
9
- font-family: sans-serif;
10
- }
11
- body {
12
- background: white;
13
- }
14
- .container {
15
- margin-top: 20px;
16
- display: grid;
17
- justify-content: center;
18
- }
19
- .description {
20
- margin: 10px auto;
21
- }
22
- .received-events {
23
- position: absolute;
24
- top: 0;
25
- }
26
- button {
27
- font-size: 100%;
28
- margin: 10px auto;
29
- }
30
- </style>
6
+ <link rel="stylesheet" href="index.css">
31
7
  </head>
32
8
 
33
9
  <script type="module">
34
10
  import '../dist/src/histogram-date-range.js';
11
+ let eventCount = 0;
35
12
  // listen to events from the component and display the data received from them
36
13
  document.addEventListener('histogramDateRangeUpdated', e => {
37
- document.querySelector('.received-events').innerHTML = JSON.stringify(
38
- e.detail
39
- );
14
+ document.querySelector('.received-events').innerHTML =
15
+ ++eventCount + ': ' + JSON.stringify(e.detail);
40
16
  });
41
17
  </script>
42
18
  <body>
@@ -60,6 +36,27 @@
60
36
  ></histogram-date-range>
61
37
  </div>
62
38
 
39
+ <div class="container">
40
+ <div class="description">range spanning negative to positive years</div>
41
+ <histogram-date-range
42
+ mindate="-1050" maxdate="2200"
43
+ bins="[ 74, 67, 17, 66, 49, 93, 47, 61, 32, 46, 53, 2,
44
+ 13, 45, 28, 1, 8, 70, 37, 74, 67, 17, 66, 49, 93,
45
+ 47, 61, 70, 37, 74, 67, 17, 66, 49, 93, 47, 61, 32,
46
+ 32, 70, 37, 74, 67, 17, 66, 49, 93, 47, 61, 32
47
+ ]"
48
+ ></histogram-date-range>
49
+ </div>
50
+
51
+
52
+
53
+ <div class="container">
54
+ <div class="description">small year range and few bins</div>
55
+ <histogram-date-range width="175" tooltipwidth="120"
56
+ mindate="2008" maxdate="2016" bins="[76104,866978,1151617,986331,218672,107410,3324]">
57
+ </histogram-date-range>
58
+ </div>
59
+
63
60
  <div class="container">
64
61
  <div class="description">
65
62
  default range with custom styling and date format
@@ -77,8 +74,8 @@
77
74
  --histogramDateRangeTooltipFontSize: 1rem;
78
75
  --histogramDateRangeInputWidth: 85px;
79
76
  "
80
- minDate="May 1, 1972"
81
- maxDate="12/21/1980"
77
+ minDate="05 May 1972"
78
+ maxDate="21 Dec 1980"
82
79
  bins="[ 85, 25, 200, 0, 0, 34, 0, 2, 5, 10, 0, 56, 10, 45, 100, 70, 50 ]"
83
80
  ></histogram-date-range>
84
81
  </div>
@@ -123,9 +120,18 @@
123
120
  });
124
121
  </script>
125
122
 
123
+ <!-- <div class="container">
124
+ <div class="description">
125
+ single bin
126
+ </div>
127
+ <histogram-date-range mindate="1926" maxdate="1926" bins="[8]">
128
+ </histogram-date-range>
129
+ </div>
130
+
126
131
  <div class="container">
127
132
  <div class="description">empty data</div>
128
133
  <histogram-date-range missingDataMessage="no data..."></histoghistogram-date-range>
129
- </div>
134
+ </div> -->
135
+
130
136
  </body>
131
137
  </html>
@@ -47,9 +47,11 @@ export declare class HistogramDateRange extends LitElement {
47
47
  set loading(value: boolean);
48
48
  /** formatted minimum date of selected date range */
49
49
  get minSelectedDate(): string;
50
+ /** updates minSelectedDate if new date is valid */
50
51
  set minSelectedDate(rawDate: string);
51
52
  /** formatted maximum date of selected date range */
52
53
  get maxSelectedDate(): string;
54
+ /** updates maxSelectedDate if new date is valid */
53
55
  set maxSelectedDate(rawDate: string);
54
56
  /** horizontal position of min date slider */
55
57
  get minSliderX(): number;
@@ -110,13 +112,13 @@ export declare class HistogramDateRange extends LitElement {
110
112
  * @returns x-position of slider
111
113
  */
112
114
  private translateDateToPosition;
115
+ /** ensure that the returned value is between minValue and maxValue */
116
+ private clamp;
113
117
  private handleMinDateInput;
114
118
  private handleMaxDateInput;
119
+ private handleKeyUp;
115
120
  private get currentDateRangeString();
116
- /** minimum selected date in milliseconds */
117
- private get minSelectedDateMS();
118
- /** maximum selected date in milliseconds */
119
- private get maxSelectedDateMS();
121
+ private getMSFromString;
120
122
  private handleBarClick;
121
123
  private get minSliderTemplate();
122
124
  private get maxSliderTemplate();
@@ -135,7 +137,7 @@ export declare class HistogramDateRange extends LitElement {
135
137
  get tooltipTemplate(): TemplateResult;
136
138
  private get noDataTemplate();
137
139
  private get activityIndicatorTemplate();
138
- static styles: import("lit").CSSResultGroup;
140
+ static styles: import("lit").CSSResult;
139
141
  render(): TemplateResult;
140
142
  }
141
143
  declare global {
@@ -3,6 +3,8 @@ import { css, html, nothing, LitElement, svg, } from 'lit';
3
3
  import { property, state, customElement } from 'lit/decorators.js';
4
4
  import { live } from 'lit/directives/live.js';
5
5
  import dayjs from 'dayjs/esm/index.js';
6
+ import customParseFormat from 'dayjs/esm/plugin/customParseFormat';
7
+ dayjs.extend(customParseFormat);
6
8
  import '@internetarchive/ia-activity-indicator/ia-activity-indicator';
7
9
  // these values can be overridden via the component's HTML (camelCased) attributes
8
10
  const WIDTH = 180;
@@ -91,12 +93,11 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
91
93
  this.move = (e) => {
92
94
  const newX = e.offsetX - this._dragOffset;
93
95
  const slider = this._currentSlider;
94
- const date = this.translatePositionToDate(newX);
95
96
  if (slider.id === 'slider-min') {
96
- this.minSelectedDate = date;
97
+ this.minSelectedDate = this.translatePositionToDate(this.validMinSliderX(newX));
97
98
  }
98
99
  else {
99
- this.maxSelectedDate = date;
100
+ this.maxSelectedDate = this.translatePositionToDate(this.validMaxSliderX(newX));
100
101
  }
101
102
  };
102
103
  }
@@ -127,8 +128,8 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
127
128
  return;
128
129
  }
129
130
  this._histWidth = this.width - this.sliderWidth * 2;
130
- this._minDateMS = dayjs(this.minDate).valueOf();
131
- this._maxDateMS = dayjs(this.maxDate).valueOf();
131
+ this._minDateMS = this.getMSFromString(this.minDate);
132
+ this._maxDateMS = this.getMSFromString(this.maxDate);
132
133
  this._binWidth = this._histWidth / this._numBins;
133
134
  this._previousDateRange = this.currentDateRangeString;
134
135
  this._histData = this.calculateHistData();
@@ -143,11 +144,16 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
143
144
  calculateHistData() {
144
145
  const minValue = Math.min(...this.bins);
145
146
  const maxValue = Math.max(...this.bins);
146
- const valueScale = this.height / Math.log1p(maxValue - minValue);
147
+ // if there is no difference between the min and max values, use a range of
148
+ // 1 because log scaling will fail if the range is 0
149
+ const valueRange = minValue === maxValue ? 1 : Math.log1p(maxValue - minValue);
150
+ const valueScale = this.height / valueRange;
147
151
  const dateScale = this.dateRangeMS / this._numBins;
148
152
  return this.bins.map((v, i) => {
149
153
  return {
150
154
  value: v,
155
+ // use log scaling for the height of the bar to prevent tall bars from
156
+ // making the smaller ones too small to see
151
157
  height: Math.floor(Math.log1p(v) * valueScale),
152
158
  binStart: `${this.formatDate(i * dateScale + this._minDateMS)}`,
153
159
  binEnd: `${this.formatDate((i + 1) * dateScale + this._minDateMS)}`,
@@ -173,51 +179,55 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
173
179
  }
174
180
  /** formatted minimum date of selected date range */
175
181
  get minSelectedDate() {
176
- return this.formatDate(this._minSelectedDate);
182
+ return this.formatDate(this.getMSFromString(this._minSelectedDate));
177
183
  }
184
+ /** updates minSelectedDate if new date is valid */
178
185
  set minSelectedDate(rawDate) {
179
186
  if (!this._minSelectedDate) {
180
187
  // because the values needed to calculate valid max/min values are not
181
188
  // available during the lit init when it's populating properties from
182
189
  // attributes, fall back to just the raw date if nothing is already set
183
190
  this._minSelectedDate = rawDate;
191
+ return;
184
192
  }
185
- const x = this.translateDateToPosition(rawDate);
186
- if (x) {
187
- const validX = this.validMinSliderX(x);
188
- this._minSelectedDate = this.translatePositionToDate(validX);
193
+ let ms = this.getMSFromString(rawDate);
194
+ if (!Number.isNaN(ms)) {
195
+ ms = Math.min(ms, this.getMSFromString(this._maxSelectedDate));
196
+ this._minSelectedDate = this.formatDate(ms);
189
197
  }
190
198
  this.requestUpdate();
191
199
  }
192
200
  /** formatted maximum date of selected date range */
193
201
  get maxSelectedDate() {
194
- return this.formatDate(this._maxSelectedDate);
202
+ return this.formatDate(this.getMSFromString(this._maxSelectedDate));
195
203
  }
204
+ /** updates maxSelectedDate if new date is valid */
196
205
  set maxSelectedDate(rawDate) {
197
206
  if (!this._maxSelectedDate) {
198
- // see comment above in the minSelectedDate setter
207
+ // because the values needed to calculate valid max/min values are not
208
+ // available during the lit init when it's populating properties from
209
+ // attributes, fall back to just the raw date if nothing is already set
199
210
  this._maxSelectedDate = rawDate;
211
+ return;
200
212
  }
201
- const x = this.translateDateToPosition(rawDate);
202
- if (x) {
203
- const validX = this.validMaxSliderX(x);
204
- this._maxSelectedDate = this.translatePositionToDate(validX);
213
+ let ms = this.getMSFromString(rawDate);
214
+ if (!Number.isNaN(ms)) {
215
+ ms = Math.max(this.getMSFromString(this._minSelectedDate), ms);
216
+ this._maxSelectedDate = this.formatDate(ms);
205
217
  }
206
218
  this.requestUpdate();
207
219
  }
208
220
  /** horizontal position of min date slider */
209
221
  get minSliderX() {
210
- var _a;
211
- return (
212
222
  // default to leftmost position if missing or invalid min position
213
- (_a = this.translateDateToPosition(this.minSelectedDate)) !== null && _a !== void 0 ? _a : this.sliderWidth);
223
+ const x = this.translateDateToPosition(this.minSelectedDate);
224
+ return this.validMinSliderX(x);
214
225
  }
215
226
  /** horizontal position of max date slider */
216
227
  get maxSliderX() {
217
- var _a;
218
- return (
219
228
  // default to rightmost position if missing or invalid max position
220
- (_a = this.translateDateToPosition(this.maxSelectedDate)) !== null && _a !== void 0 ? _a : this.width - this.sliderWidth);
229
+ const x = this.translateDateToPosition(this.maxSelectedDate);
230
+ return this.validMaxSliderX(x);
221
231
  }
222
232
  get dateRangeMS() {
223
233
  return this._maxDateMS - this._minDateMS;
@@ -230,10 +240,11 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
230
240
  const x = target.x.baseVal.value + this.sliderWidth / 2;
231
241
  const dataset = target.dataset;
232
242
  const itemsText = `item${dataset.numItems !== '1' ? 's' : ''}`;
243
+ const formattedNumItems = Number(dataset.numItems).toLocaleString();
233
244
  this._tooltipOffset =
234
245
  x + (this._binWidth - this.sliderWidth - this.tooltipWidth) / 2;
235
246
  this._tooltipContent = html `
236
- ${dataset.numItems} ${itemsText}<br />
247
+ ${formattedNumItems} ${itemsText}<br />
237
248
  ${dataset.binStart} - ${dataset.binEnd}
238
249
  `;
239
250
  this._tooltipVisible = true;
@@ -251,8 +262,10 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
251
262
  * to the position of the max slider
252
263
  */
253
264
  validMinSliderX(newX) {
254
- const validX = Math.max(newX, this.sliderWidth);
255
- return Math.min(validX, this.maxSliderX);
265
+ if (Number.isNaN(newX)) {
266
+ return this.sliderWidth;
267
+ }
268
+ return this.clamp(newX, this.sliderWidth, this.translateDateToPosition(this.maxSelectedDate));
256
269
  }
257
270
  /**
258
271
  * Constrain a proposed value for the maximum (right) slider
@@ -263,8 +276,10 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
263
276
  * then set it to the position of the min slider
264
277
  */
265
278
  validMaxSliderX(newX) {
266
- const validX = Math.max(newX, this.minSliderX);
267
- return Math.min(validX, this.width - this.sliderWidth);
279
+ if (Number.isNaN(newX)) {
280
+ return this.width - this.sliderWidth;
281
+ }
282
+ return this.clamp(newX, this.translateDateToPosition(this.minSelectedDate), this.width - this.sliderWidth);
268
283
  }
269
284
  addListeners() {
270
285
  window.addEventListener('pointermove', this.move);
@@ -341,10 +356,13 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
341
356
  * @returns x-position of slider
342
357
  */
343
358
  translateDateToPosition(date) {
344
- const milliseconds = dayjs(date).valueOf();
345
- const xPosition = this.sliderWidth +
346
- ((milliseconds - this._minDateMS) * this._histWidth) / this.dateRangeMS;
347
- return isNaN(milliseconds) || isNaN(xPosition) ? null : xPosition;
359
+ const milliseconds = this.getMSFromString(date);
360
+ return (this.sliderWidth +
361
+ ((milliseconds - this._minDateMS) * this._histWidth) / this.dateRangeMS);
362
+ }
363
+ /** ensure that the returned value is between minValue and maxValue */
364
+ clamp(x, minValue, maxValue) {
365
+ return Math.min(Math.max(x, minValue), maxValue);
348
366
  }
349
367
  handleMinDateInput(e) {
350
368
  const target = e.currentTarget;
@@ -356,25 +374,36 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
356
374
  this.maxSelectedDate = target.value;
357
375
  this.beginEmitUpdateProcess();
358
376
  }
377
+ handleKeyUp(e) {
378
+ if (e.key === 'Enter') {
379
+ const target = e.currentTarget;
380
+ target.blur();
381
+ }
382
+ }
359
383
  get currentDateRangeString() {
360
384
  return `${this.minSelectedDate}:${this.maxSelectedDate}`;
361
385
  }
362
- /** minimum selected date in milliseconds */
363
- get minSelectedDateMS() {
364
- return dayjs(this.minSelectedDate).valueOf();
365
- }
366
- /** maximum selected date in milliseconds */
367
- get maxSelectedDateMS() {
368
- return dayjs(this.maxSelectedDate).valueOf();
386
+ getMSFromString(date) {
387
+ const digitGroupCount = (date.split(/(\d+)/).length - 1) / 2;
388
+ if (digitGroupCount === 1) {
389
+ // if there's just a single set of digits, assume it's a year
390
+ const dateObj = new Date(0, 0); // start at January 1, 1900
391
+ dateObj.setFullYear(Number(date)); // override year (=> 0099-01-01) = 99 CE
392
+ return dateObj.getTime(); // get time in milliseconds
393
+ }
394
+ return dayjs(date, [this.dateFormat, DATE_FORMAT]).valueOf();
369
395
  }
370
396
  handleBarClick(e) {
371
397
  const dataset = e.currentTarget.dataset;
372
- const binStartDateMS = dayjs(dataset.binStart).valueOf();
373
- if (binStartDateMS < this.minSelectedDateMS) {
398
+ const distanceFromMinSlider = this.getMSFromString(dataset.binStart) -
399
+ this.getMSFromString(this.minSelectedDate);
400
+ const distanceFromMaxSlider = this.getMSFromString(this.maxSelectedDate) -
401
+ this.getMSFromString(dataset.binEnd);
402
+ // update the selection by moving the nearer slider
403
+ if (distanceFromMinSlider < distanceFromMaxSlider) {
374
404
  this.minSelectedDate = dataset.binStart;
375
405
  }
376
- const binEndDateMS = dayjs(dataset.binEnd).valueOf();
377
- if (binEndDateMS > this.maxSelectedDateMS) {
406
+ else {
378
407
  this.maxSelectedDate = dataset.binEnd;
379
408
  }
380
409
  this.beginEmitUpdateProcess();
@@ -466,7 +495,7 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
466
495
  @pointerenter="${this.showTooltip}"
467
496
  @pointerleave="${this.hideTooltip}"
468
497
  @click="${this.handleBarClick}"
469
- fill="${x >= this.minSliderX && x <= this.maxSliderX
498
+ fill="${x + barWidth >= this.minSliderX && x <= this.maxSliderX
470
499
  ? barIncludedFill
471
500
  : barExcludedFill}"
472
501
  data-num-items="${data.value}"
@@ -477,9 +506,17 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
477
506
  return bar;
478
507
  });
479
508
  }
480
- formatDate(rawDate) {
481
- const date = dayjs(rawDate);
482
- return date.isValid() ? date.format(this.dateFormat) : '';
509
+ formatDate(dateMS) {
510
+ if (Number.isNaN(dateMS)) {
511
+ return '';
512
+ }
513
+ const date = dayjs(dateMS);
514
+ if (date.year() < 1000) {
515
+ // years before 1000 don't play well with dayjs custom formatting, so fall
516
+ // back to displaying only the year
517
+ return String(date.year());
518
+ }
519
+ return date.format(this.dateFormat);
483
520
  }
484
521
  /**
485
522
  * NOTE: we are relying on the lit `live` directive in the template to
@@ -495,6 +532,7 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
495
532
  type="text"
496
533
  @focus="${this.cancelPendingUpdateEvent}"
497
534
  @blur="${this.handleMinDateInput}"
535
+ @keyup="${this.handleKeyUp}"
498
536
  .value="${live(this.minSelectedDate)}"
499
537
  ?disabled="${this.disabled}"
500
538
  />
@@ -508,6 +546,7 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
508
546
  type="text"
509
547
  @focus="${this.cancelPendingUpdateEvent}"
510
548
  @blur="${this.handleMaxDateInput}"
549
+ @keyup="${this.handleKeyUp}"
511
550
  .value="${live(this.maxSelectedDate)}"
512
551
  ?disabled="${this.disabled}"
513
552
  />