Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Time format depending on the locale (bcp47) #474

Open
denyaalt opened this issue Mar 20, 2021 · 9 comments · May be fixed by #985
Open

Time format depending on the locale (bcp47) #474

denyaalt opened this issue Mar 20, 2021 · 9 comments · May be fixed by #985
Labels
discussion question Further information is requested

Comments

@denyaalt
Copy link

denyaalt commented Mar 20, 2021

Hi,
How to display time in locale specific format (bcp47 language tag)?

const event=new Date();
const options={timeZone:'UTC',year:'numeric',month:'numeric',day:'numeric',hour:'numeric',minute:'numeric',second:'numeric'};

console.log(event.toLocaleDateString('ko-KR', options)); // → "2021. 3. 20. 오후 3:40:17"
console.log(event.toLocaleDateString('en-GB', options)); // → "20/03/2021, 15:40:17"
console.log(event.toLocaleDateString('en-US', options)); // → "3/20/2021, 3:40:17 PM"
console.log(event.toLocaleDateString('de-DE', options)); // → "20.3.2021, 15:40:17"
console.log(event.toLocaleDateString('ru-RU', options)); // → "20.03.2021, 15:40:17"

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString

Thanks.

@denyaalt denyaalt changed the title Time format depending on the locale Time format depending on the locale (bcp47) Mar 20, 2021
@leeoniya
Copy link
Owner

leeoniya commented Mar 20, 2021

there are a lot of possibilities here, and some will work while others won't.

here is my initial attempt: https://jsfiddle.net/vhq7dgew/1/

// cache the Intl formatter
let fmtOpts = {timeZone:'UTC',year:'numeric',month:'numeric',day:'numeric',hour:'numeric',minute:'numeric',second:'numeric'};
let format = new Intl.DateTimeFormat('ko-KR', fmtOpts).format;

function utcDate(ts) {
  return uPlot.tzDate(new Date(ts * 1e3), 'Etc/UTC');
}

let opts = {
  title: "Korean / UTC",
  tzDate: utcDate,
  width: 640,
  height: 280,
  axes: [
    {
      // axis ticks formatter
      values: (u, splits) => splits.map(ts => format(utcDate(ts)))
    }
  ],
  series: [
    {
      // legend value formatter (doesnt get proper UTC timezone)
      value: (u, ts) => format(utcDate(ts)),

      // uPlot's formatter works okay:
  //  value: "{YYYY}-{MM}-{DD} {HH}"
    },
    {
      stroke: "red",
    }
  ],
};

let data = [
  [1609459200, 1609480800],
  [        35,         71],
];

let uplot = new uPlot(opts, data, document.body);

unfortunately, it looks like the browser's built-in Intl formatter doesnt properly pick up the timezone-shifted date. uPlot's internal formatter does work, and you can use uPlot.fmtDate() to produce correct output for the indicated timezone. however, there is more involved here. first, the axis tick formatters have many different formats depending on zoom level and you would have to provide a config array for axis.values to properly format the axis ticks how you need:

uPlot/src/opts.js

Lines 137 to 150 in 1692dc3

// [0]: minimum num secs in the tick incr
// [1]: default tick format
// [2-7]: rollover tick formats
// [8]: mode: 0: replace [1] -> [2-7], 1: concat [1] + [2-7]
const _timeAxisStamps = [
// tick incr default year month day hour min sec mode
[y, yyyy, _, _, _, _, _, _, 1],
[d * 28, "{MMM}", NLyyyy, _, _, _, _, _, 1],
[d, md, NLyyyy, _, _, _, _, _, 1],
[h, "{h}" + aa, NLmdyy, _, NLmd, _, _, _, 1],
[m, hmmaa, NLmdyy, _, NLmd, _, _, _, 1],
[s, ss, NLmdyy + " " + hmmaa, _, NLmd + " " + hmmaa, _, NLhmmaa, _, 1],
[ms, ss + ".{fff}", NLmdyy + " " + hmmaa, _, NLmd + " " + hmmaa, _, NLhmmaa, _, 1],
];

you would also have to provide the korean weekday and month names (short and long). you can see how it's done here: https://github.com/leeoniya/uPlot/blob/master/demos/months-ru.html

unfortunately it's not as easy as just setting some locale parameter, but it is possible if you need a specific locale, and it is not a lot of code. alternatively you can maybe use something like a 40KB dependency: https://github.com/spencermountain/spacetime

