Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create New Cluster View to use SwiftCharts #161

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
18 changes: 18 additions & 0 deletions Cluster/Chart/BarChart/BarChartGroupBy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// BarChartGroupBy.swift
// Telemetry Viewer
//
// Created by Daniel Jilg on 04.08.23.
//

import SwiftUI

struct BarChartGroupBy: View {
var body: some View {
Text("Bar Chart GroupBy")
}
}

#Preview {
BarChartGroupBy()
}
27 changes: 27 additions & 0 deletions Cluster/Chart/BarChart/BarChartTimeSeries.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// BarChartTimeSeries.swift
// Telemetry Viewer
//
// Created by Daniel Jilg on 04.08.23.
//

import SwiftUI
import Charts
import DataTransferObjects


struct BarChartTimeSeries: View {
let query: CustomQuery
let result: TimeSeriesQueryResult

var body: some View {
Chart {
ForEach(result.rows) { row in
BarMark(
x: .value("Date", row.timestamp),
y: .value("Total Count", row.result["count"]?.value ?? 0)
)
}
}
}
}
18 changes: 18 additions & 0 deletions Cluster/Chart/BarChart/BarChartTopN.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// BarChartTopN.swift
// Telemetry Viewer
//
// Created by Daniel Jilg on 04.08.23.
//

import SwiftUI

struct BarChartTopN: View {
var body: some View {
Text("Bar Chart TopN")
}
}

#Preview {
BarChartTopN()
}
27 changes: 27 additions & 0 deletions Cluster/Chart/BarChart/ClusterBarChart.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// ClusterBarChart.swift
// Telemetry Viewer
//
// Created by Daniel Jilg on 04.08.23.
//

import SwiftUI
import DataTransferObjects

struct ClusterBarChart: View {
let query: CustomQuery
let result: QueryResult

var body: some View {
switch query.queryType {
case .timeseries:
if case let .timeSeries(result) = result {
BarChartTimeSeries(query: query, result: result)
} else {
Text("Mismatch in query type and result type")
}
default:
Text("\(query.queryType.rawValue) bar charts are not supported.")
}
}
}
27 changes: 27 additions & 0 deletions Cluster/Chart/ClusterChart.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Chart.swift
// Telemetry Viewer
//
// Created by Daniel Jilg on 04.08.23.
//

import SwiftUI
import DataTransferObjects

/// Cluster/Chart – given a query and a result, displays the result
struct ClusterChart: View {
enum ChartType {
case bar
}

let query: CustomQuery
let result: QueryResult
let type: ChartType

var body: some View {
switch type {
case .bar:
ClusterBarChart(query: query, result: result)
}
}
}
89 changes: 89 additions & 0 deletions Cluster/ChartsExperiment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// ChartsExperiment.swift
// Telemetry Viewer
//
// Created by Daniel Jilg on 04.08.23.
//

import Charts
import DataTransferObjects
import SwiftUI
import TelemetryClient


let queries: [String: CustomQuery] = [
"default": CustomQuery(
queryType: .timeseries,
dataSource: "telemetry-signals",
relativeIntervals: [.init(beginningDate: .init(.beginning, of: .day, adding: -30), endDate: .init(.end, of: .day, adding: 0))],
granularity: .day,
aggregations: [.thetaSketch(.init(type: .thetaSketch, name: "count", fieldName: "clientUser"))]
),
"daily-users": CustomQuery(
queryType: .timeseries,
dataSource: "telemetry-signals",
relativeIntervals: [.init(beginningDate: .init(.beginning, of: .day, adding: -30), endDate: .init(.end, of: .day, adding: 0))],
granularity: .day,
aggregations: [.longSum(.init(type: .longSum, name: "count", fieldName: "count"))]
),
"monthly-signals": CustomQuery(
queryType: .timeseries,
dataSource: "telemetry-signals",
relativeIntervals: [.init(beginningDate: .init(.beginning, of: .day, adding: -30), endDate: .init(.end, of: .day, adding: 0))],
granularity: .hour,
aggregations: [.longSum(.init(type: .longSum, name: "count", fieldName: "count"))]
),
]

extension TimeSeriesQueryResultRow: Identifiable {
public var id: Date { timestamp }
}

