@internetarchive/histogram-date-range 1.2.2-alpha-webdev7377.6 → 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, BinRoundingInterval } 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,EAAuB,MAAM,4BAA4B,CAAC","sourcesContent":["export { HistogramDateRange, BinRoundingInterval } 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,6 +1,6 @@
1
1
  import '@internetarchive/ia-activity-indicator';
2
2
  import { LitElement, PropertyValues, SVGTemplateResult, TemplateResult } from 'lit';
3
- export declare type BinRoundingInterval = 'month' | 'year';
3
+ export declare type BinSnappingInterval = 'none' | 'month' | 'year';
4
4
  export declare class HistogramDateRange extends LitElement {
5
5
  width: number;
6
6
  height: number;
@@ -9,8 +9,6 @@ export declare class HistogramDateRange extends LitElement {
9
9
  tooltipHeight: number;
10
10
  updateDelay: number;
11
11
  dateFormat: string;
12
- /** Optional; falls back to `dateFormat` if not provided */
13
- tooltipDateFormat?: string;
14
12
  missingDataMessage: string;
15
13
  minDate: string;
16
14
  maxDate: string;
@@ -18,11 +16,12 @@ export declare class HistogramDateRange extends LitElement {
18
16
  bins: number[];
19
17
  /** If true, update events will not be canceled by the date inputs receiving focus */
20
18
  updateWhileFocused: boolean;
21
- /** What interval bins should be rounded to for tooltip display */
22
- binRounding: BinRoundingInterval;
19
+ /** What interval bins should be rounded to for display */
20
+ binSnapping: BinSnappingInterval;
23
21
  private _tooltipOffset;
24
22
  private _tooltipContent?;
25
23
  private _tooltipVisible;
24
+ private _tooltipDateFormat?;
26
25
  private _isDragging;
27
26
  private _isLoading;
28
27
  private _minSelectedDate;
@@ -46,32 +45,46 @@ export declare class HistogramDateRange extends LitElement {
46
45
  * creating a tooltip.
47
46
  */
48
47
  private handleDataUpdate;
48
+ /**
49
+ * Rounds the given timestamp to the next full second.
50
+ */
51
+ private snapToNextSecond;
49
52
  /**
50
53
  * Rounds the given timestamp to the (approximate) nearest start of a month,
51
- * such that dates up to the 15th of the month are rounded down, while dates
52
- * past the 15th are rounded up.
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.
53
56
  */
54
- private roundToMonth;
57
+ private snapToMonth;
55
58
  /**
56
59
  * Rounds the given timestamp to the (approximate) nearest start of a year,
57
60
  * such that dates up to the end of June are rounded down, while dates in
58
61
  * July or later are rounded up.
59
62
  */
60
- private roundToYear;
63
+ private snapToYear;
61
64
  /**
62
- * Rounds the given timestamp according to the `binRounding` property.
65
+ * Rounds the given timestamp according to the `binSnapping` property.
66
+ * Default is simply to snap to the nearest full second.
63
67
  */
64
- private roundBin;
68
+ private snapTimestamp;
65
69
  private calculateHistData;
66
70
  private get hasBinData();
67
71
  private get _numBins();
68
72
  private get histogramLeftEdgeX();
69
73
  private get histogramRightEdgeX();
70
74
  /**
71
- * Returns the consumer-specified tooltip date format if set, falling back to
72
- * 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.
73
85
  */
74
- private get resolvedTooltipDateFormat();
86
+ get tooltipDateFormat(): string;
87
+ set tooltipDateFormat(value: string);
75
88
  /** component's loading (and disabled) state */
76
89
  get loading(): boolean;
77
90
  set loading(value: boolean);
@@ -161,6 +174,10 @@ export declare class HistogramDateRange extends LitElement {
161
174
  private generateSliderSVG;
162
175
  get selectedRangeTemplate(): SVGTemplateResult;
163
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;
164
181
  private formatDate;
165
182
  /**
166
183
  * NOTE: we are relying on the lit `live` directive in the template to
@@ -51,8 +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 tooltip display */
55
- this.binRounding = 'year';
54
+ /** What interval bins should be rounded to for display */
55
+ this.binSnapping = 'none';
56
56
  // internal reactive properties not exposed as attributes
57
57
  this._tooltipOffset = 0;
58
58
  this._tooltipVisible = false;
@@ -104,6 +104,9 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
104
104
  }
105
105
  else {
106
106
  this.maxSelectedDate = this.translatePositionToDate(this.validMaxSliderX(newX));
107
+ if (this.getMSFromString(this.maxSelectedDate) > this._maxDateMS) {
108
+ this.maxSelectedDate = this.maxDate;
109
+ }
107
110
  }
108
111
  };
109
112
  }
@@ -120,7 +123,8 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
120
123
  changedProps.has('minSelectedDate') ||
121
124
  changedProps.has('maxSelectedDate') ||
122
125
  changedProps.has('width') ||
123
- changedProps.has('height')) {
126
+ changedProps.has('height') ||
127
+ changedProps.has('binSnapping')) {
124
128
  this.handleDataUpdate();
125
129
  }
126
130
  }
