Override Date formatting tick hierarchy in lightningchart.js - heatmap

I'm trying to build a heatmap in LightningChart that covers a fixed 24 hours time span in 15 minutes intervals. For the sake of conveinience, that interval is inclusive, so if I'm reading a midnight-to-midnight interval, I expect to have the data for both midnights, making for an effective range of 24ΒΌ hours over 97 samples.
My problem is that, when using the datetime tick strategy, having an axis interval covering more than exactly 24 hours causes it to use the FormattingDay format, which uses a tick hierarchy of great: ISOWeek, major: DayOfMonth, minor: Hour, which is very inconveinient to read in this sort of situation.
On the left is when the great tick (ISO week, every monday) shows up in the middle of the axis (from Sunday August 21 to Monday August 22), and on the right is when only a major tick is visible (from Monday August 22 to Tuesday August 23). Both of these I find very confusing to read and not at all what I want the axis to look like. The use of months would actually be preferable, as ISO weeks aren't exactly a common layman time representation.
Is there a way to force a specific tick hierarchy to be used to label axes? Either great: Month, major: DayOfMonth, minor: Hour or great: DayOfMonth, major: Hour, minor: Minute would be good, but the default of great: ISOWeek is unusable to me.

That seems very inconvenient indeed.
Maybe in your case it would be actually easier to manually place each tick along the Y axis?
You can create custom ticks like this:
const tick = Axis.addCustomTick(UIElementBuilders.AxisTick)
Here's an example that you can reference for styling the ticks: https://www.arction.com/lightningchart-js-interactive-examples/edit/lcjs-example-0011-customTicksScrolling.html
In any case this is an important form of feedback, unfortunately there are a number of cases where the current DateTime ticks don't deliver.

Not a perfect solution, but a suitable workaround for the time being: I ended up just using the axis range extremums to detect when the value to be formatted is a great tick beyond what the axis should display (ticks pointing to the "start of the current week"/"start of next week" outside the chart have a value of midnight the previous Monday), and clamp it to the axis range, so they can just be formatted as dates.
myChart.getDefaultAxisY().setTickStrategy("DateTime", (ticks) => {
// If the axis range covers more than exactly 24h, LightningCharts uses FormattingDay instead of FormattingHour.
// That means one great tick at the start of every monday (2022-W33) but not the start every day (that's major ticks) or every month (August 1st is a regular major tick)
// This still makes for a great tick at the start of every Monday, but clamps the date to match what's inside the axis,
// and formats it like a date, so the "context" dates at both ends of the axis behave more or less like they do with FormattingHour.
return ticks.setFormattingDay(
(val, range, loc) => {
let adjustedVal;
if (range.getInnerStart() < range.getInnerEnd()) {
adjustedVal = Math.min(Math.max(range.getInnerStart(), val), range.getInnerEnd())
} else {
adjustedVal = Math.min(Math.max(range.getInnerEnd(), val), range.getInnerStart())
}
let formattedDate = new Date(adjustedVal);
return formattedDate.toLocaleDateString(loc);
},
{ month: 'long', day: '2-digit' }, // Midnight every day
{ hour: '2-digit', minute: '2-digit' } // Every hour
);
// return ticks.setFormattingDay(
// { month: 'long', day: '2-digit' }, // Midnight every ISO start of week (Monday)
// { month: 'long', day: '2-digit' }, // Midnight every day
// { hour: '2-digit', minute: '2-digit' }, // Every hour
// );
});

Related

How to process csv data (datetime) month, week, day, hour in highstock highcharts