struct ChartsExperiment: View {
var body: some View {
Grid(horizontalSpacing: 15, verticalSpacing: 15) {
GridRow {
ClusterInstrument(query: queries["default"]!, title: "default", type: .bar).padding()
.glassBackgroundEffect(in: .rect(cornerRadius: 15))
.rotation3DEffect(.init(angle: .degrees(-5), axis: .x), anchor: .bottom)

// .transform3DEffect(.init(rotation: .init(angle: .degrees(5), axis: .y)))
ClusterInstrument(query: queries["daily-users"]!, title: "Daily Users", type: .bar).padding()
.glassBackgroundEffect(in: .rect(cornerRadius: 15))
.rotation3DEffect(.init(angle: .degrees(-5), axis: .x), anchor: .bottom)
ClusterInstrument(query: queries["monthly-signals"]!, title: "Monthly Signals", type: .bar).padding()
.glassBackgroundEffect(in: .rect(cornerRadius: 15))
.rotation3DEffect(.init(angle: .degrees(-5), axis: .x), anchor: .bottom)
// .transform3DEffect(.init(rotation: .init(angle: .degrees(-5), axis: .y)))
}
GridRow {
ClusterInstrument(query: queries["default"]!, title: "default", type: .bar).padding()
.glassBackgroundEffect(in: .rect(cornerRadius: 15))
// .transform3DEffect(.init(rotation: .init(angle: .degrees(5), axis: .y)))
ClusterInstrument(query: queries["daily-users"]!, title: "Daily Users", type: .bar).padding()
.glassBackgroundEffect(in: .rect(cornerRadius: 15))
ClusterInstrument(query: queries["monthly-signals"]!, title: "Monthly Signals", type: .bar).padding()
.glassBackgroundEffect(in: .rect(cornerRadius: 15))
// .transform3DEffect(.init(rotation: .init(angle: .degrees(-5), axis: .y)))
}
GridRow {
ClusterInstrument(query: queries["default"]!, title: "default", type: .bar).padding()
.glassBackgroundEffect(in: .rect(cornerRadius: 15))
.transform3DEffect(.init(rotation: .init(angle: .degrees(5), axis: .x)))
// .transform3DEffect(.init(rotation: .init(angle: .degrees(5), axis: .y)))
ClusterInstrument(query: queries["daily-users"]!, title: "Daily Users", type: .bar).padding()
.glassBackgroundEffect(in: .rect(cornerRadius: 15))
.transform3DEffect(.init(rotation: .init(angle: .degrees(5), axis: .x)))
ClusterInstrument(query: queries["monthly-signals"]!, title: "Monthly Signals", type: .bar).padding()
.glassBackgroundEffect(in: .rect(cornerRadius: 15))
.transform3DEffect(.init(rotation: .init(angle: .degrees(5), axis: .x)))
// .transform3DEffect(.init(rotation: .init(angle: .degrees(-5), axis: .y)))
}

}

.onAppear() {
TelemetryManager.send("ChartsExperimentShown", for: "cant-generate-user")
}
}
}
26 changes: 26 additions & 0 deletions Cluster/ClusterInstrument.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// ClusterInstrument.swift
// Telemetry Viewer
//
// Created by Daniel Jilg on 04.08.23.
//

import SwiftUI
import DataTransferObjects


struct ClusterInstrument: View {
let query: CustomQuery
let title: String
let type: ClusterChart.ChartType

var body: some View {
VStack(alignment: .leading) {
Text(title)
.font(.title)
.padding(.bottom)
QueryRunner(query: query, type: type)
}
.padding()
}
}
133 changes: 133 additions & 0 deletions Cluster/QueryRunner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//
// QueryRunner.swift
// Telemetry Viewer
//
// Created by Daniel Jilg on 04.08.23.
//

import DataTransferObjects
import SwiftUI

struct QueryRunner: View {
let query: CustomQuery
let type: ClusterChart.ChartType

@State var queryResultWrapper: QueryResultWrapper?
@State var isLoading: Bool = false

var body: some View {
VStack {
if let queryResult = queryResultWrapper?.result {
ClusterChart(query: query, result: queryResult, type: .bar)
}

if let queryResultWrapper = queryResultWrapper {
Text("Calculation took \(queryResultWrapper.calculationDuration) seconds")
Text("Last updated \(queryResultWrapper.calculationFinishedAt)")
}

if isLoading {
Text("Loading...")
}
}
.onAppear {
Task {
do {
try await getQueryResult()
}
catch {
print(error)
}
}
}
}

private func getQueryResult() async throws {
isLoading = true
defer {
isLoading = false
}

let taskID = try await beginAsyncCalculation()

try await getLastSuccessfulValue(taskID)

try await waitUntilTaskStatusIsSuccessful(taskID)

try await getLastSuccessfulValue(taskID)
}
}

