{"id":91,"date":"2025-06-16T17:59:40","date_gmt":"2025-06-16T15:59:40","guid":{"rendered":"https:\/\/www.tag-manager.de\/?page_id=91"},"modified":"2025-06-16T18:28:20","modified_gmt":"2025-06-16T16:28:20","slug":"developing-a-looker-studio-community-connector","status":"publish","type":"page","link":"https:\/\/www.tag-manager.de\/?page_id=91","title":{"rendered":"Developing a Looker Studio Community Connector"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"91\" class=\"elementor elementor-91\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-62c922d e-flex e-con-boxed e-con e-parent\" data-id=\"62c922d\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-93f20a6 elementor-widget elementor-widget-text-editor\" data-id=\"93f20a6\" data-element_type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<section class=\"slide\"><em>A step-by-step guide by Ralph Keser<\/em><\/section><p><!-- Slide 2 --><\/p><section class=\"slide\"><h2>Agenda<\/h2><ul><li>Overview of Looker Studio Connectors<\/li><li>Prerequisites &amp; Setup<\/li><li>Connector Architecture<\/li><li>Building the Connector<\/li><li>Testing &amp; Deployment<\/li><li>Example: Weather API<\/li><li>Q&amp;A<\/li><\/ul><\/section><p><!-- Slide 3 --><\/p><section class=\"slide\"><h2>What Are Looker Studio Community Connectors?<\/h2><p>Pull data from any API or source into Looker Studio.<\/p><h3>Use Cases:<\/h3><ul><li>Connect to proprietary or less common APIs<\/li><li>Customize data retrieval and shape as needed<\/li><\/ul><\/section><p><!-- Slide 4 --><\/p><section class=\"slide\"><h2>Prerequisites &amp; Setup<\/h2><ul><li>Google Account with access to Apps Script<\/li><li>Google Apps Script or CLI-based connector dev<\/li><li>Familiarity with JavaScript\/TypeScript<\/li><li>Basic understanding of Looker Studio data model (dimensions, metrics)<\/li><\/ul><\/section><p><!-- Slide 5 --><\/p><section class=\"slide\"><h2>Connector Architecture<\/h2><ul><li><code>getAuthType()<\/code> \u2013 Defines how users authenticate (NONE, OAUTH2, etc.)<\/li><li><code>getConfig()<\/code> \u2013 Defines user-input parameters (API keys, etc.)<\/li><li><code>getSchema()<\/code> \u2013 Describes data fields (dimensions, metrics, types)<\/li><li><code>getData()<\/code> \u2013 Fetches data from the API, returns rows in the schema<\/li><\/ul><p><em>Optional methods:<\/em> <code>isAdminUser()<\/code>, <code>isAuthValid()<\/code>, etc.<\/p><\/section><p><!-- Slide 6 --><\/p><section class=\"slide\"><\/section><section class=\"slide\"><p>\u00a0<\/p><\/section>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-8b7c300 e-flex e-con-boxed e-con e-parent\" data-id=\"8b7c300\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-cfea741 elementor-widget elementor-widget-text-editor\" data-id=\"cfea741\" data-element_type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<h2>Step 1: Create Apps Script Project<\/h2><ol><li>Go to <a href=\"https:\/\/script.google.com\" target=\"_blank\" rel=\"noopener\">script.google.com<\/a><\/li><li>Create a new project<\/li><li>Rename the project (e.g., \u201cMy Looker Studio Connector\u201d)<\/li><li>You\u2019ll have a default <code>Code.gs<\/code> file<\/li><li>Show and edit <code>appsscript.json<\/code> as needed<\/li><\/ol>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-b467fb7 elementor-widget elementor-widget-code-highlight\" data-id=\"b467fb7\" data-element_type=\"widget\" data-widget_type=\"code-highlight.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"prismjs-default copy-to-clipboard \">\n\t\t\t<pre data-line=\"\" class=\"highlight-height language-javascript line-numbers\">\n\t\t\t\t<code readonly=\"true\" class=\"language-javascript\">\n\t\t\t\t\t<xmp>{\n  \"timeZone\": \"Europe\/Berlin\",\n  \"dependencies\": {\n  },\n  \"dataStudio\": {\n    \"name\": \"Weather Api (Diconium)\",\n    \"company\": \"Ralph\",\n    \"logoUrl\": \"https:\/\/24898528.fs1.hubspotusercontent-eu1.net\/hub\/24898528\/hubfs\/2024%20theme%20assets\/Layer%201.png?width=594&height=344&name=Layer%201.png\",\n    \"addonUrl\": \"https:\/\/www.ataraxie.de\",\n    \"supportUrl\": \"https:\/\/www.ataraxie.de\",\n    \"description\": \"Looker Weather connector\"\n  },\n  \"oauthScopes\": [\n    \"https:\/\/www.googleapis.com\/auth\/script.external_request\"\n  ],\n  \"urlFetchWhitelist\": [\n    \"https:\/\/weather.visualcrossing.com\/\",\n    \"https:\/\/archive-api.open-meteo.com\/\"\n    \n  ],\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}<\/xmp>\n\t\t\t\t<\/code>\n\t\t\t<\/pre>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-46f534c e-flex e-con-boxed e-con e-parent\" data-id=\"46f534c\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-90b6f10 elementor-widget elementor-widget-text-editor\" data-id=\"90b6f10\" data-element_type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<section class=\"slide\"><h2>Step 2: Define Authentication<\/h2><p>Simple example for no-auth connectors. If your API requires OAuth2 or API keys, configure accordingly.<\/p><\/section>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-a0a2b1e elementor-widget elementor-widget-code-highlight\" data-id=\"a0a2b1e\" data-element_type=\"widget\" data-widget_type=\"code-highlight.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"prismjs-default copy-to-clipboard \">\n\t\t\t<pre data-line=\"\" class=\"highlight-height language-javascript line-numbers\">\n\t\t\t\t<code readonly=\"true\" class=\"language-javascript\">\n\t\t\t\t\t<xmp>\/\/ https:\/\/developers.google.com\/datastudio\/connector\/reference#getauthtype\nfunction getAuthType() {\n  return { type: 'NONE' }; \/\/ No authentication required\n}<\/xmp>\n\t\t\t\t<\/code>\n\t\t\t<\/pre>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-7c4b789 e-flex e-con-boxed e-con e-parent\" data-id=\"7c4b789\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-6d731d1 elementor-widget elementor-widget-text-editor\" data-id=\"6d731d1\" data-element_type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<h2>Step 3: Configure User Inputs<\/h2><p>Define input fields for your connector\u2019s UI via <code>getConfig()<\/code>.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-dfd855f elementor-widget elementor-widget-code-highlight\" data-id=\"dfd855f\" data-element_type=\"widget\" data-widget_type=\"code-highlight.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"prismjs-default copy-to-clipboard \">\n\t\t\t<pre data-line=\"\" class=\"highlight-height language-javascript line-numbers\">\n\t\t\t\t<code readonly=\"true\" class=\"language-javascript\">\n\t\t\t\t\t<xmp>function getConfig() {\n  var config = cc.getConfig();\n\n  config.newTextInput()\n    .setId(\"latitude\")\n    .setName(\"Latitude\")\n    .setHelpText(\"Enter latitude (-90 to 90), e.g., 53.5511 for Hamburg\")\n    .setPlaceholder(\"53.5511\");\n\n  config.newTextInput()\n    .setId(\"longitude\")\n    .setName(\"Longitude\")\n    .setHelpText(\"Enter longitude (-180 to 180), e.g., 9.9937 for Hamburg\")\n    .setPlaceholder(\"9.9937\");\n\n  var timezones = [\n    ['Auto', 'auto'], ['UTC', 'UTC'], ['Europe\/London', 'Europe\/London'],\n    ['Europe\/Berlin', 'Europe\/Berlin'], ['America\/New_York', 'America\/New_York'],\n    ['America\/Chicago', 'America\/Chicago'], ['America\/Los_Angeles', 'America\/Los_Angeles'],\n    ['Asia\/Tokyo', 'Asia\/Tokyo']\n  ];\n\n  var tzSelect = config.newSelectSingle().setId(\"timezone\").setName(\"Timezone\");\n  timezones.forEach(function (tz) {\n    tzSelect.addOption(config.newOptionBuilder().setLabel(tz[0]).setValue(tz[1]));\n  });\n\n  return config.build();\n}<\/xmp>\n\t\t\t\t<\/code>\n\t\t\t<\/pre>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-54ddbee e-flex e-con-boxed e-con e-parent\" data-id=\"54ddbee\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-a1efca0 elementor-widget elementor-widget-text-editor\" data-id=\"a1efca0\" data-element_type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<h2>Step 4: Define the Schema<\/h2><p>The schema lists each column\u2019s name, label, data type, and usage (DIMENSION vs. METRIC). Ensure names match how you populate values in <code>getData()<\/code>.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-20ef85d elementor-widget elementor-widget-code-highlight\" data-id=\"20ef85d\" data-element_type=\"widget\" data-widget_type=\"code-highlight.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"prismjs-default copy-to-clipboard \">\n\t\t\t<pre data-line=\"\" class=\"highlight-height language-javascript line-numbers\">\n\t\t\t\t<code readonly=\"true\" class=\"language-javascript\">\n\t\t\t\t\t<xmp>function getSchema() {\n  return {\n    schema: [\n      { \n        name: \"date\", \n        label: \"Date\", \n        dataType: \"STRING\",\n        semantics: {\n          conceptType: \"DIMENSION\",\n          semanticType: \"YEAR_MONTH_DAY\"\n        }\n      },\n      { name: \"temperature_2m_max\", label: \"Max Temperature (\u00b0C)\", dataType: \"NUMBER\" },\n      { name: \"temperature_2m_min\", label: \"Min Temperature (\u00b0C)\", dataType: \"NUMBER\" },\n      { name: \"temperature_2m_mean\", label: \"Mean Temperature (\u00b0C)\", dataType: \"NUMBER\" },\n      { name: \"relative_humidity_2m_mean\", label: \"Mean Humidity (%)\", dataType: \"NUMBER\" },\n      { name: \"precipitation_sum\", label: \"Precipitation (mm)\", dataType: \"NUMBER\" },\n      { name: \"wind_speed_10m_max\", label: \"Max Wind Speed (km\/h)\", dataType: \"NUMBER\" },\n      { name: \"wind_direction_10m_dominant\", label: \"Dominant Wind Direction (\u00b0)\", dataType: \"NUMBER\" }\n    ]\n  };\n}\n<\/xmp>\n\t\t\t\t<\/code>\n\t\t\t<\/pre>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-d9d0f4a e-flex e-con-boxed e-con e-parent\" data-id=\"d9d0f4a\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-f10d635 elementor-widget elementor-widget-text-editor\" data-id=\"f10d635\" data-element_type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<h2>Step 5: Fetch &amp; Return Data<\/h2><ul><li>Ensure the order of values matches the schema<\/li><li>Convert arrays\/objects to strings if necessary<\/li><li>Handle null or missing values gracefully<\/li><\/ul>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-0647008 elementor-widget elementor-widget-code-highlight\" data-id=\"0647008\" data-element_type=\"widget\" data-widget_type=\"code-highlight.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"prismjs-default copy-to-clipboard \">\n\t\t\t<pre data-line=\"\" class=\"highlight-height language-javascript line-numbers\">\n\t\t\t\t<code readonly=\"true\" class=\"language-javascript\">\n\t\t\t\t\t<xmp>\nfunction getData(request) {\n  var config = request.configParams;\n  var lat = config.latitude, lon = config.longitude, tz = config.timezone || 'auto';\n\n  \/\/ Validate coordinates\n  if (!lat || !lon || isNaN(lat) || isNaN(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) {\n    cc.newUserError()\n      .setDebugText('Invalid coordinates')\n      .setText('Please provide valid latitude (-90 to 90) and longitude (-180 to 180)')\n      .throwException();\n  }\n\n  \/\/ Process fields and dates\n  var requestedFields = request.fields || [];\n  var fieldNames = requestedFields.map(function (f) { return f.name; });\n  var schema = getSchema().schema.filter(function (f) { return fieldNames.indexOf(f.name) !== -1; });\n  schema.sort(function (a, b) { return fieldNames.indexOf(a.name) - fieldNames.indexOf(b.name); });\n\n  var startDate, endDate;\n  if (request.dateRange && request.dateRange.startDate && request.dateRange.endDate) {\n    startDate = formatDateForAPI(request.dateRange.startDate);\n    endDate = formatDateForAPI(request.dateRange.endDate);\n  } else {\n    var today = new Date();\n    var pastDate = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000));\n    startDate = Utilities.formatDate(pastDate, 'UTC', 'yyyy-MM-dd');\n    endDate = Utilities.formatDate(today, 'UTC', 'yyyy-MM-dd');\n  }\n\n  \/\/ Build API URL\n  var params = {\n    latitude: lat, longitude: lon, start_date: startDate, end_date: endDate,\n    daily: 'temperature_2m_max,temperature_2m_min,temperature_2m_mean,relative_humidity_2m_mean,precipitation_sum,wind_speed_10m_max,wind_direction_10m_dominant',\n    timezone: tz\n  };\n\n  var url = 'https:\/\/archive-api.open-meteo.com\/v1\/archive?' +\n    Object.keys(params).map(function (k) { return k + '=' + encodeURIComponent(params[k]); }).join('&');\n\n  var weatherData;\n  try {\n    var response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });\n    if (response.getResponseCode() !== 200) {\n      throw new Error('API error: ' + response.getResponseCode());\n    }\n\n    weatherData = JSON.parse(response.getContentText());\n    if (!weatherData.daily || !weatherData.daily.time) {\n      throw new Error('No data available');\n    }\n\n    Logger.log('Data fetched from API');\n\n  } catch (error) {\n    cc.newUserError()\n      .setDebugText('API Error: ' + error.toString())\n      .setText('Unable to fetch weather data. Check coordinates and try again.')\n      .throwException();\n  }\n\n  \/\/ Process data efficiently\n  var daily = weatherData.daily;\n  var fieldMap = {\n    'date': daily.time,\n    'temperature_2m_max': daily.temperature_2m_max,\n    'temperature_2m_min': daily.temperature_2m_min,\n    'temperature_2m_mean': daily.temperature_2m_mean,\n    'relative_humidity_2m_mean': daily.relative_humidity_2m_mean,\n    'precipitation_sum': daily.precipitation_sum,\n    'wind_speed_10m_max': daily.wind_speed_10m_max,\n    'wind_direction_10m_dominant': daily.wind_direction_10m_dominant\n  };\n\n  var rows = daily.time.map(function (_, i) {\n    return {\n      values: schema.map(function (field) {\n        var data = fieldMap[field.name];\n        if (field.name === 'date') {\n          \/\/ Format date as YYYYMMDD for Looker Studio date range control\n          var dateStr = String(data[i] || '');\n          return dateStr.replace(\/-\/g, '');\n        }\n        return (data && data[i] !== undefined ? data[i] : null);\n      })\n    };\n  });\n\n  Logger.log('Processed ' + rows.length + ' rows');\n  return { schema: schema, rows: rows };\n}<\/xmp>\n\t\t\t\t<\/code>\n\t\t\t<\/pre>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-f130bc3 e-flex e-con-boxed e-con e-parent\" data-id=\"f130bc3\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-33efad4 elementor-widget elementor-widget-text-editor\" data-id=\"33efad4\" data-element_type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<h2>Step 6: Testing Your Connector<\/h2><ol><li>Use <code>Logger.log()<\/code> in Apps Script to debug<\/li><li>In Looker Studio:<ul><li>Create a new data source \u2192 Select \u201cBuild Your Own\u201d \u2192 Choose your connector<\/li><li>Enter config parameters (API key, etc.) \u2192 \u201cConnect\u201d<\/li><li>Verify fields \u2192 \u201cCreate Report\u201d to visualize<\/li><\/ul><\/li><\/ol>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-5c06293 elementor-widget elementor-widget-image\" data-id=\"5c06293\" data-element_type=\"widget\" data-widget_type=\"image.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<img fetchpriority=\"high\" decoding=\"async\" width=\"1024\" height=\"651\" src=\"https:\/\/www.tag-manager.de\/wp-content\/uploads\/2025\/06\/step6-1-1024x651.png\" class=\"attachment-large size-large wp-image-106\" alt=\"\" srcset=\"https:\/\/www.tag-manager.de\/wp-content\/uploads\/2025\/06\/step6-1-1024x651.png 1024w, https:\/\/www.tag-manager.de\/wp-content\/uploads\/2025\/06\/step6-1-300x191.png 300w, https:\/\/www.tag-manager.de\/wp-content\/uploads\/2025\/06\/step6-1-768x488.png 768w, https:\/\/www.tag-manager.de\/wp-content\/uploads\/2025\/06\/step6-1.png 1088w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/>\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-5e9c5a4 e-flex e-con-boxed e-con e-parent\" data-id=\"5e9c5a4\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-28a5a2d elementor-widget elementor-widget-text-editor\" data-id=\"28a5a2d\" data-element_type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<section class=\"slide\"><\/section><p><!-- Slide 12 --><\/p><section class=\"slide\"><h2>Deployment<\/h2><ol><li>Deploy in Apps Script: Click \u201cDeploy\u201d \u2192 \u201cNew Deployment\u201d \u2192 Type: \u201cWeb app\u201d<\/li><li>Set access level (e.g., \u201cAnyone\u201d if no auth required)<\/li><li>Share the deployment URL for others to use<\/li><\/ol><\/section><p><!-- Slide 13 --><\/p><section class=\"slide\"><h2>Common Pitfalls<\/h2><ul><li>Schema vs. Rows mismatch (field count must match)<\/li><li>Data type mismatches (STRING vs. NUMBER)<\/li><li>Missing\/wrong Auth if API requires credentials<\/li><li>Caching \u2013 Looker Studio may cache data<\/li><\/ul><\/section><p><!-- Slide 14 --><\/p><section class=\"slide\"><h2>Best Practices<\/h2><ul><li><strong>Error Handling:<\/strong> Return readable error messages if API fails<\/li><li><strong>Refactor:<\/strong> Keep config, schema, and data logic organized<\/li><li><strong>Logging:<\/strong> Use <code>Logger.log()<\/code> for debugging<\/li><li><strong>Documentation:<\/strong> Provide usage instructions, parameters, rate limits<\/li><\/ul><\/section><p><!-- Slide 15 --><\/p><section class=\"slide\"><h2>Resources<\/h2><ul><li><a href=\"https:\/\/developers.google.com\/datastudio\/connector\" target=\"_blank\" rel=\"noopener\">Looker Studio Developer Docs<\/a><\/li><li><a href=\"https:\/\/developers.google.com\/apps-script\/reference\" target=\"_blank\" rel=\"noopener\">Apps Script Reference<\/a><\/li><li><a href=\"https:\/\/github.com\/googledatastudio\/community-connectors\" target=\"_blank\" rel=\"noopener\">GitHub Samples for Connectors<\/a><\/li><\/ul><\/section><p><!-- Slide 16 --><\/p><section class=\"slide\"><h2>Example: Weather API (Visual Crossing)<\/h2><h3>Connector Parameters:<\/h3><ul><li><strong>API Key:<\/strong> Required for authentication with the Visual Crossing Weather API.<\/li><li><strong>Location:<\/strong> City or latitude\/longitude.<\/li><li><strong>Start and End Dates:<\/strong> Range to query.<\/li><\/ul><h3>Example Request:<\/h3><pre><code>https:\/\/weather.visualcrossing.com\/VisualCrossingWebServices\/rest\/services\/timeline\/Hamburg\/2025-01-01\/2025-01-03?unitGroup=metric&amp;include=days&amp;key=LMGNFA7QYWBT2Z4G38UB89L79&amp;contentType=json<\/code><\/pre><\/section><p><!-- Slide 17 --><\/p><section class=\"slide\"><\/section><section class=\"slide\"><h2>Q&amp;A<\/h2><p>Thank you for attending!<\/p><\/section>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-3543ff7 e-flex e-con-boxed e-con e-parent\" data-id=\"3543ff7\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-19cde18 elementor-widget elementor-widget-text-editor\" data-id=\"19cde18\" data-element_type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<section class=\"slide\"><h2>Example: Weather API (Open-Meteo)<\/h2><h3>Connector Parameters:<\/h3><ul><li><strong>Latitude:<\/strong> e.g. 53.5511 for Hamburg<\/li><li><strong>Longitude:<\/strong> e.g. 9.9937 for Hamburg<\/li><li><strong>Timezone:<\/strong> auto, UTC, or IANA names like Europe\/Berlin<\/li><li>Date range handled via Looker Studio\u2019s picker; defaults to last 30 days<\/li><\/ul><h3>Example Request:<\/h3><pre><a href=\"https:\/\/archive-api.open-meteo.com\/v1\/archive?latitude=53.5511&amp;longitude=9.9937&amp;start_date=2025-06-01&amp;end_date=2025-06-15&amp;daily=temperature_2m_max,temperature_2m_min,temperature_2m_mean,relative_humidity_2m_mean,precipitation_sum,wind_speed_10m_max\"><code>https:\/\/archive-api.open-meteo.com\/v1\/archive?latitude=53.5511&amp;longitude=9.9937&amp;start_date=2025-06-01&amp;end_date=2025-06-15&amp;daily=temperature_2m_max,temperature_2m_min,temperature_2m_mean,relative_humidity_2m_mean,precipitation_sum,wind_speed_10m_max<\/code><\/a><\/pre><\/section><p><!-- Slide 18 --><\/p><section class=\"slide\"><h2>\u00a0<\/h2><\/section>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-7b11143 e-flex e-con-boxed e-con e-parent\" data-id=\"7b11143\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-d34ac34 elementor-widget elementor-widget-code-highlight\" data-id=\"d34ac34\" data-element_type=\"widget\" data-widget_type=\"code-highlight.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"prismjs-default copy-to-clipboard \">\n\t\t\t<pre data-line=\"\" class=\"highlight-height language-javascript line-numbers\">\n\t\t\t\t<code readonly=\"true\" class=\"language-javascript\">\n\t\t\t\t\t<xmp>\/\/ Looker Studio Plugin for Open-Meteo Weather API\n\/\/ Community Connector implementation for Google Data Studio\n\nvar cc = DataStudioApp.createCommunityConnector();\n\n\/\/ https:\/\/developers.google.com\/datastudio\/connector\/reference#getauthtype\nfunction getAuthType() {\n  return { type: 'NONE' }; \/\/ No authentication required\n}\n\nfunction isAdminUser() {\n  return true; \/\/ Allow all users to act as admin\n}\n\nfunction getConfig() {\n  var config = cc.getConfig();\n\n  config.newTextInput()\n    .setId(\"latitude\")\n    .setName(\"Latitude\")\n    .setHelpText(\"Enter latitude (-90 to 90), e.g., 53.5511 for Hamburg\")\n    .setPlaceholder(\"53.5511\");\n\n  config.newTextInput()\n    .setId(\"longitude\")\n    .setName(\"Longitude\")\n    .setHelpText(\"Enter longitude (-180 to 180), e.g., 9.9937 for Hamburg\")\n    .setPlaceholder(\"9.9937\");\n\n  var timezones = [\n    ['Auto', 'auto'], ['UTC', 'UTC'], ['Europe\/London', 'Europe\/London'],\n    ['Europe\/Berlin', 'Europe\/Berlin'], ['America\/New_York', 'America\/New_York'],\n    ['America\/Chicago', 'America\/Chicago'], ['America\/Los_Angeles', 'America\/Los_Angeles'],\n    ['Asia\/Tokyo', 'Asia\/Tokyo']\n  ];\n\n  var tzSelect = config.newSelectSingle().setId(\"timezone\").setName(\"Timezone\");\n  timezones.forEach(function (tz) {\n    tzSelect.addOption(config.newOptionBuilder().setLabel(tz[0]).setValue(tz[1]));\n  });\n\n  return config.build();\n}\n\nfunction getSchema() {\n  return {\n    schema: [\n      { \n        name: \"date\", \n        label: \"Date\", \n        dataType: \"STRING\",\n        semantics: {\n          conceptType: \"DIMENSION\",\n          semanticType: \"YEAR_MONTH_DAY\"\n        }\n      },\n      { name: \"temperature_2m_max\", label: \"Max Temperature (\u00b0C)\", dataType: \"NUMBER\" },\n      { name: \"temperature_2m_min\", label: \"Min Temperature (\u00b0C)\", dataType: \"NUMBER\" },\n      { name: \"temperature_2m_mean\", label: \"Mean Temperature (\u00b0C)\", dataType: \"NUMBER\" },\n      { name: \"relative_humidity_2m_mean\", label: \"Mean Humidity (%)\", dataType: \"NUMBER\" },\n      { name: \"precipitation_sum\", label: \"Precipitation (mm)\", dataType: \"NUMBER\" },\n      { name: \"wind_speed_10m_max\", label: \"Max Wind Speed (km\/h)\", dataType: \"NUMBER\" },\n      { name: \"wind_direction_10m_dominant\", label: \"Dominant Wind Direction (\u00b0)\", dataType: \"NUMBER\" }\n    ]\n  };\n}\n\n\/\/ Helper function to format date for API (Open-Meteo uses YYYY-MM-DD)\nfunction formatDateForAPI(dateString) {\n  return dateString && dateString.length === 8 ?\n    dateString.substring(0, 4) + '-' + dateString.substring(4, 6) + '-' + dateString.substring(6, 8) :\n    dateString;\n}\n\nfunction getData(request) {\n  var config = request.configParams;\n  var lat = config.latitude, lon = config.longitude, tz = config.timezone || 'auto';\n\n  \/\/ Validate coordinates\n  if (!lat || !lon || isNaN(lat) || isNaN(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) {\n    cc.newUserError()\n      .setDebugText('Invalid coordinates')\n      .setText('Please provide valid latitude (-90 to 90) and longitude (-180 to 180)')\n      .throwException();\n  }\n\n  \/\/ Process fields and dates\n  var requestedFields = request.fields || [];\n  var fieldNames = requestedFields.map(function (f) { return f.name; });\n  var schema = getSchema().schema.filter(function (f) { return fieldNames.indexOf(f.name) !== -1; });\n  schema.sort(function (a, b) { return fieldNames.indexOf(a.name) - fieldNames.indexOf(b.name); });\n\n  var startDate, endDate;\n  if (request.dateRange && request.dateRange.startDate && request.dateRange.endDate) {\n    startDate = formatDateForAPI(request.dateRange.startDate);\n    endDate = formatDateForAPI(request.dateRange.endDate);\n  } else {\n    var today = new Date();\n    var pastDate = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000));\n    startDate = Utilities.formatDate(pastDate, 'UTC', 'yyyy-MM-dd');\n    endDate = Utilities.formatDate(today, 'UTC', 'yyyy-MM-dd');\n  }\n\n  \/\/ Build API URL\n  var params = {\n    latitude: lat, longitude: lon, start_date: startDate, end_date: endDate,\n    daily: 'temperature_2m_max,temperature_2m_min,temperature_2m_mean,relative_humidity_2m_mean,precipitation_sum,wind_speed_10m_max,wind_direction_10m_dominant',\n    timezone: tz\n  };\n\n  var url = 'https:\/\/archive-api.open-meteo.com\/v1\/archive?' +\n    Object.keys(params).map(function (k) { return k + '=' + encodeURIComponent(params[k]); }).join('&');\n\n  var weatherData;\n  try {\n    var response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });\n    if (response.getResponseCode() !== 200) {\n      throw new Error('API error: ' + response.getResponseCode());\n    }\n\n    weatherData = JSON.parse(response.getContentText());\n    if (!weatherData.daily || !weatherData.daily.time) {\n      throw new Error('No data available');\n    }\n\n    Logger.log('Data fetched from API');\n\n  } catch (error) {\n    cc.newUserError()\n      .setDebugText('API Error: ' + error.toString())\n      .setText('Unable to fetch weather data. Check coordinates and try again.')\n      .throwException();\n  }\n\n  \/\/ Process data efficiently\n  var daily = weatherData.daily;\n  var fieldMap = {\n    'date': daily.time,\n    'temperature_2m_max': daily.temperature_2m_max,\n    'temperature_2m_min': daily.temperature_2m_min,\n    'temperature_2m_mean': daily.temperature_2m_mean,\n    'relative_humidity_2m_mean': daily.relative_humidity_2m_mean,\n    'precipitation_sum': daily.precipitation_sum,\n    'wind_speed_10m_max': daily.wind_speed_10m_max,\n    'wind_direction_10m_dominant': daily.wind_direction_10m_dominant\n  };\n\n  var rows = daily.time.map(function (_, i) {\n    return {\n      values: schema.map(function (field) {\n        var data = fieldMap[field.name];\n        if (field.name === 'date') {\n          \/\/ Format date as YYYYMMDD for Looker Studio date range control\n          var dateStr = String(data[i] || '');\n          return dateStr.replace(\/-\/g, '');\n        }\n        return (data && data[i] !== undefined ? data[i] : null);\n      })\n    };\n  });\n\n  Logger.log('Processed ' + rows.length + ' rows');\n  return { schema: schema, rows: rows };\n}\n\nfunction testApiCall() {\n  return getData({\n    configParams: { latitude: \"53.5511\", longitude: \"9.9937\", timezone: \"Europe\/Berlin\" },\n    dateRange: { startDate: \"20250101\", endDate: \"20250112\" },\n    fields: [{ name: \"date\" }, { name: \"temperature_2m_max\" }, { name: \"temperature_2m_min\" }]\n  });\n}\n<\/xmp>\n\t\t\t\t<\/code>\n\t\t\t<\/pre>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>A step-by-step guide by Ralph Keser Agenda Overview of Looker Studio Connectors Prerequisites &amp; Setup Connector Architecture Building the Connector Testing &amp; Deployment Example: Weather API Q&amp;A What Are Looker Studio Community Connectors? Pull data from any API or source into Looker Studio. Use Cases: Connect to proprietary or less common APIs Customize data retrieval [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-91","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.tag-manager.de\/index.php?rest_route=\/wp\/v2\/pages\/91","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.tag-manager.de\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.tag-manager.de\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.tag-manager.de\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tag-manager.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=91"}],"version-history":[{"count":16,"href":"https:\/\/www.tag-manager.de\/index.php?rest_route=\/wp\/v2\/pages\/91\/revisions"}],"predecessor-version":[{"id":115,"href":"https:\/\/www.tag-manager.de\/index.php?rest_route=\/wp\/v2\/pages\/91\/revisions\/115"}],"wp:attachment":[{"href":"https:\/\/www.tag-manager.de\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=91"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}