if you would be interested in working through the config natively in uPlot, i can help (it would be a good demo to add), but you'll need to provide templates for the localization formats you want as well as the month/weekday names.

@leeoniya leeoniya added the question Further information is requested label Mar 20, 2021
@denyaalt
Copy link
Author

denyaalt commented Mar 20, 2021

Using the toLocaleDateString function, we can get any string names of the days of the week (short and long) and the names of the month (short and long).
And we will not need to manually fill the arrays with the names of the months, as in your example (demos/months-ru.html)

const event = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));
console.log(event.toLocaleDateString('ru-RU',{month: 'short'})); // → "дек."
console.log(event.toLocaleDateString('ru-RU',{month: 'long'})); // → "декабрь"

We can also choose to write the year in 2 digits or in 4 digits with the {year: '2-digit'} and {year: 'numeric'} options.

const event = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));
console.log(event.toLocaleDateString('ru-RU',{year:'2-digit'})); // → "12"
console.log(event.toLocaleDateString('ru-RU',{year:'numeric'})); // → "2012"

I think the most correct way would be to replace the string patterns ("{MMM}", "{DD}.{MM}.{YY}", etc.) with option objects for the toLocaleDateString function.

values:[// 	            default                                   year                                                                           month  day                                                              hour  min                                sec  mode
	[3600 * 24 * 365,   {year:'numeric'},                         null,                                                                           null, null,                                                            null, null,                              null, 1],
	[3600 * 24 * 28,    {month:'short'},                          {year:'numeric'},                                                               null, null,                                                            null, null,                              null, 1],
	[3600 * 24,         {month:'long',day:'numeric'},             {year:'numeric'},                                                               null, null,                                                            null, null,                              null, 1],
	[3600,              {hour:'numeric'},                         {year:'2-digit',month:'numeric',day:'numeric'},                                 null, {month:'numeric',day:'numeric'},                                 null, null,                              null, 1],
	[60,                {hour:'2-digit',minute:'2-digit'},        {year:'2-digit',month:'numeric',day:'numeric'},                                 null, {month:'numeric',day:'numeric'},                                 null, null,                              null, 1],
	[1,                 {second:'2-digit'},                       {year:'2-digit',month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'}, null, {month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'}, null, {hour:'2-digit',minute:'2-digit'}, null, 1],
	[0.001,             {second:'2-digit',millisecond:'numeric'}, {year:'2-digit',month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'}, null, {month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'}, null, {hour:'2-digit',minute:'2-digit'}, null, 1],
]

With this option, we do not need to add a slash between the date and month for the "en-US" locale or dot for the "de-DE" locale.

Please think about this idea, it will be a universal solution for any locale for multilingual applications.

P.S.
And I would also allow the timestamp to be entered in milliseconds, as js provides by default (for example add option on initialization).
Because now there is double math going on, we divide the timestamp by 1000, and uPlot then multiplies our timestamp back by 1000. This is an unnecessary action.

@leeoniya
Copy link
Owner

leeoniya commented Mar 21, 2021

Using the toLocaleDateString function, we can get any string names of the days of the week (short and long) and the names of the month (short and long). And we will not need to manually fill the arrays with the names of the months, as in your example (demos/months-ru.html)

that's a good point / idea :)

I think the most correct way would be to replace the string patterns ("{MMM}", "{DD}.{MM}.{YY}", etc.) with option objects for the toLocaleDateString function. With this option, we do not need to add a slash between the date and month for the "en-US" locale or dot for the "de-DE" locale.

