File size: 12,861 Bytes
216c0a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
import os
import re
import json
import importlib.util
import random
import threading

# Regular expression to extract requirements JSON from template files
REQUIREMENTS_PATTERN = re.compile(r'REQUIREMENTS_BEGIN\s*({.*?})\s*REQUIREMENTS_END', re.DOTALL)

# Dictionary to store template mappings
templates = {
    'echarts_py': {},  # chart_type -> module
    'echarts-js': {},  # chart_type -> js_file_path
    'd3-js': {},        # chart_type -> js_file_path
    'vegalite_py': {}   # chart_type -> module
}

# 全局标识符,用于跟踪是否已扫描过模板
_templates_scanned = False
_templates_lock = threading.RLock()

def _safe_registry_name(value):
    value = value.lower()
    return re.sub(r'[^a-z0-9_]+', '_', value).strip('_')

def _duplicate_alias_for_path(chart_name, item_path, engine_dir, chart_dict):
    stem = _safe_registry_name(os.path.splitext(os.path.basename(item_path))[0])
    parent = _safe_registry_name(os.path.basename(os.path.dirname(item_path)))
    rel = _safe_registry_name(os.path.splitext(os.path.relpath(item_path, engine_dir))[0])

    candidates = [
        stem,
        f"{chart_name}__{parent}",
        f"{chart_name}__{rel}",
    ]
    for candidate in candidates:
        if candidate and candidate not in chart_dict:
            return candidate

    index = 2
    while f"{chart_name}__duplicate_{index}" in chart_dict:
        index += 1
    return f"{chart_name}__duplicate_{index}"

def _register_chart_template(chart_dict, chart_name, template_info, engine_dir):
    """Register one chart name while preserving overwritten entries as aliases."""
    if chart_name in chart_dict:
        existing = chart_dict[chart_name]
        alias = _duplicate_alias_for_path(chart_name, existing['template'], engine_dir, chart_dict)
        existing_alias = dict(existing)
        existing_alias['registry_alias'] = alias
        existing_alias['alias_for_chart_name'] = chart_name
        chart_dict[alias] = existing_alias

    chart_dict[chart_name] = template_info

def load_python_template(file_path):
    """Load a Python template module from a file path"""
    module_name = os.path.basename(file_path).replace('.py', '')
    spec = importlib.util.spec_from_file_location(module_name, file_path)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module

def extract_requirements(file_path):
    """Extract requirements JSON from a template file"""
    with open(file_path, 'r', encoding='utf-8') as f:
        content = f.read()
    
    # Find requirements section
    match = REQUIREMENTS_PATTERN.search(content)
    if match:
        try:
            requirements = json.loads(match.group(1))
            return requirements
        except json.JSONDecodeError:
            print(f"Warning: Invalid JSON in requirements section of {file_path}")
    return None

def scan_directory(dir_path, engine_type, file_extension):
    """
    递归扫描目录及其子目录,寻找符合条件的模板文件
    
    Args:
        dir_path: 要扫描的目录路径
        engine_type: 引擎类型,'echarts_py', 'echarts-js' 或 'd3-js'
        file_extension: 文件扩展名,'.py' 或 '.js'
    """
    if not os.path.exists(dir_path):
        return
    
    # 遍历目录中的所有文件和子目录
    for item in os.listdir(dir_path):
        item_path = os.path.join(dir_path, item)
        
        # 如果是目录,递归扫描
        if os.path.isdir(item_path):
            scan_directory(item_path, engine_type, file_extension)
        
        # 如果是符合条件的文件
        elif os.path.isfile(item_path) and item.endswith(file_extension):
            # 对于Python文件,跳过以__开头的文件
            if file_extension == '.py' and item.startswith('__'):
                continue
                
            # 提取需求并注册模板
            requirements = extract_requirements(item_path)
            # if engine_type == 'vegalite_py':
            #     print(f"requirements: {requirements['chart_name']}")
            if requirements and 'chart_type' in requirements:
                chart_type = requirements['chart_type'].lower()
                
                # 获取chart_name,如果没有则使用文件名
                chart_name = requirements.get('chart_name', os.path.basename(item_path).split('.')[0]).lower()
                
                # 如果该chart_type还不存在,初始化一个空字典
                if chart_type not in templates[engine_type]:
                    templates[engine_type][chart_type] = {}
                
                # 根据引擎类型处理不同的模板
                if engine_type == 'echarts_py':
                    template = load_python_template(item_path)
                else:  # echarts-js 或 d3-js
                    template = item_path
                
                template_info = {
                    'engine_type': engine_type,
                    'template': template,
                    'requirements': requirements,
                    'source_chart_name': chart_name,
                }
                chart_dict = templates[engine_type][chart_type]
                engine_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), engine_type)

                # 存储模板信息为 [engine, template]
                _register_chart_template(chart_dict, chart_name, template_info, engine_dir)
                for alias_name in requirements.get('chart_aliases', []):
                    alias_name = str(alias_name).lower()
                    if not alias_name or alias_name == chart_name:
                        continue
                    alias_info = dict(template_info)
                    alias_info['registry_alias'] = alias_name
                    alias_info['alias_for_chart_name'] = chart_name
                    _register_chart_template(chart_dict, alias_name, alias_info, engine_dir)
                    
                # 计算相对于模板引擎主目录的路径
                template_dir = os.path.dirname(os.path.abspath(__file__))
                engine_dir = os.path.join(template_dir, engine_type)
                rel_path = os.path.relpath(item_path, engine_dir)
                # print(f"Registered {engine_type} template: {chart_type} -> {chart_name} -> {rel_path}")

