Can a echarts formatter function use the existing Cascading Templates functionality? - javascript

We have a chart with a time x axis. We need to tweak how the time labels are formatted in some cases, but for most cases, the very handy Cascading Templates work very nicely in Apache eCharts:
axisLabel: {
formatter: {
year: '{yyyy}',
month: '{MMM}',
day: '{dd}',
hour: '{HH}:{mm}',
minute: '{HH}:{mm}',
},
},
I would like to do something like:
axisLabel: {
formatter: (value: number, index: number) => {
if (index === 0 || index === data.length - 1) {
// print full timestamp for first and last label
return echarts.format.formatTime('{yyyy}-{MM}-{dd} {HH}:{mm}', value);
}
// and otherwise use the default from above ... possible?
},
},
I didn't find anything about this in the options docs.
In fact, echarts.format is not included in the API docs, but included in the typescript definition, so I assume it's not internally and can be used by developers ...?
Addition: eCharts does a really nice job that it e.g. automatically prints important labels bold - depending on the context:
And with a formatter function, this gets lost - or I didn't find a way yet how I can keep it.

Looking at the source I found that the formatter function is actually passed a 3rd argument with information about the tick that is missing in the documentation.
I opened PR !332 to improve documentation
Example:
// Use callback function; function parameters are axis index
formatter: function (value, index, tick) {
// Formatted to be month/day; display year only in the first label
var date = new Date(value);
var texts = [(date.getMonth() + 1), date.getDate()];
if (index === 0) {
texts.unshift(date.getYear());
}
let label = texts.join('/');
// apply bold style via rich text for significant levels, i.e. `level > 0`
return tick.level ? `{bold|${label}}` : label;
}

Related

Sorting and filtering on Date

