plotly R graph - marker symbol doesn't show in legend unless segment trace is added - r-plotly

I have a plotly graph with the following:
- line plot where censored values are plotted as open circles, and non-censored dataset points are solid.
- in some graphs a trend line will be shown (added using add_segment)
I can't seem to get the marker legend to show up when the trend line is not added.
Ideally, I would like only the 'censored datapoints' to show up in the legend, so I have added two traces for markers (1 for censored with showlegend = TRUE, the other with showlegend=FALSE). Not sure if there is another way to do this - very new to plot_ly.
# datasets
results <- structure(list(slope = 0, slope_p_val = 0.672652383888971, int_from_data = 0.06,
pct_cens = 9.3, annual_slope_units = 0, sigclass = "No evidence of trend",
sig = structure(2L, .Label = c("Significant", "Not significant"
), class = "factor"), slope_text = " ", trend_color = "#CBCBCB"), class = "data.frame", row.names = c(NA,
-1L))
dataset <- dput(dataset)
structure(list(Date = structure(c(12794, 12823, 12863, 12893,
12921, 12948, 12978, 13003, 13048, 13073, 13108, 13137, 13172,
13199, 13230, 13263, 13291, 13318, 13349, 13375, 13405, 13432,
13472, 13486, 13523, 13564, 13592, 13622, 13648, 13683, 13705,
13746, 13775, 13810, 13838, 13852, 13929, 13957, 13986, 14014,
14053, 14067, 14110, 14139, 14166, 14196, 14224, 14266, 14294,
14321, 14348, 14377, 14405, 14446, 14476, 14501, 14532, 14566,
14593, 14636, 14684, 14712, 14740, 14770, 14811, 14839, 14868,
14896, 14929, 14952, 14993, 15020, 15050, 15077, 15105, 15146,
15174, 15208, 15238, 15265, 15293, 15315, 15350, 15385, 15412,
15441, 15482, 15511, 15537, 15566, 15600, 15631, 15658, 15685,
15728, 15742, 15769, 15811, 15839, 15868, 15904, 15931, 15958,
16001, 16030, 16042, 16091, 16119, 16149, 16174, 16204, 16230,
16268, 16302, 16330, 16359, 16386, 16412), class = "Date"), cenTF = c(FALSE,
FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, TRUE, TRUE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE,
FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE), Value = c(0.05,
0.06, 0.025, 0.08, 0.06, 0.07, 0.05, 0.025, 0.025, 0.1, 0.12,
0.18, 0.14, 0.19, 0.36, 0.17, 0.09, 0.07, 0.05, 0.025, 0.05,
0.025, 0.08, 0.05, 0.06, 0.06, 0.06, 0.05, 0.07, 0.06, 0.05,
0.07, 0.06, 0.05, 0.05, 0.06, 0.05, 0.07, 0.1, 0.09, 0.025, 0.07,
0.07, 0.14, 0.17, 0.11, 0.1, 0.14, 0.17, 0.17, 0.18, 0.09, 0.08,
0.08, 0.1, 0.07, 0.07, 0.06, 0.025, 0.09, 0.07, 0.08, 0.06, 0.06,
0.06, 0.08, 0.06, 0.06, 0.05, 0.05, 0.06, 0.06, 0.11, 0.1, 0.1,
0.05, 0.06, 0.025, 0.06, 0.025, 0.06, 0.06, 0.07, 0.06, 0.05,
0.07, 0.05, 0.06, 0.05, 0.05, 0.06, 0.06, 0.025, 0.05, 0.06,
0.06, 0.06, 0.23, 0.06, 0.06, 0.06, 0.025, 0.05, 0.05, 0.1, 0.06,
0.06, 0.06, 0.07, 0.08, 0.06, 0.07, 0.06, 0.05, 0.07, 0.06, 0.06,
0.06)), row.names = c(NA, -118L), class = "data.frame")
plotly_1 <- function(dataset, results,
cen_var="cenTF", val_var="Value",
date_period =NULL
){
dataset <- dataset %>%
mutate(cenNM = case_when(!!ensym(cen_var) := FALSE ~ " ",
!!ensym(cen_var) := TRUE ~ "Value too low to detect"),
plotVal = !!ensym(val_var),
plotCen = !!ensym(cen_var),
pct_cens = round(sum(.data$plotCen, na.rm=TRUE)/sum(!is.na(.data$Value))*100, 1))
#PLOTTING PARAMETERS
legendFont <- list(
family = "sans-serif",
size = 14,
color = "#000")
#font for axis
axisFont <- list(
family = "sans-serif",
size = 17,
color = "#000")
#calculate trend line limits - confirm same fopr decdate as date...
min_x <- date_period[1]
max_x <- date_period[2]+1
min_y <- results$slope *min_x + results$int_from_data
max_y <- results$slope*max_x + results$int_from_data
#CREATE PLOT
p <- plot_ly(dataset, x = ~Date, y = ~plotVal,
color=I("#3182BD"),
type='scatter',
mode='lines',
showlegend=FALSE,
hoverinfo="none"
) %>%
add_markers(data=dataset %>% filter(plotCen == FALSE),
x = ~Date,y = ~plotVal,
color=I("#3182BD"),
symbol=I("circle"),
size=1,
showlegend=FALSE,
hoverinfo="text",
text= ~paste("Value:",plotVal,"<br>Date:",Date,"<br>Censored:",plotCen)
) %>%
add_markers(data=dataset %>% filter(plotCen == TRUE),
x = ~Date,y = ~plotVal,
size = 1,
symbol = I("circle-open"),
color=I("#3182BD"),
showlegend=TRUE,
name="Value too low to detect",
hoverinfo="text",
text=~paste("Value:",plotVal,"<br>Date:",Date,"<br>Censored:",plotCen)
)
#add trend line for significant trends only...
if(!any(results$slope_pval > alpha,results$slope == 0)){
p <- p %>%
add_segments(data=results,
x=min_x , xend=max_x, y=min_y , yend=max_y,
color="#FCBA19",
name="Long-term trend",
showlegend=TRUE,
inherit=TRUE)
}
return(p)
}
tmp <- plotly_1(dataset, results, date_period = c(2005,2014))
#glimpse(tmp)
tmp
The above does not show the markers in the legend, but
if I comment out the if(!any(results$slope_pval > alpha,results$slope == 0)) statement, the censored marker does show in the legend (as desired)

