Skip to content

Commit 04fde13

Browse files
authored
Stats: Minor visual changes based on feedback (#25404)
* Rework the Chart header * Slightly less spacing in today card * Smaller chart height and top padding * Show subrange if selected * Fix placeholder * Add missing animation * Smaller bottom overlay * Lighter blue for .views metric * Smaller font in Today card * Remove chevron from today card * Fix kerning * Fix animations * Back to blue * Update release notes * Revert the card spacing change * Update StandaloneChartCard to use the same header as the main chart and remove ChartLegendView * Remove unused ChartLegendView
1 parent 6ab5023 commit 04fde13

11 files changed

Lines changed: 105 additions & 168 deletions

Modules/Sources/JetpackStats/Cards/ChartCard.swift

Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Charts
33

44
struct ChartCard: View {
55
@ObservedObject private var viewModel: ChartCardViewModel
6+
@Environment(\.context) private var context
67

78
private var dateRange: StatsDateRange { viewModel.effectiveDateRange }
89
private var metrics: [SiteMetric] { viewModel.metrics }
@@ -11,20 +12,20 @@ struct ChartCard: View {
1112

1213
@State private var isShowingRawData = false
1314

14-
@ScaledMetric(relativeTo: .largeTitle) private var chartHeight = 180
15+
@ScaledMetric(relativeTo: .largeTitle) private var chartHeight = 140
1516

1617
init(viewModel: ChartCardViewModel) {
1718
self.viewModel = viewModel
1819
}
1920

2021
var body: some View {
2122
VStack(spacing: 0) {
22-
VStack(spacing: Constants.step1) {
23+
VStack(spacing: Constants.step0_5) {
2324
headerView(for: selectedMetric)
2425
.unredacted()
2526
contentView
2627
}
27-
.padding(.vertical, Constants.step2)
28+
.padding(.vertical, 14)
2829
.padding(.horizontal, Constants.step3)
2930
.contentShape(Rectangle())
3031
.onTapGesture {
@@ -69,22 +70,29 @@ struct ChartCard: View {
6970
}
7071
}
7172

73+
private func makeHeaderViewModel(for metric: SiteMetric) -> ChartCardHeaderView.ViewModel {
74+
let data = viewModel.chartData[selectedMetric] ?? mockChartData
75+
return ChartCardHeaderView.ViewModel(
76+
trend: viewModel.selectedBarTrend ?? .make(data, context: .regular),
77+
metricTitle: metric.localizedTitle,
78+
period: context.formatters.dateRange.string(from: viewModel.dateRange.subrange ?? viewModel.dateRange.range),
79+
showComparison: dateRange.comparison != .off
80+
)
81+
}
82+
7283
private func headerView(for metric: SiteMetric) -> some View {
73-
HStack(alignment: .center) {
74-
StatsCardTitleView(title: metric.localizedTitle)
75-
Spacer(minLength: 0)
76-
}
77-
.accessibilityElement(children: .combine)
78-
.accessibilityLabel(Strings.Accessibility.cardTitle(metric.localizedTitle))
84+
ChartCardHeaderView(viewModel: makeHeaderViewModel(for: metric))
85+
.redacted(reason: viewModel.isFirstLoad ? .placeholder : [])
86+
// Leave room for the "more" menu overlay (50pt button, 24pt card padding = 26pt overlap)
87+
.padding(.trailing, Constants.step3 + Constants.step0_5)
88+
.accessibilityElement(children: .combine)
89+
.accessibilityLabel(Strings.Accessibility.cardTitle(metric.localizedTitle))
7990
}
8091

8192
@ViewBuilder
8293
private var contentView: some View {
83-
VStack(spacing: Constants.step1) {
84-
if dateRange.comparison != .off || metrics.count == 1 {
85-
chartHeaderView
86-
.padding(.trailing, -Constants.step0_5)
87-
}
94+
// warning: important to put `chartContentView` in a container in order for animations to work properly. Do NOT remove the container.
95+
HStack {
8896
chartContentView
8997
}
9098
.environment(\.showComparison, dateRange.comparison != .off)
@@ -93,33 +101,6 @@ struct ChartCard: View {
93101
.animation(.easeInOut, value: viewModel.isFirstLoad)
94102
}
95103

96-
private var chartHeaderView: some View {
97-
// Showing currently selected (not loaded period) by design
98-
HStack(alignment: .center, spacing: 0) {
99-
if let data = viewModel.chartData[selectedMetric] {
100-
ChartValuesSummaryView(
101-
trend: viewModel.selectedBarTrend ?? .make(data, context: .regular),
102-
style: .compact
103-
)
104-
} else if viewModel.isFirstLoad {
105-
ChartValuesSummaryView(
106-
trend: .init(currentValue: 100, previousValue: 10, metric: SiteMetric.views),
107-
style: .compact
108-
)
109-
.redacted(reason: .placeholder)
110-
}
111-
112-
Spacer(minLength: 8)
113-
114-
ChartLegendView(
115-
metric: selectedMetric,
116-
currentPeriod: viewModel.dateRange.subrange?.dateInterval ?? dateRange.dateInterval,
117-
previousPeriod: viewModel.dateRange.subrange?.effectiveComparisonInterval ?? dateRange.effectiveComparisonInterval
118-
)
119-
}
120-
.dynamicTypeSize(...DynamicTypeSize.xxLarge)
121-
}
122-
123104
@ViewBuilder
124105
private var chartContentView: some View {
125106
if viewModel.isFirstLoad {
@@ -291,7 +272,7 @@ private struct CardGradientBackground: View {
291272
LinearGradient(
292273
colors: [
293274
metric.primaryColor.opacity(colorScheme == .light ? 0.03 : 0.04),
294-
Constants.Colors.secondaryBackground
275+
Constants.Colors.secondaryBackground,
295276
],
296277
startPoint: .top,
297278
endPoint: .center
@@ -320,6 +301,48 @@ public enum ChartType: String, CaseIterable, Identifiable, Codable {
320301
}
321302
}
322303

304+
// MARK: - ChartCardHeaderView
305+
306+
struct ChartCardHeaderView: View {
307+
struct ViewModel: Equatable {
308+
let trend: TrendViewModel
309+
let metricTitle: String
310+
let period: String
311+
let showComparison: Bool
312+
}
313+
314+
let viewModel: ViewModel
315+
316+
var body: some View {
317+
HStack {
318+
VStack(alignment: .leading, spacing: -1) {
319+
HStack(alignment: .lastTextBaseline, spacing: 3) {
320+
Text(viewModel.trend.formattedCurrentValue)
321+
.font(.system(.title2, design: .rounded, weight: .semibold))
322+
.kerning(-0.5)
323+
.foregroundColor(.primary)
324+
.contentTransition(.numericText())
325+
Text(viewModel.metricTitle)
326+
.font(.caption.weight(.medium))
327+
.foregroundColor(.secondary)
328+
}
329+
Text(viewModel.period)
330+
.font(.system(.caption, design: .rounded, weight: .medium))
331+
.foregroundStyle(Color.secondary)
332+
if viewModel.showComparison {
333+
Text("\(viewModel.trend.formattedChange) \(viewModel.trend.iconSign) \(viewModel.trend.formattedPercentage)")
334+
.font(.caption.weight(.semibold))
335+
.foregroundColor(viewModel.trend.sentiment.foregroundColor)
336+
.contentTransition(.numericText())
337+
.padding(.top, 5)
338+
}
339+
}
340+
Spacer(minLength: 0)
341+
}
342+
.animation(.spring, value: viewModel)
343+
}
344+
}
345+
323346
private struct ChartCardPreview: View {
324347
@StateObject var viewModel = ChartCardViewModel(
325348
configuration: ChartCardConfiguration(

Modules/Sources/JetpackStats/Cards/StandaloneChartCard.swift

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct StandaloneChartCard: View {
2222
@State private var isShowingDatePicker = false
2323
@State private var chartData: ChartData?
2424

25-
@ScaledMetric(relativeTo: .largeTitle) private var chartHeight = 180
25+
@ScaledMetric(relativeTo: .largeTitle) private var chartHeight = 140
2626

2727
@Environment(\.context) private var context
2828

@@ -54,10 +54,7 @@ struct StandaloneChartCard: View {
5454

5555
var body: some View {
5656
VStack(spacing: Constants.step1) {
57-
StatsCardTitleView(title: metric.localizedTitle)
58-
.frame(maxWidth: .infinity, alignment: .leading)
5957
chartHeaderView
60-
.padding(.trailing, -Constants.step0_5)
6158
chartContentView
6259
.padding(.horizontal, -Constants.step1)
6360
dateRangeControls
@@ -79,29 +76,24 @@ struct StandaloneChartCard: View {
7976
}
8077

8178
private var chartHeaderView: some View {
82-
// Showing currently selected (not loaded period) by design
83-
HStack(alignment: .firstTextBaseline, spacing: 0) {
84-
if let data = chartData {
85-
ChartValuesSummaryView(
86-
trend: .make(data, context: .regular),
87-
style: .compact
88-
)
89-
} else {
90-
ChartValuesSummaryView(
91-
trend: .init(currentValue: 100, previousValue: 10, metric: SiteMetric.views),
92-
style: .compact
93-
)
94-
.redacted(reason: .placeholder)
95-
}
96-
97-
Spacer(minLength: 8)
79+
ChartCardHeaderView(viewModel: makeHeaderViewModel())
80+
.redacted(reason: chartData == nil ? .placeholder : [])
81+
.padding(.trailing, Constants.step3 + Constants.step0_5)
82+
}
9883

99-
ChartLegendView(
100-
metric: metric,
101-
currentPeriod: dateRange.dateInterval,
102-
previousPeriod: dateRange.effectiveComparisonInterval
103-
)
84+
private func makeHeaderViewModel() -> ChartCardHeaderView.ViewModel {
85+
let trend: TrendViewModel
86+
if let chartData {
87+
trend = .make(chartData, context: .regular)
88+
} else {
89+
trend = TrendViewModel(currentValue: 100, previousValue: 10, metric: metric)
10490
}
91+
return ChartCardHeaderView.ViewModel(
92+
trend: trend,
93+
metricTitle: metric.localizedTitle,
94+
period: context.formatters.dateRange.string(from: dateRange),
95+
showComparison: dateRange.comparison != .off
96+
)
10597
}
10698

10799
private var chartContentView: some View {
@@ -142,19 +134,6 @@ struct StandaloneChartCard: View {
142134
}
143135
}
144136

145-
// MARK: –
146-
147-
private var trend: TrendViewModel {
148-
guard let chartData else {
149-
return TrendViewModel(currentValue: 0, previousValue: 0, metric: metric)
150-
}
151-
return TrendViewModel(
152-
currentValue: chartData.currentTotal,
153-
previousValue: chartData.previousTotal,
154-
metric: metric
155-
)
156-
}
157-
158137
private func refreshChartData() async {
159138
let chartData = await generateChartData(
160139
dataPoints: dataPoints,
@@ -302,7 +281,7 @@ private func generateChartData(
302281

303282
#Preview {
304283
struct PreviewWrapper: View {
305-
@State private var chartType: ChartType = .line
284+
@State private var chartType: ChartType = .columns
306285

307286
var body: some View {
308287
StandaloneChartCard(

Modules/Sources/JetpackStats/Cards/TodayCard.swift

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@ struct TodayCard: View {
88
@ScaledMetric(relativeTo: .title)
99
private var sparklineHeight: CGFloat = 52
1010

11-
private var isChevronHidden = false
12-
1311
init(viewModel: TodayCardViewModel) {
1412
self.viewModel = viewModel
1513
}
1614

1715
var body: some View {
18-
VStack(alignment: .leading, spacing: 6) {
16+
VStack(alignment: .leading, spacing: 2) {
1917
HStack(spacing: 32) {
2018
VStack(alignment: .leading, spacing: 0) {
2119
headerView
@@ -40,11 +38,6 @@ struct TodayCard: View {
4038
.overlay(alignment: .topTrailing) {
4139
moreMenu
4240
}
43-
.overlay(alignment: .bottomTrailing) {
44-
if !isChevronHidden {
45-
chevron
46-
}
47-
}
4841
.cardStyle()
4942
.animation(.spring, value: viewModel.data?.id)
5043
.accessibilityElement(children: .contain)
@@ -71,7 +64,7 @@ struct TodayCard: View {
7164
.font(.caption.weight(.medium))
7265
}
7366
.foregroundStyle(Color.secondary)
74-
.offset(y: 9) // Get it close to the value
67+
.offset(y: 3) // Get it close to the value
7568
.dynamicTypeSize(...DynamicTypeSize.large)
7669
}
7770

@@ -81,21 +74,6 @@ struct TodayCard: View {
8174
return formatter.string(from: Date())
8275
}
8376

84-
func chevronHidden(_ isHidden: Bool = true) -> TodayCard {
85-
var copy = self
86-
copy.isChevronHidden = isHidden
87-
return copy
88-
}
89-
90-
private var chevron: some View {
91-
Image(systemName: "chevron.down")
92-
.font(.system(size: 14, weight: .bold))
93-
.foregroundStyle(Color.secondary.opacity(0.4))
94-
.padding(.bottom, 22)
95-
.padding(.trailing, 18)
96-
.layoutPriority(1)
97-
}
98-
9977
// MARK: - Content Views
10078

10179
@ViewBuilder
@@ -114,11 +92,11 @@ struct TodayCard: View {
11492
}
11593

11694
private func makeMetricsView(with metrics: SiteMetricsSet) -> some View {
117-
HStack(alignment: .bottom, spacing: 20) {
95+
HStack(alignment: .bottom, spacing: 16) {
11896
ForEach(viewModel.configuration.metrics) { metric in
11997
if metric == .views {
12098
TodayCardProminentMetricView(value: metrics[metric], metric: metric)
121-
.offset(y: 2.5) // Compensate for the larger line height
99+
.offset(y: 1) // Compensate for the larger line height
122100
} else {
123101
TodayCardMetricView(metric: metric, value: metrics[metric])
124102
}
@@ -151,7 +129,8 @@ struct TodayCard: View {
151129
)
152130
.frame(maxWidth: .infinity)
153131
.padding(.trailing, 32)
154-
.padding(.vertical, 6)
132+
.padding(.vertical, 2)
133+
.offset(y: -9)
155134
.transition(.opacity.combined(with: .scale(scale: 0.97)))
156135
}
157136

@@ -319,8 +298,8 @@ private struct TodayCardProminentMetricView: View {
319298
var body: some View {
320299
Text(formattedValue)
321300
.contentTransition(.numericText())
322-
.font(Font.system(.title, design: .rounded, weight: .medium))
323-
.kerning(-1.0)
301+
.font(Font.system(.title2, design: .rounded, weight: .semibold))
302+
.kerning(-0.33)
324303
.foregroundColor(.primary)
325304
.lineLimit(1)
326305
.animation(.spring, value: value)

Modules/Sources/JetpackStats/Charts/BarChartView.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ struct BarChartView: View {
7171
width: .automatic
7272
)
7373
.foregroundStyle(isIncomplete ? AnyShapeStyle(incompleteBarPattern) : AnyShapeStyle(barGradient))
74-
.cornerRadius(4)
74+
.cornerRadius(5)
7575
.opacity(getOpacityForPeriodBar(for: point))
7676
}
7777
}
@@ -139,7 +139,7 @@ struct BarChartView: View {
139139
stacking: .unstacked
140140
)
141141
.foregroundStyle(Color.secondary.opacity(0.25))
142-
.cornerRadius(4)
142+
.cornerRadius(5)
143143
.opacity(getOpacityForPeriodBar(for: point))
144144
}
145145
}
@@ -270,7 +270,7 @@ struct BarChartView: View {
270270
return data.maxValue...0 // Just in case; should never happend
271271
}
272272
// Add some padding above the max value
273-
let padding = max(Int(Double(data.maxValue) * 0.66), 1)
273+
let padding = max(Int(Double(data.maxValue) * 0.33), 1)
274274
return 0...(data.maxValue + padding)
275275
}
276276

Modules/Sources/JetpackStats/Charts/LineChartView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ struct LineChartView: View {
265265
return data.maxValue...0 // Just in case; should never happend
266266
}
267267
// Add some padding above the max value
268-
let padding = max(Int(Double(data.maxValue) * 0.66), 1)
268+
let padding = max(Int(Double(data.maxValue) * 0.33), 1)
269269
return 0...(data.maxValue + padding)
270270
}
271271

Modules/Sources/JetpackStats/Screens/TrafficTabView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ struct TrafficTabView: View {
125125
self.viewModel.handleTodayCardTap()
126126
} label: {
127127
TodayCard(viewModel: viewModel)
128-
.chevronHidden(self.viewModel.isTodayFocused)
129128
}
130129
.buttonStyle(.plain)
131130
default:

0 commit comments

Comments
 (0)