Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions Modules/Sources/JetpackStats/Cards/ChartCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,13 @@ struct ChartCard: View {
@ViewBuilder
private var chartContentView: some View {
if viewModel.isFirstLoad {
mainChartView(metric: selectedMetric, data: mockChartData)
.redacted(reason: .placeholder)
// Plain shapes instead of a `Chart` with mock data. Building Swift Charts
// (and compiling its Metal shaders) during the push transition is a
// measurable part of the first-open lag, and the placeholder is replaced
// as soon as data arrives anyway.
ChartLoadingPlaceholder(chartType: selectedChartType, metric: selectedMetric)
.frame(height: chartHeight)
.padding(.horizontal, -Constants.step1)
.opacity(0.2)
.pulsating()
} else if let data = viewModel.chartData[selectedMetric] {
Expand Down Expand Up @@ -291,6 +296,53 @@ struct ChartCard: View {
}
}

/// A cheap stand-in for the chart while the first data set loads, mirroring the
/// card's selected chart type.
private struct ChartLoadingPlaceholder: View {
let chartType: ChartType
let metric: SiteMetric

private let barHeights: [CGFloat] = [0.45, 0.7, 0.55, 0.9, 0.6, 0.8, 0.5, 0.75, 0.65, 0.85, 0.4, 0.7, 0.6, 0.95]
private let lineHeights: [CGFloat] = [
0.32, 0.38, 0.35, 0.42, 0.46, 0.43, 0.50, 0.55, 0.52, 0.58, 0.63, 0.60, 0.67, 0.72, 0.69, 0.78, 0.85
]

var body: some View {
switch chartType {
case .columns:
bars
case .line:
line
}
}

private var bars: some View {
GeometryReader { proxy in
HStack(alignment: .bottom, spacing: Constants.step0_5) {
ForEach(barHeights.indices, id: \.self) { index in
RoundedRectangle(cornerRadius: 3)
.fill(metric.primaryColor)
.frame(height: proxy.size.height * barHeights[index])
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
}
}

private var line: some View {
GeometryReader { proxy in
let size = proxy.size
let step = size.width / CGFloat(lineHeights.count - 1)
Path { path in
path.addLines(lineHeights.enumerated().map { index, height in
CGPoint(x: CGFloat(index) * step, y: size.height * (1 - height))
})
}
.stroke(metric.primaryColor, style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
}
}
}

private struct CardGradientBackground: View {
let metric: SiteMetric

Expand Down Expand Up @@ -408,3 +460,13 @@ private struct ChartCardPreview: View {
}
.background(Color(.systemGroupedBackground))
}

#Preview("Loading placeholders") {
VStack(spacing: 24) {
ChartLoadingPlaceholder(chartType: .columns, metric: .views)
.frame(height: 140)
ChartLoadingPlaceholder(chartType: .line, metric: .views)
.frame(height: 140)
}
.padding()
}
41 changes: 39 additions & 2 deletions Modules/Sources/JetpackStats/Cards/TodayCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,14 @@ struct TodayCard: View {
if let data = viewModel.data {
makeSparklineView(data)
} else {
let placeholder = makeSparklineView(placeholderData)
.redacted(reason: .placeholder)
// A plain shape instead of a `Chart` with placeholder data. See the
// matching note in `ChartCard`: building Swift Charts during the push
// transition is a measurable part of the first-open lag.
let placeholder = SparklinePlaceholder(metric: .views)
.frame(maxWidth: .infinity)
.padding(.trailing, 32)
.padding(.vertical, 2)
.offset(y: -3)
if viewModel.isLoading {
placeholder.pulsating().opacity(0.33)
} else {
Expand Down Expand Up @@ -201,6 +207,31 @@ struct TodayCard: View {
}
}

/// A cheap stand-in for `SparklineChart` while the first data set loads:
/// a gently rising polyline with light jitter, echoing the bell-curve flank
/// the real chart's placeholder data used to draw. No area wash: under the
/// loading state's dimming its contribution measures ~2/255, invisible.
private struct SparklinePlaceholder: View {
let metric: SiteMetric

private let relativeHeights: [CGFloat] = [
0.08, 0.11, 0.10, 0.14, 0.16, 0.22, 0.25, 0.35, 0.41, 0.53, 0.61, 0.75, 0.83
]

var body: some View {
GeometryReader { proxy in
let size = proxy.size
let step = size.width / CGFloat(relativeHeights.count - 1)
Path { path in
path.addLines(relativeHeights.enumerated().map { index, height in
CGPoint(x: CGFloat(index) * step, y: size.height * (1 - height))
})
}
.stroke(metric.primaryColor, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
}
}
}

private struct SparklineChart: View {
let dataPoints: [(hour: Int, value: Int)]
let previousDataPoints: [(hour: Int, value: Int)]
Expand Down Expand Up @@ -349,3 +380,9 @@ private struct TodayCardPreview: View {
.cardStyle()
}
}

#Preview("Sparkline placeholder") {
SparklinePlaceholder(metric: .views)
.frame(width: 220, height: 52)
.padding()
}
2 changes: 1 addition & 1 deletion RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
27.3
-----
* [*] Stats: Improve performance when opening the Stats screen [#25938]
* [*] Stats: Improve performance when opening the Stats screen [#25938, #25931]

27.2
-----
Expand Down