Skip to content

Commit 9c94a78

Browse files
committed
add very basic conversion to vega-lite
1 parent a156f70 commit 9c94a78

6 files changed

+1195
-83
lines changed

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
"@duckdb/duckdb-wasm": "^1.29.0",
2828
"arquero": "^7.2.0",
2929
"pinia": "^2.3.1",
30+
"vega": "^5.30.0",
31+
"vega-embed": "^6.29.0",
32+
"vega-lite": "^5.23.0",
3033
"vue": "^3.4.21"
3134
},
3235
"devDependencies": {

src/components/ParserComponent.stories.ts

+50
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,56 @@ export const Default = {
3737
},
3838
};
3939

40+
export const example2 = {
41+
args: {
42+
spec: {
43+
dataSource: {
44+
key: 'penguins',
45+
source: './data/penguins.csv',
46+
},
47+
dataRepresentation: {
48+
type: 'GoGComponent',
49+
mark: 'point',
50+
encoding: {
51+
x: { source: 'penguins', field: 'flipper_length_mm' },
52+
y: { source: 'penguins', field: 'flipper_length_mm' },
53+
color: { source: 'penguins', field: 'flipper_length_mm' },
54+
},
55+
},
56+
},
57+
},
58+
};
59+
60+
export const Layering = {
61+
args: {
62+
spec: {
63+
dataSource: {
64+
key: 'penguins',
65+
source: './data/penguins.csv',
66+
},
67+
dataRepresentation: [
68+
{
69+
type: 'GoGComponent',
70+
mark: 'point',
71+
encoding: {
72+
x: { source: 'penguins', field: 'bill_length_mm' },
73+
y: { source: 'penguins', field: 'flipper_length_mm' },
74+
},
75+
},
76+
{
77+
type: 'GoGComponent',
78+
mark: 'circle',
79+
encoding: {
80+
x: { source: 'penguins', field: 'bill_length_mm' },
81+
y: { source: 'penguins', field: 'flipper_length_mm' },
82+
color: { source: 'penguins', field: 'sex' },
83+
},
84+
},
85+
],
86+
},
87+
},
88+
};
89+
4090
// export const Test2 = {
4191
// args: {
4292
// blarg: 'test 2!',

src/components/ParserComponent.vue

+82
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,116 @@
11
<script setup lang="ts">
22
import { ref, computed, watch, onMounted } from 'vue';
3+
import VegaLite from './VegaLite.vue';
34
import {
45
type ParsedUDIGrammar,
56
type UDIGrammar,
67
parseSpecification,
78
} from './Parser';
89
import { useDataSourcesStore } from '@/stores/DataSourcesStore';
10+
import { storeToRefs } from 'pinia';
911
1012
const dataSourcesStore = useDataSourcesStore();
13+
const { loading } = storeToRefs(dataSourcesStore);
1114
1215
export interface ParserProps {
1316
spec: UDIGrammar;
1417
}
1518
19+
// TODO: infer based on data, or assume schema
20+
const columnTypes = {
21+
species: 'nominal',
22+
island: 'nominal',
23+
bill_length_mm: 'quantitative',
24+
bill_depth_mm: 'quantitative',
25+
flipper_length_mm: 'quantitative',
26+
body_mass_g: 'quantitative',
27+
sex: 'nominal',
28+
};
29+
1630
const props = defineProps<ParserProps>();
1731
1832
const parsedSpec = ref<ParsedUDIGrammar | null>(null);
1933
34+
const isGoGComponent = ref<boolean>(false);
35+
const vegaLiteSpec = ref<string>('');
2036
onMounted(() => {
2137
// parse/validate grammar
2238
parsedSpec.value = parseSpecification(props.spec);
2339
for (const dataSource of parsedSpec.value.dataSource) {
2440
dataSourcesStore.initDataSource(dataSource);
2541
}
42+
buildVisualization();
43+
44+
// if (isVegaLiteCompatible(parsedSpec.value)) {
45+
// vegaLiteSpec.value = convertToVegaSpec(parsedSpec.value);
46+
// isGoGComponent.value = true;
47+
// }
2648
});
49+
50+
watch(loading, () => buildVisualization());
51+
52+
function buildVisualization(): void {
53+
console.log('build vis');
54+
// // parse/validate grammar
55+
// parsedSpec.value = parseSpecification(props.spec);
56+
// for (const dataSource of parsedSpec.value.dataSource) {
57+
// dataSourcesStore.initDataSource(dataSource);
58+
// }
59+
60+
if (isVegaLiteCompatible(parsedSpec.value)) {
61+
vegaLiteSpec.value = convertToVegaSpec(parsedSpec.value);
62+
isGoGComponent.value = true;
63+
}
64+
}
65+
66+
function isVegaLiteCompatible(spec: ParsedUDIGrammar): boolean {
67+
return Array.isArray(spec.dataRepresentation);
68+
}
69+
70+
function convertToVegaSpec(spec: ParsedUDIGrammar): string {
71+
const vegaSpec = {
72+
$schema: 'https://vega.github.io/schema/vega-lite/v5.json',
73+
// data: { url: './data/penguins.csv' },
74+
data: { values: [] },
75+
};
76+
77+
// add data
78+
// TODO: assume one data source
79+
const dataInterface = spec.dataSource[0];
80+
vegaSpec.data.values = dataSourcesStore.getDataObject(dataInterface.key);
81+
console.log(vegaSpec);
82+
// TODO: perform transformations
83+
84+
// dataInterface.
85+
86+
// add layers
87+
const inputLayers = spec.dataRepresentation;
88+
if (!Array.isArray(inputLayers)) {
89+
throw new Error('invalid spec passed to vega conversion');
90+
}
91+
const outputLayers = inputLayers.map((gogComponent) => {
92+
let encoding = {};
93+
for (let [key, value] of Object.entries(gogComponent.encoding)) {
94+
encoding[key] = {
95+
field: value.field,
96+
type: columnTypes[value.field],
97+
};
98+
}
99+
return {
100+
mark: gogComponent.mark,
101+
encoding,
102+
};
103+
});
104+
vegaSpec['layer'] = outputLayers;
105+
106+
return JSON.stringify(vegaSpec);
107+
}
27108
</script>
28109

