@internetarchive/histogram-date-range 1.2.2-alpha-webdev7377.5 → 1.2.2-alpha-webdev7377.7

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.html CHANGED
@@ -79,6 +79,33 @@
79
79
  </histogram-date-range>
80
80
  </div>
81
81
 
82
+ <div class="container">
83
+ <div class="description">bins rounded to nearest month</div>
84
+ <histogram-date-range
85
+ width="175"
86
+ tooltipwidth="120"
87
+ dateFormat="YYYY-MM"
88
+ tooltipDateFormat="MMM YYYY"
89
+ binSnapping="month"
90
+ minDate="2009-05"
91
+ maxDate="2014-08"
92
+ bins="[100,5000,2000,100,5000,2000,100,5000,2000,100,5000,2000,100,5000,2000,100]"
93
+ style="--histogramDateRangeInputWidth: 50px;"
94
+ ></histogram-date-range>
95
+ </div>
96
+
97
+ <div class="container">
98
+ <div class="description">bins rounded to nearest year</div>
99
+ <histogram-date-range
100
+ width="175"
101
+ dateFormat="YYYY"
102
+ binSnapping="year"
103
+ minDate="2009"
104
+ maxDate="2014"
105
+ bins="[100,5000,2000,100,5000,2000]"
106
+ ></histogram-date-range>
107
+ </div>
108
+
82
109
  <div class="container">
83
110
  <div class="description">
84
111
  default range with custom styling and date format
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { HistogramDateRange } from './src/histogram-date-range';
1
+ export { HistogramDateRange, BinSnappingInterval as BinRoundingInterval, } from './src/histogram-date-range';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { HistogramDateRange } from './src/histogram-date-range';
1
+ export { HistogramDateRange, } from './src/histogram-date-range';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC","sourcesContent":["export { HistogramDateRange } from './src/histogram-date-range';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,GAEnB,MAAM,4BAA4B,CAAC","sourcesContent":["export {\n HistogramDateRange,\n BinSnappingInterval as BinRoundingInterval,\n} from './src/histogram-date-range';\n"]}
@@ -1,5 +1,6 @@
1
1
  import '@internetarchive/ia-activity-indicator';
2
2
  import { LitElement, PropertyValues, SVGTemplateResult, TemplateResult } from 'lit';