extension QueryRunner {
private enum ApiVersion: String {
case v1
case v2
case v3
}

private func urlForPath(apiVersion: ApiVersion = .v1, _ path: String..., appendTrailingSlash _: Bool = false) -> URL {
URL(string: "https://api.telemetrydeck.com/api/" + "\(apiVersion.rawValue)/" + path.joined(separator: "/") + "/")!
}

private func authenticatedURLRequest(for url: URL, httpMethod: String, httpBody: Data? = nil, contentType: String = "application/json; charset=utf-8") -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = httpMethod
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
request.setValue(UserTokenDTO(id: UUID(uuidString: "7c736171-9c56-4573-b72f-f9f881c12d7b"), value: "QL41IC283ELWFYXRHC6G22VMTPFK7A3GN1L6INTHPCJGTKA4Y76YZF572P1H2U8OR6HQHUXKWP91M2URKUA49SFCYKQ2XZ8WS0WMR28N2I91OCTDHJMYBAQAIKG2QO73", user: [:]).bearerTokenAuthString, forHTTPHeaderField: "Authorization")

if let httpBody = httpBody {
request.httpBody = httpBody
}

return request
}
}

extension QueryRunner {
private func beginAsyncCalculation() async throws -> String {
// create a query task
let queryBeginURL = urlForPath(apiVersion: .v3, "query", "calculate-async")
// print(queryBeginURL)
let queryHTTPBody = try JSONEncoder.telemetryEncoder.encode(query)
var queryBeginRequest = authenticatedURLRequest(for: queryBeginURL, httpMethod: "POST", httpBody: queryHTTPBody)
queryBeginRequest.httpMethod = "POST"
let (data, _) = try await URLSession.shared.data(for: queryBeginRequest)
// print(String(data: data, encoding: .utf8) ?? "")
guard let taskID = try JSONDecoder.telemetryDecoder.decode([String: String].self, from: data)["queryTaskID"] else {
throw TransferError.decodeFailed
}

return taskID
}

private func getLastSuccessfulValue(_ taskID: String) async throws {
// pick up the finished result
let lastSuccessfulValueURL = urlForPath(apiVersion: .v3, "task", taskID, "lastSuccessfulValue")
// print(lastSuccessfulValueURL)
let lastSuccessfulValueURLRequest = authenticatedURLRequest(for: lastSuccessfulValueURL, httpMethod: "GET")
let (newQueryResultData, response) = try await URLSession.shared.data(for: lastSuccessfulValueURLRequest)
// print(String(data: newQueryResultData, encoding: .utf8) ?? "")
if (response as? HTTPURLResponse)?.statusCode == 200 {
queryResultWrapper = try JSONDecoder.telemetryDecoder.decode(QueryResultWrapper.self, from: newQueryResultData)
}
}

private func waitUntilTaskStatusIsSuccessful(_ taskID: String) async throws {
// wait for the task to finish caluclating
var taskStatus: QueryTaskStatus = .running
while taskStatus != .successful {
let taskStatusURL = urlForPath(apiVersion: .v3, "task", taskID, "status")
// print(taskStatusURL)
let taskStatusURLRequest = authenticatedURLRequest(for: taskStatusURL, httpMethod: "GET")
let (data, _) = try await URLSession.shared.data(for: taskStatusURLRequest)
// print(String(data: data, encoding: .utf8) ?? "")
let queryTaskStatus = try JSONDecoder.telemetryDecoder.decode(QueryTaskStatusStruct.self, from: data)
taskStatus = queryTaskStatus.status
try await Task.sleep(nanoseconds: 1_000_000_000)
}

if taskStatus == .error {
throw TransferError.serverError(message: "The server returned an error")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"pathsToIds" : {
"\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/GridMaterial.usda" : "CB766F92-EE55-4A63-9401-E7B8C009764D",
"\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Immersive.usda" : "65F6F990-A780-4474-B78B-572E0E4E273D",
"\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Scene.usda" : "0A9B4653-B11E-4D6A-850E-C6FCB621626C",
"\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Untitled Scene.usda" : "D560BB77-AAF3-4BDE-B7C4-989332A4688B",
"RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/_GridMaterial.usda" : "CC9FD5C7-161F-48CA-B2B9-61DD597CBD66",
"RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/_PlainMaterial.usda" : "1510F6C7-214D-4800-9C6C-F3964D5FC3E3",
"RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/GridMaterial.usda" : "66168B71-AB05-424E-8B6C-D33D6E61B08F",
"RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Immersive.usda" : "AF09ED6F-1707-48FD-8720-65B998362C09",
"RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Scene.usda" : "D66134B1-3681-4A8E-AFE5-29F257229F3B"
}
}
Loading