본문 바로가기

Function

OD 시각화 4 : python에서 그리는 화살표 머리 이동선

 

대표

 

오랜만에 올리는 OD 시각화 시리즈.

이번에는 파이썬에서 pydeck을 이용해서 이동선들을 그려보겠다. 

예전에 올렸던 OD 시각화 시리즈는 아래 링크들에 있다. 세 번째 생활이동 데이터 시각화에서 QGIS 를 이용했다면, 이번에는 python에서 그려보려고 한다.

 

 

OD 시각화 3 : 서울 생활이동 데이터를 지도 위에 옮겨보자

서울시에서 '생활이동' 데이터를 공개했다. KT의 휴대폰 시그널을 바탕으로 언제 어디서 어디로 어떤 사람들이(성연령) 이동했는지에 대한 데이터다. 서울 열린데이터광장 서울 생활이동 인구란

www.vw-lab.com

 

어떤 데이터를 사용하든간에, 시각화 함수에는 식별 가능한 출발지 고유값 열과 도착지 고유값 열, 그리고 가중치 열(여기서는 집계 이동 인원인 count) 을 넣어야 한다. 그리고 좌표 값으로 [orix, oriy], 그리고 [desx, desy]를 사용하도록 했으므로 그 이름들은 맞춰줘야 한다.

 

 

 

시각화 함수는 아래와 같이 정의했다. 되도록이면 인구 이동 외에 다른 데이터도 사용할 수 있도록 범용 함수로 만들었다. 

 

함수 안에서는 일단 집계 인원에 따라 순위를 부여한다. 이동선이 많으면 잘 안보이므로, 함수를 호출할 때 '상위 몇위' 까지 그릴 수 있도록 rank_show_num을 함께 보내도록 했다.

 

각각의 부분에 주석을 붙여 놓았으므로 필요에 따라 수정할 수 있다.

 

pydeck 은, layer 변수에 들어간 각각의 레이어들을 렌더링하면서 완성되는 형태다. 그래서 rank를 생성하고, 최대값을 구한 뒤 width 열을 만들고, tooltip을 위한 텍스트 열을 추가하는 등의 사전 작업들은 모두 ArcLayer 하위에 있는 각각의 변수에 입력된다.

 

레이어가 3개인데, 

처음의 GeoJsonLayer는 시도 경계선이 있는 지도다.

ArcLayer가 OD를 표현하는 화살표다.

TextLayer는 OD의 지역 이름들을 표시하는 텍스트 레이어다.

 

pydeck을 사용하는 사례를 많이 봤는데, 한글은 잘 사용하지 않는 것 같다.

characterSet을 아래 형식처럼 'auto'로 설정하면 여러가지 UTF-8 표준 문자들을 표현할 수 있다.

pydeck은 기본적으로 gpu 렌더링이기 때문에, 필요한 문자열들을 미리 텍스쳐로 만들어놓고 GPU에서 빠르게 렌더링한다. 기본 텍스쳐는 입력된 폰트에 대해 숫자와 영문 대소문자들 정도만 변환되어 준비되는데, characterSet을 'auto'로 설정하면 입력된 문자열들에 대해 텍스쳐를 준비해준다. 한글이 입력되었고, 지정한 폰트에서 한글을 지원하면 한글 문자를 읽어서 텍스쳐로 준비한다는 의미다.

 

아래 그림은 pydeck 페이지의 샘플 결과다.

위처럼 반원형의 호가 출발지에서 도착지로 진행함에 따라 z축으로 올라갔다 내려오는 형상이다. 반원형의 호를 따라 움직인다.

 

 

 

아래 그림이 위 시각화 코드의 결과물이다.

3차원 시각화가 어떤 측면에서는 그럴듯 해 보이지만, 또 한편으로는 원근감이 추가되기 때문에 근경과 원경의 데이터를 동등하게 비교하기 어렵다. 앞 부분이 뒷 부분을 가리기도 한다.

 

그래서 이제 이 3차원 곡선들을 2차원으로 눕히고 앞 머리에 화살표를 그려서 방향성을 더해보겠다.

 

 

 

