/* * Philip Crotwell * University of South Carolina, 2019 * https://www.seis.sc.edu */ import { AUTO_COLOR_SELECTOR, G_DATA_SELECTOR } from "./cssutil"; import { IndividualAmplitudeScale, LinkedAmplitudeScale, LinkedTimeScale, AMPLITUDE_MODE, } from "./scale"; import { DEFAULT_GRID_LINE_COLOR } from "./seismographutil"; import { SeismogramDisplayData, Seismogram } from "./seismogram"; import { isDef, validStartTime, stringify, isStringArg, nameForTimeZone } from "./util"; import { DateTime, Duration, Interval, Zone, FixedOffsetZone, IANAZone } from "luxon"; import { format as d3format } from "d3-format"; import type { AxisDomain } from "d3-axis"; import { Handlebars, registerHelpers } from "./handlebarshelpers"; registerHelpers(); export type MarginType = { top: number; right: number; bottom: number; left: number; toString?: () => string; }; export const DEFAULT_TITLE = "{{#each seisDataList}}{{onlyChangesChannel ../seisDataList @index}} {{else}}No Data{{/each}}"; export class SeismographConfigCache { titleHandlebarsCompiled: null | ((arg0: object, arg1: object) => string); xLabelHandlebarsCompiled: null | ((arg0: object, arg1: object) => string); xSublabelHandlebarsCompiled: null | ((arg0: object, arg1: object) => string); yLabelHandlebarsCompiled: null | ((arg0: object, arg1: object) => string); yLabelRightHandlebarsCompiled: | null | ((arg0: object, arg1: object) => string); ySublabelHandlebarsCompiled: null | ((arg0: object, arg1: object) => string); constructor() { this.titleHandlebarsCompiled = null; this.xLabelHandlebarsCompiled = null; this.xSublabelHandlebarsCompiled = null; this.yLabelHandlebarsCompiled = null; this.yLabelRightHandlebarsCompiled = null; this.ySublabelHandlebarsCompiled = null; } } /** * Configuration object for Seismograph display. * */ export class SeismographConfig { /** @private */ static _lastID: number; configId: number; /** @private */ __cache__: SeismographConfigCache; _timeFormat: null | ((date: Date) => string); relativeTimeFormat: (value: number) => string; amplitudeFormat: (value: number) => string; showTitle: boolean; /** @private */ _title: Array; /** @private */ isXAxis: boolean; xAxisTimeZone: null|string|Zone; isXAxisTop: boolean; /** @private */ _xLabel: string; xLabelOrientation: string; /** @private */ _xSublabel: string; xSublabelIsUnits: boolean; /** * Should grid lines be drawn for each tick on the x axis. */ xGridLines: boolean; isYAxis: boolean; isYAxisRight: boolean; isYAxisNice: boolean; /** * hint for number of ticks to show on y axis. Note this is not exact as * trying to put ticks on "even" numbers may result in slightly more or less. */ yAxisNumTickHint: number; /** @private */ _yLabel: string; /** @private */ _yLabelRight: string; yLabelOrientation: string; /** @private */ _ySublabel: string; ySublabelTrans: number; ySublabelIsUnits: boolean; yGridLines: boolean; doMarkers: boolean; markerTextOffset: number; markerTextAngle: number; markerFlagpoleBase: string; minHeight: number; maxHeight: null | number; minWidth: number; maxWidth: null | number; margin: MarginType; segmentDrawCompressedCutoff: number; //below this draw all points, above draw minmax maxZoomPixelPerSample: number; // no zoom in past point of sample // separated by pixels connectSegments: boolean; lineColors: Array; lineWidth: number; gridLineColor: string; allowZoom: boolean; wheelZoom: boolean; amplitudeMode: AMPLITUDE_MODE; doGain: boolean; windowAmp: boolean; resolutionScale: number; /** @private */ _fixedAmplitudeScale: null | Array; /** @private */ _fixedTimeScale: null | Interval; /** @private */ _linkedAmplitudeScale: null | LinkedAmplitudeScale; /** @private */ _linkedTimeScale: null | LinkedTimeScale; isRelativeTime: boolean; constructor() { this.configId = ++SeismographConfig._lastID; this.__cache__ = new SeismographConfigCache(); this.isXAxis = true; this.isXAxisTop = false; this.xAxisTimeZone = null; // defaults to UTC this.isYAxisNice = true; this.isYAxis = true; this.isYAxisRight = false; this.yAxisNumTickHint = 8; /** * Should grid lines be drawn for each tick on the X axis. * Defaults to false; */ this.xGridLines = false; /** * Should grid lines be drawn for each tick on the Y axis. * Defaults to false; */ this.yGridLines = false; /** * Color for gridlines. Defaults to gainsboro, a very light grey. */ this.gridLineColor = DEFAULT_GRID_LINE_COLOR; this._timeFormat = null; this.relativeTimeFormat = formatCountOrAmp; this.amplitudeFormat = formatCountOrAmp; this._title = [DEFAULT_TITLE]; this.showTitle = true; this._xLabel = "Time"; this.xLabelOrientation = "horizontal"; this._xSublabel = ""; this.xSublabelIsUnits = false; this._yLabel = "Amplitude"; this._yLabelRight = ""; this.yLabelOrientation = "vertical"; this._ySublabel = ""; this.ySublabelTrans = 15; this.ySublabelIsUnits = true; this.amplitudeMode = AMPLITUDE_MODE.MinMax; this.doGain = true; this.windowAmp = true; this.resolutionScale = 2; this._fixedAmplitudeScale = null; this._fixedTimeScale = null; this._linkedAmplitudeScale = new IndividualAmplitudeScale(); this._linkedTimeScale = new LinkedTimeScale( [], Duration.fromMillis(0), Duration.fromMillis(0), this.configId, ); this.isRelativeTime = false; this.doMarkers = true; this.markerTextOffset = 0.85; this.markerTextAngle = 45; this.markerFlagpoleBase = "bottom"; // bottom or center this.minHeight = 0; this.maxHeight = null; this.minWidth = 0; this.maxWidth = null; this.margin = { top: 25, right: 20, bottom: 42, left: 85, toString: function () { return `t: ${this.top} l: ${this.left} b: ${this.bottom} r: ${this.right}`; }, }; this.segmentDrawCompressedCutoff = 10; //below this draw all points, above draw minmax this.maxZoomPixelPerSample = 20; // no zoom in past point of sample this.wheelZoom = false; this.allowZoom = true; // separated by pixels this.connectSegments = false; this.lineColors = [ "skyblue", "olivedrab", "goldenrod", "firebrick", "darkcyan", "chocolate", "darkmagenta", "mediumseagreen", "rebeccapurple", "sienna", "orchid", "royalblue", "mediumturquoise", "chartreuse", "peru", "black", ]; this.lineWidth = 1; } static fromJSON(json: SeismographConfigJsonType): SeismographConfig { const seisConfig = new SeismographConfig(); const tempJson = {}; Object.assign(tempJson, json); if (Object.hasOwn(tempJson, "fixedAmplitudeScale")) { // @ts-expect-error ok as we just check hasOwn delete tempJson.fixedAmplitudeScale; } if (Object.hasOwn(tempJson, "fixedTimeScale")) { // @ts-expect-error ok as we just check hasOwn delete tempJson.fixedTimeScale; } if (Object.hasOwn(tempJson, "isLinkedTimeScale")) { // @ts-expect-error ok as we just check hasOwn delete tempJson.isLinkedTimeScale; } if (Object.hasOwn(tempJson, "isLinkedAmplitudeScale")) { // @ts-expect-error ok as we just check hasOwn delete tempJson.isLinkedAmplitudeScale; } if (Object.hasOwn(tempJson, "xAxisTimeZone")) { // @ts-expect-error ok as we just check hasOwn delete tempJson.xAxisTimeZone; } if ( Object.hasOwn(tempJson, "ySublabel") && // @ts-expect-error ok as we just check hasOwn tempJson.ySublabel.length === 0 ) { // don't set in case ySublabelIsUnits // @ts-expect-error ok as we just check hasOwn delete tempJson.ySublabel; } Object.assign(seisConfig, tempJson); if (json.isLinkedTimeScale) { seisConfig.linkedTimeScale = new LinkedTimeScale(); } else { seisConfig.fixedAmplitudeScale = json.fixedAmplitudeScale; } if (json.isLinkedAmplitudeScale) { seisConfig.linkedAmplitudeScale = new LinkedAmplitudeScale(); } else if (isDef(json.fixedAmplitudeScale)) { seisConfig.fixedAmplitudeScale = json.fixedAmplitudeScale; } else { // neither fixed nor linked, so individual seisConfig.linkedAmplitudeScale = new IndividualAmplitudeScale(); } if (json.xAxisTimeZone) { seisConfig.xAxisTimeZone = new IANAZone(json.xAxisTimeZone); } return seisConfig; } asJSON(): SeismographConfigJsonType { // kind of dumb... const out = JSON.parse(JSON.stringify(this)); out.title = out._title; delete out._title; out.xAxisTimeZone = isDef(this.xAxisTimeZone) ? ( isStringArg(this.xAxisTimeZone) ? this.xAxisTimeZone : this.xAxisTimeZone.name) : null; out.fixedAmplitudeScale = out._fixedAmplitudeScale; delete out._fixedAmplitudeScale; out.fixedTimeScale = out._fixedTimeScale; delete out._fixedTimeScale; delete out._linkedTimeScale; out.isLinkedTimeScale = isDef(this._linkedTimeScale); out.isLinkedAmplitudeScale = isDef(this._linkedAmplitudeScale) && !(this._linkedAmplitudeScale instanceof IndividualAmplitudeScale); delete out._linkedAmplitudeScale; delete out.__cache__; Object.getOwnPropertyNames(out).forEach((p) => { if (p.startsWith("_")) { const tmp = out[p]; delete out[p]; out[p.substring(1)] = tmp; } }); return out; } get fixedAmplitudeScale(): null | Array { return this._fixedAmplitudeScale; } set fixedAmplitudeScale(ts: null | Array) { if (!isDef(ts)) { throw new Error("amp scale must be defined setting fixed"); } this._fixedAmplitudeScale = ts; this._linkedAmplitudeScale = null; } get linkedAmplitudeScale(): null | LinkedAmplitudeScale { return this._linkedAmplitudeScale; } set linkedAmplitudeScale(ts: null | LinkedAmplitudeScale) { if (!isDef(ts)) { throw new Error("amp scale must be defined setting linked"); } if (this._linkedAmplitudeScale) { ts.linkAll(this._linkedAmplitudeScale.graphList); } this._linkedAmplitudeScale = ts; this._fixedAmplitudeScale = null; } /** * Enable linked amplitude scales across seismographs. * */ enableLinkedAmplitude() { // setter handles details... this.linkedAmplitudeScale = new LinkedAmplitudeScale(); } /** * Set Raw amplitude mode, plot absolute and * goes from minimun to maximum of data */ amplitudeRaw() { this.amplitudeMode = AMPLITUDE_MODE.Raw; } /** * Set MinMax amplitude mode, plot is relative and * centered on (minimun + maximum)/2 */ amplitudeMinMax() { this.amplitudeMode = AMPLITUDE_MODE.MinMax; } /** * Set Mean amplitude mode, plot is relative and * centered on mean of data */ amplitudeMean() { this.amplitudeMode = AMPLITUDE_MODE.Mean; } /** * Set WithZero amplitude mode, plot is absolute and * centered on mean of data like Raw, but also includes zero * even if all data is positive. Useful when showing * data compared to zero is helpful. */ amplitudeWithZero() { this.amplitudeMode = AMPLITUDE_MODE.Zero; } /** * True if the amplitude is "centered". * * Both MinMax and Mean center the amplitude, Raw and Zero do not. * * @returns if centered */ isCenteredAmp() { return ( this.amplitudeMode === AMPLITUDE_MODE.MinMax || this.amplitudeMode === AMPLITUDE_MODE.Mean ); } get fixedTimeScale(): null | Interval { return this._fixedTimeScale; } set fixedTimeScale(ts: null | Interval) { if (!isDef(ts)) { throw new Error("time scale must be defined"); } this._fixedTimeScale = ts; this._linkedTimeScale = null; } get linkedTimeScale(): null | LinkedTimeScale { return this._linkedTimeScale; } set linkedTimeScale(ts: null | LinkedTimeScale) { if (!isDef(ts)) { throw new Error("time scale must be defined"); } if (this._linkedTimeScale) { ts.linkAll(this._linkedTimeScale.graphList); } this._linkedTimeScale = ts; this._fixedTimeScale = null; } /** * Configures the time axis to show times in the given time zone. This * replaces timeFormat with createTimeFormatterForZone() and * sets xSublabel to be the zone name. If zone is null, uses UTC. * * @param zone string like "US/Eastern" or luxon Zone */ enableXAxisTimeZone(zone: null | Zone | string) { let zoneName: string; let tz: Zone; if (isStringArg(zone)) { zoneName = zone; tz = new IANAZone(zone); } else if (zone instanceof Zone) { tz = zone; zoneName = nameForTimeZone(tz.name); } else { tz = FixedOffsetZone.utcInstance; zoneName = "UTC"; } this.timeFormat = createTimeFormatterForZone(tz); this.xSublabel = zoneName; } /** * Time formatter used by the x axis. Defaults to UTC via * createTimeFormatterForZone(zone). * Set xAxisTimeZone to change the time zone. * @return formatter for x axis time labels */ get timeFormat() { let tz: Zone; if (this._timeFormat != null) { return this._timeFormat; } else if (isStringArg(this.xAxisTimeZone)) { tz = new IANAZone(this.xAxisTimeZone); } else if (this.xAxisTimeZone instanceof Zone) { tz = this.xAxisTimeZone; } else { tz = FixedOffsetZone.utcInstance; } return createTimeFormatterForZone(tz); } set timeFormat(formatter: (arg0: Date) => string) { this._timeFormat = formatter; } /** * gets the current title * * @returns title as an array of strings */ get title(): Array { return this._title; } /** * Sets the title as simple string or array of strings or a * handlebars template. If an array * then each item will be in a separate tspan for easier formatting. * * @param value string or array of strings to be the title */ set title(value: null | string | Array) { if (!isDef(value)) { this._title = [""]; } else if (Array.isArray(value)) { this._title = value; } else { this._title = [value]; } this.__cache__.titleHandlebarsCompiled = null; } handlebarsTitle(context: object, runtimeOptions: object): string { if (!isDef(this.__cache__.titleHandlebarsCompiled)) { if ( !isDef(this._title) || this._title.length === 0 || !isDef(this._title[0]) ) { // empty title return ""; } else if (this._title.length === 1) { this.__cache__.titleHandlebarsCompiled = Handlebars.compile( this._title[0], ); } else { this.__cache__.titleHandlebarsCompiled = Handlebars.compile( "" + this._title.join(" "), ); } } // don't think this can happen, keep typescript happy if (!this.__cache__.titleHandlebarsCompiled) { throw new Error( `Unable to compile handlebars title for ${stringify(this._title)}`, ); } return this.__cache__.titleHandlebarsCompiled(context, runtimeOptions); } /** * gets the current x label * * @returns x label */ get xLabel(): string { return this._xLabel; } /** * Sets the xLabel as simple string or a * handlebars template. * * @param value string to be the x label */ set xLabel(value: null | string) { if (!isDef(value)) { this._xLabel = ""; } else { this._xLabel = value; } this.__cache__.xLabelHandlebarsCompiled = null; } /** * gets the current x sublabel * * @returns x sublabel */ get xSublabel(): string { return this._xSublabel; } /** * Sets the xSublabel as simple string or a * handlebars template. * * @param value string to be the x sublabel */ set xSublabel(value: null | string) { if (!isDef(value)) { this._xSublabel = ""; } else { this._xSublabel = value; } this.__cache__.xSublabelHandlebarsCompiled = null; } handlebarsXLabel(context: object, runtimeOptions: object): string { if (!isDef(this.__cache__.xLabelHandlebarsCompiled)) { if (!isDef(this._xLabel) || this._xLabel.length === 0) { // empty label return ""; } else { this.__cache__.xLabelHandlebarsCompiled = Handlebars.compile( this._xLabel, ); } } // don't think this can happen, keep typescript happy if (!this.__cache__.xLabelHandlebarsCompiled) { throw new Error( `Unable to compile handlebars xLabel for ${this._xLabel}`, ); } return this.__cache__.xLabelHandlebarsCompiled(context, runtimeOptions); } handlebarsXSublabel(context: object, runtimeOptions: object): string { if (!isDef(this.__cache__.xSublabelHandlebarsCompiled)) { if (!isDef(this._xSublabel) || this._xSublabel.length === 0) { // empty label return ""; } else { this.__cache__.xSublabelHandlebarsCompiled = Handlebars.compile( this._xSublabel, ); } } // don't think this can happen, keep typescript happy if (!this.__cache__.xSublabelHandlebarsCompiled) { throw new Error( `Unable to compile handlebars xLabel for ${this._xSublabel}`, ); } return this.__cache__.xSublabelHandlebarsCompiled(context, runtimeOptions); } /** * gets the current y label * * @returns yLabel */ get yLabel(): string { return this._yLabel; } /** * Sets the ylabel as simple string or a * handlebars template. * * @param value string to be the y label */ set yLabel(value: null | string) { if (!isDef(value)) { this._yLabel = ""; } else { this._yLabel = value; } this.__cache__.yLabelHandlebarsCompiled = null; } /** * gets the current y sublabel * * @returns ySublabel */ get ySublabel(): string { return this._ySublabel; } /** * Sets the y sublabel as simple string or a * handlebars template. * * @param value string to be the y sublabel */ set ySublabel(value: null | string) { if (!isDef(value)) { this._ySublabel = ""; } else { this._ySublabel = value; } this.ySublabelIsUnits = false; this.__cache__.ySublabelHandlebarsCompiled = null; } handlebarsYLabel(context: object, runtimeOptions: object): string { if (!isDef(this.__cache__.yLabelHandlebarsCompiled)) { if (!isDef(this._yLabel) || this._yLabel.length === 0) { // empty label return ""; } else { this.__cache__.yLabelHandlebarsCompiled = Handlebars.compile( this._yLabel, ); } } // don't think this can happen, keep typescript happy if (!this.__cache__.yLabelHandlebarsCompiled) { throw new Error( `Unable to compile handlebars yLabel for ${this._yLabel}`, ); } return this.__cache__.yLabelHandlebarsCompiled(context, runtimeOptions); } handlebarsYSublabel(context: object, runtimeOptions: object): string { if (!isDef(this.__cache__.ySublabelHandlebarsCompiled)) { if (!isDef(this._ySublabel) || this._ySublabel.length === 0) { // empty label return ""; } else { this.__cache__.ySublabelHandlebarsCompiled = Handlebars.compile( this._ySublabel, ); } } // don't think this can happen, keep typescript happy if (!this.__cache__.ySublabelHandlebarsCompiled) { throw new Error( `Unable to compile handlebars yLabel for ${this._ySublabel}`, ); } return this.__cache__.ySublabelHandlebarsCompiled(context, runtimeOptions); } /** * gets the current title * * @returns yLabel */ get yLabelRight(): string { return this._yLabelRight; } /** * Sets the ylabel as simple string or a * handlebars template. * * @param value string to be the y label */ set yLabelRight(value: null | string) { if (!isDef(value)) { this._yLabelRight = ""; } else { this._yLabelRight = value; } this.__cache__.yLabelRightHandlebarsCompiled = null; } handlebarsYLabelRight(context: object, runtimeOptions: object): string { if (!isDef(this.__cache__.yLabelRightHandlebarsCompiled)) { if (!isDef(this._yLabelRight) || this._yLabelRight.length === 0) { // empty label return ""; } else { this.__cache__.yLabelRightHandlebarsCompiled = Handlebars.compile( this._yLabelRight, ); } } // don't think this can happen, keep typescript happy if (!this.__cache__.yLabelRightHandlebarsCompiled) { throw new Error( `Unable to compile handlebars yLabelRight for ${this._yLabelRight}`, ); } return this.__cache__.yLabelRightHandlebarsCompiled( context, runtimeOptions, ); } /** * Fake data to use to test alignment of seismograph axis and between canvas * and svg drawing. * * @param timeRange start and end of fake data * @param min min amplitude for fake data, default is -100 * @param max max amplitude for fake data, default is 100 * @returns fake data */ createAlignmentData( timeRange: Interval, min = -100, max = 100, ): SeismogramDisplayData { const mid = (max + min) / 2; const fakeData = Float32Array.from([ max, min, max, min, mid, mid, max, mid, mid, min, ]); const fakeSampleRate = 1 / ((1000 * timeRange.toDuration().toMillis()) / (fakeData.length - 1)); const fakeSeis = Seismogram.fromContiguousData( fakeData, fakeSampleRate, validStartTime(timeRange), ); const fakeSDD = SeismogramDisplayData.fromSeismogram(fakeSeis); return fakeSDD; } getColorForIndex(i: number): string { if (isDef(this.lineColors) && this.lineColors.length > 0) { return this.lineColors[i % this.lineColors.length]; } else { return "black"; } } createCSSForLineColors(svgClass?: string): string { let cssText = ""; const numColors = this.lineColors.length; let svgEl = "svg"; if (!isDef(svgClass)) { svgEl = `svg.${AUTO_COLOR_SELECTOR}`; } else if (svgClass.length === 0) { svgEl = "svg"; } else { svgEl = `svg.${svgClass}`; } // line width and fill for paths that are seisplotjsdata cssText = cssText + ` ${svgEl} g.${G_DATA_SELECTOR} g path { fill: none; stroke-width: 1px; } `; this.lineColors.forEach((color, index) => { cssText = cssText + ` ${svgEl} g.title text tspan:nth-child(${numColors}n+${index + 1}) { stroke: ${color}; fill: ${color}; color: ${color}; } `; // not used by actual waveform as default is canvas, not svg // is used by fftplot cssText += ` ${svgEl} g.${G_DATA_SELECTOR} g:nth-child(${numColors}n+${ index + 1 }) path { stroke: ${color}; } `; }); return cssText; } clone(): SeismographConfig { const out = new SeismographConfig(); Object.getOwnPropertyNames(this).forEach((name) => { // @ts-expect-error typescript can't handle reflections, but ok as just clone if (Array.isArray(this[name])) { // @ts-expect-error typescript can't handle reflections, but ok as just clone out[name] = this[name].slice(); } else { // @ts-expect-error typescript can't handle reflections, but ok as just clone out[name] = this[name]; } // handle margin separately out.margin = { top: this.margin.top, right: this.margin.right, bottom: this.margin.bottom, left: this.margin.left, toString: function () { return ( "t:" + this.top + " l:" + this.left + " b:" + this.bottom + " r:" + this.right ); }, }; }); // empty cache out.__cache__ = new SeismographConfigCache(); return out; } toString(): string { let outS = ""; Object.getOwnPropertyNames(this).forEach((name) => { // @ts-expect-error typescript can't handle reflections, but ok for tostring? outS += ` seisConfig.${name} = ${JSON.stringify(this[name])}\n`; }); return outS; } } export type SeismographConfigJsonType = { configId: number; showTitle: boolean; title: Array; isXAxis: boolean; xAxisTimeZone: string; isXAxisTop: boolean; xLabel: string; xLabelOrientation: string; xSublabel: string; xSublabelIsUnits: boolean; isYAxis: boolean; isYAxisRight: boolean; isYAxisNice: boolean; yLabel: string; yLabelRight: string; yLabelOrientation: string; ySublabel: string; ySublabelTrans: number; ySublabelIsUnits: boolean; doMarkers: boolean; markerTextOffset: number; markerTextAngle: number; markerFlagpoleBase: string; minHeight: number; maxHeight: null | number; minWidth: number; maxWidth: null | number; margin: MarginType; segmentDrawCompressedCutoff: number; //below this draw all points, above draw minmax maxZoomPixelPerSample: number; // no zoom in past point of sample // separated by pixels connectSegments: boolean; lineColors: Array; lineWidth: number; wheelZoom: boolean; amplitudeMode: AMPLITUDE_MODE; doGain: boolean; windowAmp: boolean; fixedAmplitudeScale: null | Array; fixedTimeScale: null | Interval; isLinkedAmplitudeScale: boolean; isLinkedTimeScale: boolean; isRelativeTime: boolean; }; export function numberFormatWrapper( formater: (arg0: number) => string, ): (domainValue: AxisDomain) => string { return function (domainValue: AxisDomain) { if (typeof domainValue === "number") { return formater(domainValue); } else { throw new Error("Can only format number, " + stringify(domainValue)); } }; } export const formatCount: (arg0: number) => string = d3format("~s"); export const formatExp: (arg0: number) => string = d3format(".2e"); export const formatCountOrAmp = function (v: number): string { return -1 < v && v < 1 && v !== 0 ? formatExp(v) : formatCount(v); }; export function createTimeFormatterForZone(timezone: Zone): (arg0: Date) => string { return (date: Date) => { if (timezone == null) {timezone = FixedOffsetZone.utcInstance;} const dt = DateTime.fromJSDate(date, {zone: timezone}); if (dt.millisecond !== 0) { return dt.toFormat(".SSS"); } else if (dt.second !== 0) { return dt.toFormat(":ss"); } else if (dt.minute !== 0) { return dt.toFormat("HH:mm"); } else if (dt.hour !== 0) { return dt.toFormat("HH:mm"); } else if (dt.day !== 0) { return dt.toFormat("LL/dd"); } else if (dt.month !== 0) { return dt.toFormat("yyyy/LL"); } else { return dt.toFormat("yyyy"); } }; } SeismographConfig._lastID = 0;