@pipsend/charts 2.3.19 → 2.3.20

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.
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * Pipsend Charts v2.3.19-dev+202610211514
3
+ * Pipsend Charts v2.3.20-dev+202610211649
4
4
  * Copyright (c) 2026 Pipsend
5
5
  * Licensed under MIT License
6
6
  * Built on TradingView Lightweight Charts™ (Apache 2.0)
@@ -5555,7 +5555,7 @@
5555
5555
  const visibleBars = ensureNotNull(this._internal_visibleStrictRange());
5556
5556
  const firstBar = Math.max(visibleBars._internal_left(), visibleBars._internal_left() - indexPerLabel);
5557
5557
  const lastBar = Math.max(visibleBars._internal_right(), visibleBars._internal_right() - indexPerLabel);
5558
- const items = this._private__tickMarks._internal_build(spacing, maxLabelWidth, this._private__options.ignoreWhitespaceIndices, this._private__indicesWithData, this._private__indicesWithDataUpdateId, (mark, barsPerMark) => this._private__horzScaleBehavior.allowsTickMark?.(mark, barsPerMark) ?? true);
5558
+ const items = this._private__tickMarks._internal_build(spacing, maxLabelWidth, this._private__options.ignoreWhitespaceIndices, this._private__indicesWithData, this._private__indicesWithDataUpdateId, (mark, barsPerMark) => this._private__horzScaleBehavior.allowsTickMark?.(mark, barsPerMark, this._private__points[mark.index - 1]) ?? true);
5559
5559
  // according to indexPerLabel value this value means "earliest index which _might be_ used as the second label on time scale"
5560
5560
  const earliestIndexOfSecondLabel = this._private__firstIndex() + indexPerLabel;
5561
5561
  // according to indexPerLabel value this value means "earliest index which _might be_ used as the second last label on time scale"
@@ -6829,6 +6829,75 @@
6829
6829
  return localDateFromUtc.toLocaleString(locale, formatOptions);
6830
6830
  }
6831
6831
 
6832
+ const DAY = 86400;
6833
+ const QUARTER_HOUR = 900;
6834
+ const zones = new Map();
6835
+ function zoneCache(timeZone) {
6836
+ let cache = zones.get(timeZone);
6837
+ if (cache === undefined) {
6838
+ let format = null;
6839
+ try {
6840
+ format = new Intl.DateTimeFormat('en-US', {
6841
+ timeZone,
6842
+ hourCycle: 'h23',
6843
+ year: 'numeric',
6844
+ month: 'numeric',
6845
+ day: 'numeric',
6846
+ hour: 'numeric',
6847
+ minute: 'numeric',
6848
+ second: 'numeric',
6849
+ });
6850
+ }
6851
+ catch (error) {
6852
+ format = null;
6853
+ }
6854
+ cache = { _internal_format: format, _internal_days: new Map(), _internal_quarters: new Map() };
6855
+ zones.set(timeZone, cache);
6856
+ }
6857
+ return cache;
6858
+ }
6859
+ function exactOffset(format, time) {
6860
+ const parts = format.formatToParts(new Date(time * 1000));
6861
+ const part = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
6862
+ const wallClock = Date.UTC(part('year'), part('month') - 1, part('day'), part('hour') % 24, part('minute'), part('second'));
6863
+ return Math.round(wallClock / 1000 - time);
6864
+ }
6865
+ /**
6866
+ * Offset of `timeZone` from UTC at `time`, in seconds. Undefined, `'UTC'` and
6867
+ * unknown zones are 0.
6868
+ *
6869
+ * Resolved once per UTC day, and to the quarter hour only on days with a
6870
+ * transition: asking `Intl` for every bar is too slow for a whole series.
6871
+ */
6872
+ function timeZoneOffset(timeZone, time) {
6873
+ if (timeZone === undefined || timeZone === 'UTC') {
6874
+ return 0;
6875
+ }
6876
+ const cache = zoneCache(timeZone);
6877
+ const format = cache._internal_format;
6878
+ if (format === null) {
6879
+ return 0;
6880
+ }
6881
+ const day = Math.floor(time / DAY);
6882
+ let wholeDay = cache._internal_days.get(day);
6883
+ if (wholeDay === undefined) {
6884
+ const start = exactOffset(format, day * DAY);
6885
+ const end = exactOffset(format, (day + 1) * DAY - 1);
6886
+ wholeDay = start === end ? start : null;
6887
+ cache._internal_days.set(day, wholeDay);
6888
+ }
6889
+ if (wholeDay !== null) {
6890
+ return wholeDay;
6891
+ }
6892
+ const quarter = Math.floor(time / QUARTER_HOUR);
6893
+ let offset = cache._internal_quarters.get(quarter);
6894
+ if (offset === undefined) {
6895
+ offset = exactOffset(format, quarter * QUARTER_HOUR);
6896
+ cache._internal_quarters.set(quarter, offset);
6897
+ }
6898
+ return offset;
6899
+ }
6900
+
6832
6901
  function hours(count) {
6833
6902
  return count * 60 * 60 * 1000;
6834
6903
  }