I have a CSV file with following format:
<pre id="csv" style="display:none">
DATES,WHOLESALE,ECOMMERCE,RETAIL,LOANS,BONDISSUER
01/10/2018 00:00,25,16,13,1,0
01/10/2018 01:00,24,5,9,3,2
01/10/2018 02:00,28,6,17,0,6
The data range is 01/10/2018 00:00 - 31/10/2018 00:00
Interval is every hour.
I am using highstock stacked column with 5 categories: WHOLESALE,ECOMMERCE,RETAIL,LOANS,BONDISSUER.
My problem is, that the highstock navigator displays the data incorrectly. I think I have to customise property in range selector or navigator, but I can't find any documentation online. I tried inputDateParser, but it didn't work. Here is the jsfiddle
inputDateParser: function (value) {
value = value.split(/[:\.]/);
return Date.UTC(
1970,
0,
1,
parseInt(value[0], 10),
parseInt(value[1], 10),
parseInt(value[2], 10),
parseInt(value[3], 10)
);
}
How do I get the data range to be correct: month of October 2018 according to the dates in CSV?
I should not see a whole year in the navigator, when I only have data for October.
Thanks much appreciated
You would need to format the dates correctly, it can be done using the beforeParse callback function, like this:
data: {
csv: document.getElementById('csv').innerHTML,
beforeParse: function(e) {
let csv = e.split('\n'); //split by newline
let processedTable = []
processedTable.push(csv[0].split(','))
for (let i = 1; i < csv.length; i++) {
let row = csv[i].split(',');
if (row.length != 6) //skip empty rows or rows with more/less columns
continue;
let date = row[0].split(' ')[0].split('/')
let time = row[0].split(' ')[1].split(':')
processedTable.push(
[(new Date(date[2], date[1] - 1, date[0], time[0], time[1], 0)).getTime(), //get the timestamp for the date
parseInt(row[1]),
parseInt(row[2]),
parseInt(row[3]),
parseInt(row[4]),
parseInt(row[5])
].join(',')
)
}
return processedTable.join('\n') //join the array into a string again
},
},
Every row is parsed, by splitting it apart, the date is found, and milliseconds since 1970 is returned by getTime(). Then we join the cells into strings, and lastly the rows into a long string. The reason we convert this back into a string, is because highcharts is going to read it in from a string.
Working JSFiddle example: https://jsfiddle.net/ewolden/spmtgv3a/
API on beforeParse: https://api.highcharts.com/highcharts/data.beforeParse

Display separate text strings each day

I want to display a text string, or a bit of code each day of the week for two weeks, then it will repeat. So through the days it will display Monday 1, Tuesday 1...Friday 1, Monday 2, Tuesday 2...Friday 2. Then it will revert back to Monday 1. If there is a way without using I-frame (have to spell like this so it will let me post the question) from another site then please say. I have tested out with I-frame but it never seems to work. So maybe a counter which when reaching 14 will revert to 1 again. Thank you in advance!
You should be able to achieve it with pure JavaScript using the following:
Date.prototype.getWeek = function() {
var onejan = new Date(this.getFullYear(),0,1);
var today = new Date(this.getFullYear(),this.getMonth(),this.getDate());
var dayOfYear = ((today - onejan +1)/86400000);
return Math.ceil(dayOfYear/7)
};
var sentences = ["first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelvth", "thirsteenth", "fourteenth"];
var date = new Date();
var day = (date.getDay()+6)%7+1;
day = day * ((date.getWeek()%2)+1);
document.getElementById("container").innerHTML = sentences[day-1];
Fiddle: https://jsfiddle.net/04wha99e/1/
Using simple JavaScript, you create an array of sentences you wish to have, and basically get the current day of the week (JavaScript Date.getDay() returns Sunday as 0, hence the workaround) and based on the week number being odd or even you multiply it by 1 on 2 getting a range of 1-14 and then return the sentence from the array (starting index 0, hence the minus 1)
The above script assumes, you have a div with the id container, where the inner html gets replaced:
<div id="container">
This is where the sentence goes
</div>

Convert data types within a custom function

I have a Google Spreadsheets I have been using to keep track of my hours worked at my job. I am trying to create a custom function to calculate my total hours for the week. Say I work 6 hours of overtime, but then take Friday off. My regular hours would be 32, and I would have 6 hours or overtime. In the event that I don't work 40 hours, I would like to adjust my total hours by taking from any overtime hours and adding to my regular hours.
I have come up with the following function, but I have not yet been able to make it work. I believe I am running into a problem with data types (the inputs are Durations), but I'm not sure how to resolve it. I am dividing by 24 because that seems to convert the values from Duration to Number, but I still can't get it to return the correct answer.
function calcAdjRegHours(regHours, otHours) {
if(regHours<(40/24));
{
if(otHours>(0/24));
{
if((regHours + otHours)>(40/24));
{
var diff = (40/24) - regHours;
regHours += diff;
return regHours;
} elseif; {
return "regHours + otHours is less than 40";
}
} elseif; {
return "there are no otHours";
}
} elseif; {
return "regHours is greater than 40";
}
}
What am I overlooking, or am I making this overly complicated?
Edit: When I call this function with inputs of 40:00, and 2:00, I get the value:
Sun Dec 31 1899 17:00:00 GMT-0700 (MST)2208988800001.6665.
If I run this function:
function calcAdjRegHours(regHours, otHours) {
return ((regHours*24 + otHours*24)/24);
}
I get: -4418114400000.
If I use "return (regHours + otHours);", I get:
Sun Dec 31 1899 17:00:00 GMT-0700 (MST)Sat Dec 30 1899 03:00:00 GMT-0700 (MST).
Something is going wrong when I try to add the variables. They are formatted as Duration, and from my research I can/need to convert them to do arithmetic. I did that by multiplying the variables by 24, adding, and then dividing by 24 again to get it back to a duration.
I ended up using: =if(F14<(40/24), (F14+G14), if(F14=(40/24), F14, F14-(40/24))). That did the trick, although I would still like to come up with a custom function that would do it a little more nicely.