3차원을 2차원으로 변경. blend 방식 수정

 

앞의 ArcLayer에 몇 부분을 수정했다.

우선 get_height를 0.1로 수정해서 z축으로 솟아오른 정도를 완화시킨다.

그리고 get_tilt 를 90도로 수정해서 위로 솟아오른 arc를 회전시켜 바닥에 눕힌다.

parameters 부분은 겹치는 선들의 색상 합성에 대한 부분이다.

자바스크립트 deck.gl에서는 luma.gl를 추가적으로 import해서 변수들을 수정하게 되는데, pydeck에서는 그럴 수 없어서 luma.gl에 내장된 상수값들을 읽어왔다.

blend 의 설정값들은 OpenGL이나 WebGL에서 흔히 사용하는 방식이고, WebGL 라이브러리로 만들어진 deck.gl 역시 같은 방식을 따른다. 

여기서는 겹치는 선들을 점점 밝게 만드는 설정인데, 그냥 그대로 옮겨 적으면 된다.

 

 

 

 

 

이제 선들이 바닥으로 누웠다. 그런데 그리 보기 좋지는 않다.

 

 

deck.gl shader 수정

 

그럼 이제 shader를 수정해서 화살표를 만들어보자.

생성된 html을 열어보면 앞 부분이 아래처럼 되어 있다.

파란색으로 강조한 부분이 pydeck 라이브러리를 부르는 부분인데, 이 라이브러리에 포함된 shader를 수정해서 위의 선들을 화살표로 만들어보겠다.

 

 

모두 다 설명하기는 어렵지만, pydeck 은 deck.gl로부터 만들어졌다. 그리고 deck.gl은 gpu 렌더링이므로 gpu 언어인 shader를 사용한다. 데이터가 shader를 병렬적으로 통과되면서 아주 빠르게 개체들을 그려낸다.

 

pydeck을 만들 때 설마 shader까지는 수정하지 않았을 것이란 추측을 했고, 다행히도 shader는 그대로였기 때문에 deck.gl에서 테스트해보면서 shader를 수정했고, 위의 index.js 에서 해당 shader 부분을 찾아서 바꿔치기 했다.

 

shader에서는 아래의 세 부분을 고쳤다. 왼쪽이 원본, 오른쪽이 수정본이다.

 

 

arcLayer의 vertex shader는 segmentRatio가 0.0부터 1.0까지 증가하면서 전체 곡선을 완성하게 된다. 

일단 반원형의 호를 그리는 부분을 sin 곡선(0~180도)으로 수정했다.

 

 

 

그 다음에는 진행률에 따른 두께를 수정했다. 중앙 부분이 가장 두꺼워지도록 하고, 

마지막에 화살표 머리를 그리기 위해 0.97 정도의 진행률에서 갑자기 두꺼워지도록 했다.

 

 

 

마지막으로, 화살표 머리를 좀 더 자연스럽게 그리기 위해 머리가 넓어지는 부분의 진행률을 약간 뒤로 되돌렸다. 즉, 등간격으로 진행되면 화살표 머리가 사다리꼴로 그려지는데, 갑자기 벌어지는 부분의 진행률을 벌어지기 직전의 부분에 근접하도록 만들어서 좀 더 자연스러운 머리가 그려지도록 했다. 

 

위의 그림과 같은 개념이다.

만약 심하게 뒤로 되돌리거나 직전 단계와 일치시키면, 셰이더에서 설정된 삼각형의 방향이 뒤집히면서 화살표 머리가 깨져 보이게 된다. 이 작업의 전제조건 자체가 기존 라이브러리의 체계를 뒤흔들지 않는 것이기 때문에, 위와 같은 정도에서 마무리했다.

 

 

 

추후 패치 방식으로 생성된 html 수정

 

이렇게 수정한 shader는 ref 폴더의 dist_index.js 에 미리 준비되어 있다.

앞의 설명처럼 이미 만들어진 html 파일에서 해당 <script> 부분을 수정하면 결과물 html 파일이 완성된다.

 

아래처럼 패치 함수를 만들었다.

앞에서 설명한 내용 이외에도 폰트가 제대로 적용되도록 하는 부분도 넣었다.