@@ -6868,29 +6937,36 @@
6868
6937
  function cast(t) {
6869
6938
  return t;
6870
6939
  }
6871
- function fillWeightsForPoints(sortedTimePoints, startIndex = 0) {
6940
+ // Business days are dates, not instants: never shifted
6941
+ function zonedTimestamp(point, timeZone) {
6942
+ const time = cast(point.time);
6943
+ return time._internal_businessDay === undefined ? time._internal_timestamp + timeZoneOffset(timeZone, time._internal_timestamp) : time._internal_timestamp;
6944
+ }
6945
+ function fillWeightsForPoints(sortedTimePoints, startIndex = 0, timeZone) {
6872
6946
  if (sortedTimePoints.length === 0) {
6873
6947
  return;
6874
6948
  }
6875
- let prevTime = startIndex === 0 ? null : cast(sortedTimePoints[startIndex - 1].time)._internal_timestamp;
6949
+ let prevTime = startIndex === 0 ? null : zonedTimestamp(sortedTimePoints[startIndex - 1], timeZone);
6876
6950
  let prevDate = prevTime !== null ? new Date(prevTime * 1000) : null;
6877
6951
  let totalTimeDiff = 0;
6878
6952
  for (let index = startIndex; index < sortedTimePoints.length; ++index) {
6879
6953
  const currentPoint = sortedTimePoints[index];
6880
- const currentDate = new Date(cast(currentPoint.time)._internal_timestamp * 1000);
6954
+ const currentTime = zonedTimestamp(currentPoint, timeZone);
6955
+ const currentDate = new Date(currentTime * 1000);
6881
6956
  if (prevDate !== null) {
6882
6957
  currentPoint.timeWeight = weightByTime(currentDate, prevDate);
6883
6958
  }
6884
- totalTimeDiff += cast(currentPoint.time)._internal_timestamp - (prevTime || cast(currentPoint.time)._internal_timestamp);
6885
- prevTime = cast(currentPoint.time)._internal_timestamp;
6959
+ totalTimeDiff += currentTime - (prevTime || currentTime);
6960
+ prevTime = currentTime;
6886
6961
  prevDate = currentDate;
6887
6962
  }
6888
6963
  if (startIndex === 0 && sortedTimePoints.length > 1) {
6889
6964
  // let's guess a weight for the first point
6890
6965
  // let's say the previous point was average time back in the history
6891
6966
  const averageTimeDiff = Math.ceil(totalTimeDiff / (sortedTimePoints.length - 1));
6892
- const approxPrevDate = new Date((cast(sortedTimePoints[0].time)._internal_timestamp - averageTimeDiff) * 1000);
6893
- sortedTimePoints[0].timeWeight = weightByTime(new Date(cast(sortedTimePoints[0].time)._internal_timestamp * 1000), approxPrevDate);
6967
+ const firstTime = zonedTimestamp(sortedTimePoints[0], timeZone);
6968
+ const approxPrevDate = new Date((firstTime - averageTimeDiff) * 1000);
6969
+ sortedTimePoints[0].timeWeight = weightByTime(new Date(firstTime * 1000), approxPrevDate);
6894
6970
  }
6895
6971
  }
6896
6972
 
@@ -7045,16 +7121,15 @@
7045
7121
  }
7046
7122
  }
7047
7123
  formatHorzItem(item) {
7048
- const tp = item;
7049
- return this._private__dateTimeFormatter._internal_format(new Date(tp._internal_timestamp * 1000));
7124
+ return this._private__dateTimeFormatter._internal_format(new Date(this._private__zonedTimePoint(item)._internal_timestamp * 1000));
7050
7125
  }
