diff --git a/README.md b/README.md index 7f2d6ea0..8f3275c3 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,26 @@ ![Image of Flex the T-Rex](./assets/flex.png) + ## Why the Tableau Extensions API? -The Extensions API lets you do more without leaving Tableau. Build Tableau extensions that can interact and communicate with Tableau, and embed them directly in your workbooks. + +The Extensions API lets you do more without leaving Tableau. Build Tableau dashboard and viz extensions that can interact and communicate with Tableau, and embed them directly in your workbooks. + +* Build viz extensions to create new viz types that Tableau users can access through the worksheet Marks card. + + + + + +* Build dashboard extensions to add new features and functionality to Tableau that users can access through the dashboard. + + + ## Setup and Running Samples ### Prerequisites + * You must have Node.js and npm installed. You can get these from [https://nodejs.org](https://nodejs.org). ### Install Extensions API SDK Components and Start Server @@ -27,13 +41,14 @@ The Extensions API lets you do more without leaving Tableau. Build Tableau exten **npm start** -5. Launch Tableau and try the sample extensions in a dashboard. The samples are located in the `Samples` folder. +5. Launch Tableau and try a dashboard sample extension in a dashboard, or a viz extension in a worksheet. The dashboard and viz extension samples are located in the `Samples` folder. - >**Note** The local web server you start just serves to host the extension samples and extensions used in the tutorial, which have URLs similar to the following: `http://localhost:8765/Samples/DataSources/datasources.html` or `http://localhost:8765/Samples-Typescript/DataSources/datasources.html` - > This local web server is not intended to serve the Extensions API Help pages. + >**Note** The local web server you start just serves to host the extension samples and tutorial. These extensions have URLs similar to the following: `http://localhost:8765/Samples/Dashboard/DataSources/datasources.html`. + > This local web server is not intended to serve the Extensions API Help pages. > View the Help on GitHub at [https://tableau.github.io/extensions-api](https://tableau.github.io/extensions-api). ### Typescript Development + Samples written in Typescript are located in the `Samples-Typescript` folder. If you want to use TypeScript to write your extensions, you can run a script that starts up the HTTP server and actively listens for changes to the `.ts` files located in the `Samples-Typescript` folder. You can then add your extension to the folder and use the script to transpile your extension to JavaScript. @@ -41,7 +56,7 @@ If you want to use TypeScript to write your extensions, you can run a script tha **npm run dev** -For more information, see [Use TypeScript with the Extensions API](https://tableau.github.io/extensions-api/docs/trex_typescript.html). +For more information, see [Use TypeScript with the Extensions API](https://tableau.github.io/extensions-api/docs/core/trex_typescript). ### Sandboxed Extension Development Environment @@ -51,18 +66,19 @@ Tableau is introducing development support for Sandboxed Extensions with Tableau **npm run start-sandbox** -2. Launch Tableau (Tableau 2019.3 and later) and try the sample Sandboxed Extension in a dashboard. You can find the `.trex` file and sample code in the `Samples\UINamespace-sandboxed` folder. +2. Launch Tableau (Tableau 2019.3 and later) and try the sample Sandboxed dashboard extension in a dashboard. You can find the `.trex` file and sample code in the `Samples\Dashboard\UINamespace-sandboxed` folder. -For more information, see [Create and Test Sandboxed Extensions](https://tableau.github.io/extensions-api/docs/trex_sandbox_test.html). +For more information, see [Create and Test Sandboxed Extensions](https://tableau.github.io/extensions-api/docs/security/trex_sandbox_test). ## Contributions + Contributions and improvements by the community are welcomed! See the LICENSE file for current open-source licensing and use information. Before we can accept pull requests from contributors, we require a signed [Contributor License Agreement (CLA)](https://tableau.github.io/contributing.html). To submit a contribution, please fork the repository then submit a pull request to the `main` branch. ## Code Style -Our sample code follows the [Semi-Standard Style](https://github.com/Flet/semistandard) for JavaScript samples linting and [tslint](https://palantir.github.io/tslint/) for TypeScript. If you add your own extension code to the Samples or Samples-Typescript directories, you can run `npm run lint` to validate the style of your code. Please run this command before submitting any pull requests for Sample code. +Our sample code follows the [Semi-Standard Style](https://github.com/Flet/semistandard) for JavaScript samples linting and [tslint](https://palantir.github.io/tslint/) for TypeScript. If you add your own extension code to the Samples or Samples-Typescript directories, you can run `npm run lint` to validate the style of your code. Please run this command before submitting any pull requests for Sample code. `npx semistandard --fix` to fix linting issues which can be fixed automatically. diff --git a/Samples-Typescript/Annotation/Annotation.trex b/Samples-Typescript/Dashboard/Annotation/Annotation.trex similarity index 97% rename from Samples-Typescript/Annotation/Annotation.trex rename to Samples-Typescript/Dashboard/Annotation/Annotation.trex index 1d1b4b78..ba39183a 100644 --- a/Samples-Typescript/Annotation/Annotation.trex +++ b/Samples-Typescript/Dashboard/Annotation/Annotation.trex @@ -7,7 +7,7 @@ 1.10 - http://localhost:8765/Samples-Typescript/Annotation/annotation.html + http://localhost:8765/Samples-Typescript/Dashboard/Annotation/annotation.html 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 diff --git a/Samples-Typescript/Annotation/annotation.html b/Samples-Typescript/Dashboard/Annotation/annotation.html similarity index 87% rename from Samples-Typescript/Annotation/annotation.html rename to Samples-Typescript/Dashboard/Annotation/annotation.html index 6a014b0a..403f4efc 100644 --- a/Samples-Typescript/Annotation/annotation.html +++ b/Samples-Typescript/Dashboard/Annotation/annotation.html @@ -10,10 +10,10 @@ - + - +
diff --git a/Samples-Typescript/Annotation/annotation.ts b/Samples-Typescript/Dashboard/Annotation/annotation.ts similarity index 100% rename from Samples-Typescript/Annotation/annotation.ts rename to Samples-Typescript/Dashboard/Annotation/annotation.ts diff --git a/Samples-Typescript/DashboardLayout/DashboardLayout.trex b/Samples-Typescript/Dashboard/DashboardLayout/DashboardLayout.trex similarity index 96% rename from Samples-Typescript/DashboardLayout/DashboardLayout.trex rename to Samples-Typescript/Dashboard/DashboardLayout/DashboardLayout.trex index 39b11050..14085265 100644 --- a/Samples-Typescript/DashboardLayout/DashboardLayout.trex +++ b/Samples-Typescript/Dashboard/DashboardLayout/DashboardLayout.trex @@ -7,7 +7,7 @@ 1.7 - http://localhost:8765/Samples-Typescript/DashboardLayout/dashboardLayout.html + http://localhost:8765/Samples-Typescript/Dashboard/DashboardLayout/dashboardLayout.html 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 diff --git a/Samples-Typescript/DashboardLayout/dashboardLayout.html b/Samples-Typescript/Dashboard/DashboardLayout/dashboardLayout.html similarity index 86% rename from Samples-Typescript/DashboardLayout/dashboardLayout.html rename to Samples-Typescript/Dashboard/DashboardLayout/dashboardLayout.html index c2253baa..4e2b95c0 100644 --- a/Samples-Typescript/DashboardLayout/dashboardLayout.html +++ b/Samples-Typescript/Dashboard/DashboardLayout/dashboardLayout.html @@ -10,10 +10,10 @@ - + - +
diff --git a/Samples-Typescript/DashboardLayout/dashboardLayout.ts b/Samples-Typescript/Dashboard/DashboardLayout/dashboardLayout.ts similarity index 100% rename from Samples-Typescript/DashboardLayout/dashboardLayout.ts rename to Samples-Typescript/Dashboard/DashboardLayout/dashboardLayout.ts diff --git a/Samples-Typescript/DashboardObjectVisibility/DashboardObjectVisibility.trex b/Samples-Typescript/Dashboard/DashboardObjectVisibility/DashboardObjectVisibility.trex similarity index 96% rename from Samples-Typescript/DashboardObjectVisibility/DashboardObjectVisibility.trex rename to Samples-Typescript/Dashboard/DashboardObjectVisibility/DashboardObjectVisibility.trex index 14c8c3b8..d1b02e8c 100644 --- a/Samples-Typescript/DashboardObjectVisibility/DashboardObjectVisibility.trex +++ b/Samples-Typescript/Dashboard/DashboardObjectVisibility/DashboardObjectVisibility.trex @@ -7,7 +7,7 @@ 1.7 - http://localhost:8765/Samples-Typescript/DashboardObjectVisibility/dashboardObjectVisibility.html + http://localhost:8765/Samples-Typescript/Dashboard/DashboardObjectVisibility/dashboardObjectVisibility.html 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 diff --git a/Samples-Typescript/DashboardObjectVisibility/dashboardObjectVisibility.html b/Samples-Typescript/Dashboard/DashboardObjectVisibility/dashboardObjectVisibility.html similarity index 82% rename from Samples-Typescript/DashboardObjectVisibility/dashboardObjectVisibility.html rename to Samples-Typescript/Dashboard/DashboardObjectVisibility/dashboardObjectVisibility.html index 778d911e..0364caaa 100644 --- a/Samples-Typescript/DashboardObjectVisibility/dashboardObjectVisibility.html +++ b/Samples-Typescript/Dashboard/DashboardObjectVisibility/dashboardObjectVisibility.html @@ -9,10 +9,10 @@ - + - +
diff --git a/Samples-Typescript/DashboardObjectVisibility/dashboardObjectVisibility.tsx b/Samples-Typescript/Dashboard/DashboardObjectVisibility/dashboardObjectVisibility.tsx similarity index 100% rename from Samples-Typescript/DashboardObjectVisibility/dashboardObjectVisibility.tsx rename to Samples-Typescript/Dashboard/DashboardObjectVisibility/dashboardObjectVisibility.tsx diff --git a/Samples-Typescript/DataSources/DataSources.trex b/Samples-Typescript/Dashboard/DataSources/DataSources.trex similarity index 94% rename from Samples-Typescript/DataSources/DataSources.trex rename to Samples-Typescript/Dashboard/DataSources/DataSources.trex index 1bbdf172..18d89ee4 100644 --- a/Samples-Typescript/DataSources/DataSources.trex +++ b/Samples-Typescript/Dashboard/DataSources/DataSources.trex @@ -7,7 +7,7 @@ 0.8 - http://localhost:8765/Samples-Typescript/DataSources/datasources.html + http://localhost:8765/Samples-Typescript/Dashboard/DataSources/datasources.html iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAlhJREFUOI2Nkt9vy1EYh5/3bbsvRSySCZbIxI+ZCKsN2TKtSFyIrV2WuRCJuBiJWxfuxCVXbvwFgiEtposgLFJElnbU1SxIZIIRJDKTrdu+53Uhra4mce7Oe57Pcz7JOULFisViwZ+29LAzOSjQYDgz1ZcCvWuXV11MJpN+OS/lm6179teqH0yDqxPTCyKSA8DcDsyOmOprnCaeP7459pdgy969i0LTC3IO/RQMyoHcQN+3cnljW3dNIFC47qDaK3g7BwdTkwBaBELT4ZPOUVWgKl4ZBnjxJPUlMDnTDrp0pmr6RHFeEjjcUUXPDGeSEwDN0Xg8sivxMhJNjGzbHd8PkM3eHRfkrBM5NkcQaY2vUnTlrDIA0NoaX+KLXFFlowr14tvVpqb2MICzmQcKqxvbumv+NAhZGCCIPwEw6QWXKYRL/VUXO0+rAUJiPwAk5MIlgVfwPjjHLCL1APmHN94ZdqeYN+NW/mn6I4BvwQYchcLnwFhJMDiYmlRxAzjpKWZkYkUCcZ2I61wi37tLbYyjiN0fHk5Oz3nGSLSzBbNHCF35R7f6K1/hN9PRhek11FrymfQQQKB4+Gl05P2qNRtmETlXW7e+b2z01dfycGNbfFMAbqNyKp9Jp4rzOT8RYFs0njJkc2iqsCObvTsOsDWWqA5C1uFy+Uz/oXJeKwVT4h0RmPUXhi79vuC0Ku6yOffTK3g9lfxfDQAisY516sg5kfOCiJk7HoLt2cf9b/9LANAc7dznm98PagG1fUOZ9IP5uMB8Q4CPoyNvausapkTt3rNMuvdf3C/o6+czhtdwmwAAAABJRU5ErkJggg== diff --git a/Samples-Typescript/DataSources/datasources.html b/Samples-Typescript/Dashboard/DataSources/datasources.html similarity index 96% rename from Samples-Typescript/DataSources/datasources.html rename to Samples-Typescript/Dashboard/DataSources/datasources.html index 1179cafc..3a8a1126 100644 --- a/Samples-Typescript/DataSources/datasources.html +++ b/Samples-Typescript/Dashboard/DataSources/datasources.html @@ -15,10 +15,10 @@ - + - +
diff --git a/Samples-Typescript/DataSources/datasources.ts b/Samples-Typescript/Dashboard/DataSources/datasources.ts similarity index 100% rename from Samples-Typescript/DataSources/datasources.ts rename to Samples-Typescript/Dashboard/DataSources/datasources.ts diff --git a/Samples-Typescript/Filtering/Filtering.trex b/Samples-Typescript/Dashboard/Filtering/Filtering.trex similarity index 94% rename from Samples-Typescript/Filtering/Filtering.trex rename to Samples-Typescript/Dashboard/Filtering/Filtering.trex index e724c05e..a3286e95 100644 --- a/Samples-Typescript/Filtering/Filtering.trex +++ b/Samples-Typescript/Dashboard/Filtering/Filtering.trex @@ -7,7 +7,7 @@ 0.8 - http://localhost:8765/Samples-Typescript/Filtering/filtering.html + http://localhost:8765/Samples-Typescript/Dashboard/Filtering/filtering.html iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAlhJREFUOI2Nkt9vy1EYh5/3bbsvRSySCZbIxI+ZCKsN2TKtSFyIrV2WuRCJuBiJWxfuxCVXbvwFgiEtposgLFJElnbU1SxIZIIRJDKTrdu+53Uhra4mce7Oe57Pcz7JOULFisViwZ+29LAzOSjQYDgz1ZcCvWuXV11MJpN+OS/lm6179teqH0yDqxPTCyKSA8DcDsyOmOprnCaeP7459pdgy969i0LTC3IO/RQMyoHcQN+3cnljW3dNIFC47qDaK3g7BwdTkwBaBELT4ZPOUVWgKl4ZBnjxJPUlMDnTDrp0pmr6RHFeEjjcUUXPDGeSEwDN0Xg8sivxMhJNjGzbHd8PkM3eHRfkrBM5NkcQaY2vUnTlrDIA0NoaX+KLXFFlowr14tvVpqb2MICzmQcKqxvbumv+NAhZGCCIPwEw6QWXKYRL/VUXO0+rAUJiPwAk5MIlgVfwPjjHLCL1APmHN94ZdqeYN+NW/mn6I4BvwQYchcLnwFhJMDiYmlRxAzjpKWZkYkUCcZ2I61wi37tLbYyjiN0fHk5Oz3nGSLSzBbNHCF35R7f6K1/hN9PRhek11FrymfQQQKB4+Gl05P2qNRtmETlXW7e+b2z01dfycGNbfFMAbqNyKp9Jp4rzOT8RYFs0njJkc2iqsCObvTsOsDWWqA5C1uFy+Uz/oXJeKwVT4h0RmPUXhi79vuC0Ku6yOffTK3g9lfxfDQAisY516sg5kfOCiJk7HoLt2cf9b/9LANAc7dznm98PagG1fUOZ9IP5uMB8Q4CPoyNvausapkTt3rNMuvdf3C/o6+czhtdwmwAAAABJRU5ErkJggg== diff --git a/Samples-Typescript/Filtering/filtering.html b/Samples-Typescript/Dashboard/Filtering/filtering.html similarity index 93% rename from Samples-Typescript/Filtering/filtering.html rename to Samples-Typescript/Dashboard/Filtering/filtering.html index 6e7270b8..cf1571c0 100644 --- a/Samples-Typescript/Filtering/filtering.html +++ b/Samples-Typescript/Dashboard/Filtering/filtering.html @@ -15,10 +15,10 @@ - + - +
diff --git a/Samples-Typescript/Filtering/filtering.ts b/Samples-Typescript/Dashboard/Filtering/filtering.ts similarity index 100% rename from Samples-Typescript/Filtering/filtering.ts rename to Samples-Typescript/Dashboard/Filtering/filtering.ts diff --git a/Samples-Typescript/Formatting/formatting.html b/Samples-Typescript/Dashboard/Formatting/formatting.html similarity index 94% rename from Samples-Typescript/Formatting/formatting.html rename to Samples-Typescript/Dashboard/Formatting/formatting.html index 2bd43452..14ee412d 100644 --- a/Samples-Typescript/Formatting/formatting.html +++ b/Samples-Typescript/Dashboard/Formatting/formatting.html @@ -11,10 +11,10 @@ - + - + + + + +
+ + diff --git a/Samples/Viz/Sankey/sankey.js b/Samples/Viz/Sankey/sankey.js new file mode 100644 index 00000000..27762323 --- /dev/null +++ b/Samples/Viz/Sankey/sankey.js @@ -0,0 +1,728 @@ +/* global d3 */ +/* global tinycolor */ + +const backgroundColor = tinycolor('white'); + +async function Sankey (encodedData, encodingMap, width, height, selectedTupleIds, styles) { + const palette = ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f', '#edc949', '#af7aa1', '#ff9da7', '#9c755f', '#bab0ab']; + + const xPadding = 2; + const yPadding = 1; + const levelWidth = 100; + const top = 20; + + const layout = computeSankeyLayout( + d3.sankey, + encodedData, + top, + width, + height, + levelWidth + xPadding * 2, + yPadding, + palette + ); + + // Create an SVG container. + const svg = d3 + .create('svg') + .attr('class', tableau.ClassNameKey.Worksheet) + .attr('width', width) + .attr('height', height) + .attr('viewBox', [0, 0, width, height]) + .attr('style', 'max-width: 100%; height: auto; height: intrinsic;') + .attr('font-family', styles?.fontWeight) + .attr('font-weight', styles?.fontFamily) + .attr('font-size', styles?.fontSize) + .attr('font-style', styles?.fontStyle) + .attr('text-decoration', styles?.textDecoration); + + const selectedNodeIndexes = getSelectedNodes(layout.links, selectedTupleIds); + + // Create the rects that represent the nodes. + svg.append('g') + .selectAll() + .data(layout.nodes) + .join('rect') + .attr('x', d => d.x0 + xPadding) + .attr('y', d => d.y0) + .attr('height', d => d.y1 - d.y0) + .attr('width', d => d.x1 - d.x0 - xPadding * 2) + .attr('fill', (d, index) => getColor(d.color, selectedTupleIds, selectedNodeIndexes.has(index))); + + // Creates the paths that represent the links. + const links = svg.append('g') + .attr('fill-opacity', 0.5) + .style('cursor', 'pointer') + .selectAll() + .data(layout.links) + .join('path') + .attr('d', getLinkPath) + .attr('fill', d => getLinkColor(d, selectedTupleIds)); + + // Add labels on the nodes. + svg.append('g') + .selectAll() + .data(layout.nodes) + .join('text') + .attr('x', d => (d.x1 + d.x0) / 2) + .attr('y', d => (d.y1 + d.y0) / 2) + .attr('dy', '0.35em') + .attr('text-anchor', 'middle') + .text(d => d.name) + .attr('fill', (d, index) => getColor('black', selectedTupleIds, selectedNodeIndexes.has(index))); + + // Add top labels + if (encodingMap.level) { + // Pick one representetive node for each level + const levels = []; + for (const node of layout.nodes) { levels[node.layer] = node; } + + svg.append('g') + .selectAll() + .data(levels) + .join('text') + .attr('font-weight', 'bold') + .attr('x', d => (d.x1 + d.x0) / 2) + .attr('y', top / 2) + .attr('text-anchor', 'middle') + .text(d => encodingMap.level[d.layer].name) + .attr('fill', styles?.color); + } + + // Container for rendering selected elements (rendered last) + const selectionLayer = svg.append('g'); + const hoveringLayer = svg.append('g'); + + const linksPerTupleId = getLinksPerTupleId(links); + + renderSelection(selectedTupleIds, linksPerTupleId, selectionLayer, hoveringLayer); + + return { + hoveringLayer, + linksPerTupleId, + viz: svg.node() + }; +} + +function getLinksPerTupleId (links) { + const linksPerTupleId = new Map(); + links.each(function (d) { + let list = linksPerTupleId.get(d.tupleId); + if (!list) { + list = []; + linksPerTupleId.set(d.tupleId, list); + } + list.push(d3.select(this)); + }); + + return linksPerTupleId; +} + +function getSelectedNodes (links, selectedTupleIds) { + const selectedNodeIndexes = new Map(); + + for (const link of links) { + if (selectedTupleIds.has(link.tupleId)) { + selectedNodeIndexes.set(link.source.index); + selectedNodeIndexes.set(link.target.index); + } + } + + return selectedNodeIndexes; +} + +// Render selected elements on a separate top-level layer +function renderSelection (selectedTupleIds, linksPerTupleId, selectionLayer, highlightingLayer) { + selectionLayer.selectAll('*').remove(); + highlightingLayer.selectAll('*').remove(); + + const selectedLinks = []; + + for (const id of selectedTupleIds.keys()) { selectedLinks.push(...linksPerTupleId.get(id)); } + + let outline = selectionLayer.selectAll('g.selectionOutline'); + + // Render the outline first (to dissolve selected elemens borders) + if (outline.empty()) { + outline = selectionLayer + .append('g') + .attr('class', 'selectionOutline'); + } + + outline.selectAll() + .data(selectedLinks) + .join('path') + .attr('d', link => link.attr('d')) // Copy path geometry from the 'normal' element + .datum(link => link.datum()); // Bind data from the 'normal' elememt + + // Render filled elements without borders + let fill = selectionLayer.selectAll('g.selection'); + + if (fill.empty()) { + fill = selectionLayer + .append('g') + .attr('class', 'selection'); + } + + fill.selectAll() + .data(selectedLinks) + .join('path') + .attr('d', link => link.attr('d')) // Copy path geometry from the 'normal' element + .datum(link => link.datum()); // Bind data from the 'normal' elememt +} + +// Render hovered elements on a separate top-level layer +function renderHoveredElements (hoveredTupleIds, linksPerTupleId, hoveringLayer) { + if (!hoveringLayer) return; + + hoveringLayer.selectAll('*').remove(); + + const hoveredLinks = []; + for (const id of hoveredTupleIds.keys()) { hoveredLinks.push(...linksPerTupleId.get(id)); } + + hoveringLayer.selectAll() + .data(hoveredLinks) + .join('path') + .attr('d', (link) => link.attr('d')) + .attr('class', 'highlighting'); +} + +async function renderViz (rawData, encodingMap, selectedMarksIds, styles) { + const encodedData = getEncodedData(rawData, encodingMap); + + const content = document.getElementById('content'); + content.innerHTML = ''; + + const sankey = await Sankey( + encodedData, + encodingMap, + content.offsetWidth, + content.offsetHeight, + selectedMarksIds, + styles + ); + + content.appendChild(sankey.viz); + + return sankey; +} + +// Uses getVisualSpecificationAsync to build a map of encoding identifiers (specified in the .trex file) +// to fields that the user has placed on the encoding's shelf. +// Only encodings that have fields dropped on them will be part of the encodingMap. +async function getEncodingMap () { + const worksheet = tableau.extensions.worksheetContent.worksheet; + const visualSpec = await worksheet.getVisualSpecificationAsync(); + + const encodingMap = {}; + + if (visualSpec.activeMarksSpecificationIndex < 0) { return encodingMap; } + + const marksCard = visualSpec.marksSpecifications[visualSpec.activeMarksSpecificationIndex]; + for (const encoding of marksCard.encodings) { + if (!encodingMap[encoding.id]) { encodingMap[encoding.id] = []; } + + encodingMap[encoding.id].push(encoding.field); + } + + return encodingMap; +} + +window.onload = tableau.extensions.initializeAsync().then(async () => { + // Get the worksheet that the Viz Extension is running in + const worksheet = tableau.extensions.worksheetContent.worksheet; + + // Save these outside the scope below for handling resizing without refetching the data + let summaryData = {}; + let encodingMap = {}; + let selectedMarks = new Map(); + const hoveredMarks = new Map(); + let linksPerTupleId = new Map(); + let hoveringLayer; + + const styles = tableau.extensions.environment.workbookFormatting?.formattingSheets + ?.find(x => x.classNameKey === 'tableau-worksheet')?.cssProperties; + + // Use the extensions API to get the summary data and map of encodings to fields, + // and render the connected scatterplot. + const updateDataAndRender = async () => { + // Use extensions API to update the table of data and the map from encodings to fields + [summaryData, encodingMap] = await Promise.all([ + getSummaryDataTable(worksheet), + getEncodingMap(worksheet) + ]); + + // Selection can change on any data changes + selectedMarks = await getSelection(worksheet, summaryData); + ({ hoveringLayer, linksPerTupleId } = await renderViz(summaryData, encodingMap, selectedMarks, styles)); + }; + + // Handle re-rendering when the page is resized + onresize = async () => { + ({ hoveringLayer, linksPerTupleId } = await renderViz(summaryData, encodingMap, selectedMarks, styles)); + }; + + // Listen to event for when the summary data backing the worksheet has changed. + // This tells us that we should refresh the data and encoding map. + worksheet.addEventListener( + tableau.TableauEventType.SummaryDataChanged, updateDataAndRender); + + // Setup interactivity events + document.body.addEventListener('click', async (e) => { + onClick(e, selectedMarks, hoveredMarks); + ({ hoveringLayer, linksPerTupleId } = await renderViz(summaryData, encodingMap, selectedMarks, styles)); + }); + + document.body.addEventListener('mousemove', e => onMouseMove(e, hoveredMarks, linksPerTupleId, hoveringLayer)); + document.body.addEventListener('mouseout', e => onMouseMove(e, hoveredMarks, linksPerTupleId, hoveringLayer)); + + // Do the initial update and render + updateDataAndRender(); +}); + +function onClick (e, selectedTupleIds, hoveredTupleIds) { + const elem = d3.select(document.elementFromPoint(e.pageX, e.pageY)); + const data = elem?.datum(); + const tupleId = data === undefined ? null : data.tupleId; + + if (elem && tupleId !== null && tupleId !== undefined) { + if (selectedTupleIds.has(tupleId)) { + // User clicked on an already selected item + // Only one item is selected - deselect it + if (selectedTupleIds.size === 1) selectedTupleIds.clear(); + // Remove an item from selection + else if (e.ctrlKey) selectedTupleIds.delete(tupleId); + else { + selectedTupleIds.clear(); + selectedTupleIds.set(tupleId); + } + } else { + if (!e.ctrlKey) selectedTupleIds.clear(); + selectedTupleIds.set(tupleId); + } + } else if (!e.ctrlKey) { + // Clicking outside of any element will clear all element, unless CTRL is pressed + selectedTupleIds.clear(); + } + + selectTuples(e.pageX, e.pageY, selectedTupleIds, hoveredTupleIds); +} + +async function selectTuples (x, y, selectedTupleIds, hoveredTupleIds) { + clearHoveredMarks(hoveredTupleIds); + getWorksheet().selectTuplesAsync([...selectedTupleIds.keys()], tableau.SelectOptions.Simple, { tooltipAnchorPoint: { x, y } }); +} + +async function onMouseMove (e, hoveredTupleIds, linksPerTupleId, hoveringLayer) { + const elem = d3.select(document.elementFromPoint(e.pageX, e.pageY)); + const data = elem?.node() ? elem.datum() : undefined; + const tupleId = data === undefined ? null : data.tupleId; + + const hadHoveredTupleBefore = hoveredTupleIds.size !== 0; + + clearHoveredMarks(hoveredTupleIds); + + if (elem && tupleId !== null && tupleId !== undefined) { + hoveredTupleIds.set(tupleId); + getWorksheet().hoverTupleAsync(parseInt(tupleId), { tooltipAnchorPoint: { x: e.pageX, y: e.pageY } }); + } else if (hadHoveredTupleBefore) { getWorksheet().hoverTupleAsync(parseInt(tupleId), { tooltipAnchorPoint: { x: e.pageX, y: e.pageY } }); } + + renderHoveredElements(hoveredTupleIds, linksPerTupleId, hoveringLayer); +} + +function clearHoveredMarks (hoveredTupleIds) { + hoveredTupleIds.clear(); +} + +async function getSelection (worksheet, allMarks) { + const selectedMarks = await worksheet.getSelectedMarksAsync(); + + return findIdsOfSelectedMarks(allMarks, selectedMarks); +} + +// Go through all selected marks and find their exact match in the data table +// Use the index of the mark in the data table to compute tupleId +function findIdsOfSelectedMarks (allMarks, selectedMarks) { + const columns = selectedMarks.data[0].columns; + const selectedMarkMap = new Map(); + const selectedMarksIds = new Map(); + + for (const selectedMark of convertToListOfNamedRows(selectedMarks.data[0])) { + let key = ''; + for (const col of columns) { key += selectedMark[col.fieldName].value + '\x00'; } + + selectedMarkMap.set(key, selectedMark); + } + + let tupleId = 1; + for (const mark of allMarks) { + let key = ''; + for (const col of columns) { key += mark[col.fieldName].value + '\x00'; } + + if (selectedMarkMap.has(key)) { selectedMarksIds.set(tupleId); } + + tupleId++; + } + + return selectedMarksIds; +} + +// Takes a page of data, which has a list of DataValues (dataTablePage.data) +// and a list of columns and puts the data in a list where each entry is an +// object that maps from field names to DataValues +// (example of a row being: { SUM(Sales): ..., SUM(Profit): ..., Ship Mode: ..., }) +function convertToListOfNamedRows (dataTablePage) { + const rows = []; + const columns = dataTablePage.columns; + const data = dataTablePage.data; + for (let i = 0; i < data.length; ++i) { + const row = {}; + for (let j = 0; j < columns.length; ++j) { + row[columns[j].fieldName] = data[i][columns[j].index]; + } + row.tupleId = i + 1; + rows.push(row); + } + return rows; +} + +// Gets each page of data in the summary data and returns a list of rows of data +// associated with field names. +async function getSummaryDataTable (worksheet) { + let rows = []; + + // Fetch the summary data using the DataTableReader + const dataTableReader = await worksheet.getSummaryDataReaderAsync( + undefined, + { ignoreSelection: true } + ); + for ( + let currentPage = 0; + currentPage < dataTableReader.pageCount; + currentPage++ + ) { + const dataTablePage = await dataTableReader.getPageAsync(currentPage); + rows = rows.concat(convertToListOfNamedRows(dataTablePage)); + } + await dataTableReader.releaseAsync(); + + return rows; +} + +// Converts each data row from a object map to a object map +// For example, { SUM(Sales): 10.23, Ship Mode: 'Next Day', Category: 'Office Supplies' } will be converted to +// { edge: [10.23], levels: ['Next Day', 'Office Supplies'] } if SUM(Sales) is on the edge encoding and Ship Mode ad Category are on the level encoding +function getEncodedData (data, encodingMap) { + const encodedData = []; + + let tupleId = 1; + for (const row of data) { + const encodedRow = {}; + + for (const encName in encodingMap) { + const fields = encodingMap[encName]; + + encodedRow[encName] = []; + + for (const field of fields) { encodedRow[encName].push(row[field.name]); } + } + + encodedRow.tupleId = tupleId; + tupleId++; + + encodedData.push(encodedRow); + } + + return encodedData; +} + +//= ================= +// COLOR FUNCTIONS +//= ================= +function getLinkColor (link, selectedTupleIds) { + const color = link.color ?? 'lightgray'; + + return getColor(color, selectedTupleIds); +} + +function getColor (color, selectedTupleIds, doNotFog) { + return (selectedTupleIds.size > 0 && doNotFog !== true) ? calculateFogColor(color) : color; +} + +/* +function getLabelColor(color, selectedTupleIds) { + color = getAutoLabelColor(color ?? 'rgb(102, 102, 102)'); + return getColor(color, selectedTupleIds); +} + +// Takes two possible options for a color (a light and a dark color option) and returns the one with the ideal +// contrast to the background color. +function getAutoLabelColor (fgColorStr, bgColorStr) { + const Black = tinycolor('black'); + const White = tinycolor('white'); + + const fgColor = tinycolor(fgColorStr); + const bgColor = tinycolor(bgColorStr); + + const fgColorIsLight = isLuminanceAboveThreshold(fgColor); + const dark = fgColorIsLight ? Black : fgColor; + const light = fgColorIsLight ? fgColor : White; + + const autoColor = isLuminanceAboveThreshold(bgColor) ? dark : light; + + return autoColor.toHexString(); +} + +function isLuminanceAboveThreshold (color) { + return color.getLuminance() > 0.3149999976158142; +} +*/ + +const fogBlendFactor = getFogBlendFactor(backgroundColor); +const { foggedBackgroundRed, foggedBackgroundGreen, foggedBackgroundBlue } = computeFoggedBackgroundColor(backgroundColor, fogBlendFactor); + +// When one or more elements are selected, everything else is fogged out. +function computeFoggedBackgroundColor (color, fogBlendFactor) { + const CloseToWhite = 245; + + if (color.r >= CloseToWhite && color.g >= CloseToWhite && color.b >= CloseToWhite) { color = tinycolor({ r: CloseToWhite, g: CloseToWhite, b: CloseToWhite }); } + + color = color.toRgb(); + const foggedBackgroundRed = (1 - fogBlendFactor) * color.r >>> 0; + const foggedBackgroundGreen = (1 - fogBlendFactor) * color.g >>> 0; + const foggedBackgroundBlue = (1 - fogBlendFactor) * color.b >>> 0; + + return { foggedBackgroundRed, foggedBackgroundGreen, foggedBackgroundBlue }; +} + +function calculateFogColor (colorStr) { + const color = tinycolor(colorStr).toRgb(); + + const fogR = foggedBackgroundRed + color.r * fogBlendFactor >>> 0; + const fogG = foggedBackgroundGreen + color.g * fogBlendFactor >>> 0; + const fogB = foggedBackgroundBlue + color.b * fogBlendFactor >>> 0; + + return tinycolor({ r: fogR, g: fogG, b: fogB }).toHexString(); +} + +function getFogBlendFactor (color) { + color = color.toRgb(); + + const DefaultFogBlendFactor = 0.1850000023841858; + const DarkBgFogBlendFactor = 0.2750000059604645; + const DarkBgThreshold = 75; + const isDarkBackground = color.r <= DarkBgThreshold && color.g <= DarkBgThreshold && color.b <= DarkBgThreshold; + return (isDarkBackground ? DarkBgFogBlendFactor : DefaultFogBlendFactor); +} + +function getLinkPath (d) { + const midX = (d.source.x1 + d.target.x0) / 2; + let path = `M ${d.source.x1} ${d.y0 - d.width / 2}`; + path += ` C ${midX} ${d.y0 - d.width / 2} ${midX} ${d.y1 - d.width / 2} ${d.target.x0} ${d.y1 - d.width / 2}`; + path += ` L ${d.target.x0} ${d.y1 + d.width / 2}`; + path += ` C ${midX} ${d.y1 + d.width / 2} ${midX} ${d.y0 + d.width / 2} ${d.source.x1} ${d.y0 + d.width / 2}`; + path += ' Z'; // close the path + return path; +} + +function computeSankeyLayout ( + d3Sankey, + data, + top, + width, + height, + nodeWidth, + padding, + palette +) { + // `nodes` are `rectangles` in the UI + // `links` are what we call `edges` or `noodles` in the UI + if (data.length === 0) return { links: [], nodes: [] }; + + const firstRow = data[0]; + + const levelCount = Array.isArray(firstRow.level) ? firstRow.level.length : 0; + const hasLinks = firstRow.edge !== undefined; + + if (levelCount === 0) { + // Create a dummy link that follows the object schema returned by d3 + const link = { + y0: (height - top) / 2, + y1: (height - top) / 2, + source: { + x1: nodeWidth + }, + target: { + x0: width - nodeWidth + }, + width: 2, + color: null + }; + return { nodes: [], links: [link] }; + } + + let links = []; + let nodes = []; + + if (levelCount === 1) { + // Compute links for each pair of adjacent levels + for (const row of data) { + const link = { + source: { name: row.level[0].value }, + target: { name: row.level[0].value }, + value: getLinkValue(row, hasLinks), + tupleId: row.tupleId + }; + + links.push(link); + nodes.push(link.source); + nodes.push(link.target); + } + } else { + // Compute a map of nodes for each level + const nodesPerLevel = []; + + for (let i = 0; i < levelCount; i++) nodesPerLevel.push(new Map()); + + for (const row of data) { + for (let i = 0; i < levelCount; i++) nodesPerLevel[i].set(row.level[i].value, { name: row.level[i].value }); + } + nodes.push(...nodesPerLevel[0].values()); // initialize with the first level nodes + + // Compute links for each pair of adjacent levels + for (let i = 0; i < levelCount - 1; i++) { + for (const row of data) { + links.push({ + source: nodesPerLevel[i].get(row.level[i].value), + target: nodesPerLevel[i + 1].get(row.level[i + 1].value), + value: getLinkValue(row, hasLinks), + tupleId: row.tupleId + }); + } + + nodes.push(...nodesPerLevel[i + 1].values()); + } + + // We have two goals for our sorting of links. We want the links associated with a particular + // tuple to "meet up" at the nodes, so that they form a mostly continuous path end-to-end. We + // also don't want the links to be a complete webbed mess. We try to make sure that the links + // between the first two levels are pleasing, and then we let d3 do its best with the rest. + // To create a pleasing set of links between level 0 and 1, we sort the links so that all + // links between level-0/node-0 and level-1/node-0 are adjacent, followed by all links + // between level-0/node-0 and level-1/node-1, etc. This minimizes cross-over in the first + // set of links. We try to apply the same idea to the rest of the levels, but the sort on + // level-0 will almost always win in practice. The bands will leave level-1 at the same height + // as they entered, but who knows where they'll go after that! d3 will do its best by reordering + // nodes in later levels, which may also improve appearance. + + links.sort((lhs, rhs) => { + const isLevelNLink = (link, n) => link.source === nodesPerLevel[n].get(link.source.name); + for (let level = 0; level < nodesPerLevel.length; level++) { + const lhsLevelNLink = links.find((elem) => elem.tupleId === lhs.tupleId && isLevelNLink(elem, level)); + const rhsLevelNLink = links.find((elem) => elem.tupleId === rhs.tupleId && isLevelNLink(elem, level)); + + if (!lhsLevelNLink) { + return rhsLevelNLink ? -1 : 0; + } + if (!rhsLevelNLink) { + return 1; + } + + const lhsLevelNSourceNodeIndex = nodes.indexOf(lhsLevelNLink.source); + const rhsLevelNSourceNodeIndex = nodes.indexOf(rhsLevelNLink.source); + if (lhsLevelNSourceNodeIndex !== rhsLevelNSourceNodeIndex) { + return lhsLevelNSourceNodeIndex < rhsLevelNSourceNodeIndex ? -1 : 1; + } + + const lhsLevelNTargetNodeIndex = nodes.indexOf(lhsLevelNLink.target); + const rhsLevelNTargetNodeIndex = nodes.indexOf(rhsLevelNLink.target); + if (lhsLevelNTargetNodeIndex !== rhsLevelNTargetNodeIndex) { + return lhsLevelNTargetNodeIndex < rhsLevelNTargetNodeIndex ? -1 : 1; + } + } + + if (lhs.tupleId !== rhs.tupleId) { + return lhs.tupleId < rhs.tupleId ? -1 : 1; + } + + return 0; + }); + } + + // Set the sankey diagram properties + const sankey = d3Sankey() + .nodeWidth(nodeWidth) + .nodePadding(padding) + .linkSort(null) + .nodeSort(null) + .extent([ + [0, top], + [width, height] + ]) + .nodes(nodes) + .links(links); + + // Compute the layout + sankey({ nodes, links }); + + // If there are any NaN values in nodes or links, render will fail. + // Set both to [] so that we get an empty display instead. TFS 1481434. + if (elementOfArrayContainsNaNProperty(nodes) || elementOfArrayContainsNaNProperty(links)) { + nodes = []; + links = []; + return { nodes, links }; + } + + if (levelCount === 1) { + nodes = nodes.filter((node) => node.depth === 0); + } + + nodes = nodes.filter((node) => node.value); + links = links.filter((link) => nodes.includes(link.source) && nodes.includes(link.target)); + + // Add colors, this needs to be the very last thing done to ensure palette is applied correctly + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + node.color = palette[i % palette.length]; + for (const link of node.sourceLinks) { + link.color = node.color; + } + } + + return { nodes, links }; +} + +function elementOfArrayContainsNaNProperty (dataArray) { + // If any of these properties of a node or link element contain a NaN value, the viz will not render. + const propertiesToCheckForNaN = ['x', 'y', 'x0', 'y0', 'x1', 'y1', 'width', 'height']; + + for (const element of dataArray) { + if (!element) continue; + + for (const propertyName in element) { + if (!propertiesToCheckForNaN.includes(propertyName)) continue; + + if (Object.prototype.hasOwnProperty.call(element, propertyName)) { + const thisValue = element[propertyName]; + if (typeof thisValue === 'number' && isNaN(thisValue)) { + return true; + } + } + } + } + return false; +} + +function getLinkValue (row, hasLinks) { + if (!hasLinks) return 1; // If there's no field on edge, make all links the same weight + + const value = row.edge[0].value; + + return typeof value === 'number' && !isNaN(value) ? Math.max(0, value) : 0; +} + +function getWorksheet () { + return tableau.extensions.worksheetContent + ? tableau.extensions.worksheetContent.worksheet + : tableau.extensions.dashboardContent.dashboard.worksheets[0]; +} diff --git a/Samples/Viz/Sankey/sankey.trex b/Samples/Viz/Sankey/sankey.trex new file mode 100644 index 00000000..afbce1f3 --- /dev/null +++ b/Samples/Viz/Sankey/sankey.trex @@ -0,0 +1,35 @@ + + + + en_US + + Sankey + + 1.11 + + http://localhost:8765/Samples/Viz/Sankey/sankey.html + + + + Level + + discrete-dimension + discrete-measure + + + + + Edge + + continuous-measure + continuous-dimension + + + + + + + Sankey + + + diff --git a/Samples/Viz/Settings/Settings.trex b/Samples/Viz/Settings/Settings.trex new file mode 100644 index 00000000..12838d71 --- /dev/null +++ b/Samples/Viz/Settings/Settings.trex @@ -0,0 +1,19 @@ + + + + en_US + + Settings Sample + + 1.11 + + http://localhost:8765/Samples/Viz/Settings/settings.html + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAlhJREFUOI2Nkt9vy1EYh5/3bbsvRSySCZbIxI+ZCKsN2TKtSFyIrV2WuRCJuBiJWxfuxCVXbvwFgiEtposgLFJElnbU1SxIZIIRJDKTrdu+53Uhra4mce7Oe57Pcz7JOULFisViwZ+29LAzOSjQYDgz1ZcCvWuXV11MJpN+OS/lm6179teqH0yDqxPTCyKSA8DcDsyOmOprnCaeP7459pdgy969i0LTC3IO/RQMyoHcQN+3cnljW3dNIFC47qDaK3g7BwdTkwBaBELT4ZPOUVWgKl4ZBnjxJPUlMDnTDrp0pmr6RHFeEjjcUUXPDGeSEwDN0Xg8sivxMhJNjGzbHd8PkM3eHRfkrBM5NkcQaY2vUnTlrDIA0NoaX+KLXFFlowr14tvVpqb2MICzmQcKqxvbumv+NAhZGCCIPwEw6QWXKYRL/VUXO0+rAUJiPwAk5MIlgVfwPjjHLCL1APmHN94ZdqeYN+NW/mn6I4BvwQYchcLnwFhJMDiYmlRxAzjpKWZkYkUCcZ2I61wi37tLbYyjiN0fHk5Oz3nGSLSzBbNHCF35R7f6K1/hN9PRhek11FrymfQQQKB4+Gl05P2qNRtmETlXW7e+b2z01dfycGNbfFMAbqNyKp9Jp4rzOT8RYFs0njJkc2iqsCObvTsOsDWWqA5C1uFy+Uz/oXJeKwVT4h0RmPUXhi79vuC0Ku6yOffTK3g9lfxfDQAisY516sg5kfOCiJk7HoLt2cf9b/9LANAc7dznm98PagG1fUOZ9IP5uMB8Q4CPoyNvausapkTt3rNMuvdf3C/o6+czhtdwmwAAAABJRU5ErkJggg== + + + + Settings Sample + + + diff --git a/Samples/Viz/Settings/settings.html b/Samples/Viz/Settings/settings.html new file mode 100644 index 00000000..20ea384b --- /dev/null +++ b/Samples/Viz/Settings/settings.html @@ -0,0 +1,61 @@ + + + + + + + Settings Sample + + + + + + + + + + + + + + + +
+ +
+

Current Settings

+
+ + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ +
+
+ +
+ +
+
+ +
+ + diff --git a/Samples/Viz/Settings/settings.js b/Samples/Viz/Settings/settings.js new file mode 100644 index 00000000..2c68f093 --- /dev/null +++ b/Samples/Viz/Settings/settings.js @@ -0,0 +1,82 @@ +'use strict'; + +// Wrap everything in an anonymous function to avoid polluting the global namespace +(function () { + $(document).ready(function () { + tableau.extensions.initializeAsync().then(function () { + // First, check for any saved settings and populate our UI based on them. + buildSettingsTable(tableau.extensions.settings.getAll()); + }, function (err) { + // Something went wrong in initialization + console.log('Error while Initializing: ' + err.toString()); + }); + + $('#save').click(saveSetting); + }); + + function eraseSetting (key, row) { + // This change won't be persisted until settings.saveAsync has been called. + tableau.extensions.settings.erase(key); + + // Remove the setting from the UI immediately. + row.remove(); + + // Save in the background, saveAsync results don't need to be handled immediately. + tableau.extensions.settings.saveAsync(); + + updateUIState(Object.keys(tableau.extensions.settings.getAll()).length > 0); + } + + function buildSettingsTable (settings) { + // Clear the table first. + $('#settingsTable > tbody tr').remove(); + const settingsTable = $('#settingsTable > tbody')[0]; + + // Add an entry to the settings table for each setting. + for (const settingKey in settings) { + const newRow = settingsTable.insertRow(settingsTable.rows.length); + const keyCell = newRow.insertCell(0); + const valueCell = newRow.insertCell(1); + const eraseCell = newRow.insertCell(2); + + const eraseSpan = document.createElement('span'); + eraseSpan.className = 'glyphicon glyphicon-trash'; + eraseSpan.addEventListener('click', function () { eraseSetting(settingKey, newRow); }); + + keyCell.innerHTML = settingKey; + valueCell.innerHTML = settings[settingKey]; + eraseCell.appendChild(eraseSpan); + } + + updateUIState(Object.keys(settings).length > 0); + } + + function saveSetting () { + const settingKey = $('#keyInput').val(); + const settingValue = $('#valueInput').val(); + + tableau.extensions.settings.set(settingKey, settingValue); + + // Save the newest settings via the settings API. + tableau.extensions.settings.saveAsync().then((currentSettings) => { + // This promise resolves to a list of the current settings. + // Rebuild the UI with that new list of settings. + buildSettingsTable(currentSettings); + + // Clears the settings of content. + $('#settingForm').get(0).reset(); + }); + } + + // This helper updates the UI depending on whether or not there are settings + // that exist in the worksheet. Accepts a boolean. + function updateUIState (settingsExist) { + if (settingsExist) { + $('#settingsTable').removeClass('hidden').addClass('show'); + $('#noSettingsWarning').removeClass('show').addClass('hidden'); + } else { + $('#noSettingsWarning').removeClass('hidden').addClass('show'); + $('#settingsTable').removeClass('show').addClass('hidden'); + } + } +})(); diff --git a/Samples/Viz/UINamespace/uiNamespace.html b/Samples/Viz/UINamespace/uiNamespace.html new file mode 100644 index 00000000..da1f9a05 --- /dev/null +++ b/Samples/Viz/UINamespace/uiNamespace.html @@ -0,0 +1,56 @@ + + + + + + + Settings Sample + + + + + + + + + + + + + + + +
+

+

+
+ Configure extension to proceed. +
+

+
+
Dialog Style
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + diff --git a/Samples/Viz/UINamespace/uiNamespace.js b/Samples/Viz/UINamespace/uiNamespace.js new file mode 100644 index 00000000..709fdc3a --- /dev/null +++ b/Samples/Viz/UINamespace/uiNamespace.js @@ -0,0 +1,111 @@ +'use strict'; + +/** + * UINamespace Sample Extension + * + * This sample extension demonstrates how to use the UI namespace + * to create a popup dialog with additional UI that the user can interact with. + * The content in this dialog is actually an extension as well (see the + * uiNamespaceDialog.js for details). + * + * This sample is an extension that auto refreshes datasources in a worksheet + */ + +// Wrap everything in an anonymous function to avoid polluting the global namespace +(function () { + const defaultIntervalInMin = '5'; + let activeDatasourceIdList = []; + + $(document).ready(function () { + tableau.extensions.initializeAsync().then(function () { + // This event allows for the parent extension and popup extension to keep their + // settings in sync. This event will be triggered any time a setting is + // changed for this extension, in the parent or popup (i.e. when settings.saveAsync is called). + tableau.extensions.settings.addEventListener(tableau.TableauEventType.SettingsChanged, (settingsEvent) => { + updateExtensionBasedOnSettings(settingsEvent.newSettings); + }); + + document.getElementById('configure').onclick = configure; + }); + }); + + function configure () { + // This uses the window.location.origin property to retrieve the scheme, hostname, and + // port where the parent extension is currently running, so this string doesn't have + // to be updated if the extension is deployed to a new location. + const popupUrl = `${window.location.origin}/Samples/Viz/UINamespace/uiNamespaceDialog.html`; + + // This checks for the selected dialog style in the radio form. + let dialogStyle; + const dialogStyleOptions = document.getElementsByName('dialogStyleRadio'); + if (dialogStyleOptions[0].checked) { + dialogStyle = tableau.DialogStyle.Modal; + } else if (dialogStyleOptions[1].checked) { + dialogStyle = tableau.DialogStyle.Modeless; + } else { + dialogStyle = tableau.DialogStyle.Window; + } + + /** + * This is the API call that actually displays the popup extension to the user. The + * popup is always a modal dialog. The only required parameter is the URL of the popup, + * which must be the same domain, port, and scheme as the parent extension. + * + * The developer can optionally control the initial size of the extension by passing in + * an object with height and width properties. The developer can also pass a string as the + * 'initial' payload to the popup extension. This payload is made available immediately to + * the popup extension. In this example, the value '5' is passed, which will serve as the + * default interval of refresh. + */ + tableau.extensions.ui + .displayDialogAsync(popupUrl, defaultIntervalInMin, { height: 500, width: 500, dialogStyle }) + .then((closePayload) => { + // The promise is resolved when the dialog has been expectedly closed, meaning that + // the popup extension has called tableau.extensions.ui.closeDialog. + $('#inactive').hide(); + $('#active').show(); + + // The close payload is returned from the popup extension via the closeDialog method. + $('#interval').text(closePayload); + setupRefreshInterval(closePayload); + }) + .catch((error) => { + // One expected error condition is when the popup is closed by the user (meaning the user + // clicks the 'X' in the top right of the dialog). This can be checked for like so: + switch (error.errorCode) { + case tableau.ErrorCodes.DialogClosedByUser: + console.log('Dialog was closed by user'); + break; + default: + console.error(error.message); + } + }); + } + + /** + * This function sets up a JavaScript interval based on the time interval selected + * by the user. This interval will refresh all selected datasources. + */ + function setupRefreshInterval (interval) { + setInterval(function () { + const worksheet = tableau.extensions.worksheetContent.worksheet; + worksheet.getDataSourcesAsync().then(function (datasources) { + datasources.forEach(function (datasource) { + if (activeDatasourceIdList.indexOf(datasource.id) >= 0) { + datasource.refreshAsync(); + } + }); + }); + }, interval * 60 * 1000); + } + + /** + * Helper that is called to set state anytime the settings are changed. + */ + function updateExtensionBasedOnSettings (settings) { + if (settings.selectedDatasources) { + activeDatasourceIdList = JSON.parse(settings.selectedDatasources); + $('#datasourceCount').text(activeDatasourceIdList.length); + } + } +})(); diff --git a/Samples/Viz/UINamespace/uiNamespace.trex b/Samples/Viz/UINamespace/uiNamespace.trex new file mode 100644 index 00000000..c5f0c059 --- /dev/null +++ b/Samples/Viz/UINamespace/uiNamespace.trex @@ -0,0 +1,19 @@ + + + + en_US + + UI Namespace Sample + + 1.11 + + http://localhost:8765/Samples/Viz/UINamespace/uinamespace.html + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAlhJREFUOI2Nkt9vy1EYh5/3bbsvRSySCZbIxI+ZCKsN2TKtSFyIrV2WuRCJuBiJWxfuxCVXbvwFgiEtposgLFJElnbU1SxIZIIRJDKTrdu+53Uhra4mce7Oe57Pcz7JOULFisViwZ+29LAzOSjQYDgz1ZcCvWuXV11MJpN+OS/lm6179teqH0yDqxPTCyKSA8DcDsyOmOprnCaeP7459pdgy969i0LTC3IO/RQMyoHcQN+3cnljW3dNIFC47qDaK3g7BwdTkwBaBELT4ZPOUVWgKl4ZBnjxJPUlMDnTDrp0pmr6RHFeEjjcUUXPDGeSEwDN0Xg8sivxMhJNjGzbHd8PkM3eHRfkrBM5NkcQaY2vUnTlrDIA0NoaX+KLXFFlowr14tvVpqb2MICzmQcKqxvbumv+NAhZGCCIPwEw6QWXKYRL/VUXO0+rAUJiPwAk5MIlgVfwPjjHLCL1APmHN94ZdqeYN+NW/mn6I4BvwQYchcLnwFhJMDiYmlRxAzjpKWZkYkUCcZ2I61wi37tLbYyjiN0fHk5Oz3nGSLSzBbNHCF35R7f6K1/hN9PRhek11FrymfQQQKB4+Gl05P2qNRtmETlXW7e+b2z01dfycGNbfFMAbqNyKp9Jp4rzOT8RYFs0njJkc2iqsCObvTsOsDWWqA5C1uFy+Uz/oXJeKwVT4h0RmPUXhi79vuC0Ku6yOffTK3g9lfxfDQAisY516sg5kfOCiJk7HoLt2cf9b/9LANAc7dznm98PagG1fUOZ9IP5uMB8Q4CPoyNvausapkTt3rNMuvdf3C/o6+czhtdwmwAAAABJRU5ErkJggg== + + + + UI Namespace Sample + + + diff --git a/Samples/Viz/UINamespace/uiNamespaceDialog.html b/Samples/Viz/UINamespace/uiNamespaceDialog.html new file mode 100644 index 00000000..2b53b2ef --- /dev/null +++ b/Samples/Viz/UINamespace/uiNamespaceDialog.html @@ -0,0 +1,40 @@ + + + + + + + Settings Sample + + + + + + + + + + + + + + + +
+

Auto Data Source Refresh Extension

+

+ This Extension refreshes the selected datasources at the selected interval. +

+
+ +
Apply to these datasources:
+
+
+
Refresh interval in minutes:
+ + +
+ +
+ + diff --git a/Samples/Viz/UINamespace/uiNamespaceDialog.js b/Samples/Viz/UINamespace/uiNamespaceDialog.js new file mode 100644 index 00000000..c85635b1 --- /dev/null +++ b/Samples/Viz/UINamespace/uiNamespaceDialog.js @@ -0,0 +1,124 @@ +'use strict'; + +/** + * UINamespace Sample Extension + * + * This is the popup extension portion of the UINamespace sample, please see + * uiNamespace.js in addition to this for context. This extension is + * responsible for collecting configuration settings from the user and communicating + * that info back to the parent extension. + * + * This sample demonstrates two ways to do that: + * 1) The suggested and most common method is to store the information + * via the settings namespace. The parent can subscribe to notifications when + * the settings are updated, and collect the new info accordingly. + * 2) The popup extension can receive and send a string payload via the open + * and close payloads of initializeDialogAsync and closeDialog methods. This is useful + * for information that does not need to be persisted into settings. + */ + +// Wrap everything in an anonymous function to avoid polluting the global namespace +(function () { + /** + * This extension collects the IDs of each datasource the user is interested in + * and stores this information in settings when the popup is closed. + */ + const datasourcesSettingsKey = 'selectedDatasources'; + let selectedDatasources = []; + + $(document).ready(function () { + // The only difference between an extension in a worksheet and an extension + // running in a popup is that the popup extension must use the method + // initializeDialogAsync instead of initializeAsync for initialization. + // This has no affect on the development of the extension but is used internally. + tableau.extensions.initializeDialogAsync().then(function (openPayload) { + // The openPayload sent from the parent extension in this sample is the + // default time interval for the refreshes. This could alternatively be stored + // in settings, but is used in this sample to demonstrate open and close payloads. + $('#interval').val(openPayload); + $('#closeButton').click(closeDialog); + + const worksheet = tableau.extensions.worksheetContent.worksheet; + const visibleDatasources = []; + selectedDatasources = parseSettingsForActiveDataSources(); + + // Loop through datasources in this sheet and create a checkbox UI + // element for each one. The existing settings are used to + // determine whether a datasource is checked by default or not. + worksheet.getDataSourcesAsync().then(function (datasources) { + datasources.forEach(function (datasource) { + const isActive = selectedDatasources.indexOf(datasource.id) >= 0; + + if (visibleDatasources.indexOf(datasource.id) < 0) { + addDataSourceItemToUI(datasource, isActive); + visibleDatasources.push(datasource.id); + } + }); + }); + }); + }); + + /** + * Helper that parses the settings from the settings namesapce and + * returns a list of IDs of the datasources that were previously + * selected by the user. + */ + function parseSettingsForActiveDataSources () { + let activeDatasourceIdList = []; + const settings = tableau.extensions.settings.getAll(); + if (settings.selectedDatasources) { + activeDatasourceIdList = JSON.parse(settings.selectedDatasources); + } + + return activeDatasourceIdList; + } + + /** + * Helper that updates the internal storage of datasource IDs + * any time a datasource checkbox item is toggled. + */ + function updateDatasourceList (id) { + const idIndex = selectedDatasources.indexOf(id); + if (idIndex < 0) { + selectedDatasources.push(id); + } else { + selectedDatasources.splice(idIndex, 1); + } + } + + /** + * UI helper that adds a checkbox item to the UI for a datasource. + */ + function addDataSourceItemToUI (datasource, isActive) { + const containerDiv = $('
'); + + $('', { + type: 'checkbox', + id: datasource.id, + value: datasource.name, + checked: isActive, + click: function () { + updateDatasourceList(datasource.id); + } + }).appendTo(containerDiv); + + $('