How do I create a SQL calendar with reccuring events that can be easily queried?

I checked several older questions regarding this topic like this one: Calendar Recurring/Repeating Events - Best Storage Method however, the answers are pretty bad performance-wise and cumbersome in implementation. From another answer, it's easy to tell why the accepted answer is a bad idea: a month of events takes 90 queries. Which is unacceptable.
Anyways, here's the issue I'm facing so that you don't have re-read those questions:
Storing events in such a way to allow them to recur. Think Google Calendar where you can specify patterns like "happens on the 1st of the month, every month" or "happens every 2nd monday of the month" (the latter is less important to me.
Querying a list of events for a time period. For example, I want to show someone a list of events for the next 2 months and I don't want to query for every single day of the month. Doing that would just kill the server (per user among thousands of rows of events)
DB agnostic. I use PgSQL and saw many answers for this question on other forums using either MS SQL-specific stuff or Oracle.
I'd appreciate any help! I read a couple of papers on the topic and still can't find something I can make work in SQL specifically.
The solution I have come up with is that I have an event table that has five fields defining the recurrence of the event, as defined below. I then also have a schedule table which I populate with the actual occurrence of the events. I do require an end date, and even when they specify something to go out to a couple years out, it is a monthly type event which does not create that many entries into the schedule table.
So, the event is stored in an event table, with a startDateTime and an endDateTime that describe the entire duration of the event if there is no recurrence. These two datetime fields also define the overall start and end of the event if it is a recurring event. In that same event table, we have the five fields defining recurrence, as laid out below.
The Schedule table stores individual occurrences of each event. So it has an eventId, startDateTime, and endDateTime. This start and end refer only to each occurrence, not the overall span.
For querying for all the scheduled occurrences happening in a period of time, I just query the schedule table checking for any occurrences that match this condition:
select * from schedule where schedule.startDateTime < #queryPeriodEnd and schedule.endDateTime > #queryPeriodStart
This query gives me only the schedule entries that happen partially or wholly within my query period. For getting the event data, it's a simple matter of joining to the event table.
The interesting part is calculating something like the second thursday of the month. That happens in the actual code for figuring out all the scheduled occurrences for a given event. I am also enclosing my code for that below.
EVENT RECURRENCE FIELDS
recurs
0=no recurrence
1=daily
2=weekly
3=monthly
recurs_interval
this is how many of the periods between recurrences. If the event recurs every 5 days, recurs_interval will have a 5 and recurs will have 1. If the event recurs every 3 weeks, recurs_interval will have a 3 and recurs will have a 2.
recurs_day
If the user selected monthly type recurrence, on a given day of the month (ex: 10th or the 14th). This has that date. The value is 0 if the user did not select monthly or specific day of month recurrence. The value is 1 to 31 otherwise.
recurs_ordinal
if the user selected a monthly type recurrence, but an ordinal type of day (ex: first monday, second thursday, last friday). This will have that ordinal number. The value is 0 if the user did not select this type of recurrence.
1=first
2=second
3=third
4=fourth
5=last
recurs_weekdays
for weekly and monthly-ordinal recurrence this stores the weekdays where the recurrence happens. 1=Sunday
2=Monday
4=Tuesday
8=Wednesday
16=Thursday
32=Friday
64=Saturday
So, every 4 weeks on Saturday and Sunday would be
recurs=2, recurs_interval=4, recurs_weekdays=65 (64 + 1)
Similarly, Every three months on the first Friday of the month would be
recurs=3, recurs_interval=3, recurs_ordinal=1, recurs_weekdays=32
CODE
thisEvent.occurrences = new List<ScheduleInstance>();
DateTime currentDateTime = (DateTime) thisEvent.start;
DateTime currentEndTime;
BitArray WeekDayRecurrenceBits = new BitArray(new Byte[] {(Byte) thisEvent.recursWeekdays});
while (currentDateTime < thisEvent.end)
{
currentEndTime = new DateTime(currentDateTime.Year, currentDateTime.Month, currentDateTime.Day,
thisEvent.end.Value.Hour, thisEvent.end.Value.Minute, thisEvent.end.Value.Second);
switch (thisEvent.recurs)
{
case (RecurrenceTypeEnum.None):
AddOccurrenceToRooms(thisEvent, currentDateTime, currentEndTime);
currentDateTime = (DateTime)thisEvent.end;
break;
case (RecurrenceTypeEnum.Daily):
AddOccurrenceToRooms(thisEvent, currentDateTime, currentEndTime);
currentDateTime = currentDateTime.AddDays(thisEvent.recursInterval);
break;
case (RecurrenceTypeEnum.Weekly):
int indexIntoCurrentWeek = (int) currentDateTime.DayOfWeek;
while ((indexIntoCurrentWeek < 7) && (currentDateTime < thisEvent.end))
{
if (WeekDayRecurrenceBits[(int) currentDateTime.DayOfWeek])
{
AddOccurrenceToRooms(thisEvent, currentDateTime, currentEndTime);
}
currentDateTime = currentDateTime.AddDays(1);
currentEndTime = currentEndTime.AddDays(1);
indexIntoCurrentWeek++;
}
currentDateTime = currentDateTime.AddDays(7 * (thisEvent.recursInterval - 1));
break;
case (RecurrenceTypeEnum.Monthly):
if (thisEvent.recursDay == 0)
{
DateTime FirstOfTheMonth = new DateTime(currentDateTime.Year, currentDateTime.Month, 1);
int daysToScheduleOccurrence = ((thisEvent.recursWeekdays - (int)FirstOfTheMonth.DayOfWeek + 7) % 7)
+ ((thisEvent.recursOrdinal - 1) * 7)
- currentDateTime.Day + 1;
if (daysToScheduleOccurrence >= 0)
{
currentDateTime = currentDateTime.AddDays(daysToScheduleOccurrence);
currentEndTime = currentEndTime.AddDays(daysToScheduleOccurrence);
if (currentDateTime < thisEvent.end)
{
AddOccurrenceToRooms(thisEvent, currentDateTime, currentEndTime);
}
}
}
else
{
if (currentDateTime.Day <= thisEvent.recursDay && thisEvent.recursDay <= DateTime.DaysInMonth(currentDateTime.Year, currentDateTime.Month) )
{
currentDateTime = currentDateTime.AddDays(thisEvent.recursDay - currentDateTime.Day);
currentEndTime = currentEndTime.AddDays(thisEvent.recursDay - currentEndTime.Day);
AddOccurrenceToRooms(thisEvent, currentDateTime, currentEndTime);
}
}
currentDateTime = currentDateTime.AddDays((currentDateTime.Day - 1) * -1).AddMonths(thisEvent.recursInterval);
break;
default:
break;
}
}

Why is 1401-01-01 a Saturday in Ruby but a Thursday in MySQL?

In Ruby:
> require 'time'
=> true
> Date.new(1401, 1, 1).saturday?
=> true
With MySQL:
SELECT dayofweek('1401-01-01')
This returns 5 which is Thursday.
In the OSX Calendar, this is also a Thursday.
What is causing this discrepancy?
I strongly suspect that one of those environments is taking into account the change from the Julian calendar to the Gregorian calendar (at various times since the 16th century depending on place).
Using my Noda Time library, we can see which is which:
using System;
using NodaTime;
class Test
{
static void Main()
{
var julian = new LocalDate(1401, 1, 1,
CalendarSystem.GetJulianCalendar(4));
var gregorian = new LocalDate(1401, 1, 1,
CalendarSystem.GetGregorianCalendar(4));
Console.WriteLine("Julian: {0}", julian.IsoDayOfWeek);
Console.WriteLine("Gregorian: {1}", gregorian.IsoDayOfWeek);
}
}
Output:
Saturday
Thursday
So it looks like Ruby has taken the transition into account, but MySQL hasn't.
The big question is: which calendar system were you interested in? (In 1401 you probably want the Julian one - but if you have dates in the 16th century or later, it becomes trickier...)