7051
- allowsTickMark(tickMark, barsPerMark) {
7126
+ allowsTickMark(tickMark, barsPerMark, previous) {
7052
7127
  const filter = this._private__options.timeScale.tickMarkFilter;
7053
7128
  if (filter === undefined) {
7054
7129
  return true;
7055
7130
  }
7056
7131
  const tickMarkType = weightToTickMarkType(tickMark.weight, this._private__options.timeScale.timeVisible, this._private__options.timeScale.secondsVisible);
7057
- return filter(tickMark.originalTime, tickMarkType, barsPerMark);
7132
+ return filter(tickMark.originalTime, tickMarkType, barsPerMark, previous === undefined ? null : previous.originalTime);
7058
7133
  }
7059
7134
  formatTickmark(tickMark, localizationOptions) {
7060
7135
  const tickMarkType = weightToTickMarkType(tickMark.weight, this._private__options.timeScale.timeVisible, this._private__options.timeScale.secondsVisible);
@@ -7065,7 +7140,7 @@
7065
7140
  return tickMarkString;
7066
7141
  }
7067
7142
  }
7068
- return defaultTickMarkFormatter(tickMark.time, tickMarkType, localizationOptions.locale);
7143
+ return defaultTickMarkFormatter(this._private__zonedTimePoint(tickMark.time), tickMarkType, localizationOptions.locale);
7069
7144
  }
7070
7145
  maxTickMarkWeight(tickMarks) {
7071
7146
  let maxWeight = tickMarks.reduce(markWithGreaterWeight$2, tickMarks[0]).weight;
@@ -7077,7 +7152,17 @@
7077
7152
  return maxWeight;
7078
7153
  }
7079
7154
  fillWeightsForPoints(sortedTimePoints, startIndex) {
7080
- fillWeightsForPoints(sortedTimePoints, startIndex);
7155
+ fillWeightsForPoints(sortedTimePoints, startIndex, this._private__timeZone());
7156
+ }
7157
+ _private__timeZone() {
7158
+ return this._private__options?.localization.timeZone;
7159
+ }
7160
+ _private__zonedTimePoint(timePoint) {
7161
+ if (timePoint._internal_businessDay !== undefined) {
7162
+ return timePoint;
7163
+ }
7164
+ const offset = timeZoneOffset(this._private__timeZone(), timePoint._internal_timestamp);
7165
+ return offset === 0 ? timePoint : { _internal_timestamp: (timePoint._internal_timestamp + offset) };
7081
7166
  }
7082
7167
  static _internal_applyDefaults(options) {
7083
7168
  return merge({ localization: { dateFormat: 'dd MMM \'yy' } }, options ?? {});
@@ -11079,6 +11164,10 @@
11079
11164
  this._private__sortedTimePoints = [];
11080
11165
  this._private__horzScaleBehavior = horzScaleBehavior;
11081
11166
  }
11167
+ _internal_refillTimeWeights() {
11168
+ this._private__horzScaleBehavior.fillWeightsForPoints(this._private__sortedTimePoints, 0);
11169
+ return this._private__sortedTimePoints;
11170
+ }
11082
11171
  _internal_destroy() {
11083
11172
  this._private__pointDataByTimePoint.clear();
11084
11173
  this._private__seriesRowsBySeries.clear();
@@ -12583,7 +12672,13 @@
12583
12672
  throw new Error(`colorParsers option should not be changed once the chart has been created.`);
12584
12673
  }
12585
12674
  }
12675
+ const timeZone = this.options().localization.timeZone;
12586
12676
  this._private__chartWidget._internal_applyOptions(toInternalOptions(options));
12677
+ if (this.options().localization.timeZone !== timeZone) {
12678
+ const model = this._private__chartWidget._internal_model();
12679
+ model._internal_timeScale()._internal_update(this._private__dataLayer._internal_refillTimeWeights(), 0);
12680
+ model._internal_fullUpdate();
12681
+ }
12587
12682
  }
12588
12683
  options() {
12589
12684
  return this._private__chartWidget._internal_options();
@@ -19221,8 +19316,9 @@
19221
19316
  const DEFAULT_SESSIONS = [
19222
19317
  {
19223
19318
  name: 'Tokyo',
19224
- startHour: 0,
19225
- endHour: 9,
19319
+ startHour: 9,
19320
+ endHour: 18,
19321
+ timeZone: 'Asia/Tokyo',
19226
19322
  color: '#FF6B00',
19227
19323
  opacity: 0.1,
19228
19324
  },
@@ -19230,13 +19326,15 @@
19230
19326
  name: 'London',
19231
19327
  startHour: 8,
19232
19328
  endHour: 17,
19329
+ timeZone: 'Europe/London',
19233
19330
  color: '#2962FF',
19234
19331
  opacity: 0.1,
19235
19332
  },
19236
19333
  {
19237
19334
  name: 'New York',
19238
- startHour: 13,
19239
- endHour: 22,
19335
+ startHour: 8,
19336
+ endHour: 17,
19337
+ timeZone: 'America/New_York',
19240
19338
  color: '#089981',
19241
19339
  opacity: 0.1,
19242
19340
  },
@@ -19322,79 +19420,49 @@
19322
19420
  }
