• 데이터를 보여주는 것을 넘어서 수학 함수를 플롯할 수 있게 되었다.

    • 날씨 트렌드, 기분과 생체 신호 추척, Math Notes 앱의 수학 그래프 등
    • 벡터화된 플로팅 API를 사용해서 더 큰 데이터를 효과적으로 그릴 수 있게 되었다.
  • 함수 플롯

    • LinePlot: 단일 함수를 시각화
    • AreaPlot: 두 함수 사이의 영역을 채워서 시각화
    • 모두 접근성 대응이 되어 있다.
      • Audio Graph도 지원
  • ex. 히스토그램을 함수로 플롯하기

    스크린샷 2024-06-16 오후 7.06.05.png

    • 히스토그램

      Chart {
        ForEach(bins) { bin in
          BarMark(
            x: .value("Capacity density", bin.range),
            y: .value("Probability", bin.probability)
          )
        }
      }
      
    • 라인 플롯 추가

      Chart {
        LinePlot(
          x: "Capacity density", y: "Probability"
        ) { x in // (Double) -> Double
          normalDistribution( // 정규분포 값을 구하는 커스텀 함수
            x,
            mean: mean, 
            standardDeviation: standardDeviation
          )
        }
      
        ForEach(bins) { bin in
          BarMark(
            x: .value("Capacity density", bin.range),
            y: .value("Probability", bin.probability)
          )
        }
      }
      
    • 히스토그램과 다른 색을 가지도록 커스텀

      Chart {
        LinePlot(
          x: "Capacity density", y: "Probability"
        ) { x in
          normalDistribution(x, ...)
        }
        .foregroundStyle(.gray)
        .opacity(0.2)
      }
      
  • ex. 두 함수간의 AreaPlot

    Chart {
      AreaPlot(
        x: "x", yStart: "cos(x)", yEnd: "sin(x)"
      ) { x in
        (yStart: cos(x / 180 * .pi),
         yEnd: sin(x / 180 * .pi))
      }
    }
    
  • 데이터와 다르는 함수는 정의역에 제한이 없다.

    • 기본적으로 Swift Chart는 함수를 샘플링해서 도메인을 추론한다.

    • 이것도 커스텀이 가능하다.

      스크린샷 2024-06-16 오후 9.42.21.png

      Chart {
        AreaPlot(
          x: "x", yStart: "cos(x)", yEnd: "sin(x)"
        ) { x in
          (yStart: cos(x / 180 * .pi),
           yEnd: sin(x / 180 * .pi))
        }
      }
      .chartXScale(domain: -315...225)
      .chartYScale(domain: -5...5)
      
  • 아예 플롯되는 함수 범위도 조정이 가능하다.

    스크린샷 2024-06-16 오후 9.42.41.png

    Chart {
      AreaPlot(
        x: "x", yStart: "cos(x)", yEnd: "sin(x)",
        domain: -135...45
      ) { x in
        (yStart: cos(x / 180 * .pi),
         yEnd: sin(x / 180 * .pi))
      }
    }
    .chartXScale(domain: -315...225)
    .chartYScale(domain: -5...5)
    
  • 매개변수 방정식도 지원한다.

    스크린샷 2024-06-16 오후 9.42.03.png

    Chart {
      LinePlot(
        x: "x", y: "y", t: "t", domain: -.pi ... .pi
      ) { t in
        let x = sqrt(2) * pow(sin(t), 3)
        let y = cos(t) * (2 - cos(t) - pow(cos(t), 2))
        return (x, y)
      }
    }
    .chartXScale(domain: -3...3)
    .chartYScale(domain: -4...2)
    
  • 값이 정의되지 않는 범위를 표현하기 위해서는 nan을 반환하면 된다.

    스크린샷 2024-06-16 오후 9.41.50.png

    Chart {
      LinePlot(x: "x", y: "1 / x") { x in
        if x < 0 {
          return .nan
        }
        return x + 1
      }
    }
    .chartXScale(domain: -5...10)
    .chartYScale(domain: -5...10)
    

    스크린샷 2024-06-16 오후 9.44.31.png

    Chart {
      LinePlot(x: "x", y: "1 / x") { x in
        guard x != 0 else {
          return .nan
        }
        return 1 / x
      }
    }
    .chartXScale(domain: -10...10)
    .chartYScale(domain: -10...10)
    
  • 플롯은 함수뿐 아니라 큰 데이터 셋을 시각화하는데도 좋다.

    • 그래서 다른 마커 타입들에도 플롯 API를 추가 했다.

      스크린샷 2024-06-16 오후 9.55.10.png

  • Vectorized Plot

    • Swift Chart는 자유로운 커스텀이 가능하다.

      Chart {
        ForEach(model.data) {
          if $0.capacityDensity > 0.0001 {
            RectangleMark(
              x: .value("Longitude", $0.x),
              y: .value("Latitude", $0.y)
            )
            .foregroundStyle(by: .value("Axis type", $0.axisType))
          } else {
            PointMark(
              x: .value("Longitude", $0.x),
              y: .value("Latitude", $0.y)
            )
            .opacity(0.5)
          }
        }
      }
      
    • 하지만 보통은 전체 데이터에 동일한 Mark를 적용한다.

      Chart {
        ForEach(model.data) {
          RectangleMark(
            x: .value("Longitude", $0.x),
            y: .value("Latitude", $0.y)
          )
          .foregroundStyle(by: .value("Axis type", $0.panelAxisType))
          .opacity($0.capacityDensity)
        }
      }
      
    • 새로운 Vectorized 플롯 API는 전체 데이터를 인자로 받아서 더 효율적인 처리를 가능하게 한다.

      Chart {
        RectanglePlot(
          model.data,
          x: .value("Longitude", \\.x),
          y: .value("Latitude", \\.y)
        )
        .foregroundStyle(by: .value("Axis type", \\.panelAxisType))
        .opacity(\\.capacityDensity)
      }
      
  • ex. 전미 태양광 설치위치 시각화

    • 데이터모델 정의

      struct DataPoint: Identifiable {
      	let id: Int
      	
      	let capacity: Double
      	let panelAxisType: String
      	
      	let xLongitude: Double
      	let yLatitude: Double
      }
      
    • 평면에 그리기 위해서는 Albers projection을 적용해야 한다.

      • 계산 프로퍼티로 할 수도 있을 것이다. 이 경우는 getter가 매번 호출될 것이다.

        extension DataPoint {
        	var x: Double { ... }
        	var y: Double { ... }
        }
        
      • 하지만 저장 프로퍼티로 하면 고정된 메모리 오프셋으로 Swift Chart가 접근할 수 있어서 성능을 최대화할 수 있다.

        struct DataPoint: Identifiable {
        	let id: Int
        	
        	let capacity: Double
        	let panelAxisType: String
        	
        	let xLongitude: Double
        	let yLatitude: Double
        	
        	// Albers projection
        	var x: Double
        	var y: Double
        }
        
    • PointPlot 적용

      • keyPath를 사용해서 순차적으로 순회하지 않고도 값을 가져온다.

      스크린샷 2024-06-16 오후 10.12.53.png

      Chart {
      	contiguousUSMap
        RectanglePlot(
          model.data,
          x: .value("Longitude", \\.x),
          y: .value("Latitude", \\.y)
        )
      }
      
    • modifier에서도 keyPath를 받는다.

      스크린샷 2024-06-16 오후 10.15.21.png

      Chart {
      contiguousUSMap
        RectanglePlot(
          model.data,
          x: .value("Longitude", \\.x),
          y: .value("Latitude", \\.y)
        )
        .foregroundStyle(by: .value("Axis type", \\.panelAxisType))
        .opacity(\\.capacityDensity)
      }
      

      스크린샷 2024-06-16 오후 10.15.29.png

  • 언제 Vectorized Plot API를 써야 하는가?

    • 데이터셋이 크고
    • 모두 동일한 modifier와 plot을 적용할 때
  • 기존 Mark API를 사용할 때

    • 적은 데이터 포인트
    • 각 포인트 별로 각자 커스텀이 필요할 때
    • 혹은 zIndex를 사용한 복잡한 커스텀 레이어링이 필요할 때
  • vectorized plot을 효과적으로 쓰기 위해서는

    • 같은 스타일을 가진 데이터들을 묶어서 적용하기
    • 계산 프로퍼티는 피할 것
    • 사용할 스타일이나 전체 정의역 범위를 알고 있다면 명시하는 게 좋다.
    • 세부적인 스타일링은 큰 데이터 셋에서는 티가 안날 수 있어서 이런 건 스킵하는 게 좋다.