The problem is, that you are setting showlegend = FALSE in the plot_ly call, which has a global effect on the plot. If you add another add_lines instead of passing the data for the line trace directly in plot_ly you get the desired result:
library(plotly)
library(dplyr)
# datasets
alpha <- 0.6 # not defined in question
results <- structure(list(slope = 0, slope_p_val = 0.672652383888971, int_from_data = 0.06,
pct_cens = 9.3, annual_slope_units = 0, sigclass = "No evidence of trend",
sig = structure(2L, .Label = c("Significant", "Not significant"
), class = "factor"), slope_text = " ", trend_color = "#CBCBCB"), class = "data.frame", row.names = c(NA, -1L))
dataset <- structure(list(Date = structure(c(12794, 12823, 12863, 12893,
12921, 12948, 12978, 13003, 13048, 13073, 13108, 13137, 13172,
13199, 13230, 13263, 13291, 13318, 13349, 13375, 13405, 13432,
13472, 13486, 13523, 13564, 13592, 13622, 13648, 13683, 13705,
13746, 13775, 13810, 13838, 13852, 13929, 13957, 13986, 14014,
14053, 14067, 14110, 14139, 14166, 14196, 14224, 14266, 14294,
14321, 14348, 14377, 14405, 14446, 14476, 14501, 14532, 14566,
14593, 14636, 14684, 14712, 14740, 14770, 14811, 14839, 14868,
14896, 14929, 14952, 14993, 15020, 15050, 15077, 15105, 15146,
15174, 15208, 15238, 15265, 15293, 15315, 15350, 15385, 15412,
15441, 15482, 15511, 15537, 15566, 15600, 15631, 15658, 15685,
15728, 15742, 15769, 15811, 15839, 15868, 15904, 15931, 15958,
16001, 16030, 16042, 16091, 16119, 16149, 16174, 16204, 16230,
16268, 16302, 16330, 16359, 16386, 16412), class = "Date"),
cenTF = c(FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, TRUE, TRUE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE,
FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE),
Value = c(0.05, 0.06, 0.025, 0.08, 0.06, 0.07, 0.05, 0.025, 0.025, 0.1, 0.12,
0.18, 0.14, 0.19, 0.36, 0.17, 0.09, 0.07, 0.05, 0.025, 0.05,
0.025, 0.08, 0.05, 0.06, 0.06, 0.06, 0.05, 0.07, 0.06, 0.05,
0.07, 0.06, 0.05, 0.05, 0.06, 0.05, 0.07, 0.1, 0.09, 0.025, 0.07,
0.07, 0.14, 0.17, 0.11, 0.1, 0.14, 0.17, 0.17, 0.18, 0.09, 0.08,
0.08, 0.1, 0.07, 0.07, 0.06, 0.025, 0.09, 0.07, 0.08, 0.06, 0.06,
0.06, 0.08, 0.06, 0.06, 0.05, 0.05, 0.06, 0.06, 0.11, 0.1, 0.1,
0.05, 0.06, 0.025, 0.06, 0.025, 0.06, 0.06, 0.07, 0.06, 0.05,
0.07, 0.05, 0.06, 0.05, 0.05, 0.06, 0.06, 0.025, 0.05, 0.06,
0.06, 0.06, 0.23, 0.06, 0.06, 0.06, 0.025, 0.05, 0.05, 0.1, 0.06,
0.06, 0.06, 0.07, 0.08, 0.06, 0.07, 0.06, 0.05, 0.07, 0.06, 0.06,
0.06)), row.names = c(NA, -118L), class = "data.frame")
plotly_1 <- function(dataset,
results,
cen_var = "cenTF",
val_var = "Value",
date_period = NULL) {
dataset <- dataset %>%
mutate(
cenNM = case_when(
!!ensym(cen_var) := FALSE ~ " ",!!ensym(cen_var) := TRUE ~ "Value too low to detect"
),
plotVal = !!ensym(val_var),
plotCen = !!ensym(cen_var),
pct_cens = round(sum(.data$plotCen, na.rm = TRUE) / sum(!is.na(.data$Value)) *
100, 1)
)
#PLOTTING PARAMETERS
legendFont <- list(family = "sans-serif",
size = 14,
color = "#000")
#font for axis
axisFont <- list(family = "sans-serif",
size = 17,
color = "#000")
#calculate trend line limits - confirm same fopr decdate as date...
min_x <- date_period[1]
max_x <- date_period[2] + 1
min_y <- results$slope * min_x + results$int_from_data
max_y <- results$slope * max_x + results$int_from_data
#CREATE PLOT
p <- plot_ly(dataset,
type = 'scatter',
mode = 'lines',
hoverinfo = "none") %>%
add_lines(
x = ~ Date,
y = ~ plotVal,
color = I("#3182BD"),
showlegend = FALSE
) %>%
add_markers(
data = dataset %>% filter(plotCen == FALSE),
x = ~ Date,
y = ~ plotVal,
color = I("#3182BD"),
symbol = I("circle"),
size = 1,
showlegend = FALSE,
hoverinfo = "text",
text = ~ paste(
"Value:",
plotVal,
"<br>Date:",
Date,
"<br>Censored:",
plotCen
)
) %>%
add_markers(
data = dataset %>% filter(plotCen == TRUE),
x = ~ Date,
y = ~ plotVal,
size = 1,
symbol = I("circle-open"),
color = I("#3182BD"),
showlegend = TRUE,
name = "Value too low to detect",
hoverinfo = "text",
text = ~ paste(
"Value:",
plotVal,
"<br>Date:",
Date,
"<br>Censored:",
plotCen
)
)
# add trend line for significant trends only...
if (!any(results$slope_p_val > alpha, results$slope == 0)) {
p <- p %>%
add_segments(
data = results,
x = min_x ,
xend = max_x,
y = min_y ,
yend = max_y,
color = "#FCBA19",
name = "Long-term trend",
showlegend = TRUE,
inherit = TRUE
)
}
return(p)
}
tmp <- plotly_1(dataset, results, date_period = c(2005, 2014))
#glimpse(tmp)
tmp
Two more comments:
added alpha <- 0.6 as it wasn't defined.
Changed if(!any(results$slope_pval > alpha,results$slope == 0)){ to results$slope_p_val

Related

is it possible to create custom legend items

I want to create custom legend item instead of the default one which just shows series 0, is it possible to do so when creating a SfCatesian line chart ?
SfCartesianChart(
legend: Legend(
isVisible: true,
position: LegendPosition.right,
toggleSeriesVisibility: true,
title: LegendTitle(text:'Sales by day',)),
margin: EdgeInsets.only(top: 25),
plotAreaBorderWidth: 0,
enableAxisAnimation: true,
primaryXAxis: CategoryAxis(
majorGridLines: const MajorGridLines(width: 0, color: Colors.transparent)),
primaryYAxis: NumericAxis(
minimum: 0,
maximum: 5,
interval: 1,
opposedPosition: true,
axisLine: const AxisLine(width: 0),
majorTickLines: const MajorTickLines(size: 0)),
series: <LineSeries<_ChartData, String>>[
LineSeries<_ChartData, String>(
name: "Daily sales",
animationDuration: 2500,
dataSource: <_ChartData>[
_ChartData('Mon', 3, Color.fromRGBO(255, 0, 0, 1)),
_ChartData('Tues', 4, Color.fromRGBO(0, 255, 0, 1)),
_ChartData('Wed', 3, Color.fromRGBO(0, 0, 255, 1)),
],
xValueMapper: (_ChartData sales, _) => sales.x,
yValueMapper: (_ChartData sales, _) => sales.y,
pointColorMapper: (_ChartData sales, _) => sales.lineColor,
width: 2,)];,
);
We suggest you the legendItemBuilder property in the legend, using this you can be able to customize the legend item based on your requirement. We have shared the demo sample, UG, and KB link below for your reference.
Sample: https://flutter.syncfusion.com/#/cartesian-charts/legend/chart-with-customized-legend
UG: https://help.syncfusion.com/flutter/cartesian-charts/legend#legend-item-template
KB: https://www.syncfusion.com/kb/13055/

show text widget with no data for a bar which has zero value synfusion stacked bar graph flutter

Need to show no data available text in place of bar which has zero value , y axis labels are
string and x axis are integers
please refer below for the 1st image is acutal result and 2nd image is expected result . 1st image in which empty place which has zero value instead of empty place need to show no data available text
SfCartesianChart(
crosshairBehavior: _crosshairBehavior,
primaryXAxis: CategoryAxis(
majorTickLines: MajorTickLines(size: 20, width: 0),
axisBorderType: AxisBorderType.withoutTopAndBottom,
labelAlignment: LabelAlignment.center,
borderWidth: 2.0,
labelStyle: TextStyle(
fontFamily: 'Roboto',
fontSize: 14,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w500),
//maximumLabelWidth: 100,
labelPosition: ChartDataLabelPosition.outside,
borderColor: Colors.blueGrey[800]),
primaryYAxis: CategoryAxis(
plotBands: <PlotBand>[
PlotBand(
isVisible: widget.isFleetAverageVisible,
start: 30,
end: 29,
borderWidth: 0.3,
borderColor: Colors.black,
)
],
interval: 25,
maximumLabels: 4,
maximum: 100,
minimum: 0,
majorTickLines: MajorTickLines(size: 15, width: 0),
majorGridLines: MajorGridLines(dashArray: [5, 5]),
),
series: stackedWidget(sortSelected != null
? sortSelected == true
? SortingOrder.ascending
: SortingOrder.descending
: SortingOrder.none),
legend: Legend(
isVisible: true,
position: LegendPosition.bottom,
overflowMode: LegendItemOverflowMode.wrap,
),
onLegendTapped: (LegendTapArgs args) {
},
),
List<ChartSeries> stackedWidget(SortingOrder sortingOrder) {
return <ChartSeries>[
StackedBarSeries<ChartData, String>(
sortingOrder: sortingOrder,
legendIconType: LegendIconType.horizontalLine,
legendItemText: "series1",
width: 0.3,
spacing: 0.2,
dataSource: chartData,
//list of data
xValueMapper: (ChartData data, _) => data.x,
yValueMapper: (ChartData data, _) => data.y1,
sortFieldValueMapper: (ChartData sales, _) => sales.y1,
color: Color.fromRGBO(51, 102, 255, 1))
];
}
Try giving the DataLabelSettings() to the dataLabelSettings: property on StackedBarSeries():
List<ChartSeries> stackedWidget(SortingOrder sortingOrder) {
return <ChartSeries>[
StackedBarSeries<ChartData, String>(
dataLabelSettings: DataLabelSettings(
builder: (
data,
point,
series,
pointIndex,
seriesIndex,
) {
if (chartData[pointIndex].y == 0 || chartData[pointIndex].y == null) {
return Row(children: const [
Icon(Icons.warning_amber_outlined),
Text("No data available"),
]);
} else {
return const SizedBox();
}
},
isVisible: true,
),
sortingOrder: sortingOrder,
legendIconType: LegendIconType.horizontalLine,
legendItemText: "series1",
width: 0.3,
spacing: 0.2,
dataSource: chartData,
//list of data
xValueMapper: (ChartData data, _) => data.x,
yValueMapper: (ChartData data, _) => data.y1,
sortFieldValueMapper: (ChartData sales, _) => sales.y1,
color: Color.fromRGBO(51, 102, 255, 1))
];
}

Optimal number of clusters - Error in FUNcluster(x, i, ...) : more cluster centers than distinct data points

I have these data and I need to find the optimal number of clusters of this table.
The values can be either 0, 0.5 or 1
library(NbClust)
library(factoextra)
library(pheatmap)
tab=structure(list(`57-B1` = c(1, 0.5, 0.5, 1, 1, 0.5), `57-B3` = c(0.5,
0.5, 0.5, 0, 0.5, 0.5), `57-C1` = c(1, 0.5, 0.5, 0.5, 1, 0.5),
`57-C5` = c(1, 0.5, 0.5, 1, 1, 1), `57-H2` = c(1, 0.5, 0.5,
0, 1, 1), `57-H4` = c(0.5, 0.5, 0.5, 0, 0.5, 0.5), `61-1-B1` = c(0.5,
0.5, 0.5, 0, 0.5, 0.5), `61-1-C2` = c(0.5, 0.5, 0.5, 0, 0.5,
0.5), `61-1-C5` = c(0.5, 0.5, 0.5, 0, 0.5, 0.5), `61-1-H1` = c(0.5,
0.5, 0, 0, 0.5, 0.5), `61-1-H3` = c(0.5, 0.5, 0.5, 0, 0.5,
0.5), `61-1-H5` = c(0.5, 0.5, 0, 0.5, 0.5, 0.5), `62-2_H2` = c(0.5,
0.5, 0.5, 0, 0.5, 0.5), `62_1_C2` = c(0.5, 0.5, 0, 0.5, 0.5,
0.5), `62_1_C5` = c(0.5, 0.5, 0.5, 0, 0.5, 0.5), `FL-39-C3` = c(0.5,
0.5, 0.5, 0, 0.5, 0.5), `FL-41-1-C3` = c(0.5, 0.5, 0.5, 0,
0.5, 0.5), `FL-57-B1` = c(0.5, 0.5, 0.5, 0, 0.5, 0.5), `FL-57-B2` = c(0.5,
0.5, 0.5, 0, 0.5, 0.5), `FL-57-C2` = c(0.5, 0.5, 0, 0.5,
0.5, 0.5), `FL-57-C3` = c(1, 1, 1, 0, 1, 1), `FL-57-C5` = c(1,
0.5, 0.5, 1, 1, 1), `FL-57-H1` = c(1, 0.5, 0.5, 1, 1, 1),
`FL-57-H4` = c(0.5, 0.5, 0, 0, 0.5, 0.5), `FL-57-H5` = c(0.5,
0.5, 0.5, 0, 0.5, 0.5), `FL-61-1-B1` = c(0.5, 0.5, 0.5, 0,
0.5, 0.5), `FL-61-1-B4` = c(0.5, 0.5, 0.5, 0, 0.5, 0.5),
`FL-61-1-C2` = c(0.5, 0.5, 0, 0, 0.5, 0.5), `FL-61-1-C4` = c(0.5,
0.5, 0.5, 0, 0.5, 0.5), `FL-61-1-H3` = c(0.5, 0.5, 0.5, 0,
0.5, 0.5), `FL-61-1-H4` = c(0.5, 0.5, 0.5, 0, 0.5, 0.5),
`FL-61-1-H5` = c(0.5, 0.5, 0, 0.5, 0.5, 0.5), `FL-62-1-C3` = c(0.5,
0.5, 0, 0.5, 0.5, 0.5), `FL-62-2-H2` = c(0.5, 0.5, 0.5, 0,
0.5, 0.5), `FL-73-H1` = c(0.5, 0.5, 0.5, 0, 0.5, 0.5), P_57_F = c(0.5,
0.5, 0.5, 0, 0.5, 0.5), P_57_M = c(0.5, 0.5, 0.5, 0, 0.5,
0.5)), row.names = c("g1", "g2", "g3", "g4", "g5", "g6"), class = "data.frame")
I tried both on scaled and non-scale values:
fviz_nbclust(scale(tab), kmeans, method = "wss")
fviz_nbclust(tab, kmeans, method = "wss")
and I get this error:
Error in FUNcluster(x, i, ...) :
more cluster centers than distinct data points.
how can I fix it?
Many thanks for your help !
I maybe found the solutions: it was sufficient to specify k.max = any number lower than nrow(tab)

How to fill color inside a shape created using CustomPainter drawPath?

So, I've created a shape using drawPath and drawArc from CustomPainter, the PaintingStyle is stroke, but when I change it to fill, it only fills the arcs and not the whole shape.
I want to fill the shape I created with a color, so how can I fill the shape with a particular color?
class CustomShapeCard extends CustomPainter {
CustomShapeCard({#required this.strokeWidth, #required this.color});
final double strokeWidth;
final Color color;
#override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..color = color;
var path = Path();
path.moveTo(size.width * 0.1, size.height * 0.2);
path.lineTo(size.width * 0.1, size.height * 0.9);
canvas.drawPath(path, paint);
canvas.drawArc(
Rect.fromCenter(
center: Offset((size.width * 0.2) - 14, size.height * 0.9),
height: 50,
width: 50,
),
math.pi / 2,
math.pi / 2,
false,
paint,
);
path.moveTo((size.width * 0.2) - 14, (size.height * 0.9) + 25);
path.lineTo((size.width * 0.9) - 25, size.height * 0.9 + 25);
canvas.drawPath(path, paint);
canvas.drawArc(
Rect.fromCenter(
center: Offset((size.width * 0.9) - 25, size.height * 0.9),
height: 50,
width: 50,
),
math.pi / 2,
-math.pi / 2,
false,
paint,
);
path.moveTo((size.width * 0.9), (size.height * 0.9));
path.lineTo(size.width * 0.9, size.height * 0.35);
canvas.drawPath(path, paint);
canvas.drawArc(
Rect.fromCenter(
center: Offset((size.width * 0.9) - 25, size.height * 0.35),
height: 50,
width: 50,
),
-math.pi / 2,
math.pi / 2,
false,
paint,
);
path.moveTo((size.width * 0.9) - 25, (size.height * 0.35) - 25);
path.lineTo(size.width * 0.25, (size.height * 0.35) - 25);
canvas.drawPath(path, paint);
canvas.drawArc(
Rect.fromCenter(
center: Offset((size.width * 0.25), (size.height * 0.35) - 50),
height: 50,
width: 50,
),
math.pi / 2,
math.pi / 3,
false,
paint,
);
path.moveTo((size.width * 0.25) - 20, (size.height * 0.35) - 35);
path.lineTo(size.width * 0.1, size.height * 0.2);
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
When the PaintingStyle is stroke, I get this,
When I change PaintingStyle to fill, I get,
To fill a shape like this with a color,
the arcToPoint() should be used instead of drawArc().

peak detection for growing time series using Swift

Would anyone have a good algorithm to measure peaks in growing time series data using Swift (v3)? So, detect peaks as the data is streaming in.
E.g. a Swift version of the smooth z-wave algorithm. That algorithm seems to be suitable.
I would need to detect the peaks as shown below. The data contains positive and negative numbers. Output should be a counter of the peaks, and/or true/false for that specific sample.
Sample dataset (summary of the last series):
let samples = [0.01, -0.02, -0.02, 0.01, -0.01, -0.01, 0.00, 0.10, 0.31,
-0.10, -0.73, -0.68, 0.21, 1.22, 0.67, -0.59, -1.04, 0.06, 0.42, 0.07,
0.03, -0.18, 0.11, -0.06, -0.02, 0.16, 0.21, 0.03, -0.68, -0.89, 0.18,
1.31, 0.66, 0.07, -1.62, -0.16, 0.67, 0.19, -0.42, 0.23, -0.05, -0.01,
0.03, 0.06, 0.27, 0.15, -0.50, -1.18, 0.11, 1.30, 0.93, 0.16, -1.32,
-0.10, 0.55, 0.23, -0.03, -0.23, 0.16, -0.04, 0.01, 0.12, 0.35, -0.38,
-1.11, 0.07, 1.46, 0.61, -0.68, -1.16, 0.29, 0.54, -0.05, 0.02, -0.01,
0.12, 0.23, 0.29, -0.75, -0.95, 0.11, 1.51, 0.70, -0.30, -1.48, 0.13,
0.50, 0.18, -0.06, -0.01, -0.02, 0.03, -0.02, 0.06, 0.03, 0.03, 0.02,
-0.01, 0.01, 0.02, 0.01]
Update: Thanks to Jean-Paul for the initial Swift port. But not sure the z-wave algo is the right one for this dataset. lag=10,threshold=3,influence=0.2 works fine for the last series of the dataset, but I have not been able to find a combination that comes close for the complete dataset.
The issues: with a big lag the first data samples are not included, I need one signal per peak and the algorithm would need further work to be made more efficient.
E.g. result for full dataset, using the Python code, and (e.g.) lag=5,threshold=2.5,influence=0.7 is missing peaks for series 1 and 2, and showing too many false positives in the quiet periods:
Full dataset (should result in 25 peaks):
let samples = [-1.38, -0.97, -1.20, -2.06, -2.26, -0.99, 0.11, -0.47, -0.95, -2.61, -0.88, -0.74, -1.12, -1.19, -1.12, -1.04, -0.72, -1.21, -2.61, -1.41, -0.23, -0.27, -0.43, -1.77, -2.75, -0.61, -0.73, -1.53, -1.02, -1.14, -1.12, -1.06, -0.78, -0.72, -2.41, -1.55, -0.01, -0.44, -0.47, -2.02, -1.66, -0.43, -0.93, -1.51, -0.86, -1.06, -1.10, -0.88, -0.84, -1.26, -2.59, -0.92, 0.29, -0.50, -1.31, -2.40, -0.88, -0.56, -1.09, -1.14, -1.09, -0.90, -0.99, -0.84, -0.75, -2.59, -1.34, -0.08, -0.36, -0.50, -1.89, -1.60, -0.55, -0.78, -1.46, -0.96, -0.97, -1.18, -0.98, -1.10, -1.07, -1.06, -1.79, -1.78, -1.54, -1.25, -1.00, -0.46, -0.27, -0.20, -0.15, -0.13, -0.11, -0.13, -0.09, -0.09, -0.05, 0.02, 0.20, -0.31, -1.35, -0.03, 1.34, 0.52, 0.80, -0.91, -1.26, -0.10, -0.10, 0.53, 0.93, 0.60, -0.83, -1.87, -0.21, 1.26, 0.44, 0.86, 0.73, -2.05, -1.66, 0.31, 1.04, 0.72, 0.63, -0.01, -2.14, -0.48, 0.77, 0.63, 0.58, 0.66, -1.01, -1.28, 0.18, 0.44, 0.09, -0.27, -0.06, 0.06, -0.18, -0.01, -0.08, -0.07, -0.06, -0.06, -0.07, -0.07, -0.06, -0.05, -0.04, -0.03, -0.02, -0.02, -0.03, -0.03, -0.01, 0.01, 0.00, 0.01, 0.05, 0.12, 0.16, 0.25, 0.29, -0.16, -0.69, -1.05, -0.84, -0.54, -0.07, 0.46, 1.12, 1.05, 0.77, 0.68, 0.63, 0.39, -0.96, -1.61, -0.68, -0.14, -0.03, 0.22, 0.31, 0.15, -0.02, 0.11, 0.14, 0.00, 0.04, 0.18, 0.27, 0.14, -0.05, -0.03, -0.08, -0.41, -0.94, -1.03, -0.50, 0.02, 0.52, 1.10, 1.03, 0.79, 0.69, 0.55, -0.34, -1.17, -0.89, -0.54, -0.22, 0.37, 0.47, 0.39, 0.23, 0.00, -0.02, 0.05, 0.10, 0.12, 0.09, 0.05, -0.12, -0.50, -0.89, -0.89, -0.48, 0.00, 0.43, 1.03, 0.95, 0.67, 0.64, 0.47, -0.07, -0.85, -1.02, -0.73, -0.08, 0.38, 0.46, 0.32, 0.15, 0.01, -0.01, 0.09, 0.20, 0.23, 0.19, 0.12, -0.50, -1.17, -0.97, -0.12, 0.15, 0.70, 1.31, 0.97, 0.45, 0.27, -0.73, -1.00, -0.52, -0.27, 0.10, 0.33, 0.34, 0.23, 0.07, -0.04, -0.27, -0.24, 0.10, 0.21, 0.05, -0.07, 0.04, 0.21, 0.29, 0.16, -0.45, -1.13, -0.93, -0.28, 0.04, 0.72, 1.35, 1.05, 0.56, 0.43, 0.17, -0.59, -1.38, -0.76, 0.10, 0.44, 0.46, 0.35, 0.12, -0.07, -0.05, -0.01, -0.07, -0.04, 0.01, 0.01, 0.06, 0.02, -0.03, -0.05, 0.00, 0.01, -0.02, -0.03, -0.02, -0.01, 0.00, -0.01, 0.00, -0.01, 0.00, -0.01, -0.01, 0.00, 0.01, -0.01, -0.01, 0.00, 0.00, 0.01, 0.01, 0.01, 0.04, 0.06, 0.05, 0.05, 0.04, 0.03, 0.00, -0.12, -0.16, -0.09, -0.01, 0.14, 0.07, 0.06, 0.00, -0.03, 0.00, 0.06, 0.06, -0.04, -0.11, -0.02, 0.13, 0.18, 0.21, 0.01, -0.31, -0.92, -1.35, -0.62, 0.03, 0.78, 1.36, 1.07, 0.59, 0.75, 0.42, -1.65, -3.16, -0.97, 0.24, 1.44, 1.50, 0.84, 0.47, 0.56, 0.40, -1.50, -2.71, -1.22, 0.01, 1.20, 1.55, 0.92, 0.44, 0.66, 0.73, -0.43, -2.34, -2.28, -0.72, 0.36, 1.41, 1.56, 0.89, 0.54, 0.67, 0.39, -1.78, -2.75, -1.07, -0.07, 1.16, 1.65, 0.80, 0.47, 0.73, 0.86, -0.24, -1.52, -1.68, -0.39, 0.02, 0.38, 0.60, 0.49, 0.02, -0.42, -0.31, -0.01, 0.08, 0.00, -0.07, -0.05, -0.01, -0.02, -0.04, -0.05, -0.02, -0.01, -0.02, -0.02, -0.03, -0.05, -0.04, -0.03, -0.01, -0.01, 0.00, -0.01, 0.00, 0.01, 0.00, 0.00, 0.00, 0.01, 0.01, -0.01, -0.03, -0.02, -0.01, 0.00, 0.00, 0.00, -0.01, 0.01, 0.00, -0.01, 0.02, 0.07, 0.15, 0.28, 0.31, 0.08, -0.26, -0.54, -0.96, -1.08, -0.27, 0.01, 0.45, 1.18, 1.07, 0.71, 0.65, 0.20, -0.80, -1.30, -0.74, -0.24, 0.29, 0.47, 0.34, 0.15, 0.02, 0.03, -0.02, -0.16, -0.13, 0.05, 0.09, -0.01, -0.08, -0.06, 0.03, 0.13, 0.19, 0.23, 0.18, 0.10, -0.07, -0.44, -0.91, -1.05, -0.64, -0.08, 0.50, 1.12, 1.35, 0.89, 0.58, 0.54, -0.58, -1.27, -1.20, -0.48, 0.19, 0.62, 0.62, 0.37, -0.01, -0.35, -0.33, 0.07, 0.29, 0.10, -0.14, -0.10, 0.07, 0.07, 0.01, 0.03, 0.09, 0.20, 0.32, 0.26, -0.02, -0.32, -0.78, -1.25, -0.93, -0.16, 0.30, 0.88, 1.40, 1.14, 0.72, 0.48, -0.54, -1.21, -1.13, -0.41, 0.18, 0.51, 0.53, 0.36, 0.11, -0.03, -0.09, -0.28, -0.11, 0.11, 0.15, 0.04, -0.08, -0.04, 0.04, 0.09, 0.16, 0.26, 0.43, 0.09, -0.88, -1.46, -0.64, -0.16, 0.43, 1.37, 1.34, 0.84, 0.52, -0.17, -0.87, -1.22, -0.76, 0.03, 0.47, 0.60, 0.36, 0.04, -0.09, -0.03, 0.02, -0.04, 0.04, 0.12, 0.13, 0.19, 0.27, 0.31, 0.18, -0.42, -0.99, -1.13, -0.75, -0.22, 0.50, 1.42, 1.41, 0.98, 0.51, 0.29, -0.69, -1.59, -0.88, -0.13, 0.31, 0.49, 0.46, 0.30, 0.05, -0.08, -0.03, 0.01, -0.04, -0.06, 0.02, 0.03, 0.01, -0.02, 0.01, 0.04, 0.06, 0.04, 0.03, 0.02, 0.03, 0.03, 0.01, -0.01, 0.00, 0.02, 0.00, 0.02, 0.02, 0.02, -0.02, -0.01, 0.02, 0.02, 0.01, 0.02, 0.02, 0.02, 0.02, 0.04, 0.03, 0.01, 0.01, 0.02, 0.01, 0.01, 0.01, 0.02, 0.01, 0.00, 0.01, 0.01, 0.00, 0.00, 0.01, 0.00, 0.00, 0.01, 0.00, 0.02, 0.00, 0.00, 0.01, 0.01, 0.00, 0.00, 0.01, 0.01, 0.00, 0.00, 0.00, 0.01, 0.01, 0.00, 0.01, 0.00, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00]
I am therefore not sure the z-wave algorithm is the right approach for this kind of dataset.
Translation of smooth z-score algo into Swift
Well, to quickly help you out: here is a translation of the algo into Swift: Demo in Swift Sandbox
Warning: I am by no means a swift programmer, so there could be mistakes in there!
Also note that I have turned off negative signals, as for OP's purpose we only want positive signals.
Swift code:
import Glibc // or Darwin/ Foundation/ Cocoa/ UIKit (depending on OS)
// Function to calculate the arithmetic mean
func arithmeticMean(array: [Double]) -> Double {
var total: Double = 0
for number in array {
total += number
}
return total / Double(array.count)
}
// Function to calculate the standard deviation
func standardDeviation(array: [Double]) -> Double
{
let length = Double(array.count)
let avg = array.reduce(0, {$0 + $1}) / length
let sumOfSquaredAvgDiff = array.map { pow($0 - avg, 2.0)}.reduce(0, {$0 + $1})
return sqrt(sumOfSquaredAvgDiff / length)
}
// Function to extract some range from an array
func subArray<T>(array: [T], s: Int, e: Int) -> [T] {
if e > array.count {
return []
}
return Array(array[s..<min(e, array.count)])
}
// Smooth z-score thresholding filter
func ThresholdingAlgo(y: [Double],lag: Int,threshold: Double,influence: Double) -> ([Int],[Double],[Double]) {
// Create arrays
var signals = Array(repeating: 0, count: y.count)
var filteredY = Array(repeating: 0.0, count: y.count)
var avgFilter = Array(repeating: 0.0, count: y.count)
var stdFilter = Array(repeating: 0.0, count: y.count)
// Initialise variables
for i in 0...lag-1 {
signals[i] = 0
filteredY[i] = y[i]
}
// Start filter
avgFilter[lag-1] = arithmeticMean(array: subArray(array: y, s: 0, e: lag-1))
stdFilter[lag-1] = standardDeviation(array: subArray(array: y, s: 0, e: lag-1))
for i in lag...y.count-1 {
if abs(y[i] - avgFilter[i-1]) > threshold*stdFilter[i-1] {
if y[i] > avgFilter[i-1] {
signals[i] = 1 // Positive signal
} else {
// Negative signals are turned off for this application
//signals[i] = -1 // Negative signal
}
filteredY[i] = influence*y[i] + (1-influence)*filteredY[i-1]
} else {
signals[i] = 0 // No signal
filteredY[i] = y[i]
}
// Adjust the filters
avgFilter[i] = arithmeticMean(array: subArray(array: filteredY, s: i-lag, e: i))
stdFilter[i] = standardDeviation(array: subArray(array: filteredY, s: i-lag, e: i))
}
return (signals,avgFilter,stdFilter)
}
// Demo
let samples = [0.01, -0.02, -0.02, 0.01, -0.01, -0.01, 0.00, 0.10, 0.31,
-0.10, -0.73, -0.68, 0.21, 1.22, 0.67, -0.59, -1.04, 0.06, 0.42, 0.07,
0.03, -0.18, 0.11, -0.06, -0.02, 0.16, 0.21, 0.03, -0.68, -0.89, 0.18,
1.31, 0.66, 0.07, -1.62, -0.16, 0.67, 0.19, -0.42, 0.23, -0.05, -0.01,
0.03, 0.06, 0.27, 0.15, -0.50, -1.18, 0.11, 1.30, 0.93, 0.16, -1.32,
-0.10, 0.55, 0.23, -0.03, -0.23, 0.16, -0.04, 0.01, 0.12, 0.35, -0.38,
-1.11, 0.07, 1.46, 0.61, -0.68, -1.16, 0.29, 0.54, -0.05, 0.02, -0.01,
0.12, 0.23, 0.29, -0.75, -0.95, 0.11, 1.51, 0.70, -0.30, -1.48, 0.13,
0.50, 0.18, -0.06, -0.01, -0.02, 0.03, -0.02, 0.06, 0.03, 0.03, 0.02,
-0.01, 0.01, 0.02, 0.01]
// Run filter
let (signals,avgFilter,stdFilter) = ThresholdingAlgo(y: samples, lag: 10, threshold: 3, influence: 0.2)
// Print output to console
print("\nOutput: \n ")
for i in 0...signals.count - 1 {
print("Data point \(i)\t\t sample: \(samples[i]) \t signal: \(signals[i])\n")
}
// Raw data for creating a plot in Excel
print("\n \n Raw data for creating a plot in Excel: \n ")
for i in 0...signals.count - 1 {
print("\(i+1)\t\(samples[i])\t\(signals[i])\t\(avgFilter[i])\t\(stdFilter[i])\n")
}
With the result for the sample data (for lag = 10, threshold = 3, influence = 0.2):
Update
You can improve the performance of the algorithm by using different values for the lag of the mean and the standard deviation. E.g.:
// Smooth z-score thresholding filter
func ThresholdingAlgo(y: [Double], lagMean: Int, lagStd: Int, threshold: Double, influenceMean: Double, influenceStd: Double) -> ([Int],[Double],[Double]) {
// Create arrays
var signals = Array(repeating: 0, count: y.count)
var filteredYmean = Array(repeating: 0.0, count: y.count)
var filteredYstd = Array(repeating: 0.0, count: y.count)
var avgFilter = Array(repeating: 0.0, count: y.count)
var stdFilter = Array(repeating: 0.0, count: y.count)
// Initialise variables
for i in 0...lagMean-1 {
signals[i] = 0
filteredYmean[i] = y[i]
filteredYstd[i] = y[i]
}
// Start filter
avgFilter[lagMean-1] = arithmeticMean(array: subArray(array: y, s: 0, e: lagMean-1))
stdFilter[lagStd-1] = standardDeviation(array: subArray(array: y, s: 0, e: lagStd-1))
for i in max(lagMean,lagStd)...y.count-1 {
if abs(y[i] - avgFilter[i-1]) > threshold*stdFilter[i-1] {
if y[i] > avgFilter[i-1] {
signals[i] = 1 // Positive signal
} else {
signals[i] = -1 // Negative signal
}
filteredYmean[i] = influenceMean*y[i] + (1-influenceMean)*filteredYmean[i-1]
filteredYstd[i] = influenceStd*y[i] + (1-influenceStd)*filteredYstd[i-1]
} else {
signals[i] = 0 // No signal
filteredYmean[i] = y[i]
filteredYstd[i] = y[i]
}
// Adjust the filters
avgFilter[i] = arithmeticMean(array: subArray(array: filteredYmean, s: i-lagMean, e: i))
stdFilter[i] = standardDeviation(array: subArray(array: filteredYstd, s: i-lagStd, e: i))
}
return (signals,avgFilter,stdFilter)
}
Then using for example let (signals,avgFilter,stdFilter) = ThresholdingAlgo(y: samples, lagMean: 10, lagStd: 100, threshold: 2, influenceMean: 0.5, influenceStd: 0.1) can give a lot better results:
DEMO