• Pie Charts 추가

    • 전체 값에서 각 카테고리가 차지하는 비중을 나타낸다.

    • 축이 없고, 정확도가 크게 중요하지 않다.

    • 전체와 부분의 관계를 직관적으로 보여준다.

    • 기존 Mark 조합으로 Chart를 만들던 것 대로 만들수 있다.

      • 새로운 SectorMark를 사용한다.
      • 극좌표계를 사용한다.(angle)
        • 인자로 주어지는 angle값은 normalize된다.
      • 각 섹터가 보이는 부분도 커스텀 가능.
        • inner radius를 키우면 donut chart가 된다.
    • ex. 기존 stacked barMark 기반 chart를 pie chart로 바꾸기

      Chart(data, id: \\.name) { element in
        BarMark(
          x: .value("Sales", element.sales),
          stacking: .normalized
        )
        .foregroundStyle(by: .value("Name", element.name))
      }
      .chartXAxis(.hidden)
      

      스크린샷 2023-06-17 오후 2.58.02.png

      Chart(data, id: \\.name) { element in
        SectorMark(
          angle: .value("Sales", element.sales)
        )
        .foregroundStyle(by: .value("Name", element.name))
      }
      

      스크린샷 2023-06-17 오후 2.58.51.png

    • 조각 사이의 간격주기

      • 각 조각에 개별적으로 적용되므로 두 조각의 inset 합이 실제 두 조각 사이 간격이다.
      Chart(data, id: \\.name) { element in
        SectorMark(
          angle: .value("Sales", element.sales),
          angularInset: 1.5
        )
        .foregroundStyle(by: .value("Name", element.name))
      }
      

      스크린샷 2023-06-17 오후 3.01.26.png

    • 각 조각에 cornerradius주기

      Chart(data, id: \\.name) { element in
        SectorMark(
          angle: .value("Sales", element.sales),
          angularInset: 1.5
        )
        .cornerRadius(5)
        .foregroundStyle(by: .value("Name", element.name))
      }
      

      스크린샷 2023-06-17 오후 3.03.39.png

    • donut chart로 바꾸기

      Chart(data, id: \\.name) { element in
        SectorMark(
          angle: .value("Sales", element.sales),
          innerRadius: .ratio(0.618),
          angularInset: 1.5
        )
        .cornerRadius(5)
        .foregroundStyle(by: .value("Name", element.name))
      }
      

      스크린샷 2023-06-17 오후 3.04.28.png

    • 도넛 차트 가운데 텍스트 넣기

      Chart(data, id: \\.name) { element in
        SectorMark(
          angle: .value("Sales", element.sales),
          innerRadius: .ratio(0.618),
          angularInset: 1.5
        )
        .cornerRadius(5)
        .foregroundStyle(by: .value("Name", element.name))
      }
      .chartBackground { chartProxy in
        GeometryReader { geometry in
          let frame = geometry[chartProxy.plotAreaFrame]
          VStack {
            Text("Most Sold Style")
              .font(.callout)
              .foregroundStyle(.secondary)
            Text(mostSold)
              .font(.title2.bold())
              .foregroundColor(.primary)
          }
          .position(x: frame.midX, y: frame.midY)
        }
      }
      
  • Selection

    • 상호작용이 가능해지면 사용자가 자연스럽게 점진적으로 상세 정보를 탐색할 수 있게 된다.

      • `ex. 애플 건강앱 심박수 차트
    • selection 핸들링하기

      • iOS는 한손 터치, macOS는 호버
      struct LocationDetailsChart: View {
        @Binding var rawSelectedDate: Date?
      
        var selectedDate: Date? {
          guard let rawSelectedDate else { return nil }
          return data.first?.sales.first(where: {
            let endOfDay = endOfDay(for: $0.day)
            return ($0.day ... endOfDay).contains(rawSelectedDate)
          })?.day
        }
      
        var body: some View {
      		// Chart 구현
          .chartXSelection(value: $rawSelectedDate)
        }
      }
      
    • chart body

      Chart {
        ForEach(data) { series in
          ForEach(series.sales, id: \\.day) { element in
            LineMark(
              x: .value("Day", element.day, unit: .day),
              y: .value("Sales", element.sales)
            )
          }
        }
        if let selectedDate {
          RuleMark(
            x: .value("Selected", selectedDate, unit: .day)
          )
          .foregroundStyle(Color.gray.opacity(0.3))
          .offset(yStart: -10)
          .zIndex(-1) // lineMark보다 뒤에
          .annotation(
            position: .top, spacing: 0,
            overflowResolution: .init( // chart 바깥에 그리기 위해서
              x: .fit(to: .chart), // chart의 가로범위를 벗어나서 그려지지는 않게
              y: .disabled // chart를 침범하지 않기를
            )
          ) {
            valueSelectionPopover
          }
        }
      }
      .chartXSelection(value: $rawSelectedDate)
      
    • 범위 선택

      • iOS는 두손가락 탭, macOS는 드래그
      Chart(data) { series in
        ForEach(series.sales, id: \\.day) { element in
          LineMark(
            x: .value("Day", element.day, unit: .day),
            y: .value("Sales", element.sales)
          )
        }
        ...
      }
      .chartXSelection(value: $rawSelectedDate)
      .chartXSelection(range: $rawSelectedRange)
      
    • 커스텀 제스쳐

      Chart(data) { series in
        ForEach(series.sales, id: \\.day) { element in
          LineMark(
            x: .value("Day", element.day, unit: .day),
            y: .value("Sales", element.sales)
          )
        }
        ...
      }
      .chartXSelection(value: $rawSelectedDate)
      .chartGesture { proxy in
        DragGesture(minimumDistance: 0)
          .onChanged { proxy.selectXValue(at: $0.location.x) }
          .onEnded { _ in selectedDate = nil }
      }
      
    • pieChart에서의 selection

      Chart(data, id: \\.name) { element in
        SectorMark(
          angle: .value("Sales", element.sales),
          innerRadius: .ratio(0.618),
          angularInset: 1.5
        )
        .cornerRadius(5)
        .foregroundStyle(by: .value("Name", element.name))
        .opacity(element.name == selectedName ? 1.0 : 0.3)
      }
      .chartAngleSelection(value: $selectedAngle)
      
  • Scrolling

    Chart {
      ForEach(SalesData.last365Days, id: \\.day) {
        BarMark(
          x: .value("Day", $0.day, unit: .day),
          y: .value("Sales", $0.sales)
        )
      }
      .foregroundStyle(.blue)
    }
    .chartScrollableAxes(.horizontal) // 가로 스크롤 가능하게
    .chartXVisibleDomain(length: 3600 * 24 * 30) // 전체 도메인 크기 설정
    .chartScrollPosition(x: $scrollPosition) // 현재 스크롤 위치 설정
    .chartScrollTargetBehavior( // 스크롤 동작 커스텀
      .valueAligned(
        matching: DateComponents(hour: 0),
        majorAlignment: .matching(DateComponents(day: 1))))