I have a date column and need to be able to both sort and filter on it. The data comes in as strings like 2010-12-23 and can pre-processed as needed. It should be shown as 23.12.2010. Some internationalization will come later.
I wonder what's the proper internal representation:
a string like "23.12.2010" is bad for sorting (it could be done by sorting on function result, but it'd be slow)
a string like "2010-12-23" sorts correctly, can be formatted easily, but filtering for 23.12 does not work (it could be done, but it'd be slow)
Date would probably get sorted correctly, but filtering would be slow
moment could be the solution, no idea
My current idea is to create an object containing both milliseconds and the displayed string, so that all operations can be fast. But I'd bet that someone was that smart before me....
Let's assume that showing dates in the form like 2010-12-23 is unacceptable, otherwise the problem is solved. To summarize, the problem is that I need to
display and filter in the DD.MM.YYYY format
sort according to the numerical value (or equivalently, as if it was in th ISO format).
I think the method you're proposing wouldn't run in to too many performance issues, unless you're going for really old browsers or mobile devices.
I've mocked up an example to do a quick (performance) test. First, I'm defining an object that holds a value optimized for sorting, and a value optimized for display:
var MyDate = function(dateString) {
var date = new Date(dateString);
var displayValue = "{0}.{1}.{2}"
.replace("{0}", prefixZeroIfNeeded(date.getUTCDate()))
.replace("{1}", prefixZeroIfNeeded(date.getUTCMonth() + 1))
.replace("{2}", date.getUTCFullYear());
return {
sortKey: date.getTime(),
displayValue: displayValue
};
};
The prefixZeroIfNeeded method ensures we get the DD.MM format rather than the dd.mm one:
var prefixZeroIfNeeded = function(nr) {
return nr < 10 ? "0" + nr : "" + nr;
};
Then, we need some data to convert:
var data = [];
var myDates = data
.map(MyDate)
.sort(function(date1, date2) {
return date1.sortKey - date2.sortKey;
});
Finally, a quick example of a very basic search function:
var searchMyDates = function(str) {
return myDates.filter(function(myDate) {
return myDate.displayValue.indexOf(str) !== -1;
});
};
Now, we can create some mockup data and check how long it would actually take to A) map and sort the raw strings to the MyDate objects, and B) search for a string in our collection.
Here's how I generated the raw data:
for (var i = 0; i < 10000; i += 1) {
var y = Math.floor(Math.random() * 101) + 1900;
var m = prefixZeroIfNeeded(Math.floor(Math.random() * 13));
var d = prefixZeroIfNeeded(Math.floor(Math.random() * 29));
data.push(y + "-" + d + "-" + m);
}
Using console.time to measure, processing the data on my machine (A) takes around 40ms. Searching for the string .12. takes around 5-10ms.
Concluding: I think you were definitely on the right track and could continue work in the proposed direction. However, in my personal experience, I've learned that whenever I start work on a feature that involves dates and times, moment.js is the way to go. You'll eventually run in to daylight saving time, time zones, you name it and regret you thought it was simple...
Let me know if this is of any help.
Edit: the code in a snippet (check your browser console for output)
var data = [];
var prefixZeroIfNeeded = function(nr) {
return nr < 10 ? "0" + nr : "" + nr;
};
// Generate random data:
for (var i = 0; i < 10000; i += 1) {
var y = Math.floor(Math.random() * 101) + 1900;
var m = prefixZeroIfNeeded(Math.floor(Math.random() * 13));
var d = prefixZeroIfNeeded(Math.floor(Math.random() * 29));
data.push(y + "-" + d + "-" + m);
}
var MyDate = function(dateString) {
var date = new Date(dateString);
var displayValue = "{0}.{1}.{2}"
.replace("{0}", prefixZeroIfNeeded(date.getUTCDate()))
.replace("{1}", prefixZeroIfNeeded(date.getUTCMonth() + 1))
.replace("{2}", date.getUTCFullYear());
return {
sortKey: date.getTime(),
displayValue: displayValue
};
};
console.time("Map and sorting");
var myDates = data
.map(MyDate)
.sort(function(date1, date2) {
return date1.sortKey - date2.sortKey;
});
var searchMyDates = function(str) {
return myDates.filter(function(myDate) {
return myDate.displayValue.indexOf(str) !== -1;
});
};
console.timeEnd("Map and sorting");
console.time("Search");
console.log("Searching for the month 12, %d results.", searchMyDates(".12.").length);
console.timeEnd("Search");
This may help you a bit. I have used the same thing working with React. Here is a link for Moment.js -
http://momentjs.com/docs/#/displaying/format/
If you go under Display -> Format on the right menu bar, you'll see localized formats, you will need to use format L - pre defined format from moment which will show you 09/04/1986 (September 4, 1986); otherwise you can create your own using DD-MM-YYYY format.
For Example, The way I used in React for my exercise is
To define a variable using let:
let deadlineFormated = Moment(this.props.ToDoItem.deadline).format('llll');
Hope this helps for Angular!
Gist: Decouple sorting and filtering. Do sorting on the internal representation and filtering on the presentation.
Sort on internal representation that is in any naturally sortable format. Your raw YYYY-MM-DD date strings would work, so would parsing them into Date objects. The performance difference could be negligible unless you're dealing with lots and lots of rows -- but in that case you would already have other issues with latency and rendering performance.
It's more intuitive if free-text filtering is done on what's displayed (the presentation). So if you're formatting the dates as "May 7, 2016", do a substring match on that. If you're formatting the dates as DD.MM.YYYY, do a substring match on that.
If filtering is driven by actual date selections from controls like a date picker or a select field, you can do the filtering on the internal representation.
Try with this:
Get Unixtimestamp for date (i.e. Numeric format) and use jquery sort.
Please check this example for jquery sort. Regarding example replace your unixtimestamp to value.
<ul id="datelist">
<li value="1360013296">Date 1</li>
<li value="1360013297">Date 2</li>
<li value="1360013298">Date 3</li>
<li value="1360013299">Date 4</li>
</ul>
https://jsfiddle.net/ajaygokhale/bohgoq8o/
To reliably implement sorting converting it into a Date Object is recommended (new Date(str))
If you need to be flexible in formatting moment has formatting support (check moment.format()) as well. Moment has pretty deep locale support as well.
You can always keep it a Date Object as the source of truth and for filtering you could do Date.toString() just at the time of filtering. This returns a string you could then filter with.

Show only one month name in FullCalendar when WeekView wraps two different months