3
+ export declare type BinSnappingInterval = 'none' | 'month' | 'year';
3
4
  export declare class HistogramDateRange extends LitElement {
4
5
  width: number;
5
6
  height: number;
@@ -8,8 +9,6 @@ export declare class HistogramDateRange extends LitElement {
8
9
  tooltipHeight: number;
9
10
  updateDelay: number;
10
11
  dateFormat: string;
11
- /** Optional; falls back to `dateFormat` if not provided */
12
- tooltipDateFormat?: string;
13
12
  missingDataMessage: string;
14
13
  minDate: string;
15
14
  maxDate: string;
@@ -17,9 +16,12 @@ export declare class HistogramDateRange extends LitElement {
17
16
  bins: number[];
18
17
  /** If true, update events will not be canceled by the date inputs receiving focus */
19
18
  updateWhileFocused: boolean;
19
+ /** What interval bins should be rounded to for display */
20
+ binSnapping: BinSnappingInterval;
20
21
  private _tooltipOffset;
21
22
  private _tooltipContent?;
22
23
  private _tooltipVisible;
24
+ private _tooltipDateFormat?;
23
25
  private _isDragging;
24
26
  private _isLoading;
25
27
  private _minSelectedDate;
@@ -44,19 +46,45 @@ export declare class HistogramDateRange extends LitElement {
44
46
  */
45
47
  private handleDataUpdate;
46
48
  /**
47
- * Rounds the given timestamp to the nearest start of a new month
49
+ * Rounds the given timestamp to the next full second.
48
50
  */
49
- private roundToMonth;
51
+ private snapToNextSecond;
52
+ /**
53
+ * Rounds the given timestamp to the (approximate) nearest start of a month,
54
+ * such that dates up to and including the 15th of the month are rounded down,
55
+ * while dates past the 15th are rounded up.
56
+ */
57
+ private snapToMonth;
58
+ /**
59
+ * Rounds the given timestamp to the (approximate) nearest start of a year,
60
+ * such that dates up to the end of June are rounded down, while dates in
61
+ * July or later are rounded up.
62
+ */
63
+ private snapToYear;
64
+ /**
65
+ * Rounds the given timestamp according to the `binSnapping` property.
66
+ * Default is simply to snap to the nearest full second.
67
+ */
68
+ private snapTimestamp;
50
69
  private calculateHistData;
51
70
  private get hasBinData();
52
71
  private get _numBins();
53
72
  private get histogramLeftEdgeX();
54
73
  private get histogramRightEdgeX();
55
74
  /**
56
- * Returns the consumer-specified tooltip date format if set, falling back to
57
- * the general `dateFormat` property and then the default format if none is set.
75
+ * Approximate size in ms of the interval to which bins are snapped.
76
+ */
77
+ private get snapInterval();
78
+ /**
79
+ * Offset added to the end of each bin to ensure disjoin intervals, if applicable.
80
+ */
81
+ private get snapEndOffset();
82
+ /**
83
+ * Optional date format to use for tooltips only.
84
+ * Falls back to `dateFormat` if not provided.
58
85
  */
59
- private get resolvedTooltipDateFormat();
86
+ get tooltipDateFormat(): string;
87
+ set tooltipDateFormat(value: string);
60
88
  /** component's loading (and disabled) state */
61
89
  get loading(): boolean;
62
90
  set loading(value: boolean);
@@ -146,6 +174,10 @@ export declare class HistogramDateRange extends LitElement {
146
174
  private generateSliderSVG;
147
175
  get selectedRangeTemplate(): SVGTemplateResult;
148
176
  get histogramTemplate(): SVGTemplateResult[];
177
+ /** Whether the first arg represents a date strictly before the second arg */
178
+ private isBefore;
179
+ /** Whether the first arg represents a date strictly after the second arg */
180
+ private isAfter;
149
181
  private formatDate;
150
182
  /**
151
183
  * NOTE: we are relying on the lit `live` directive in the template to
@@ -51,6 +51,8 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
51
51
  this.bins = [];
52
52
  /** If true, update events will not be canceled by the date inputs receiving focus */
53
53
  this.updateWhileFocused = false;
54
+ /** What interval bins should be rounded to for display */
55
+ this.binSnapping = 'none';
54
56
  // internal reactive properties not exposed as attributes
55
57
  this._tooltipOffset = 0;
56
58
  this._tooltipVisible = false;
@@ -102,6 +104,9 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
102
104
  }
103
105
  else {
104
106
  this.maxSelectedDate = this.translatePositionToDate(this.validMaxSliderX(newX));
107
+ if (this.getMSFromString(this.maxSelectedDate) > this._maxDateMS) {
108
+ this.maxSelectedDate = this.maxDate;
109
+ }
105
110
  }
106
111
  };
107
112
  }
@@ -118,7 +123,8 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
118
123
  changedProps.has('minSelectedDate') ||
119
124
  changedProps.has('maxSelectedDate') ||
120
125
  changedProps.has('width') ||
121
- changedProps.has('height')) {
126
+ changedProps.has('height') ||
127
+ changedProps.has('binSnapping')) {
122
128
  this.handleDataUpdate();
123
129
  }
124
130
  }
@@ -134,8 +140,10 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
134
140
  return;
135
141
  }
136
142
  this._histWidth = this.width - this.sliderWidth * 2;
137
- this._minDateMS = this.getMSFromString(this.minDate);
138
- this._maxDateMS = this.getMSFromString(this.maxDate);
143
+ this._minDateMS = this.snapTimestamp(this.getMSFromString(this.minDate));
144
+ // NB: The max date string, converted as-is to ms, represents the _start_ of the final date interval; we want the _end_.
145
+ this._maxDateMS =
146
+ this.snapTimestamp(this.getMSFromString(this.maxDate) + this.snapInterval) + this.snapEndOffset;
139
147
  this._binWidth = this._histWidth / this._numBins;
140
148
  this._previousDateRange = this.currentDateRangeString;
141
149
  this._histData = this.calculateHistData();
@@ -147,14 +155,49 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
147
155
  : this.maxDate;
148
156
  }
149
157
  /**
150
- * Rounds the given timestamp to the nearest start of a new month
158
+ * Rounds the given timestamp to the next full second.
151
159
  */
