""" Discrete Choice / Conjoint Analysis Layout Present annotators with 2-4 product/concept profiles defined by attribute-level combinations and ask them to choose the preferred one (or "none"). Enables estimation of attribute importance through experimental design. Research: Green & Srinivasan (1990); Louviere, Flynn & Marley (2015). """ import json import logging from .identifier_utils import ( safe_generate_layout, generate_element_identifier, generate_validation_attribute, escape_html_content, generate_layout_attributes ) logger = logging.getLogger(__name__) DEFAULT_PROFILES_PER_SET = 3 def generate_conjoint_layout(annotation_scheme): """ Generate HTML for a Discrete Choice / Conjoint Analysis interface. Args: annotation_scheme (dict): Configuration including: - name: Schema identifier - description: Display description - profiles_per_set: Number of profiles to show (2-4) - attributes: List of {name, levels} dicts - show_none_option: Whether to show "None of these" option - profiles_field: Data field with pre-specified profiles (null = generate) Returns: tuple: (html_string, key_bindings) """ return safe_generate_layout(annotation_scheme, _generate_conjoint_layout_internal) def _generate_conjoint_layout_internal(annotation_scheme): schema_name = annotation_scheme['name'] description = annotation_scheme['description'] profiles_per_set = annotation_scheme.get('profiles_per_set', DEFAULT_PROFILES_PER_SET) attributes = annotation_scheme.get('attributes', []) show_none_option = annotation_scheme.get('show_none_option', True) profiles_field = annotation_scheme.get('profiles_field', None) if not attributes and not profiles_field: raise ValueError(f"conjoint schema '{schema_name}' requires 'attributes' or 'profiles_field'") layout_attrs = generate_layout_attributes(annotation_scheme) validation = generate_validation_attribute(annotation_scheme) identifiers = generate_element_identifier(schema_name, schema_name, "radio") config_json = json.dumps({ 'profiles_per_set': profiles_per_set, 'attributes': attributes, 'profiles_field': profiles_field, }) # Build profile cards (placeholder structure — populated by JS from data or generated) profile_cards_html = "" for i in range(profiles_per_set): profile_num = i + 1 radio_id = f"{identifiers['id']}-profile-{profile_num}" # Build attribute rows placeholder attr_rows = "" for attr in attributes: attr_rows += f""" {escape_html_content(attr['name'])} — """ profile_cards_html += f"""
Option {profile_num}
{attr_rows}
""" none_option_html = "" if show_none_option: none_radio_id = f"{identifiers['id']}-none" none_option_html = f"""
""" html = f"""
{escape_html_content(description)}
{profile_cards_html}
{none_option_html}
""" logger.info(f"Generated conjoint layout for {schema_name}") return html, []