I am developing using fullcalendar.js, and in the week view, when a week is from 2 different months (for example 27 july - 2 august) the fullcalendar week view shows the two months text. I am searching everywhere but there is no solution for this. Maybe stackoverflow users can help me.
That's what I have:
And that's what I need:
I see the date format but is MMMM YYYY, and it returns two months or one automatically and it seems impossible to change this.
In Calendar.defaults (aprox. line 8300 in non-minimized code) object I can notice this:
titleRangeSeparator: ' \u2014 ', // emphasized dash
monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option
As I explained, monthYearFormat seems to only be one month, but in a specific moment it merges with titleRangeSeparator to become two months.
Do you know how this is solvable?
Thank you.
EDIT
I found the functions that make this complex string, but is used by month and day views that I don't want to change (I need only to week view). The code is the next. How can I modify this code to solve it?
// Date Range Formatting
// -------------------------------------------------------------------------------------------------
// TODO: make it work with timezone offset
// Using a formatting string meant for a single date, generate a range string, like
// "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
// If the dates are the same as far as the format string is concerned, just return a single
// rendering of one date, without any separator.
function formatRange(date1, date2, formatStr, separator, isRTL) {
var localeData;
date1 = fc.moment.parseZone(date1);
date2 = fc.moment.parseZone(date2);
localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8
// Expand localized format strings, like "LL" -> "MMMM D YYYY"
formatStr = localeData.longDateFormat(formatStr) || formatStr;
// BTW, this is not important for `formatDate` because it is impossible to put custom tokens
// or non-zero areas in Moment's localized format strings.
separator = separator || ' - ';
return formatRangeWithChunks(
date1,
date2,
getFormatStringChunks(formatStr),
separator,
isRTL
);
}
fc.formatRange = formatRange; // expose
function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
var chunkStr; // the rendering of the chunk
var leftI;
var leftStr = '';
var rightI;
var rightStr = '';
var middleI;
var middleStr1 = '';
var middleStr2 = '';
var middleStr = '';
// Start at the leftmost side of the formatting string and continue until you hit a token
// that is not the same between dates.
for (leftI=0; leftI<chunks.length; leftI++) {
chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]);
if (chunkStr === false) {
break;
}
leftStr += chunkStr;
}
// Similarly, start at the rightmost side of the formatting string and move left
for (rightI=chunks.length-1; rightI>leftI; rightI--) {
chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]);
if (chunkStr === false) {
break;
}
rightStr = chunkStr + rightStr;
}
// The area in the middle is different for both of the dates.
// Collect them distinctly so we can jam them together later.
for (middleI=leftI; middleI<=rightI; middleI++) {
middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
}
if (middleStr1 || middleStr2) {
if (isRTL) {
middleStr = middleStr2 + separator + middleStr1;
}
else {
middleStr = middleStr1 + separator + middleStr2;
}
}
return leftStr + middleStr + rightStr;
}
This isn't directly supported unfortunately, but there is still a better way than modifying the FC source (that get's messy with patches and stuff).
There are several render hooks available that we can use to fix the formatting after the fact. viewRender doesn't work because it's called before the title changes. So we can use eventAfterAllRender instead.
eventAfterAllRender:function(){
if(view.name!=="agendaWeek")
return;
var $title = $("#calendar").find(".fc-toolbar h2"); //Make sure this is the right selector
var text = $title.text();
text = text.match(/.*? /)+text.match(/[0-9]+/);
$title.text(text); //replace text
}
JSFiddle - titleFormat hack
Not the most elegant thing in the world, but it should work better than modifying the source. Let me know if there are any issues.
Edit:
Also, if you're having problems with it flashing the wrong dateformat before the correct one, use css the make the title invisible. Then add a class to the element in eventAfterAllRender that makes it visible again.

validate and sort date in jquery

