I'm wondering if it's possible to define a custom ordering or format for the xAxis in highcharts. My dataset has a date time which would be used for the xAxis however my client has specified that it should show the 'Period' on the xAxis. A period is specified as a 30 minute slice - meaning there are 48 periods in a day.
The range of the data is from a period on the previous day to the current day with that period. For example 06/03/2017 Period 11 (10:00) to 07/03/2017 Period 11, the xAxis should look like so:
Currently I've attempted to do this by fiddling with the actual dateTime of each point, so that it is set to Y-m-d H:i:{Period} and then using the dateFormat to only show seconds on the xAxis. However this leaves a gap in data between 2017-03-06 23:59:{Period 48} and 2017-03-07 00:00:{Period 11}
Assuming your series-data is arranged as pairs of [<timestamp in milliseconds>, <value>] you can leave your data alone and simple do calculations and presentation of the label.
To show the labels as we want we use xAxis.labels.formatter (and possibly tickInterval to space them as we want). For example:
xAxis: {
type: 'datetime',
labels: {
formatter: function() {
return createLabelFromTimestamp(this.value);
}
},
tickInterval: 1800000
}
The calculation of the period number (as described in question) can be done for example like this:
function createLabelFromTimestamp(timestamp) {
var hms = timestamp % (1800000 * 48);
var period = hms / 1800000;
// if 0-th period, show date instead
if(period == 0) {
var date = new Date(timestamp);
return date.toDateString();
}
// otherwise show period number
else {
return period;
}
}
We simply modulo away the day and divide to find what 30-minute segment we are in.
See this JSFiddle example of it in action.
Related
I have a Chart with WeatherKit.HourWeather objects spanning over multiple days on the x axis. However, I want to exclude the nighttime hours. It looks like I can do this with the chartXScale modifier like this:
let myDataSeperatedByHours = arrayWithAllDates.filter { ... }.sorted(...) // Array of WeatherKit.HourWeather objects filtered by isDaylight = true and sorted by date
let allDaytimeDates = myDataSeperatedByHours.map { $0.date } //only the Date objects
Chart {
ForEach(myDataSeperatedByHours, id: \.date) { hourData in
LineMark(
x: .value("hour", hourData.date),
y: .value("value", hourData.value)
)
}
}
.chartXAxis {
AxisMarks(position: .bottom, values: allDaytimeDates) { axisValue in
if let date = axisValue.as(Date.self) {
AxisValueLabel(
"\(Self.shortTimeFormatter.calendar.component(.hour, from: date))"
)
}
}
}
.chartXScale(domain: allDaytimeDates, type: .category)
However the Chart still displays part where there is no value. (the nighttime)
I want everything removed when there is night. I've marked it green on the image below. Maybe I have to use two Charts next to each other. One for every day, but I can't believe that there's no way to do it with one Chart only.
I've created an example app that you can download and test here: https://github.com/Iomegan/DateChartIssue
As per chart scale modifier documentation for domain parameter:
The possible data values along the x axis in the chart. You can define the domain with a ClosedRange for number or Date values (e.g., 0 ... 500), and with an array for categorical values (e.g., ["A", "B", "C"])
It seems for date type values this function is expecting a range but since you are specifying an array the method invocation traps.
Instead of providing the domain directly, you can provide an automatic scale domain modifying the inferred domain. To set the domain to your calculated allDaytimeDates use:
.chartXScale(domain: .automatic(dataType: Date.self) { dates in
dates = allDaytimeDates
})
Update 1
There are multiple approaches you can try to ignore night time date scale on X-axis. The simpler and not recommended approach is to provided X-axis value in your line mark as a String instead of a Date.
The issue with specifying X-axis value as Date is you can only supply a range for the axis scale and you can't just pick multiple ranges as scale for your axis as of now and similarly you can't specify your scale to ignore certain range or values (i.e. night time). With specifying X-axis value as string you will be able to just ignore night time values:
LineMark(
x: .value("hour", "\(hourData.date)"),
y: .value("value", hourData.value)
)
The demerit with this approach is temprature variations as obtained from this graph is wrong as all your data points will be just separated equally regardless of their date value.
The preferred approach is to manually adjust the X-axis position for next day's data points. For your scenario you can create a DayHourWeather type with custom X-position value:
struct DayHourWeather: Plottable {
let position: TimeInterval // Manually calculated X-axis position
let date: Date
let temp: Double
let series: String // The day this data belongs to
var primitivePlottable: TimeInterval { position }
init?(primitivePlottable: TimeInterval) { nil }
init(position: TimeInterval, date: Date, temp: Double, series: String) {
self.position = position
self.date = date
self.temp = temp
self.series = series
}
}
You can customize the position data to move daytime plots closer together ignoring night time values. Then you can create DayHourWeathers from your HourWeathers:
/// assumes `hourWeathers` are filtered containing only day time data and already sorted
func getDayHourWeathers(from hourWeathers: [HourWeather]) -> [DayHourWeather] {
let padding: TimeInterval = 10000 // distance between lat day's last data point and next day's first data point
var translation: TimeInterval = 0 // The negetive translation required on X-axis for certain day
var series: Int = 0 // Current day series
var result: [DayHourWeather] = []
result.reserveCapacity(hourWeathers.count)
for (index, hourWeather) in hourWeathers.enumerated() {
defer {
result.append(
.init(
position: hourWeather.date.timeIntervalSince1970 - translation,
date: hourWeather.date,
temp: hourWeather.temp,
series: "Day \(series + 1)"
)
)
}
guard
index > 0,
case let lastWeather = hourWeathers[index - 1],
!Calendar.current.isDate(lastWeather.date, inSameDayAs: hourWeather.date)
else { continue }
// move next day graph to left occupying previous day's night scale
translation = hourWeather.date.timeIntervalSince1970 - (result.last!.position + padding)
series += 1
}
return result
}
Now to plot your chart you can use the newly created DayHourWeather values:
var body: some View {
let dayWeathers = getDayHourWeathers(from: myDataSeperatedByHours)
Chart {
ForEach(dayWeathers, id: \.date) { hourData in
LineMark(
x: .value("hour", hourData.position), // custom X-axis position calculated
y: .value("value", hourData.temp)
)
.foregroundStyle(by: .value("Day", hourData.series))
}
}
.chartXScale(domain: dayWeathers.first!.position...dayWeathers.last!.position) // provide scale range for calculated custom X-axis positions
}
Note that with above changes your X-axis marker will display your custom X-axis positions. To change it back to the actual date label you want to display you can specify custom X-axis label:
.chartXAxis {
AxisMarks(position: .bottom, values: dayWeathers) {
AxisValueLabel(
"\(Self.shortTimeFormatter.calendar.component(.hour, from: dayWeathers[$0.index].date))"
)
}
}
The values argument for AxisMarks only accepts an array of Plottable items, this is why confirming DayHourWeather to Plottable is needed. After above changes the chart obtained will look similar to this:
Note that I have created a different series for each day data. Although you can combine them into a single series, I will advise against doing so as the resulting chart is misleading to viewer since you are removing part of the X-axis scale.
I used configured Highstock's "tickPositioner" to set ticks dynamically for different ranges of data. For example, if the range is 1 hour max => 1 tick every 10 minutes. Here is some code of the function I put in tickPositioner (which is in xAxis config):
if (xDataRange <= oneHour) {
// If range is 1 hour max => 1 tick every 10 minutes
increment = oneMinute*10;
positions.info.unitName = "minute";
} else if (xDataRange > oneHour && xDataRange <= oneDay) {
// If range is between 1 hour and 1 day => 1 tick every hour
increment = oneHour;
positions.info.unitName = "hour";
} else { ... }
Here is an illustrating fiddle : http://jsfiddle.net/E6GHC/124/
(I know that the range choices are not the best here.)
The QUESTION: I would like to do the same with the minorTicks.
As you can see in the Fiddle, when you click on the month button the ticks are positionned each week and minorTicks are each day. But this is static configuration (minorTickInterval: oneDay).
I have some ideas and I have tried them and nothing seems to work out..
So if someone have any suggestion ? I would be veryyy thankful.
The perfect thing would have been to be have to set/update the minorTickInterval in tickPositioner function.
I have set up a fiddle which allows you to move the highstock navigator and see underneath the times selected plus a sum of the values for the selected period.
http://jsfiddle.net/o8dLh3m5/3/
The problem I am having is that when the selection contains too much data, the chart.series[0].data array is empty so I can't calculate the total.
Could someone please explain what is happening (ie where is this threshold set?), and what are my options for calculating totals when the data returned is larger than the threshold.
Thanks in advance,
xAxis:{
type: 'datetime',
events: {
afterSetExtremes:function(event){
// convert to dd/mm/yyyy hh:mm
var start_date = new Date(event.min);
var end_date = new Date(event.max);
$('#id_start_date').text( moment(start_date).format('DD/MM/YYYY HH:mm') );
$('#id_end_date').text( moment(end_date).format('DD/MM/YYYY HH:mm') );
// get totals
var sum = 0, chartOb = this;
$.each(chartOb.series, function(series_id){
$.each(chartOb.series[series_id].data, function(i,point){
// array returned is empty!
When number of points exceeds cropThreshold, then array can be empty. I think it would be better to use series.processedYData to calculate that sum. That is just an array with actually displayed y-values on a chart in one series.
Note: It's not part of official API but can be used ;)
Is there a way to get only three steps on the xAxis, one at start, one in the middle and one in the end. I've played around with xAxis.labels.steps but I couldn't get an reliable result.
Note the xAxis type is datetime.
There is at least three ways to achieve that (which are automatically):
Use tickPositioner (the best IMHO): you have full access to decide where tick should be set (for example: [min, average, max]).
Use tickPixelInterval: useful only when you have fixed width of the chart, and when interval will be multiple of ncie numbers (like 0 - 1 - 2, or 100 - 200 - 300)
Use tickInterval: useful only when you know range of xAxis, for example 0-10, so you can set tickInterval: 5
And jsfFiddle with compare: http://jsfiddle.net/FQ68Y/3/
You can do this with the tickPositions option or the tickPositioner function:
http://api.highcharts.com/highcharts#xAxis.tickPositioner
tickPositions: [0, 1, 2]
or something like:
tickPositioner: function () {
var positions = [],
tick = Math.floor(this.dataMin),
increment = Math.ceil((this.dataMax - this.dataMin) / 2);
for (; tick - increment <= this.dataMax; tick += increment) {
positions.push(tick);
}
return positions;
}
As the other answers mention there is the tickPositioner field. For some reason it dont work out of the box. I had to add an info property to the returning array to get the xAxis to display formatted dates instead of just the milliseconds, as found in this fiddle.
tickPositioner: function (min, max) {
var ticks = [min, min + (max - min) / 2, max];
ticks.info = {
unitName: 'hour',
higherRanks: {}
};
return ticks;
}
I've got a chart that has a large number of data points. I'm setting the extremes right now so it only shows a smaller number - about 100 or so at a time. The issue I have is I don't want to disable the range selector or the manual input for dates, but currently the user can expand the range selector to include the entire data set. Is there any way to limit this so they can only grab, at most, X number of points?
I don't want to use data grouping either, I have different data for different data groups, so it's important the grouping does not change. Thanks in advance for any pointers on this one!
Unfortunately you can't disable range selector for such purpose, but you can overwrite extremes after user change them, see:
api.highcharts.com/highstock#xAxis.events.afterSetExtremes So for example, if user takes range for 4 days and you want max for 2 days - grab one of min or max, and add/remove two days in extremes (call chart.xAxis.setExtremes() with new extremes). Of course, it will call another afterSetExtremes, so it would be infinite loop, however now extremes will be proper (two days), so it wouldn't call another setExtremes().
Here's a working example of using afterSetExtremes (like Dexter describes) to create a maximum range from the range selector :
http://jsfiddle.net/B7vCR/3/
xAxis: {
minRange:6 * 30 * 24 * 3600 * 1000,
events: {
afterSetExtremes: function(e) {
var maxDistance = 10 * 30 * 24 * 3600 * 1000; //8 months time
var xaxis = this;
if ((e.max - e.min) > maxDistance) {
var min = e.max - maxDistance;
var max = e.max;
window.setTimeout(function() {
xaxis.setExtremes(min, max);
}, 1);
}
}
}
}
From this post
http://forum.highcharts.com/viewtopic.php?f=12&t=21741