├── .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 |
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 | {field.name}
43 | ))}
44 |
45 |
46 |
47 | {tables.DEFAULT.map((row, i) => (
48 | {getRow(row)}
49 | ))}
50 |
51 |
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 |
--------------------------------------------------------------------------------