Skip to content

Commit

Permalink
Add a new visualization type: Vega
Browse files Browse the repository at this point in the history
Vega and Vega-Lite allow users to specify a JSON configuration
to declaratively define a complex interactive visualization.

It could thoeretically become the foundation of many visualizations
we already have.

This commit adds integration with Vega and Vega-Lite, with a lot
of code borrowed from the official Vega Editor[1].

[1] https://vega.github.io/editor/
  • Loading branch information
ktmud committed Aug 14, 2020
1 parent 2e47f42 commit 65191ff
Show file tree
Hide file tree
Showing 45 changed files with 2,136 additions and 81 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ npm-debug.log

client/cypress/screenshots
client/cypress/videos

client/app/assets/less/**/*.css
client/app/visualizations/vega/vega.css
1 change: 1 addition & 0 deletions client/app/components/dashboards/ExpandedWidgetDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function ExpandedWidgetDialog({ dialog, widget }) {
footer={<Button onClick={dialog.dismiss}>Close</Button>}>
<VisualizationRenderer
visualization={widget.visualization}
query={widget.getQuery()}
queryResult={widget.getQueryResult()}
context="widget"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ function visualizationWidgetMenuOptions({ widget, canEditDashboard, onParameters
const widgetQueryResult = widget.getQueryResult();
const isQueryResultEmpty = !widgetQueryResult || !widgetQueryResult.isEmpty || widgetQueryResult.isEmpty();

const downloadLink = fileType => widgetQueryResult.getLink(widget.getQuery().id, fileType);
const downloadName = fileType => widgetQueryResult.getName(widget.getQuery().name, fileType);
const query = widget.getQuery();
const downloadLink = fileType => query.getDataUrl(fileType);
const downloadName = fileType => widgetQueryResult.getName(query.name, fileType);
return compact([
<Menu.Item key="download_csv" disabled={isQueryResultEmpty}>
{!isQueryResultEmpty ? (
Expand Down Expand Up @@ -256,6 +257,7 @@ class VisualizationWidget extends React.Component {
return (
<div className="body-row-auto scrollbox">
<VisualizationRenderer
query={widget.getQuery()}
visualization={widget.visualization}
queryResult={widgetQueryResult}
filters={filters}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ function EditVisualizationDialog({ dialog, visualization, query, queryResult })
<Editor
type={type}
data={data}
query={query}
options={options}
visualizationName={name}
onOptionsChange={onOptionsChanged}
Expand All @@ -214,9 +215,11 @@ function EditVisualizationDialog({ dialog, visualization, query, queryResult })
<Filters filters={filters} onChange={setFilters} />
<div className="scrollbox" data-test="VisualizationPreview">
<Renderer
context="editor"
type={type}
data={filteredData}
options={options}
data={filteredData}
query={query}
visualizationName={name}
onOptionsChange={onOptionsChanged}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export default function VisualizationRenderer(props) {
type={visualization.type}
options={options}
data={filteredData}
query={props.query}
context={props.context}
visualizationName={visualization.name}
addonBefore={showFilters && <Filters filters={filters} onChange={setFilters} />}
/>
Expand All @@ -74,6 +76,7 @@ export default function VisualizationRenderer(props) {
VisualizationRenderer.propTypes = {
visualization: VisualizationType.isRequired,
queryResult: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
query: PropTypes.object,
filters: FiltersType,
showFilters: PropTypes.bool,
context: PropTypes.oneOf(["query", "widget"]).isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function wrapComponentWithSettings(WrappedComponent) {
"tableCellMaxJSONSize",
"allowCustomJSVisualizations",
"hidePlotlyModeBar",
"basePath",
]),
});

Expand Down
1 change: 1 addition & 0 deletions client/app/pages/queries/QuerySource.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ function QuerySource(props) {
<QueryVisualizationTabs
queryResult={queryResult}
visualizations={query.visualizations}
query={query}
showNewVisualizationButton={queryFlags.canEdit && queryResultData.status === ExecutionStatus.DONE}
canDeleteVisualizations={queryFlags.canEdit}
selectedTab={selectedVisualization}
Expand Down
1 change: 1 addition & 0 deletions client/app/pages/queries/QueryView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ function QueryView(props) {
{loadedInitialResults && (
<QueryVisualizationTabs
queryResult={queryResult}
query={query}
visualizations={query.visualizations}
showNewVisualizationButton={queryFlags.canEdit && queryResultData.status === ExecutionStatus.DONE}
canDeleteVisualizations={queryFlags.canEdit}
Expand Down
7 changes: 6 additions & 1 deletion client/app/pages/queries/VisualizationEmbed.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,12 @@ export default function VisualizationEmbed({ queryId, visualizationId, apiKey, o
)}
{error && <div className="alert alert-danger" data-test="ErrorMessage">{`Error: ${error}`}</div>}
{!error && queryResults && (
<VisualizationRenderer visualization={visualization} queryResult={queryResults} context="widget" />
<VisualizationRenderer
visualization={visualization}
queryResult={queryResults}
context="widget"
query={query}
/>
)}
{!queryResults && refreshStartedAt && (
<div className="d-flex justify-content-center">
Expand Down
10 changes: 9 additions & 1 deletion client/app/pages/queries/components/QueryVisualizationTabs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export default function QueryVisualizationTabs({
onAddVisualization,
onDeleteVisualization,
refreshButton,
query,
...props
}) {
const visualizations = useMemo(
Expand Down Expand Up @@ -143,7 +144,12 @@ export default function QueryVisualizationTabs({
/>
}>
{queryResult ? (
<VisualizationRenderer visualization={visualization} queryResult={queryResult} context="query" />
<VisualizationRenderer
query={query}
visualization={visualization}
queryResult={queryResult}
context="query"
/>
) : (
<EmptyState
title="Query Has no Result"
Expand All @@ -159,6 +165,7 @@ export default function QueryVisualizationTabs({

QueryVisualizationTabs.propTypes = {
queryResult: PropTypes.object, // eslint-disable-line react/forbid-prop-types
query: PropTypes.object,
visualizations: PropTypes.arrayOf(PropTypes.object),
selectedTab: PropTypes.number,
showNewVisualizationButton: PropTypes.bool,
Expand All @@ -171,6 +178,7 @@ QueryVisualizationTabs.propTypes = {

QueryVisualizationTabs.defaultProps = {
queryResult: null,
query: null,
visualizations: [],
selectedTab: null,
showNewVisualizationButton: false,
Expand Down
8 changes: 0 additions & 8 deletions client/app/services/query-result.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,14 +423,6 @@ class QueryResult {
});
}

getLink(queryId, fileType, apiKey) {
let link = `api/queries/${queryId}/results/${this.getId()}.${fileType}`;
if (apiKey) {
link = `${link}?api_key=${apiKey}`;
}
return link;
}

getName(queryName, fileType) {
return `${queryName.replace(/ /g, "_") + moment(this.getUpdatedAt()).format("_YYYY_MM_DD")}.${fileType}`;
}
Expand Down
15 changes: 14 additions & 1 deletion client/app/services/query.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import qs from "qs";
import moment from "moment";
import debug from "debug";
import Mustache from "mustache";
Expand All @@ -22,7 +23,7 @@ import {
import location from "@/services/location";

import { Parameter, createParameter } from "./parameters";
import { currentUser } from "./auth";
import { clientConfig, currentUser } from "./auth";
import QueryResult from "./query-result";

Mustache.escape = identity; // do not html-escape values
Expand Down Expand Up @@ -172,6 +173,18 @@ export class Query {
return url;
}

getDataUrl(format = "json", download = true) {
const params = {};
if (this.api_key) {
params.api_key = this.api_key;
}
if (download === false) {
params.download = "false";
}
const paramStr = qs.stringify(params);
return `${clientConfig.basePath}api/queries/${this.id}/results.${format}${paramStr ? "?" + paramStr : ""}`;
}

getQueryResultPromise() {
return this.getQueryResult().toPromise();
}
Expand Down
1 change: 0 additions & 1 deletion client/app/services/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ class Widget {
if (!this.query && this.visualization) {
this.query = new Query(this.visualization.query);
}

return this.query;
}

Expand Down
13 changes: 13 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es2015",
"allowSyntheticDefaultImports": true,
"module": "commonjs",
"baseUrl": ".",
"paths": {
"@/*": ["./client/app/*"],
"extensions/*": ["./client/app/extensions/*"],
}
},
"exclude": ["./client/dist", "node_modules"]
}
19 changes: 15 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"numeral": "^2.0.6",
"path-to-regexp": "^3.1.0",
"prop-types": "^15.6.1",
"qs": "^6.9.4",
"query-string": "^6.9.0",
"react": "^16.13.1",
"react-ace": "^9.1.3",
Expand Down
4 changes: 3 additions & 1 deletion redash/handlers/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import datetime

from flask import request
from flask_login import login_required, current_user

Expand Down Expand Up @@ -36,7 +38,7 @@ def outdated_queries():
"queries": QuerySerializer(
outdated_queries, with_stats=True, with_last_modified_by=False
).serialize(),
"updated_at": manager_status["last_refresh_at"],
"updated_at": manager_status.get("last_refresh_at", datetime.now()),
}
return json_response(response)

Expand Down
8 changes: 7 additions & 1 deletion viz-lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,13 @@ function Example() {
data={exampleData}
onChange={setOptions}
/>
<Renderer type="COUNTER" visualizationName="Example Visualization" options={options} data={exampleData} />
<Renderer
context="editor"
type="COUNTER"
visualizationName="Example Visualization"
options={options}
data={exampleData}
/>
</div>
);
}
Expand Down
Loading

0 comments on commit 65191ff

Please sign in to comment.