def scan_templates(force=False):
    """
    扫描模板目录并构建映射
    
    Args:
        force: 如果为True,即使已经扫描过也会强制重新扫描
    """
    global _templates_scanned

    with _templates_lock:
        # 如果已经扫描过且不强制重新扫描,则直接返回
        if _templates_scanned and not force:
            return templates

        # 清空现有模板
        templates['vegalite_py'].clear()
        templates['echarts_py'].clear()
        templates['echarts-js'].clear()
        templates['d3-js'].clear()

        template_dir = os.path.dirname(os.path.abspath(__file__))

        # 扫描 echarts_py 目录及子目录
        echarts_py_dir = os.path.join(template_dir, 'echarts_py')
        scan_directory(echarts_py_dir, 'echarts_py', '.py')

        # 扫描 echarts-js 目录及子目录
        echarts_js_dir = os.path.join(template_dir, 'echarts-js')
        scan_directory(echarts_js_dir, 'echarts-js', '.js')

        # 扫描 d3-js 目录及子目录
        d3_js_dir = os.path.join(template_dir, 'd3-js')
        scan_directory(d3_js_dir, 'd3-js', '.js')

        # 扫描 vegalite_py 目录及子目录
        vegalite_py_dir = os.path.join(template_dir, 'vegalite_py')
        scan_directory(vegalite_py_dir, 'vegalite_py', '.py')

        # 标记已完成扫描
        _templates_scanned = True
        return templates

def get_template_for_chart_type(chart_type, engine_preference=None):
    """
    Get the best template for a given chart type
    
    Args:
        chart_type: The chart type to look for
        engine_preference: Optional list of engine preferences ['echarts_py', 'echarts-js', 'd3-js']
                          in the order of preference
                          
    Returns:
        tuple of (engine, template) where template is either a module or file path
    """
    global _templates_scanned
    
    # 如果尚未扫描模板,先扫描
    if not _templates_scanned:
        scan_templates()
    
    chart_type = chart_type.lower()
    if engine_preference is None:
        engine_preference = ['echarts_py', 'echarts-js', 'd3-js']
    
    # Try each engine in order of preference
    for engine in engine_preference:
        if chart_type in templates[engine]:
            # 如果存在多个chart_name的template,随机返回一个
            chart_names = list(templates[engine][chart_type].keys())
            if chart_names:
                selected_name = random.choice(chart_names)
                template_info = templates[engine][chart_type][selected_name]
                return template_info['engine_type'], template_info['template']
    
    # Try partial matches
    for engine in engine_preference:
        for template_type in templates[engine]:
            if chart_type in template_type or template_type in chart_type:
                # 随机选择一个chart_name
                chart_names = list(templates[engine][template_type].keys())
                if chart_names:
                    selected_name = random.choice(chart_names)
                    template_info = templates[engine][template_type][selected_name]
                    return template_info['engine_type'], template_info['template']
    
    return None, None

def get_template_for_chart_name(chart_name, engine_preference=None):
    """
    Get the template for a specific chart name
    
    Args:
        chart_name: The chart name to look for
        engine_preference: Optional list of engine preferences ['echarts_py', 'echarts-js', 'd3-js']
                          in the order of preference
                          
    Returns:
        tuple of (engine, template) where template is either a module or file path
    """
    global _templates_scanned
    
    # 如果尚未扫描模板,先扫描
    if not _templates_scanned:
        scan_templates()
    
    chart_name = chart_name.lower()
    
    # Try each engine in order of preference
    for engine in templates:
        for chart_type, chart_dict in templates[engine].items():
            if chart_name in chart_dict:
                template_info = chart_dict[chart_name]
                return template_info['engine_type'], template_info['template']
    
    # Try partial matches
    # for engine in engine_preference:
    #     for chart_type, chart_dict in templates[engine].items():
    #         for name in chart_dict:
    #             if chart_name in name or name in chart_name:
    #                 return chart_dict[name]
    
    # 找到重叠最多的匹配
    best_match = None
    max_overlap = 0
    best_result = None
    # print("chart_dict:", templates[engine])
    
    for engine in templates:
        for chart_type, chart_dict in templates[engine].items():
            for name in chart_dict:
                # 计算两个字符串的重叠长度
                overlap = len(set(chart_name) & set(name))
                if overlap > max_overlap:
                    #print("overlap:", overlap)
                    max_overlap = overlap
                    best_match = name
                    #print("best_match:", best_match)
                    template_info = chart_dict[name]
                    best_result = (template_info['engine_type'], template_info['template'])
                    #print("best_result:", best_result)
    if best_result:
        return best_result
    
    return None, None

def get_template_for_template_key(template_key):
    """
    Get the template for an exact registry key in the form
    "<engine>/<chart_type>/<chart_name>".
    """
    global _templates_scanned

    if not _templates_scanned:
        scan_templates()

    try:
        engine, chart_type, chart_name = template_key.split("/", 2)
    except ValueError:
        return None, None

    chart_type = chart_type.lower()
    chart_name = chart_name.lower()
    template_info = templates.get(engine, {}).get(chart_type, {}).get(chart_name)
    if not template_info:
        return None, None
    return template_info["engine_type"], template_info["template"]

# 在主模块运行时,扫描模板并打印信息
if __name__ == '__main__':
    scan_templates()
    print("\nAvailable templates:")
    for engine, templates_dict in templates.items():
        print(f"\n{engine}:")
        for chart_type, chart_names_dict in templates_dict.items():
            print(f"  - {chart_type}:")
            for chart_name in chart_names_dict:
                print(f"    * {chart_name}")