19323
19421
  }
19324
19422
  _private__highlightSession(session, data) {
19325
- let sessionStart = null;
19326
- let sessionData = [];
19327
- for (const point of data) {
19328
- const time = point.time;
19329
- const hour = this._private__getUTCHour(time);
19330
- const isInSession = this._private__isTimeInSession(hour, session.startHour, session.endHour);
19331
- if (isInSession) {
19332
- if (!sessionStart) {
19333
- sessionStart = time;
19334
- }
19335
- sessionData.push(point);
19336
- }
19337
- else {
19338
- if (sessionStart && sessionData.length > 0) {
19339
- // End of session, create highlight box
19340
- this._private__createSessionBox(sessionStart, sessionData[sessionData.length - 1].time, session);
19341
- sessionStart = null;
19342
- sessionData = [];
19343
- }
19344
- }
19345
- }
19346
- // Handle case where session extends to end of visible range
19347
- if (sessionStart && sessionData.length > 0) {
19348
- this._private__createSessionBox(sessionStart, sessionData[sessionData.length - 1].time, session);
19349
- }
19423
+ this._private__eachRun(data, (time) => this._private__inSession(time, session), (startTime, endTime) => this._private__createSessionBox(startTime, endTime, session));
19350
19424
  }
19425
+ // Per bar, since each session reads the clock of its own zone
19351
19426
  _private__highlightOverlaps(data) {
19352
- // Find overlapping sessions
19353
19427
  const sessions = this._private__options.sessions;
19354
19428
  for (let i = 0; i < sessions.length; i++) {
19355
19429
  for (let j = i + 1; j < sessions.length; j++) {
19356
- const session1 = sessions[i];
19357
- const session2 = sessions[j];
19358
- // Check if sessions overlap
19359
- const overlap = this._private__getSessionOverlap(session1, session2);
19360
- if (overlap) {
19361
- this._private__highlightOverlapPeriod(overlap, data);
19362
- }
19430
+ this._private__eachRun(data, (time) => this._private__inSession(time, sessions[i]) && this._private__inSession(time, sessions[j]), (startTime, endTime) => this._private__createOverlapBox(startTime, endTime));
19363
19431
  }
19364
19432
  }
19365
19433
  }
