Использовать API Connector в DataLens можно для множества вещей, на этой странице разберем основные подходы, как делать базовые запросы через API и разберем интересные примеры использования.
Основные шаги подключения API
Простое видео, как сделать запрос к API внутри DataLens
То есть, основные шаги
Создать API или получить информацию по адресу и формату работы с некоторым API
Создать подключение к API
Создать чарт Editor
Написать и настроить код на Sources для интерактивной работы с API
Обработать результат на Prepare
Пример: простой чат внутри DataLens
Python Код для API
import psycopg2import osimport csvimport iodefhandler(event,context): params = event.get('multiValueQueryStringParameters',{}) chat_id = params.get('chat_id',[None])[0] message = params.get('message',[None])[0] author = params.get('author',[None])[0] message_key = params.get('message_key',[None])[0]ifnot chat_id:return{'statusCode':400,'body':"Ошибка: параметр 'chat_id' является обязательным.",'headers':{'Content-Type':'text/csv; charset=UTF-8'},} pg_pass = os.environ.get('PG_PASS')try: conn = psycopg2.connect(user="user1",password=pg_pass,host="rc1a-***.mdb.yandexcloud.net",port="6432",database="db1") cur = conn.cursor()# 1. Обработка message_key: проверка и вставкаif message_key isnotNone: cur.execute("SELECT 1 FROM chat_messages WHERE chat_id = %s AND message_key = %s",(chat_id, message_key))ifnot cur.fetchone():# ключ не найден — вставляем полную строку cur.execute("INSERT INTO chat_messages (chat_id, author, message, message_key, dttm) ""VALUES (%s, %s, %s, %s, NOW())",(chat_id, author, message, message_key)) conn.commit()# 2. Обычная логика добавления сообщения (если нет message_key)elif message and author: stop_words =['политика','религия','другие чувствительные темы']ifnotany(w in message.lower()for w in stop_words): cur.execute("SELECT 1 FROM chat_messages ""WHERE author = %s AND message = %s AND chat_id = %s ""AND dttm > NOW() - INTERVAL '1 minute'",(author, message, chat_id))ifnot cur.fetchone(): cur.execute("INSERT INTO chat_messages (chat_id, author, message, dttm) ""VALUES (%s, %s, %s, NOW())",(chat_id, author, message)) conn.commit()# 3. Получение последних 30 сообщений cur.execute(""" SELECT author, message, dttm FROM ( SELECT author, message, dttm FROM chat_messages WHERE chat_id = %s ORDER BY dttm DESC LIMIT 30 ) AS last_30 ORDER BY dttm ASC; """,(chat_id,)) rows = cur.fetchall() output = io.StringIO() w = csv.writer(output) w.writerow(['author','message','dttm'])ifnot rows: w.writerow(['-','-','-'])else:for r in rows: w.writerow([r[0], r[1], r[2].isoformat()])return{'statusCode':200,'body': output.getvalue(),'headers':{'Content-Type':'text/csv; charset=UTF-8'},}except psycopg2.Error as e:return{'statusCode':500,'body':f"Ошибка базы данных: {e}",'headers':{'Content-Type':'text/csv; charset=UTF-8'},}finally:if'cur'inlocals(): cur.close()if'conn'inlocals(): conn.close()
Editor.updateParams({"send_it":[""]});functionprint(mess){console.log(mess);}functionparseCsvToObjects(csvString){constlines=csvString.trim().split('\n');// Функция, которая разбивает строку на значения с учётом кавычекfunctionparseCsvLine(line){constresult= [];letcurrent='';letinQuotes=false;for (leti=0;i<line.length;i++) {constchar=line[i];if (char==='"') {// Если текущий символ кавычка, проверяем следующий чтобы определить если это экранированиеif (inQuotes&&line[i+1] ==='"') {// Экранированная кавычка двойным двойнымcurrent+='"';i++;// пропускаем следующий символ}else{inQuotes=!inQuotes;// переключаем состояние в/из кавычек}}elseif (char===','&&!inQuotes) {// Разделитель поля только если не внутри кавычекresult.push(current);current='';}else{current+=char;}}result.push(current);returnresult;}constheaders=parseCsvLine(lines[0]).map(h=>h.trim());returnlines.slice(1).map(line=>{constvalues=parseCsvLine(line);constobj={};headers.forEach((header,index)=>{obj[header] =values[index] ?values[index].trim() :'';});returnobj;});}varparams=Editor.getParams();varcurrent_user=params.user[0] !=''?params.user[0]:Editor.getUserLogin();functioncsvToMarkdownChat(csvString){constdata=parseCsvToObjects(csvString);returndata.map(({author,message,dttm})=>{constisCurrentUser=author===current_user;consttime=newDate(dttm).toLocaleTimeString([],{ hour:'2-digit', minute:'2-digit', hour12:false// 24-часовой формат});// Иконка пользователяconstuserIcon=isCurrentUser?'👤':'👥';// Строка автора и времениconstauthorTimeLine=isCurrentUser?`> **${userIcon} Вы (${time})**`:`**${userIcon}${author} (${time})**`;// Сообщение с правильным выравниваниемconstmessageLine=isCurrentUser?`{blue}(${message.replace(/\n/g,'\n> ')})`// Сохраняем переносы строк в цитате:message.replace(/\n/g,'\n');// Обычное сообщениеreturn`${authorTimeLine}\n${messageLine}`;}).join('\n\n');}constresponse=Editor.getLoadedData();print(response['myApiDataSource'].data.body.result);print(parseCsvToObjects(response['myApiDataSource'].data.body.result));constmarkdown=csvToMarkdownChat(response['myApiDataSource'].data.body.result);module.exports={markdown};
В данной статье будут по шагам освещены основные моменты, которые нужно знать, чтобы эффективно делать чарты в Editor в продукте DataLens. Основной способ донесения информации — короткие видеоролики
Что такое Editor?
Чарт или селектор Editor — это отдельный объект в DataLens, логика которого описывается JS-кодом.
Чарты Editor гибче Wizard, так как позволяет подключаться к нескольким источникам данных и отрисовывать любую требуемую визуализацию через HTML и SVG.
Селекторы Editor позволят вам реализовать кодозависимые поля выбора с уникальной логикой.
Общая схема работы объекта приведена ниже, детально о вкладках и примерах их заполнения смотрите видео.
Разбираем простой чарт Editor
В видео ниже будет разобран по шагам простой чарт Editor. По ходу статьи будем усложнять логику, которую можно реализовать в Editor.
// КОД ВКЛАДКИ SOURCES Простого чартаconst{buildSource}=require('libs/dataset/v2');constparams=Editor.getParams();constdate_interval=params.date_interval[0];constlimit=parseInt(params.limit[0]);constmetrics_list=params.metrics_list;// создаем пустой массив для заполнения условийletwhere= [];// колонки для выбораletcols= [];// параметры в датасетletfilled_params= []// Базовое заполнение фильтра по дате как есть// поле для фильтрацииconstdate_dim=params.date_dimension[0];// парсим даты из интервалаconst{from:from_date,to:to_date}=Editor.resolveInterval(date_interval);// собираем объект для фильтраconstdateFilter={column:date_dim, operation:'BETWEEN', values: [from_date,to_date]}// добавляем в массив wherewhere.push(dateFilter)// добавляем колонку region_deliverycols.push('region_delivery');// добавляем колонки для выбора метрикcols.push(...metrics_list);module.exports={ dataset:buildSource({ id:Editor.getId('dataset'), columns:cols, where:where, parameters:filled_params, limit:limit, order_by:[{'direction':'DESC','column':metrics_list[0]}]})}
// КОД ВКЛАДКИ PREPARE Простого чарта// Импорт необходимых библиотек и получение параметровconstDataset=require('libs/dataset/v2');// Библиотека для обработки данныхconstparams=Editor.getParams();// Получение параметров из редактораconstloadedData=Editor.getLoadedData();// Получаем загруженные данныеconstpreparedData=Dataset.processData(loadedData,'dataset',Editor);// Обрабатываем данныеconsole.log(preparedData)// Получаем параметры для сводной таблицыconstmetrics_list=params.metrics_list;// Список метрик// Измерения для строк (до 4 измерений, пустые отфильтрованы)constdimensions=Object.keys(preparedData[0]).filter(key=>!metrics_list.includes(key));// Формируем заголовкиconsthead= [// Сначала добавляем измерения...dimensions.map(dim=> ({ id:dim, name:dim, type:'text'})),// Затем метрики с индикаторами...metrics_list.map(metric=> ({ id:metric, name:metric,}))];// Формируем строкиconstrows=preparedData.map(row=> ({ cells: [// Ячейки для измерений...dimensions.map(dim=> ({ value:row[dim],})),// Ячейки для метрик с индикаторами...metrics_list.map(metric=> ({ value:row[metric]})) ]}));module.exports={head,rows};
Как в Editor получить значения фильтров и параметров с дашборда
Для того, чтобы наш чарт был интерактивным, полезным, чтобы мы могли взаимодействовать с ним, как с объектами Wizard, надо, чтобы любые параметры и фильтры, задаваемые в дэше, влияли на отправляемый запрос к датасету и/или подключению. Давайте посмотрим, как получить все передаваемые с дэша параметры и фильтры
Обработка параметров через встроенные функции объекта Editor
Когда мы поняли, что всё, что есть на дэше, можно переиспользовать внутри Editor, надо понять, как это сделать оптимально, давайте разберем основные полезные методы объекта Editor
// парсинг датconst{from:from_date,to:to_date}=ce_obj.resolveInterval(date_interval);// парсинг полей с операциямиconst{operation:suffx,value:vala}=Editor.resolveOperation(value);// парсинг относительных датconstval=Editor.resolveRelative(val);
Посмотрите видео с детальным разъяснением
Полный пример кода из видео
// КОД ВКЛАДКИ PREPARE Обработки параметров и фильтровconst{buildSource}=require('libs/dataset/v2');const{dateTimeParse,dateTime,addDays,addUnits,startOf}=require('@gravity-ui/date-utils');constFORMAT='YYYY-MM-DD'functionarrayChecker(some){if (Array.isArray(some)) {returnsome}else{return [some]}}functionvalArrayFromPrefix(arr){letvals= [];arr.forEach((element)=>{const{operation:suffx,value:vala}=Editor.resolveOperation(element);vals.push(vala);});returnvals;}functiongetDateFilters(ce_obj,params){letdate_interval=params.date_interval[0];letdate_dim=params.filter_date_dimension[0];letscale_name=params.date_scale[0];letscale_dict={'day':'D','week':'W','month':'M'};const{from:from_date,to:to_date}=ce_obj.resolveInterval(date_interval);letright_date=to_date;letleft_date=from_date;console.log(right_date,left_date);constdateTo=dateTimeParse(right_date).add(1,scale_name).startOf(scale_dict[scale_name]).add(-1,'day').format(FORMAT);constdateFrom=dateTimeParse(left_date).startOf(scale_dict[scale_name]).format(FORMAT);constdateFilter={column:date_dim, operation:'BETWEEN', values: [dateFrom,dateTo]}returndateFilter;}constparams=Editor.getParams();// для удобства все параметры переносим в переменные без парамсconstdate_interval=params.date_interval[0];constlimit=parseInt(params.row_limit[0]);constmetrics_list=params.metrics_list;constparams_to_send=params.params_to_send[0];constfilter_fields=params.filter_fields_ids['0'].split('|');constfilter_mass=params.mass_filter_fields_ids['0'].split('|');// массив для заполнения условийletwhere= [];// колонки для выбораletcols= [];// передавать параметры в датасетletfilled_params= []// 1) Фильтр на даты - отдельная обработкаletq=getDateFilters(Editor,params);where.push(q)// Фильтры и параметры дэша - все по циклуfor (const[key,value]ofObject.entries(params)) {// добавляем все параметры из переданных и не пустыхif (params_to_send.includes(key)) {filled_params.push({id:key,value:value.toString()});};if (filter_fields.includes(key) &&value!='') {letval,suff;if (value[0] &&value[0].substr(0,2) ==='__') {const{operation:suffx,value:vala}=Editor.resolveOperation(value);val=value.length==1?vala:valArrayFromPrefix(value);suff=suffx;}else{val=value;suff='IN';}if (Editor.resolveRelative(val) !=null) {val=Editor.resolveRelative(val);}if (Editor.resolveInterval(value) !=null) {val= [Editor.resolveInterval(value)['from'],Editor.resolveInterval(value)['to']];suff='BETWEEN'}if (filter_mass.includes(key)) {suff='IN';val=value[0].split(' ');}where.push({type:'id',column:key, operation:suff, values:arrayChecker(val)})}if ((key.startsWith('dimension_') ||key.startsWith('dim_col')) &&value[0] !='') {cols.push(value[0]);}}// добавляем колонки для выбора данныхcols.push(...metrics_list);module.exports={ dataset:buildSource({ id:Editor.getId('dataset'), columns:cols, where:where, parameters:filled_params, order:params.date_dimension[0], limit:limit*2, order_by:[{'direction':'DESC','column':metrics_list[0]}]})}
Обработка событий click и tooltip
Очень важная часть в дашборде — интерактивность. Рассмотрим, как добавить интерактивность в Advanced-чарты в виде tooltip и обработки кликов на элементы
// КОД ВКЛАДКИ PREPARE Работа с кликами и тултипами// Данные для двух метрикconstprimaryData= [{x:'Категория A', y:30, id:'A'},{x:'Категория B', y:80, id:'B'},{x:'Категория C', y:45, id:'C'},{x:'Категория D', y:60, id:'D'},{x:'Категория E', y:20, id:'E'}];constsecondaryData= [{x:'Категория A', y:50, id:'A'},{x:'Категория B', y:40, id:'B'},{x:'Категория C', y:75, id:'C'},{x:'Категория D', y:30, id:'D'}];constselectedMetric='primary';// Конфигурацияconstconfig={ data:{primaryData:primaryData,secondaryData:secondaryData}, selectedMetric:selectedMetric};module.exports={ render:Editor.wrapFn({fn:function(dimensions,config){const{width,height}=dimensions;conststate=Chart.getState() ||{};constselectedItem=state.selectedItem||config.selectedMetric;constcurrentData=selectedItem==='secondary'?config.data.secondaryData:config.data.primaryData;constsortedData= [...currentData].sort((a,b)=>b.y-a.y);// Создаем контейнерconstcontainer=document.createElement('div');container.style.setProperty('display','flex');container.style.setProperty('flex-direction','column');container.style.setProperty('height','100%');container.style.setProperty('font-family','sans-serif');// Блок метрикconstmetricsContainer=document.createElement('div');metricsContainer.style.setProperty('display','flex');metricsContainer.style.setProperty('margin','10px');metricsContainer.style.setProperty('gap','10px');// Определяем, какая метрика выбранаconstisPrimarySelected=selectedItem==='primary';// Кнопка "Основная"constprimaryBtn=document.createElement('div');primaryBtn.innerHTML='Основная метрика';primaryBtn.style.setProperty('padding','8px 12px');primaryBtn.style.setProperty('border-radius','4px');primaryBtn.style.setProperty('cursor','pointer');primaryBtn.style.setProperty('user-select','none');primaryBtn.style.setProperty('background-color',isPrimarySelected?'#1e88e5':'#e0e0e0');primaryBtn.style.setProperty('color',isPrimarySelected?'white':'black');primaryBtn.setAttribute('data-id','primary');metricsContainer.appendChild(primaryBtn);// Кнопка "Альтернативная"constsecondaryBtn=document.createElement('div');secondaryBtn.innerHTML='Альтернативная метрика';secondaryBtn.style.setProperty('padding','8px 12px');secondaryBtn.style.setProperty('border-radius','4px');secondaryBtn.style.setProperty('cursor','pointer');secondaryBtn.style.setProperty('user-select','none');// СТИЛИ в зависимости от выбораsecondaryBtn.style.setProperty('background-color',!isPrimarySelected?'#1e88e5':'#e0e0e0');secondaryBtn.style.setProperty('color',!isPrimarySelected?'white':'black');secondaryBtn.setAttribute('data-id','secondary');metricsContainer.appendChild(secondaryBtn);container.appendChild(metricsContainer);// Блок графикаconstchartContainer=document.createElement('div');chartContainer.style.setProperty('flex','1');chartContainer.style.setProperty('position','relative');chartContainer.style.setProperty('margin','10px');// Создаем SVGconstsvg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.setAttribute('width','100%');svg.setAttribute('height','90%');svg.setAttribute('viewBox',`-20 0 ${width}${height}`);svg.style.setProperty('overflow','visible');constmargin={top:20, right:20, bottom:40, left:50};constinnerWidth=width-margin.left-margin.right;constinnerHeight=height-margin.top-margin.bottom;// Группа для отступовconstg=document.createElementNS('http://www.w3.org/2000/svg','g');g.setAttribute('transform',`translate(${margin.left},${margin.top})`);svg.appendChild(g);// Масштабы: теперь x — это значения, y — категорииconstxScale=d3.scaleLinear().domain([0,d3.max(sortedData,d=>d.y)]).nice().range([0,innerWidth]);constyScale=d3.scaleBand().domain(sortedData.map(d=>d.x)).range([0,innerHeight]).padding(0.2);// Ось X (внизу)constxAxis=d3.axisBottom(xScale);constxAxisGroup=document.createElementNS('http://www.w3.org/2000/svg','g');xAxisGroup.setAttribute('transform',`translate(0,${innerHeight})`);g.appendChild(xAxisGroup);d3.select(xAxisGroup).call(xAxis);// Ось Y (слева)constyAxis=d3.axisLeft(yScale);constyAxisGroup=document.createElementNS('http://www.w3.org/2000/svg','g');g.appendChild(yAxisGroup);d3.select(yAxisGroup).call(yAxis);// Столбцы (теперь горизонтальные)constbars=document.createElementNS('http://www.w3.org/2000/svg','g');g.appendChild(bars);d3.select(bars).selectAll('rect').data(sortedData).enter().append('rect').attr('y',d=>yScale(d.x)).attr('x',0).attr('height',yScale.bandwidth()).attr('width',d=>xScale(d.y)).attr('fill','#4caf50').attr('data-id',d=>d.id).attr('cursor','pointer');svg.appendChild(g);chartContainer.appendChild(svg);container.appendChild(chartContainer);returnEditor.generateHtml(container.outerHTML);}, args: [config], libs: ['d3']}), events:{ click:Editor.wrapFn({fn:function(event,config){constclickedId=event.target?.getAttribute('data-id');if (!clickedId) return;// Проверяем, кликнули ли по кнопке метрикиif (clickedId==='primary'||clickedId==='secondary') {console.log('got')// Обновляем параметры действия для фильтрацииChart.setState({ selectedItem:clickedId});}}, args: [config]})}, tooltip:{ renderer:Editor.wrapFn({fn:function(event,config){constdataId=event.target?.getAttribute('data-id');if (!dataId) returnnull;// Проверяем, не является ли это кнопкой метрикиif (dataId==='primary'||dataId==='secondary') {returnnull;}// Определяем, какая метрика выбранаconststate=Chart.getState() ||{};constselectedItem=state.selectedItem||config.selectedMetric;constcurrentData=selectedItem==='secondary'?config.data.secondaryData:config.data.primaryData;constitem=currentData.find(d=>d.id===dataId);if (!item) returnnull;returnEditor.generateHtml(` <div style="padding: 10px; font-family: sans-serif; font-size: 14px;"> <div><strong>Категория:</strong> ${item.x}</div> <div><strong>Значение:</strong> ${item.y}</div> </div> `);}, args: [config]})}};
JS Selectors
С помощью селекторов JS можно поддержать совершенно уникальные различные сценарии скрытия / показа селекторов, разрыв связей с селекторами, сортировку и многое другое. Начнем с простого — как с целом сделать JS селектор с Датасетом
const{buildSource}=require('libs/dataset/v2');constparams=Editor.getParams();// Для удобства все параметры переносим в переменные без paramsconstselectors_date_interval=params.selectors_date_interval[0];constfilter_fields_ids=params.filter_fields_ids['0'].split('|');constfilter_fields_names=params.filter_fields_names['0'].split('|');constfilterObject=filter_fields_names.reduce((acc,id,index)=>{acc[id] =filter_fields_ids[index];returnacc;},{});constselector_aliases=params.selector_aliases['0'].split('|');functionensureArray(value){returnArray.isArray(value) ?value: [value];}// определение фильтра по дате - всегда добавляем в селектfunctiongetDateFilterPoP(params){constdate_dim=params.fast_date_dimension[0];const{from:from_date,to:to_date}=Editor.resolveInterval(selectors_date_interval);constright_date=to_date;letdateFilter={ column:date_dim, operation:'BETWEEN', values: [from_date,to_date],};returndateFilter;}// Массив для заполнения условий фильтрацииconstwhere= [];// Колонки для выбораconstcols= [];// Параметры для передачи в датасетconstfilled_params= [];// 1) Добавляем фильтр дат для PoP (Period over Period) сравненияwhere.push(getDateFilterPoP(params));// Обрабатываем все параметрыfor (const[key,value]ofObject.entries(params)) {// Добавляем все параметры из переданных и не пустых// Обрабатываем фильтрыif (filter_fields_ids.includes(key) &&value[0] !==''&&value.length!=0) {letval,suff;// Проверяем формат входных данныхif (value[0] &&value[0].substr(0,2) ==='__') {const{operation:suffx,value:vala}=Editor.resolveOperation(value);val=vala;suff=suffx;}else{val=value;suff='IN';}// Добавляем условие в массив wherewhere.push({ type:'id', column:key, operation:suff, values:ensureArray(val)});}}// Экспортируем настройки для buildSourceletall_filters={}selector_aliases.forEach((item,index)=>{console.log(filterObject[item])all_filters[item] =buildSource({ id:Editor.getId("dataset"), columns: [item], where:where.filter(object=>{returnobject.column!==filterObject[item];}),})});console.log(all_filters);module.exports=all_filters;
// пустой массив расчетных полей letupd= [];// добавляем поле с именем orField и туда в формулу прописываем что и как хотимupd.push({ action:'add_field', field:{'title':"orField", data_type:'string', type:'MEASURE', formula:"isnull([region_delivery]) or [region_delivery] = 'Саратовская область'"}})// добавляем в where это поле как тру логическоеconstorFilter={column:'orField', operation:'EQ', values: [true]}where.push(orFilter)module.exports={ dataset:buildSource({ id:Editor.getId('dataset'), columns:cols, where:where, parameters:filled_params, limit:limit,// раздел updates updates:upd, order_by:[{'direction':'DESC','column':metrics_list[0]}]})}