웹 폰트를 사용하지만, 웹페이지 렌더링 중 언제든 업데이트 할 수 있는 일반적인 폰트 표출방식과는 달리, 미리 텍스쳐를 만들어놓는 방식이기 때문에 웹폰트가 모두 로딩된 후에 deck 인스턴스를 실행해야 하기 때문이다.

In [ ]:
def patch_shader_in_html(writename) :
       
    # Step 1: 지정한 HTML 파일 읽기 with utf-8 encoding
    with open(writename, "r", encoding="utf-8") as file:
        html_content = file.read()

    # 주어진 조건을 충족하는 정규 표현식을 생성합니다.
    pattern = r"<script src='https:[^']*?@deck\.gl[^']*?index\.js'><\/script>"

    # 해당 패턴을 찾아서 <script></script>로 바꿉니다.
    html_content = re.sub(pattern, '<script></script>', html_content)
    
    # Step 4: deck_index.js 파일 내용 읽기 with utf-8 encoding
    with open(deck_index_path, "r", encoding="utf-8") as file:
        js_content = file.read()

    # <script></script> 태그 사이에 js 내용 삽입
    html_content = html_content.replace("<script></script>", f"<script>{js_content}</script>")

    ############### 폰트 관련 패치
    custom_css = """
        @font-face {
            font-family: 'Pretendard-Bold';
            font-weight: 700;
            font-display: swap;
            src: local('Pretendard Bold'), url('../ref/Pretendard-Bold.woff2') format('woff2'), url('../ref/Pretendard-Bold.woff') format('woff');
        }

        body {
            font-family: 'Pretendard-Bold';
            margin: 0;
            padding: 0;
        }
        /* 추가적인 CSS 스타일은 여기에 넣을 수 있습니다 */
    """

    # <style> 태그 직후에 custom_css 내용 삽입
    html_content = html_content.replace("<style>", "<style>" + custom_css)

    ############ 폰트 로딩 후 deck에서 texture를 만들도록 기다리는 부분
    # 해당 문자열을 찾는 정규 표현식
    pattern = r"(const deckInstance = createDeck\({[\s\S]*?}\);)"

    # 찾은 문자열을 함수 내에서 사용하기 위해 저장
    deck_instance_string = re.search(pattern, html_content).group(1)

    replacement_string = f"""
    if ('fonts' in document) {{
        document.fonts.load('1em Pretendard-Bold').then(function () {{
            // 웹폰트가 로딩되면 createDeck 함수를 실행
            {deck_instance_string}
        }});
    }} else {{
        // fonts API를 지원하지 않는 브라우저에 대한 fallback
        {deck_instance_string}
    }}
    """
    # 해당 패턴을 찾아서 새로운 문자열로 변경
    html_content = re.sub(pattern, replacement_string, html_content)


    # Step 5: 변경된 내용으로 HTML 파일 다시 저장 with utf-8 encoding
    with open(writename, "w", encoding="utf-8") as file:
        file.write(html_content)
 
이 함수를 make_od_rank_map 마지막 부분에 추가해주면 된다.
 

 

 

이제 화살표가 완성되었다.

 

 

아주 짧은 화살표와 아주 긴 화살표는 좀 어색해보인다. 진행률에 따라 머리가 그려지는 방식이기 때문에, 선이 아주 길면 머리도 늘어진다. 기존 shader에 입력된 변수를 바탕으로 수정하다 보니 아주 자연스럽게 그리기에는 조금 한계가 있었다.

 

 

 

물론 tilt를 0으로 두어  3차원으로 그려도 자연스럽게 보인다.

 

 

 

 

pydeck은 GPU 기반 렌더링이므로 몇 만개의 이동선도 부드럽게 조작된다.

 

 

 

pydeck의 기본 툴팁 기능을 통해 마우스를 올려놓으면, 각 이동선 개체들의 데이터를 열람할 수도 있다.

 

 

이제까지의 코드는 모두 아래의 링크에 정리되어 있다.

 

 

GitHub - vuski/odArrowVis

Contribute to vuski/odArrowVis development by creating an account on GitHub.

github.com