19366
- _private__getSessionOverlap(session1, session2) {
19367
- const start = Math.max(session1.startHour, session2.startHour);
19368
- const end = Math.min(session1.endHour, session2.endHour);
19369
- if (start < end) {
19370
- return { _internal_start: start, _internal_end: end };
19371
- }
19372
- return null;
19373
- }
19374
- _private__highlightOverlapPeriod(overlap, data) {
19375
- let overlapStart = null;
19376
- let overlapData = [];
19434
+ _private__eachRun(data, inside, emit) {
19435
+ let runStart = null;
19436
+ let runEnd = null;
19377
19437
  for (const point of data) {
19378
- const time = point.time;
19379
- const hour = this._private__getUTCHour(time);
19380
- const isInOverlap = hour >= overlap._internal_start && hour < overlap._internal_end;
19381
- if (isInOverlap) {
19382
- if (!overlapStart) {
19383
- overlapStart = time;
19384
- }
19385
- overlapData.push(point);
19438
+ if (inside(point.time)) {
19439
+ runStart = runStart ?? point.time;
19440
+ runEnd = point.time;
19386
19441
  }
19387
- else {
19388
- if (overlapStart && overlapData.length > 0) {
19389
- this._private__createOverlapBox(overlapStart, overlapData[overlapData.length - 1].time);
19390
- overlapStart = null;
19391
- overlapData = [];
19392
- }
19442
+ else if (runStart !== null && runEnd !== null) {
19443
+ emit(runStart, runEnd);
19444
+ runStart = null;
19445
+ runEnd = null;
19393
19446
  }
19394
19447
  }
19395
- if (overlapStart && overlapData.length > 0) {
19396
- this._private__createOverlapBox(overlapStart, overlapData[overlapData.length - 1].time);
19448
+ if (runStart !== null && runEnd !== null) {
19449
+ emit(runStart, runEnd);
19450
+ }
19451
+ }
19452
+ _private__inSession(time, session) {
19453
+ const hour = this._private__hourIn(time, session.timeZone);
19454
+ if (session.startHour < session.endHour) {
19455
+ return hour >= session.startHour && hour < session.endHour;
19456
+ }
19457
+ // Crosses midnight
19458
+ return hour >= session.startHour || hour < session.endHour;
19459
+ }
19460
+ _private__hourIn(time, timeZone) {
19461
+ const seconds = typeof time === 'number' ? time : typeof time === 'string' ? Date.parse(time) / 1000 : NaN;
19462
+ if (!Number.isFinite(seconds)) {
19463
+ return 0;
19397
19464
  }
19465
+ return new Date((seconds + timeZoneOffset(timeZone, seconds)) * 1000).getUTCHours();
19398
19466
  }
19399
19467
  _private__createSessionBox(startTime, endTime, session) {
19400
19468
  // Validate times
@@ -19424,29 +19492,6 @@
19424
19492
  };
19425
19493
  this._private__sessionBoxes.push(box);
19426
19494
  }
19427
- _private__isTimeInSession(hour, startHour, endHour) {
19428
- if (startHour < endHour) {
19429
- return hour >= startHour && hour < endHour;
19430
- }
19431
- else {
19432
- // Session crosses midnight
19433
- return hour >= startHour || hour < endHour;
19434
- }
19435
- }
19436
- _private__getUTCHour(time) {
19437
- // Convert time to UTC hour
19438
- if (typeof time === 'number') {
19439
- // Unix timestamp in seconds
19440
- const date = new Date(time * 1000);
19441
- return date.getUTCHours();
19442
- }
19443
- else if (typeof time === 'string') {
19444
- // ISO string
19445
- const date = new Date(time);
19446
- return date.getUTCHours();
19447
- }
19448
- return 0;
19449
- }
19450
19495
  _private__clearSessionBoxes() {
19451
19496
  // Clear all session boxes
19452
19497
  this._private__sessionBoxes = [];
@@ -30599,6 +30644,14 @@
30599
30644
 
30600
30645
  /** Undo steps kept, each a full snapshot of every drawing. */
30601
30646
  const UNDO_HISTORY_LIMIT = 100;
30647
+ /**
30648
+ * Points for a restored freehand tool. Built with bracket notation: a plain
30649
+ * `{ time, price }` literal gets its keys renamed by the build, and the tool,
30650
+ * which reads them by their public names, comes back drawing nothing.
30651
+ */
30652
+ function restorePoints(points) {
30653
+ return points.map((point) => makeSerializedPoint(readSerializedNumber(point, 'price') ?? 0, readSerializedTime(point)));
30654
+ }
30602
30655
  /**
30603
30656
  * Central manager for all drawing tools - TradingView style
30604
30657
  * Handles multiple tools, selection, deletion, and keyboard shortcuts
@@ -31078,16 +31131,8 @@
31078
31131
  ...style,
31079
31132
  });
31080
31133
  case 'polygon':
31081
- // Bracket notation on the way out too, not just on the way in: a
31082
- // plain `{ time, price }` literal here gets its keys renamed and
31083
- // the restored corners come back empty.
31084
31134
  return createPolygonTool(context, {
31085
- points: points.map((_, index) => {
31086
- const vertex = {};
31087
- vertex['time'] = pt(index)._internal_t;
31088
- vertex['price'] = pt(index)._internal_p;
31089
- return vertex;
31090
- }),
31135
+ points: restorePoints(points),
31091
31136
  ...style,
31092
31137
  });
31093
31138
  case 'crossLine':
@@ -31153,12 +31198,12 @@
31153
31198
  });
31154
31199
  case 'arrow':
31155
31200
  return createArrowTool(context, {
31156
- points: points.map((p) => ({ _internal_time: p['time'], _internal_price: p['price'] })),
31201
+ points: restorePoints(points),
31157
31202
  ...style,
31158
31203
  });
31159
31204
  case 'brush':
31160
31205
  return createBrushTool(context, {
31161
- points: points.map((p) => ({ _internal_time: p['time'], _internal_price: p['price'] })),
31206
+ points: restorePoints(points),
31162
31207
  ...style,
31163
31208
  });
31164
31209
  case 'text':
@@ -31630,7 +31675,7 @@
31630
31675
  * Returns the current version as a string. For example `'1.0.0'`.
31631
31676
  */
31632
31677
  function version() {
31633
- return "2.3.19-dev+202610211514";
31678
+ return "2.3.20-dev+202610211649";
31634
31679
  }
31635
31680
 
31636
31681
  var PipsendChartsModule = /*#__PURE__*/Object.freeze({
@@ -31749,6 +31794,7 @@
31749
31794
  setSeriesVolumes: setSeriesVolumes,
31750
31795
  setVolumeProfileData: setVolumeProfileData,
31751
31796
  timeFromCoordinate: timeFromCoordinate,
31797
+ timeZoneOffset: timeZoneOffset,
31752
31798
  version: version
31753
31799
  });
31754
31800