29110
<template>
30111
<div>Blargen Flargen</div>
31112
<div>{{ props.spec }}</div>
113+
<VegaLite v-if="isGoGComponent" :spec="vegaLiteSpec" />
32114
</template>
33115

34116
<style scoped lang="scss"></style>

src/components/VegaLite.vue

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<script setup>
2+
import { ref, onMounted } from 'vue';
3+
import vegaEmbed from 'vega-embed';
4+
import { defineProps } from 'vue';
5+
import { watch } from 'vue';
6+
7+
const props = defineProps({
8+
spec: {
9+
type: String,
10+
required: true,
11+
},
12+
});
13+
14+
const vegaContainer = ref(null);
15+
16+
const errorMessage = ref(null);
17+
18+
function updateVegaChart() {
19+
let specObject;
20+
try {
21+
specObject = JSON.parse(props.spec);
22+
} catch (error) {
23+
console.error('Error parsing spec', error);
24+
errorMessage.value = 'Error parsing spec: ' + error;
25+
// clear the container so the chart doesn't show up
26+
vegaContainer.value.innerHTML = '';
27+
return;
28+
}
29+
30+
vegaEmbed(vegaContainer.value, specObject)
31+
.then((result) => {
32+
console.log('Chart rendered successfully');
33+
errorMessage.value = null;
34+
})
35+
.catch((error) => {
36+
console.error('Error rendering chart', error);
37+
errorMessage.value = 'Error rendering chart: ' + error;
38+
// clear the container so the chart doesn't show up
39+
vegaContainer.value.innerHTML = '';
40+
});
41+
}
42+
43+
onMounted(() => {
44+
updateVegaChart();
45+
});
46+
47+
watch(() => props.spec, updateVegaChart);
48+
</script>
49+
50+
<template>
51+
<div ref="vegaContainer" class="vega-chart-container"></div>
52+
<div v-if="errorMessage" class="error-message">{{ errorMessage }}</div>
53+
</template>
54+
55+
<style scoped>
56+
.vega-chart-container {
57+
width: 100%;
58+
height: 100%;
59+
max-width: 600px;
60+
overflow-x: auto;
61+
}
62+
63+
.error-message {
64+
color: red;
65+
}
66+
</style>

src/stores/DataSourcesStore.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,29 @@ export const useDataSourcesStore = defineStore('DataSourcesStore', () => {
2323
// connection = await db.connect();
2424
// });
2525

26+
const loading = ref<boolean>(true);
27+
2628
async function initDataSource(dataSource: DataSource): Promise<void> {
2729
if (getDataSource(dataSource.key)) return;
28-
const dest: ColumnTable = await loadCSV(dataSource.source, {
29-
delimiter: '\t',
30-
});
30+
const dest: ColumnTable = await loadCSV(dataSource.source);
3131
dataSources.value[dataSource.key] = { source: dataSource, dest };
3232
// TODO: actually create data interface object
3333
console.log(dataSources.value);
34+
loading.value = false;
3435
}
3536

3637
function getDataSource(key: string): DataInterface | null {
3738
if (!(key in dataSources.value)) return null;
3839
return dataSources.value[key];
3940
}
4041

41-
return { dataSources, initDataSource };
42+
function getDataObject(key: string): any {
43+
const dataInterface = getDataSource(key);
44+
if (dataInterface === null) return null;
45+
// TODO, incorporate data transformations
46+
// maybe filter to only fields used?
47+
return dataInterface.dest.objects();
48+
}
49+
50+
return { dataSources, loading, initDataSource, getDataObject };
4251
});

0 commit comments

Comments
 (0)