@@ -136,8 +140,10 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
136
140
  return;
137
141
  }
138
142
  this._histWidth = this.width - this.sliderWidth * 2;
139
- this._minDateMS = this.getMSFromString(this.minDate);
140
- 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;
141
147
  this._binWidth = this._histWidth / this._numBins;
142
148
  this._previousDateRange = this.currentDateRangeString;
143
149
  this._histData = this.calculateHistData();
@@ -148,15 +154,21 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
148
154
  ? this.maxSelectedDate
149
155
  : this.maxDate;
150
156
  }
157
+ /**
158
+ * Rounds the given timestamp to the next full second.
159
+ */
160
+ snapToNextSecond(timestamp) {
161
+ return Math.ceil(timestamp / 1000) * 1000;
162
+ }
151
163
  /**
152
164
  * Rounds the given timestamp to the (approximate) nearest start of a month,
153
- * such that dates up to the 15th of the month are rounded down, while dates
154
- * past the 15th are rounded up.
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.
155
167
  */
156
- roundToMonth(timestamp) {
168
+ snapToMonth(timestamp) {
157
169
  const d = new Date(timestamp);
158
170
  const [year, month, day] = [d.getFullYear(), d.getMonth(), d.getDate()];
159
- return day < 16 // Obviously only an approximation, but good enough
171
+ return day < 16 // Obviously only an approximation, but good enough for snapping
160
172
  ? new Date(year, month, 1).getTime()
161
173
  : new Date(year, month + 1, 1).getTime();
162
174
  }
@@ -165,20 +177,27 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
165
177
  * such that dates up to the end of June are rounded down, while dates in
166
178
  * July or later are rounded up.
167
179
  */
168
- roundToYear(timestamp) {
180
+ snapToYear(timestamp) {
169
181
  const d = new Date(timestamp);
170
182
  const [year, month] = [d.getFullYear(), d.getMonth()];
171
183
  return month < 6 // NB: months are 0-indexed, so 6 = July
172
- ? new Date(year, 1, 1).getTime()
173
- : new Date(year + 1, 1, 1).getTime();
184
+ ? new Date(year, 0, 1).getTime()
185
+ : new Date(year + 1, 0, 1).getTime();
174
186
  }
175
187
  /**
176
- * Rounds the given timestamp according to the `binRounding` property.
188
+ * Rounds the given timestamp according to the `binSnapping` property.
189
+ * Default is simply to snap to the nearest full second.
177
190
  */
178
- roundBin(timestamp) {
179
- if (this.binRounding === 'month')
180
- return this.roundToMonth(timestamp);
181
- return this.roundToYear(timestamp);
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
+ }
182
201
  }
183
202
  calculateHistData() {
184
203
  const { bins, height, dateRangeMS, _numBins, _minDateMS } = this;
@@ -190,15 +209,25 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
190
209
  const valueScale = height / valueRange;
191
210
  const dateScale = dateRangeMS / _numBins;
192
211
  return bins.map((v, i) => {
193
- const binStartMS = this.roundBin(i * dateScale + _minDateMS);
194
- const binEndMS = this.roundBin((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}`;
195
223
  return {
196
224
  value: v,
197
225
  // use log scaling for the height of the bar to prevent tall bars from
198
226
  // making the smaller ones too small to see
199
227
  height: Math.floor(Math.log1p(v) * valueScale),
200
- binStart: this.formatDate(binStartMS, this.resolvedTooltipDateFormat),
201
- binEnd: this.formatDate(binEndMS, this.resolvedTooltipDateFormat),
228
+ binStart,
229
+ binEnd,
230
+ tooltip,
202
231
  };
203
232
  });
204
233
  }
@@ -218,12 +247,37 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
218
247
  return this.width - this.sliderWidth;
219
248
  }
220
249
  /**
221
- * Returns the consumer-specified tooltip date format if set, falling back to
222
- * 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.
267
+ */
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.
223
274
  */
224
- get resolvedTooltipDateFormat() {
275
+ get tooltipDateFormat() {
225
276
  var _a, _b;
226
- 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;
227
281
  }
228
282
  /** component's loading (and disabled) state */
229
283
  get loading() {
@@ -282,7 +336,8 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
282
336
  }
283
337
  /** horizontal position of max date slider */
284
338
  get maxSliderX() {
285
- 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));
286
341
  return this.validMaxSliderX(x);
287
342
  }
288
343
  get dateRangeMS() {
@@ -296,15 +351,12 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
296
351
  const x = target.x.baseVal.value + this.sliderWidth / 2;
297
352
  const dataset = target.dataset;
298
353
  const itemsText = `item${dataset.numItems !== '1' ? 's' : ''}`;
299
- const datesText = dataset.binStart === dataset.binEnd
300
- ? `${dataset.binStart}` // If start/end are the same, just render a single value
301
- : `${dataset.binStart} - ${dataset.binEnd}`;
302
354
  const formattedNumItems = Number(dataset.numItems).toLocaleString();
303
355
  this._tooltipOffset =
304
356
  x + (this._binWidth - this.sliderWidth - this.tooltipWidth) / 2;
305
357
  this._tooltipContent = html `
306
358
  ${formattedNumItems} ${itemsText}<br />
307
- ${datesText}
359
+ ${dataset.tooltip}
308
360
  `;
309
361
  this._tooltipVisible = true;
310
362
  }
@@ -401,9 +453,10 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
401
453
  * @returns string representation of date
402
454
  */
403
455
  translatePositionToDate(x) {
404
- // use Math.ceil to round up to fix case where input like 1/1/2010 would get
405
- // translated to 12/31/2009
406
- 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);
407
460
  return this.formatDate(this._minDateMS + milliseconds);
408
461
  }
409
462
  /**
@@ -563,35 +616,52 @@ let HistogramDateRange = class HistogramDateRange extends LitElement {
563
616
  }
564
617
  get histogramTemplate() {
565
618
  const xScale = this._histWidth / this._numBins;
566
- const barWidth = xScale - 0.5;
619
+ const barWidth = xScale - 1;
567
620
  let x = this.sliderWidth; // start at the left edge of the histogram
568
- // the stroke-dasharray style below creates a transparent border around the
569
- // right edge of the bar, which prevents user from encountering a gap
570
- // between adjacent bars (eg when viewing the tooltips or when trying to
571
- // extend the range by clicking on a bar)
572
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}`;
573
632
  const bar = svg `
574
633
  <rect
575
634
  class="bar"
576
- style='stroke-dasharray: 0 ${barWidth} ${data.height} ${barWidth} 0 ${data.height};'
577
- x="${x}"
578
- y="${this.height - data.height}"
579
- width="${barWidth}"
580
- height="${data.height}"
581
- @pointerenter="${this.showTooltip}"
582
- @pointerleave="${this.hideTooltip}"
583
- @click="${this.handleBarClick}"
584
- fill="${x + barWidth >= this.minSliderX && x <= this.maxSliderX
585
- ? barIncludedFill
586
- : barExcludedFill}"
587
- data-num-items="${data.value}"
588
- data-bin-start="${data.binStart}"
589
- 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}
590
648
  />`;
591
649
  x += xScale;
592
650
  return bar;
593
651
  });
594
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
+ }
595
665
  formatDate(dateMS, format = this.dateFormat) {
596
666
  if (Number.isNaN(dateMS)) {
597
667
  return '';
@@ -845,9 +915,6 @@ __decorate([
845
915
  __decorate([
846
916
  property({ type: String })
847
917
  ], HistogramDateRange.prototype, "dateFormat", void 0);
848
- __decorate([
849
- property({ type: String })
850
- ], HistogramDateRange.prototype, "tooltipDateFormat", void 0);
851
918
  __decorate([
852
919
  property({ type: String })
853
920
  ], HistogramDateRange.prototype, "missingDataMessage", void 0);
@@ -868,7 +935,7 @@ __decorate([
868
935
  ], HistogramDateRange.prototype, "updateWhileFocused", void 0);
869
936
  __decorate([
870
937
  property({ type: String })
871
- ], HistogramDateRange.prototype, "binRounding", void 0);
938
+ ], HistogramDateRange.prototype, "binSnapping", void 0);
872
939
  __decorate([
873
940
  state()
874
941
  ], HistogramDateRange.prototype, "_tooltipOffset", void 0);
@@ -878,12 +945,18 @@ __decorate([
878
945
  __decorate([
879
946
  state()
880
947
  ], HistogramDateRange.prototype, "_tooltipVisible", void 0);
948
+ __decorate([
949
+ state()
950
+ ], HistogramDateRange.prototype, "_tooltipDateFormat", void 0);
881
951
  __decorate([
882
952
  state()
883
953
  ], HistogramDateRange.prototype, "_isDragging", void 0);
884
954
  __decorate([
885
955
  state()
886
956
  ], HistogramDateRange.prototype, "_isLoading", void 0);
957
+ __decorate([
958
+ property({ type: String })
959
+ ], HistogramDateRange.prototype, "tooltipDateFormat", null);
887
960
  __decorate([
888
961
  property({ type: Boolean })
889
962
  ], HistogramDateRange.prototype, "loading", null);