152
- roundToMonth(timestamp) {
153
- const date = new Date(timestamp);
154
- const [y, m, d] = [date.getFullYear(), date.getMonth(), date.getDate()];
155
- return d < 15
156
- ? new Date(y, m, 1).getTime()
157
- : new Date(y, m + 1, 1).getTime();
160
+ snapToNextSecond(timestamp) {
161
+ return Math.ceil(timestamp / 1000) * 1000;
162
+ }
163
+ /**
164
+ * Rounds the given timestamp to the (approximate) nearest start of a month,
165
+ * such that dates up to and including the 15th of the month are rounded down,
166
+ * while dates past the 15th are rounded up.
167
+ */
168
+ snapToMonth(timestamp) {
169
+ const d = new Date(timestamp);
170
+ const [year, month, day] = [d.getFullYear(), d.getMonth(), d.getDate()];
171
+ return day < 16 // Obviously only an approximation, but good enough for snapping
172
+ ? new Date(year, month, 1).getTime()
173
+ : new Date(year, month + 1, 1).getTime();
174
+ }
175
+ /**
176
+ * Rounds the given timestamp to the (approximate) nearest start of a year,
177
+ * such that dates up to the end of June are rounded down, while dates in
178
+ * July or later are rounded up.
179
+ */
180
+ snapToYear(timestamp) {
181
+ const d = new Date(timestamp);
182
+ const [year, month] = [d.getFullYear(), d.getMonth()];
183
+ return month < 6 // NB: months are 0-indexed, so 6 = July
184
+ ? new Date(year, 0, 1).getTime()
185
+ : new Date(year + 1, 0, 1).getTime();
186
+ }
187
+ /**
188
+ * Rounds the given timestamp according to the `binSnapping` property.
189
+ * Default is simply to snap to the nearest full second.
190
+ */
191
+ snapTimestamp(timestamp) {
192
+ switch (this.binSnapping) {
193
+ case 'year':
194
+ return this.snapToYear(timestamp);
195
+ case 'month':
196
+ return this.snapToMonth(timestamp);
197
+ case 'none':
198
+ default:
199
+ return this.snapToNextSecond(timestamp);
200
+ }
158
201
  }
159
202
  calculateHistData() {
160
203
  const { bins, height, dateRangeMS, _numBins, _minDateMS } = this;
@@ -166,15 +209,25 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
166
209
  const valueScale = height / valueRange;
167
210
  const dateScale = dateRangeMS / _numBins;
168
211
  return bins.map((v, i) => {
169
- const binStartMS = this.roundToMonth(i * dateScale + _minDateMS);
170
- const binEndMS = this.roundToMonth((i + 1) * dateScale + _minDateMS) - 1;
212
+ const binStartMS = this.snapTimestamp(i * dateScale + _minDateMS);
213
+ const binStart = this.formatDate(binStartMS);
214
+ const binEndMS = this.snapTimestamp((i + 1) * dateScale + _minDateMS) +
215
+ this.snapEndOffset;
216
+ const binEnd = this.formatDate(binEndMS);
217
+ const tooltipStart = this.formatDate(binStartMS, this.tooltipDateFormat);
218
+ const tooltipEnd = this.formatDate(binEndMS, this.tooltipDateFormat);
219
+ // If start/end are the same, just render a single value
220
+ const tooltip = tooltipStart === tooltipEnd
221
+ ? tooltipStart
222
+ : `${tooltipStart} - ${tooltipEnd}`;
171
223
  return {
172
224
  value: v,
173
225
  // use log scaling for the height of the bar to prevent tall bars from
174
226
  // making the smaller ones too small to see
175
227
  height: Math.floor(Math.log1p(v) * valueScale),
176
- binStart: this.formatDate(binStartMS, this.resolvedTooltipDateFormat),
177
- binEnd: this.formatDate(binEndMS, this.resolvedTooltipDateFormat),
228
+ binStart,
229
+ binEnd,
230
+ tooltip,
178
231
  };
179
232
  });
180
233
  }
@@ -194,12 +247,37 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
194
247
  return this.width - this.sliderWidth;
195
248
  }
196
249
  /**
197
- * Returns the consumer-specified tooltip date format if set, falling back to
198
- * the general `dateFormat` property and then the default format if none is set.
250
+ * Approximate size in ms of the interval to which bins are snapped.
251
+ */
252
+ get snapInterval() {
253
+ const yearMS = 31536000000; // A 365-day approximation of ms in a year
254
+ const monthMS = 2592000000; // A 30-day approximation of ms in a month
255
+ switch (this.binSnapping) {
256
+ case 'year':
257
+ return yearMS;
258
+ case 'month':
259
+ return monthMS;
260
+ case 'none':
261
+ default:
262
+ return 0;
263
+ }
264
+ }
265
+ /**
266
+ * Offset added to the end of each bin to ensure disjoin intervals, if applicable.
199
267
  */
