SwiftUI Charts - Plotting Hours and Minutes on the y axis - swiftui

I'm plotting sunrise - date against time, and it works but not perfectly.
How do I plot Hours and Minutes on the y axis, as a compound time.
If I just plots hours I get a step chart:
Chart{
ForEach(sunriseSunset) {item in
LineMark(
x: .value("Date", item.sunrise),
y: .value("Time", (Calendar.current.dateComponents(components, from: item.sunrise).hour!))
)
}
}
Alternatively if I multiple the hours * 60 and add the minutes I get the correct curve but the intervals on the RHS are in minutes. Is there a way to express the time as hours/minutes?
Chart{
ForEach(sunriseSunset) {item in
LineMark(
x: .value("Date", item.sunrise),
y: .value("Time", ((Calendar.current.dateComponents(components, from: item.sunrise).hour!) * 60) + (Calendar.current.dateComponents(components, from: item.sunrise).minute!))
)
}
}

Related

Animations bound to array values causing EXC_BAD_ACCESS

I have a swiftUI view that has a number of animations which are bound to an array of structs. When I test the app on my phone I keep getting EXC_BAD_ACCESS. This is what the top of the backtrace looks like:
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=2, address=0x16f7cfff0)
frame #0: 0x00000001892665c8 SwiftUI`SwiftUI.DefaultCombiningAnimator.value(in: τ_0_0.Value, for: Swift.Double) -> Swift.Optional<τ_0_0.Value> + 48
frame #1: 0x00000001892fac40 SwiftUI`SwiftUI.AnimatorBox.value(in: τ_0_0.Value, for: Swift.Double) -> Swift.Optional<τ_0_0.Value> + 168
frame #2: 0x0000000189264a28 SwiftUI`SwiftUI.AnyAnimator.value(in: τ_0_0, for: Swift.Double) -> Swift.Optional<τ_0_0> + 156
frame #3: 0x0000000189266898 SwiftUI`SwiftUI.DefaultCombiningAnimator.value(in: τ_0_0.Value, for: Swift.Double) -> Swift.Optional<τ_0_0.Value> + 768
frame #4: 0x00000001892fac40 SwiftUI`SwiftUI.AnimatorBox.value(in: τ_0_0.Value, for: Swift.Double) -> Swift.Optional<τ_0_0.Value> + 168
frame #5: 0x0000000189264a28 SwiftUI`SwiftUI.AnyAnimator.value(in: τ_0_0, for: Swift.Double) -> Swift.Optional<τ_0_0> + 156
frame #6: 0x0000000189266898 SwiftUI`SwiftUI.DefaultCombiningAnimator.value(in: τ_0_0.Value, for: Swift.Double) -> Swift.Optional<τ_0_0.Value> + 768
frame #7: 0x00000001892fac40 SwiftUI`SwiftUI.AnimatorBox.value(in: τ_0_0.Value, for: Swift.Double) -> Swift.Optional<τ_0_0.Value> + 168
frame #8: 0x0000000189264a28 SwiftUI`SwiftUI.AnyAnimator.value(in: τ_0_0, for: Swift.Double) -> Swift.Optional<τ_0_0> + 156
The previous 4000 frames all repeat the same sequence of AnyAnimator / AnimatorBox / DefaultCombiningAnimator. The only difference I can see is that in frame #0 the DefaultCombiningAnimator ends in '-> Swift.Optional<τ_0_0.Value> + 48' instead of '+ 768'.
Can anyone give me a clue as to what (if anything) this is telling me. The underlying array is constantly updated but remains a fixed size with no appends or deletions.
This is the animated view element:
ZStack {
//MARK: - Fret Cents Label
NoteLabel()
.fill(activeFret == thisFret ? .white : intonationModel.gString[thisFret].hasBeenSet ? .black : .gray)
.opacity(0.8)
.shadow(color: .black, radius: 4, x: 0, y: 3)
VStack(spacing: 0) {
Text(String(format: "%.1f", cents))
.font(Font.custom("Karantina-Regular", size: scaleHeight * 0.06))
.foregroundColor(activeFret == thisFret ? .black : .white)
}
}
.frame(width: width * 0.9 , height: scaleHeight * 0.1)
.position(x: width * 0.6, y: scaleHeight * 0.5)
.offset(y: yScaleCents * abs(cents) <= 10 ?
scaleHeight * (yScaleCents * 0.05 * -cents) :
scaleHeight * 0.5 * -(abs(cents) / cents))
.animation(.easeInOut(duration: 0.2), value: cents)
This is where it is called in the parent view:
ForEach($intonationModel.gString.indices, id: \.self) { index in
let columnWidth = fretWidth + (120 / (CGFloat(index + 1) * 4))
if index > 0 {
VStack(spacing: 0) {
IntonatorChartColumn(
thisFret: index,
scaleHeight: scaleHeight,
scaleMargin: scaleMargin,
fretboardHeight: fretboardHeight,
fretLabelHeight: fretLabelHeight,
yScaleCents: self.yScaleCents,
stringGap: stringGap,
showFretboard: $showFretboard,
cents: $intonationModel.gString[index].cents,
note: $intonationModel.gString[index].closestNote,
octave: $intonationModel.gString[index].octave,
activeFret: $intonationModel.currentFret
)
.frame(width: columnWidth)
.id(index)
}
.padding(.top, topMargin)
}
}
The app is refusing to crash when I'm looking for zombies, even though it will reliably crash when not being monitored. I'll have a go at re-working my code to avoid the use of indices (as advised by jnpdx).
I believe there were two things going on here.
It seems like the view with the animations was updating even when it was in the background. Ensuring the published variables only updated when the view was in the foreground stopped the crashing.
The incorrect use of ForEach which I've changed to the following:
ForEach(Array(zip(intonationEngine.gString.indices, intonationEngine.gString)) , id: \.0) {index, note in
Since doing this the crashes have stopped.

Converting a timestamp to freshness index

I have a column in dataframe which has article and its publication date (timestamp). I need to use this information to find out a freshness score of an article.
articleId publicationDate
0 581354 2017-09-17 15:16:55
1 581655 2017-09-18 07:37:51
2 580864 2017-09-16 06:44:39
3 581610 2017-09-18 06:30:30
4 581605 2017-09-18 07:22:24
Most recent article should get higher score. Timewindow should be half an hour (2 articles published in half an hour must get same score)
Some of the code below might be redundant but it seems to work:
df['score'] = df['publicationDate'] - df['publicationDate'].max()
df['score'] = (df['score'] / np.timedelta64(1, 'm')).apply(lambda x: (round(x / 30) * 30 + 30) / 30 if x else x).rank(method='max')
So you convert timedelta to minutes, then round it to 30, and finally rank that value.
It can also be a one-liner if you please:
df['score'] = ((df['publicationDate'] - df['publicationDate'].max()) / np.timedelta64(1, 'm')).apply(lambda x: (round(x / 30) * 30 + 30) / 30 if x else x).rank(method='max')
Explaination:
(df['publicationDate'] - df['publicationDate'].max() - subtract all dates from most recent one
(df['score'] / np.timedelta64(1, 'm')) - convert timedelta into minutes
.apply(lambda x: (round(x / 30) * 30 + 30) / 30 if x else x) - roundup to 30 minutes excluding most recent timestamp
.rank(method='max') rank the results giving upper value to all those that have same rank.
EDIT:
To change rank of those older than 2 days you can use this:
df['diff'] = (df['publicationDate'] - df['publicationDate'].max()).apply(lambda x: x.days)
df.loc[df['diff']<=-2, 'score'] = 0
First line will give you timedelta in whole days, and second one will change rank to 0 where days are less or equal to -2.

Horizontal distance between points drawn

I try to draw a normal XY plot using a TChart (TeeChart) component in Embarcadero RAD Studio. When I add new points that have evenly spaced x values, e. g.
x: 1 2 3 4 5
y: 10 20 5 8 100
everything is drawn OK.
But when I add points that are unevenly spaced on the x axis, e. g.
x: 1 2 100 120 150
y: 10 20 5 8 100
the chart is drawn in such a way that the points still have the same distance between each other on the x axis. That is the distance between points 1-2 is the same as between 2-100. Is it possible to draw a proportional XY plot?
This is my sample code:
Series1->Add(10, 1);
Series1->Add(20, 2);
Series1->Add(5, 100);
Series1->Add(8, 120);
Series1->Add(100, 150);
The style of Series1 is Line.
Instead of calling Add, you need to call AddXY to add XY points.

How can I plot (x,y,z) coordinates dynamically

My application obtains x,y,z coordinates like this:
x: -0.020941
y: -0.241276
z: 0.956
--------------
x: 0.0782352
y: -0.159108
z: 0.923
--------------
x: 0.0665857
y: -0.140757
z: 0.885
--------------
x: 0.0485952
y: -0.0859762
z: 0.785
--------------
x: 0.04494
y: -0.0477933
z: 0.749
--------------
x: -0.183467
y: 0.0505905
z: 0.64
--------------
x: -0.0519514
y: -0.0137343
z: 0.627
--------------
x: -0.0630648
y: -0.0206495
z: 0.586
--------------
x: -0.0774924
y: -0.0189667
z: 0.569
--------------
x: 0.0100971
y: -0.0100971
z: 0.558
--------------
x: 0.00456857
y: -0.0126905
z: 0.533
--------------
x: 0.000491429
y: -0.00835429
z: 0.516
--------------
x: -0.0227838
y: -0.01018
z: 0.509
--------------
x: -0.0222133
y: -0.00589333
z: 0.476
--------------
x: -0.10161
y: -0.00850476
z: 0.47
--------------
x: -0.0775429
y: 0.0162095
z: 0.46
--------------
x: -0.0897705
y: 0.0219057
z: 0.451
--------------
What I'm doing is every 3 seconds I'm taking the smallest z value from this stream and plotting it's position within a 2d array (using it's x and y position)
my question is how do I plot this result as I don't know what the maximum or minimum score could be and they're all double type, not int for the index. Is there a 'nice' way of plotting this? I'd also like to be able to manipulate this data based on it's array index. e.g. find where data is clustering, etc.
It seems that your issue is a result of having your heart set on using an array. Using a vector would allow you to add grid points dynamically as well as averaging (and performing other calculations) whenever you wanted.
That being said, if you really want to use an array then like you have noticed you cannot use doubles and/or negative values as indices. You could work out a system by adding an offset and multiplying your x,y values to get rounded integers and a center not at (0,0) but maybe at (2,2) or something instead but this would be really unwieldy and confusing.

Duplicated Numbers on Google Charts Vertical Axis

I was working on a bugfix with Google visualizations for a combo (column/line) chart when one of the axes went strange and started repeating numbers ( ie: 1, 1, 2, 2 instead of 1, 2, 3, 4). Please see the image below
(source: rackcdn.com)
Here are the chart option settings:
// Instantiate and draw our chart, passing in some options.
var frequency_by_day_options = {
vAxes: [
{format:'#,###', title:"Call Volume"},
{format: '#%', title:'Missed Call Rate',
viewWindow:{
max:1,
}}
],
legend: {position: 'none'},
chartArea: { height:'60%', width:'60%'},
width: 800,
height: 350,
backgroundColor: 'transparent',
bar: { groupWidth: '90%'},
isStacked: true,
seriesType: 'bars',
series: {0: {type:'bar', targetAxisIndex:0}, 1: {type:'line', targetAxisIndex:1},},
colors: ['blue', 'green'],
animation:{
duration: 1000,
easing: 'out',},
};
I have no idea what's going on here. Even when I comment out all of the vAxis options I still observe this behavior. Any ideas on what i'm doing wrong? This is driving me nuts :)
I was facing same issue when we have less number of boxes (let say 1 or 2). I resolved it with maxValue option.
Just add maxValue = 4 option in your vaxis. It will add 5 (0 to 4) gridlines at all times. If your maxValue is more that 4, it will adjust automatically.
Mine is working fine with options (title : 'No of boxes', format : '#', minValue:0, maxValue :4). minValue = 0 will not allow negatives as its no of boxes.
I am going to guess that the left-side vAxis is not 4, but actually 2. The 5 labels are 0, 0.5, 1, 1.5, 2.
Since you have format set as "#,###" it will not show decimals. If you change it to "#,###.#" then it will show 0, 0.5, 1, 1.5, 2.
There are a dozen ways to solve it, but the easiest may be to make sure that your axis values are only whole number values with a javascript function like this:
// Take the Max/Min of all data values in all graphs
var totalMax = 3;
var totalMin = -1;
// Figure out the largest number (positive or negative)
var biggestNumber = Math.max(Math.abs(totalMax),Math.abs(totalMin));
// Round to an exponent of 10 appropriate for the biggest number
var roundingExp = Math.floor(Math.log(biggestNumber) / Math.LN10);
var roundingDec = Math.pow(10,roundingExp);
// Round your max and min to the nearest exponent of 10
var newMax = Math.ceil(totalMax/roundingDec)*roundingDec;
var newMin = Math.floor(totalMin/roundingDec)*roundingDec;
// Determine the range of your values
var range = newMax - newMin;
// Calculate the best factor for number of gridlines (2-5 gridlines)
// If the range of numbers divided by 2 or 5 is a whole number, use it
for (var i = 2; i <= 5; ++i) {
if ( Math.round(range/i) = range/i) {
var gridlines = i
}
}