if you look at the default config array, you'll see that many tick formats have 2 lines, so there needs to be linebreak support -- easy with templates, but with Intl options objects i guess we'd need to support arrays in each slot. another point is that {year:'2-digit',month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'} is really difficult to visually parse to get an idea of the output, especially its length, which is important for adjusting tick density, etc...but i do understand that the trade-off is universal compat across locales. some good news is that the performance is comparable: https://jsfiddle.net/20aznd7w/2/, and does not have the overhead of creating intermediate tzDate objects, which is actually pretty expensive. also support for the required functions looks okay: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat

if you want to put some work into a PR, i'd be open to additionally supporting config objects, or maybe already-compiled formatting functions in that config array and other places that accept fmtDate templates. formatting functions would have the benefit of already encoding the locale (new Intl.DateTimeFormat('en-US', fmtOpts).format). the tweaks to the compiler would be pretty simple, and we'd have to tweak everywhere that calls fmtDate and pass plain Date objects and not tzDate-adjusted date objects.

uPlot/src/opts.js

Lines 263 to 267 in 1692dc3

export function timeAxisStamps(stampCfg, fmtDate) {
return stampCfg.map(s => s.map((v, i) =>
i == 0 || i == 8 || v == null ? v : fmtDate(i == 1 || s[8] == 0 ? v : s[1] + v)
));
}

P.S. And I would also allow the timestamp to be entered in milliseconds, as js provides by default (for example add option on initialization). Because now there is double math going on, we divide the timestamp by 1000, and uPlot then multiplies our timestamp back by 1000. This is an unnecessary action.

it exists:

uPlot/dist/uPlot.d.ts

Lines 322 to 323 in 1692dc3

/** timestamp multiplier that yields 1 millisecond */
ms?: 1e-3 | 1; // 1e-3

the reasoning behind using unix timestamps originally is that for most datasets of server monitoring, 1s is sufficient resolution, and over the wire adding 000 to every timestamp bloats the payload pretty fast for no good reason. the math is extremely cheap though. anyways, both are supported now. it still multiplies by 1 then internally ;)

@denyaalt
Copy link
Author

You can call toLocaleDateString for each string separately with different parameters.
And in values, write an array of object options, which can then be combined into several lines.

values: [... [{month:'short'},{year:'numeric'}] ...]
values[n].map((options)=>event.toLocaleDateString('de-DE',options)).join("\n")

I'm new to js and some uPlot code is hard to understand, so I probably won't be able to change functions in uPlot ...
But I can write some function here, tell me what we have at the input and what should we get at the output?

@leeoniya
Copy link
Owner

can you provide a config array that uses opts objects (and arrays for line breaks) that should produce the matching tick formats as the existing one based on fmtDate templates? i would like to see what this looks like.

@denyaalt
Copy link
Author

Unfortunately, I haven't written anything yet, this is just an idea that it would be better to use locales to output the date in uPlot.
I can write some part of the functions, but it is difficult for me to understand the internal uPlot code.
If you tell me what is the input and what data we should receive at the output of the function, I will write and post my version here.

@leeoniya
Copy link
Owner

leeoniya commented Mar 23, 2021

I can write some part of the functions, but it is difficult for me to understand the internal uPlot code.

you don't need to understand the internals for what i am asking.

i would like for you to create an equivalent tick formatting table using opts objects (as discussed above). the current output needs to be reproducible using the opts objects format. if you can prove this can be done, then i will do some additional work to test it in uPlot. you can paste your converted table in this issue (no need to integrate into uPlot yet, i will do this). i do not know the exact details of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat to properly reproduce the current table, and do not have time to do so currently.

uPlot/src/opts.js

Lines 137 to 150 in 1692dc3

// [0]: minimum num secs in the tick incr
// [1]: default tick format
// [2-7]: rollover tick formats
// [8]: mode: 0: replace [1] -> [2-7], 1: concat [1] + [2-7]
const _timeAxisStamps = [
// tick incr default year month day hour min sec mode
[y, yyyy, _, _, _, _, _, _, 1],
[d * 28, "{MMM}", NLyyyy, _, _, _, _, _, 1],
[d, md, NLyyyy, _, _, _, _, _, 1],
[h, "{h}" + aa, NLmdyy, _, NLmd, _, _, _, 1],
[m, hmmaa, NLmdyy, _, NLmd, _, _, _, 1],
[s, ss, NLmdyy + " " + hmmaa, _, NLmd + " " + hmmaa, _, NLhmmaa, _, 1],
[ms, ss + ".{fff}", NLmdyy + " " + hmmaa, _, NLmd + " " + hmmaa, _, NLhmmaa, _, 1],
];

@denyaalt
Copy link
Author

Ok, when I have the result, I'll post it here.

@Christian-Sander
Copy link

Christian-Sander commented Oct 11, 2021

I think this might be more or less what you're looking for, please correct me if you mean something else.
https://jsfiddle.net/8axt0odu/

It would require to exchange the formatting function (fmtDate?) with Intl.DateTimeFormat().format() and add new lines and colons where needed manually. I think this should not be a problem because these are inserted at defined places?

Also it would be good if it was possible to switch between automatic locale and manually set locale so the user can switch the language if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion question Further information is requested
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants