├── .gitignore ├── README.md ├── dist ├── index.html └── vizframe.html ├── package-lock.json ├── package.json ├── scripts └── build.js ├── src ├── components │ ├── ErrorMessage.js │ ├── MainComponent.js │ └── MyTable.js ├── index.css ├── index.js ├── index.json ├── localMessage.js ├── manifest.json ├── react-starter-icon.png └── utils │ └── DataContext.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Starter Data Studio Community Visualization 2 | 3 | This component is designed to be used as a starting point for building Data Studio visualizations using the React framework. The included React component, ``, dynamically renders headers and rows from Data Studio column names and values. 4 | 5 | ## Notes 6 | 7 | ### Context API 8 | 9 | This example uses the [React Context API](https://reactjs.org/docs/context.html) to make data from Data Studio (field names, values, styling, theme) easily accessible to nested components without prop drilling. While not required, it can add efficiency to a larger project with multiple components requiring access to theme/styling values, etc. 10 | 11 | ### Styling 12 | 13 | CSS is built using the [Emotion](https://emotion.sh/docs/introduction) library, which is compatible with multiple formats (template strings, objects, etc.), including styled components. 14 | 15 | ## Deployed version 16 | 17 | Manifest path of the deployed version of this visualization: 18 | 19 | ``` 20 | gs://anvil-data-studio-react-starter 21 | ``` 22 | 23 | Component ID: 24 | 25 | ``` 26 | react 27 | ``` 28 | 29 | ## Authors 30 | 31 | This component was built by [Anvil Analytics + Insights](https://anvilinsights.com) 32 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Community Viz 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /dist/vizframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dsccViz": { 3 | "gcsDevBucket": "gs://anvil-data-studio-react-starter-dev", 4 | "gcsProdBucket": "gs://anvil-data-studio-react-starter", 5 | "jsFile": "index.js", 6 | "jsonFile": "index.json", 7 | "cssFile": "index.css", 8 | "print": "printMessage.js" 9 | }, 10 | "scripts": { 11 | "build:dev": "NODE_ENV=production node scripts/build.js dev", 12 | "build:prod": "NODE_ENV=production node scripts/build.js prod", 13 | "push:dev": "dscc-scripts viz push -d dev", 14 | "push:prod": "dscc-scripts viz push -d prod", 15 | "update_message": "dscc-scripts viz update_message -f object", 16 | "start": "dscc-scripts viz start", 17 | "deploy:dev": "yarn build:dev && yarn push:dev", 18 | "deploy:prod": "yarn build:prod && yarn push:prod" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.5.0", 22 | "@babel/preset-env": "^7.5.0", 23 | "@babel/preset-react": "^7.0.0", 24 | "@google/dscc": "^0.3.8", 25 | "@google/dscc-scripts": "^1.0.7", 26 | "babel-loader": "^8.0.6" 27 | }, 28 | "dependencies": { 29 | "@emotion/core": "^10.0.14", 30 | "prop-types": "^15.7.2", 31 | "react": "^16.8.6", 32 | "react-dom": "^16.8.6" 33 | }, 34 | "babel": { 35 | "presets": [ 36 | "@babel/preset-env", 37 | "@babel/preset-react" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const bluebird = require('bluebird'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const fs = require('mz/fs'); 4 | const path = require('path'); 5 | const webpack = require('webpack'); 6 | const webpackOptions = require('../webpack.config'); 7 | 8 | const buildOptions = buildValues => { 9 | if (buildValues.devMode) { 10 | const devOptions = { 11 | mode: 'development', 12 | }; 13 | Object.assign(webpackOptions, devOptions); 14 | } else { 15 | const prodOptions = { 16 | mode: 'production', 17 | }; 18 | Object.assign(webpackOptions, prodOptions); 19 | } 20 | 21 | return webpackOptions; 22 | }; 23 | 24 | const build = async devMode => { 25 | const devBucket = process.env.npm_package_dsccViz_gcsDevBucket; 26 | const prodBucket = process.env.npm_package_dsccViz_gcsProdBucket; 27 | const deployBucket = devMode ? devBucket : prodBucket; 28 | 29 | const encoding = 'utf-8'; 30 | const webpackOptions = buildOptions(devMode); 31 | const compiler = webpack(webpackOptions); 32 | 33 | const compilerRun = bluebird.promisify(compiler.run, {context: compiler}); 34 | 35 | await compilerRun(); 36 | 37 | const manifestSrc = path.resolve(process.env.PWD, 'src', 'manifest.json'); 38 | const manifestDest = path.resolve(process.env.PWD, 'build', 'manifest.json'); 39 | const manifestContents = await fs.readFile(manifestSrc, encoding); 40 | const newManifest = manifestContents 41 | .replace(/YOUR_GCS_BUCKET/g, deployBucket) 42 | .replace(/"DEVMODE_BOOL"/, `${devMode}`); 43 | 44 | return fs.writeFile(manifestDest, newManifest); 45 | }; 46 | 47 | const main = () => { 48 | const devArg = process.argv[2]; 49 | const devMode = devArg === 'dev' ? true : false; 50 | build(devMode); 51 | }; 52 | 53 | main(); 54 | -------------------------------------------------------------------------------- /src/components/ErrorMessage.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from '@emotion/core' 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | const wrapperStyle = css` 7 | flex-direction: column; 8 | display: flex; 9 | justify-content: center; 10 | align-items: stretch; 11 | background: rgba(40, 40, 40, 0.8); 12 | height: 100vh; 13 | width: 100vw; 14 | position: absolute; 15 | z-index: 1000000; 16 | top: 0; 17 | ` 18 | 19 | const pStyle = css` 20 | text-align: center; 21 | color: #fff; 22 | align-self: center; 23 | font-size: 14px; 24 | text-transform: uppercase; 25 | padding: 5px; 26 | ` 27 | 28 | const ErrorMessage = ({ message }) => { 29 | return ( 30 |
31 |

{message}

32 |
33 | ) 34 | } 35 | 36 | ErrorMessage.propTypes = { 37 | message: PropTypes.string, 38 | } 39 | 40 | export default ErrorMessage 41 | -------------------------------------------------------------------------------- /src/components/MainComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MyTable from './MyTable'; 3 | 4 | const MainComponent = props => { 5 | if (!props.fields || !props.tables || !props.tables.DEFAULT) { 6 | return
Loading...
; 7 | } 8 | 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default MainComponent; 17 | -------------------------------------------------------------------------------- /src/components/MyTable.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import {css, jsx} from '@emotion/core'; 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import {DataContext} from '../utils/DataContext'; 7 | 8 | const MyTable = props => { 9 | // In this case, data could've been passed via props from the , 10 | // but in a larger example, it can be helpful to use the Context API to load 11 | // data in multiple components without prop drilling. 12 | 13 | // DataContext was populated by the in index.js 14 | const {value: data} = React.useContext(DataContext); 15 | 16 | const {fields, tables, style} = data; 17 | const allFields = fields.dimID.concat(fields.metricID); 18 | 19 | // Use default value as an initial backup 20 | const cellBackgroundColor = 21 | style.cellBackgroundColor.value || style.cellBackgroundColor.defaultValue; 22 | 23 | const tableStyle = css` 24 | padding: 10px; 25 | background: ${cellBackgroundColor.color}; 26 | `; 27 | 28 | const getRow = tableRow => { 29 | const allColumns = tableRow.dimID.concat(tableRow.metricID); 30 | return allColumns.map((x, i) => ( 31 | 32 | {x} 33 | 34 | )); 35 | }; 36 | 37 | return ( 38 | 39 | 40 | 41 | {allFields.map(field => ( 42 | 43 | ))} 44 | 45 | 46 | 47 | {tables.DEFAULT.map((row, i) => ( 48 | {getRow(row)} 49 | ))} 50 | 51 |
{field.name}
52 | ); 53 | }; 54 | 55 | MyTable.propTypes = {}; 56 | 57 | export default MyTable; 58 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvilinsights/data-studio-react-starter/218b2efaebb438b5d58b666b51c5cc38070e9a42/src/index.css -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, Global, jsx } from '@emotion/core' 3 | import * as dscc from '@google/dscc' 4 | import * as React from 'react' 5 | import * as ReactDOM from 'react-dom' 6 | 7 | import MainComponent from './components/MainComponent' 8 | import { DataProvider } from './utils/DataContext' 9 | import ErrorMessage from './components/ErrorMessage' 10 | 11 | const LOCAL = process.env.NODE_ENV !== 'production' 12 | 13 | const setup = () => { 14 | const mainDiv = document.createElement('div') 15 | mainDiv.id = 'app' 16 | document.body.appendChild(mainDiv) 17 | 18 | ReactDOM.render(, document.getElementById('app')) 19 | } 20 | 21 | class AppComponent extends React.Component { 22 | constructor(props) { 23 | super(props) 24 | this.handleDataUpdate.bind(this) 25 | 26 | this.state = { 27 | body: '', 28 | } 29 | } 30 | 31 | componentDidMount() { 32 | if (LOCAL) { 33 | const local = require('./localMessage.js') 34 | this.handleDataUpdate(local.message) 35 | } else { 36 | dscc.subscribeToData(data => this.handleDataUpdate(data), { 37 | transform: dscc.objectTransform, 38 | }) 39 | } 40 | } 41 | 42 | static getDerivedStateFromError(error) { 43 | return { hasError: !!error } 44 | } 45 | 46 | handleDataUpdate(data) { 47 | // Called each time a new dataset is passed in from 48 | // Data Studio, including config (style) changes. 49 | this.setState({ ...data }) 50 | } 51 | 52 | render() { 53 | const { hasError } = this.state 54 | 55 | const styles = css` 56 | /* Global styles here */ 57 | ` 58 | return ( 59 | 60 | 61 | 62 | {hasError ? ( 63 | 64 | ) : ( 65 | 66 | )} 67 | 68 | 69 | ) 70 | } 71 | } 72 | 73 | setup() 74 | -------------------------------------------------------------------------------- /src/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "id": "reactStarter", 5 | "label": "React Starter", 6 | "elements": [ 7 | { 8 | "id": "dimID", 9 | "label": "Dimensions", 10 | "type": "DIMENSION", 11 | "options": { 12 | "min": 1, 13 | "max": 10 14 | } 15 | }, 16 | { 17 | "id": "metricID", 18 | "label": "Metrics", 19 | "type": "METRIC", 20 | "options": { 21 | "min": 1, 22 | "max": 10 23 | } 24 | } 25 | ] 26 | } 27 | ], 28 | "style": [ 29 | { 30 | "id": "config", 31 | "label": "Config", 32 | "elements": [ 33 | { 34 | "id": "cellBackgroundColor", 35 | "label": "Cell Background Color", 36 | "type": "FILL_COLOR", 37 | "defaultValue": "#D9D9D9" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/localMessage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provides the mock "data" received 3 | * by your visualization code when you develop 4 | * locally. 5 | * 6 | */ 7 | export const message = { 8 | tables: { 9 | DEFAULT: [ 10 | { 11 | dimID: ['Campaign 1'], 12 | metricID: [16, 329, 1], 13 | }, 14 | { 15 | dimID: ['Campaign 2'], 16 | metricID: [31, 1002, 4], 17 | }, 18 | { 19 | dimID: ['Campaign 3'], 20 | metricID: [51, 1231, 11], 21 | }, 22 | { 23 | dimID: ['Campaign 4'], 24 | metricID: [41, 1522, 16], 25 | }, 26 | ], 27 | }, 28 | fields: { 29 | dimID: [ 30 | { 31 | id: 'qt_nzqx6a0xvb', 32 | name: 'Campaign', 33 | type: 'TEXT', 34 | concept: 'DIMENSION', 35 | }, 36 | ], 37 | metricID: [ 38 | { 39 | id: 'qt_8isx6a0xvb', 40 | name: 'Clicks', 41 | type: 'NUMBER', 42 | concept: 'METRIC', 43 | }, 44 | { 45 | id: 'qt_8isx6a0xvc', 46 | name: 'Impressions', 47 | type: 'NUMBER', 48 | concept: 'METRIC', 49 | }, 50 | { 51 | id: 'qt_8isx6asdf0xvc', 52 | name: 'Conversions', 53 | type: 'NUMBER', 54 | concept: 'METRIC', 55 | }, 56 | ], 57 | }, 58 | style: { 59 | cellBackgroundColor: { 60 | value: { 61 | color: '#d1d1d1', 62 | opacity: 1, 63 | }, 64 | defaultValue: { 65 | color: '#d9d9d9', 66 | }, 67 | }, 68 | }, 69 | } 70 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Starter", 3 | "organization": "Anvil Analytics + Insights", 4 | "description": "A starter component built with React", 5 | "logoUrl": "https://raw.githubusercontent.com/googledatastudio/experimental-visualizations/master/viz/react-starter/src/react-starter-icon.png", 6 | "organizationUrl": "https://anvilinsights.com", 7 | "supportUrl": "https://github.com/anvilinsights/data-studio-react-starter/issues", 8 | "packageUrl": "https://github.com/anvilinsights/data-studio-react-starter/tree/master/", 9 | "devMode": "DEVMODE_BOOL", 10 | "components": [ 11 | { 12 | "id": "react", 13 | "name": "React Starter", 14 | "description": "A starter component built with React", 15 | "iconUrl": "https://raw.githubusercontent.com/anvilinsights/data-studio-react-starter/master/src/react-starter-icon.png", 16 | "resource": { 17 | "js": "YOUR_GCS_BUCKET/index.js", 18 | "config": "YOUR_GCS_BUCKET/index.json", 19 | "css": "YOUR_GCS_BUCKET/index.css" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/react-starter-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvilinsights/data-studio-react-starter/218b2efaebb438b5d58b666b51c5cc38070e9a42/src/react-starter-icon.png -------------------------------------------------------------------------------- /src/utils/DataContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const DataContext = React.createContext({}) 4 | 5 | export function DataProvider(props) { 6 | return ( 7 | {props.children} 8 | ) 9 | } 10 | 11 | export const DataConsumer = DataContext.Consumer 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const CopyWebpackPlugin = require('copy-webpack-plugin') 3 | 4 | const CSS_FILE = process.env.npm_package_dsccViz_cssFile 5 | const MANIFEST = 'manifest.json' 6 | const INDEX_JSON = 'index.json' 7 | 8 | const IS_DEV = process.env.NODE_ENV !== 'production' 9 | 10 | module.exports = [ 11 | { 12 | mode: IS_DEV ? 'development' : 'production', 13 | entry: './src/index.js', 14 | devServer: { 15 | contentBase: './dist', 16 | }, 17 | output: { 18 | filename: 'index.js', 19 | path: path.resolve(__dirname, 'build'), 20 | }, 21 | plugins: [ 22 | new CopyWebpackPlugin([ 23 | { from: path.join('src', CSS_FILE), to: '.' }, 24 | { from: path.join('src', MANIFEST), to: '.' }, 25 | { from: path.join('src', INDEX_JSON), to: '.' }, 26 | ]), 27 | ], 28 | // Loaders configuration -> ADDED IN THIS STEP 29 | // We are telling webpack to use "babel-loader" for .js and .jsx files 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.js$/, 34 | exclude: /node_modules/, 35 | use: ['babel-loader'], 36 | }, 37 | ], 38 | }, 39 | resolve: { 40 | extensions: ['.js', '.json'], 41 | }, 42 | }, 43 | ] 44 | --------------------------------------------------------------------------------