200
- get resolvedTooltipDateFormat() {
268
+ get snapEndOffset() {
269
+ return this.binSnapping !== 'none' && this._numBins > 1 ? -1 : 0;
270
+ }
271
+ /**
272
+ * Optional date format to use for tooltips only.
273
+ * Falls back to `dateFormat` if not provided.
274
+ */
275
+ get tooltipDateFormat() {
201
276
  var _a, _b;
202
- return (_b = (_a = this.tooltipDateFormat) !== null && _a !== void 0 ? _a : this.dateFormat) !== null && _b !== void 0 ? _b : DATE_FORMAT;
277
+ return (_b = (_a = this._tooltipDateFormat) !== null && _a !== void 0 ? _a : this.dateFormat) !== null && _b !== void 0 ? _b : DATE_FORMAT;
278
+ }
279
+ set tooltipDateFormat(value) {
280
+ this._tooltipDateFormat = value;
203
281
  }
204
282
  /** component's loading (and disabled) state */
205
283
  get loading() {
@@ -232,7 +310,7 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
232
310
  }
233
311
  /** formatted maximum date of selected date range */
234
312
  get maxSelectedDate() {
235
- return this.formatDate(this.getMSFromString(this._maxSelectedDate) - 1);
313
+ return this.formatDate(this.getMSFromString(this._maxSelectedDate));
236
314
  }
237
315
  /** updates maxSelectedDate if new date is valid */
238
316
  set maxSelectedDate(rawDate) {
@@ -258,7 +336,8 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
258
336
  }
259
337
  /** horizontal position of max date slider */
260
338
  get maxSliderX() {
261
- const x = this.translateDateToPosition(this.maxSelectedDate);
339
+ const maxSelectedDateMS = this.snapTimestamp(this.getMSFromString(this.maxSelectedDate) + this.snapInterval);
340
+ const x = this.translateDateToPosition(this.formatDate(maxSelectedDateMS));
262
341
  return this.validMaxSliderX(x);
263
342
  }
264
343
  get dateRangeMS() {
@@ -272,15 +351,12 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
272
351
  const x = target.x.baseVal.value + this.sliderWidth / 2;
273
352
  const dataset = target.dataset;
274
353
  const itemsText = `item${dataset.numItems !== '1' ? 's' : ''}`;
275
- const datesText = dataset.binStart === dataset.binEnd
276
- ? `${dataset.binStart}` // If start/end are the same, just render a single value
277
- : `${dataset.binStart} - ${dataset.binEnd}`;
278
354
  const formattedNumItems = Number(dataset.numItems).toLocaleString();
279
355
  this._tooltipOffset =
280
356
  x + (this._binWidth - this.sliderWidth - this.tooltipWidth) / 2;
281
357
  this._tooltipContent = html `
282
358
  ${formattedNumItems} ${itemsText}<br />
283
- ${datesText}
359
+ ${dataset.tooltip}
284
360
  `;
285
361
  this._tooltipVisible = true;
286
362
  }
@@ -377,9 +453,10 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
377
453
  * @returns string representation of date
378
454
  */
379
455
  translatePositionToDate(x) {
380
- // use Math.ceil to round up to fix case where input like 1/1/2010 would get
381
- // translated to 12/31/2009
382
- const milliseconds = Math.ceil(((x - this.sliderWidth) * this.dateRangeMS) / this._histWidth);
456
+ // Snap to the nearest second, fixing the case where input like 1/1/2010
457
+ // would get translated to 12/31/2009 due to slight discrepancies from
458
+ // pixel boundaries and floating point error.
459
+ const milliseconds = this.snapToNextSecond(((x - this.sliderWidth) * this.dateRangeMS) / this._histWidth);
383
460
  return this.formatDate(this._minDateMS + milliseconds);
384
461
  }
385
462
  /**
@@ -539,35 +616,52 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
539
616
  }
540
617
  get histogramTemplate() {
541
618
  const xScale = this._histWidth / this._numBins;
542
- const barWidth = xScale - 0.5;
619
+ const barWidth = xScale - 1;
543
620
  let x = this.sliderWidth; // start at the left edge of the histogram
544
- // the stroke-dasharray style below creates a transparent border around the
545
- // right edge of the bar, which prevents user from encountering a gap
546
- // between adjacent bars (eg when viewing the tooltips or when trying to
547
- // extend the range by clicking on a bar)
548
621
  return this._histData.map(data => {
622
+ const { minSelectedDate, maxSelectedDate } = this;
623
+ const barHeight = data.height;
624
+ const binIsBeforeMin = this.isBefore(data.binEnd, minSelectedDate);
625
+ const binIsAfterMax = this.isAfter(data.binStart, maxSelectedDate);
626
+ const barFill = binIsBeforeMin || binIsAfterMax ? barExcludedFill : barIncludedFill;
627
+ // the stroke-dasharray style below creates a transparent border around the
628
+ // right edge of the bar, which prevents user from encountering a gap
629
+ // between adjacent bars (eg when viewing the tooltips or when trying to
630
+ // extend the range by clicking on a bar)
631
+ const barStyle = `stroke-dasharray: 0 ${barWidth} ${barHeight} ${barWidth} 0 ${barHeight}`;
549
632
  const bar = svg `
550
633
  <rect
551
634
  class="bar"
552
- style='stroke-dasharray: 0 ${barWidth} ${data.height} ${barWidth} 0 ${data.height};'
553
- x="${x}"
554
- y="${this.height - data.height}"
555
- width="${barWidth}"
556
- height="${data.height}"
557
- @pointerenter="${this.showTooltip}"
558
- @pointerleave="${this.hideTooltip}"
559
- @click="${this.handleBarClick}"
560
- fill="${x + barWidth >= this.minSliderX && x <= this.maxSliderX
561
- ? barIncludedFill
562
- : barExcludedFill}"
563
- data-num-items="${data.value}"
564
- data-bin-start="${data.binStart}"
565
- data-bin-end="${data.binEnd}"
635
+ style=${barStyle}
636
+ x=${x}
637
+ y=${this.height - barHeight}
638
+ width=${barWidth}
639
+ height=${barHeight}
640
+ @pointerenter=${this.showTooltip}
641
+ @pointerleave=${this.hideTooltip}
642
+ @click=${this.handleBarClick}
643
+ fill=${barFill}
644
+ data-num-items=${data.value}
645
+ data-bin-start=${data.binStart}
646
+ data-bin-end=${data.binEnd}
647
+ data-tooltip=${data.tooltip}
566
648
  />`;
567
649
  x += xScale;
568
650
  return bar;
569
651
  });
570
652
  }
653
+ /** Whether the first arg represents a date strictly before the second arg */
654
+ isBefore(date1, date2) {
655
+ const date1MS = this.getMSFromString(date1);
656
+ const date2MS = this.getMSFromString(date2);
657
+ return date1MS < date2MS;
658
+ }
659
+ /** Whether the first arg represents a date strictly after the second arg */
660
+ isAfter(date1, date2) {
661
+ const date1MS = this.getMSFromString(date1);
662
+ const date2MS = this.getMSFromString(date2);
663
+ return date1MS > date2MS;
664
+ }
571
665
  formatDate(dateMS, format = this.dateFormat) {
572
666
  if (Number.isNaN(dateMS)) {
573
667
  return '';
@@ -821,9 +915,6 @@ __decorate([
821
915
  __decorate([
822
916
  property({ type: String })
823
917
  ], HistogramDateRange.prototype, "dateFormat", void 0);
824
- __decorate([
825
- property({ type: String })
826
- ], HistogramDateRange.prototype, "tooltipDateFormat", void 0);
827
918
  __decorate([
828
919
  property({ type: String })
829
920
  ], HistogramDateRange.prototype, "missingDataMessage", void 0);
@@ -842,6 +933,9 @@ __decorate([
842
933
  __decorate([
843
934
  property({ type: Boolean })
844
935
  ], HistogramDateRange.prototype, "updateWhileFocused", void 0);
936
+ __decorate([
937
+ property({ type: String })
938
+ ], HistogramDateRange.prototype, "binSnapping", void 0);
845
939
  __decorate([
846
940
  state()
847
941
  ], HistogramDateRange.prototype, "_tooltipOffset", void 0);
@@ -851,12 +945,18 @@ __decorate([
851
945
  __decorate([
852
946
  state()
853
947
  ], HistogramDateRange.prototype, "_tooltipVisible", void 0);
948
+ __decorate([
949
+ state()
950
+ ], HistogramDateRange.prototype, "_tooltipDateFormat", void 0);
854
951
  __decorate([
855
952
  state()
856
953
  ], HistogramDateRange.prototype, "_isDragging", void 0);
857
954
  __decorate([
858
955
  state()
859
956
  ], HistogramDateRange.prototype, "_isLoading", void 0);
957
+ __decorate([
958
+ property({ type: String })
959
+ ], HistogramDateRange.prototype, "tooltipDateFormat", null);
860
960
  __decorate([
861
961
  property({ type: Boolean })
862
962
  ], HistogramDateRange.prototype, "loading", null);