I am using the table sorter plugin to sort my tables.
I want to be able to catch date column in format:
dd/MM/yyyy HH:mm
and then sort them correctly (for this I have to switch days with years).
Here is what I've so far:
ts.addParser({
id: "hebreLongDate",
is: function (s) {
return /\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4} d{1,2}:d{1,2}/.test(s);
}, format: function (s, table) {
var c = table.config;
s = s.replace(/\-/g, "/");
// reformat the string in ISO format
s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1");
return $.tablesorter.formatFloat(new Date(s).getTime());
}, type: "numeric"
});
It does not work.
I would appreciate any help, especially if it comes with an explantation on the meaning of the correct regex.
Thanks,
Omer
The parser doesn't really validate the date. The is function only detects if the format matches the pattern for the format function which is why it is just easier to make it return false and manually set the parser for a column using the headers option:
headers: {
1: { sorter: "hebreLongDate" }
},
The is function above is requiring a HH:mm within the pattern, so if the first table cell in the column doesn't match, it ignores that parser. So either way it would be better to manually set the parser.
Anyway, here is how I would write the parser you are describing (demo):
$.tablesorter.addParser({
id: "hebreLongDate",
is: function(s) {
return false;
},
format: function(s, table, cell, cellIndex) {
s = s
// replace separators
.replace(/\s+/g," ").replace(/[\-.,]/g, "/")
// reformat dd/mm/yyyy to yyyy/mm/dd
.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$2/$1");
return s ? $.tablesorter.formatFloat( (new Date(s).getTime() || ''), table) : s;
},
type: "numeric"
});
As for explaining the regex, there isn't that much of a difference between the code above and what you have in your question. The biggest difference is that the above code ensures that only one space exists between the date and time and that the date can be separated by a slash, dash, period, comma or space (i.e. 1-1-2000, 1 1 2000 etc).
Update: if you want to have this parser be autodetected, then use the following is regex (updated demo). But it is important to note, that this regex cannot distinguish mmddyyyy from ddmmyyyy so it will always detect ddmmyyyy. To override this, set the header sorter option to "shortDate":
is: function(s) {
// testing for ##-##-####, so it's not perfect; time is optional
return (/(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})/).test((s || '').replace(/\s+/g," ").replace(/[\-.,]/g, "/"));
},

jquery tablesorter: addParser does not sort properly

I have created a table and sorted it with tablesorter. One of the columns is a mix of letters and numbers (chr1, chr2, ..., chr10, ... , chrM). I want this column to be sorted as if it were only numbers (from 1 to 22 and then X, Y and M in this order).
I have created my own parser and it works but just for some rows. Then, I find another block of rows which are correctly sorted followed by some other blocks. I do not know why this blocks are formed.
The code is here. Maybe with a smaller table it would work properly, because of that I have shown a big one.
Thanks in advance!
Try this parser (demo)
$.tablesorter.addParser({
// set a unique id
id: 'chrom',
is: function (s) {
// return false so this parser is not auto detected
return false;
},
format: function (s) {
// format your data for normalization
return s.toLowerCase()
.replace('chr', '')
.replace('x', '97')
.replace('y', '98')
.replace('m', '99');
},
// set type, either numeric or text
type: 'numeric'
});

jQuery table sorting and currency issue

This one may be a bit specialized, but here goes anyway:
Page for reference: http://greg-j.com/icvm/anticartel/search-results.html
Plugin for reference: http://tablesorter.com/
If you look at the last two columns for "Total Fines", you'll see the currency output includes $x.xxx billions and $x.xxx millions. The built-in currency parser does not account for this format. The plugin, fortunately, allows for you to write your own parser. However, I'm not getting anywhere.
See if this works, I haven't tested it:
$.tablesorter.addParser({
id: 'monetary',
'is': function(s) {
return false;
},
format: function(s) {
var i = s.split('$').join('');
var suffixes = [
{name:'thousand', mult: 1000},
{name:'million', mult: 1000000},
{name:'billion', mult: 1000000000},
{name:'trillion', mult: 1000000000000}
];
for (var j in suffixes) {
if (i.indexOf(' '+suffixes[j].name) != -1) {
i = i.split(' '+suffixes[j].name).join('');
val = parseFloat(i) * suffixes[j].mult;
}
}
return val;
},
type: 'numeric'
});
$("#cartels").tablesorter({
widgets: ['zebra'],
sortList: [[0,0]],
headers: {
4: {
sorter:'monetary'
},
5: {
sorter:'monetary'
}
}
});
Can you post the code you've tried?
It looks like what they are doing in the example you posted is assigning each word with a number representation, and then sorting by that:
return s.toLowerCase().replace(/good/,2).replace(/medium/,1).replace(/bad/,0);
So in your case one way might be to replace million with the correct number of zeros and the same for billion. So essentially $1 million gets evaluated to $1,000,000 as far as the parser is concerned.
return s.toLowerCase().replace(/million/,000000).replace(/billion/,000000000);
So s is evaluating to $1000000 once the replace function is evaluated.
Just a thought. Not sure if it works, but it might get you on the right track.

Categories