@kanaries/graphic-walker 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fields/datasetFields/index.d.ts +3 -3
- package/dist/graphic-walker.es.js +7167 -7152
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +96 -96
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/store/index.d.ts +11 -1
- package/package.json +5 -3
- package/src/App.tsx +140 -0
- package/src/assets/kanaries.ico +0 -0
- package/src/components/clickMenu.tsx +29 -0
- package/src/components/container.tsx +16 -0
- package/src/components/dataTypeIcon.tsx +20 -0
- package/src/components/liteForm.tsx +16 -0
- package/src/components/modal.tsx +85 -0
- package/src/components/sizeSetting.tsx +95 -0
- package/src/components/tabs/pureTab.tsx +70 -0
- package/src/config.ts +57 -0
- package/src/constants.ts +1 -0
- package/src/dataSource/config.ts +62 -0
- package/src/dataSource/dataSelection/csvData.tsx +77 -0
- package/src/dataSource/dataSelection/gwFile.tsx +38 -0
- package/src/dataSource/dataSelection/index.tsx +57 -0
- package/src/dataSource/dataSelection/publicData.tsx +57 -0
- package/src/dataSource/index.tsx +78 -0
- package/src/dataSource/pannel.tsx +71 -0
- package/src/dataSource/table.tsx +125 -0
- package/src/dataSource/utils.ts +47 -0
- package/src/fields/aestheticFields.tsx +23 -0
- package/src/fields/components.tsx +159 -0
- package/src/fields/datasetFields/dimFields.tsx +45 -0
- package/src/fields/datasetFields/fieldPill.tsx +10 -0
- package/src/fields/datasetFields/index.tsx +28 -0
- package/src/fields/datasetFields/meaFields.tsx +58 -0
- package/src/fields/fieldsContext.tsx +59 -0
- package/src/fields/filterField/filterEditDialog.tsx +143 -0
- package/src/fields/filterField/filterPill.tsx +113 -0
- package/src/fields/filterField/index.tsx +61 -0
- package/src/fields/filterField/slider.tsx +236 -0
- package/src/fields/filterField/tabs.tsx +421 -0
- package/src/fields/obComponents/obFContainer.tsx +40 -0
- package/src/fields/obComponents/obPill.tsx +48 -0
- package/src/fields/posFields/index.tsx +33 -0
- package/src/fields/select.tsx +31 -0
- package/src/fields/utils.ts +31 -0
- package/src/index.css +13 -0
- package/src/index.tsx +12 -0
- package/src/insightBoard/index.tsx +30 -0
- package/src/insightBoard/mainBoard.tsx +203 -0
- package/src/insightBoard/radioGroupButtons.tsx +50 -0
- package/src/insightBoard/selectionSpec.ts +113 -0
- package/src/insightBoard/std2vegaSpec.ts +184 -0
- package/src/insightBoard/utils.ts +32 -0
- package/src/insights.ts +408 -0
- package/src/interfaces.ts +154 -0
- package/src/locales/en-US.json +140 -0
- package/src/locales/i18n.ts +50 -0
- package/src/locales/zh-CN.json +140 -0
- package/src/main.tsx +10 -0
- package/src/models/visSpecHistory.ts +129 -0
- package/src/renderer/index.tsx +104 -0
- package/src/segments/visNav.tsx +48 -0
- package/src/services.ts +139 -0
- package/src/store/commonStore.ts +158 -0
- package/src/store/index.tsx +53 -0
- package/src/store/visualSpecStore.ts +586 -0
- package/src/utils/autoMark.ts +34 -0
- package/src/utils/index.ts +251 -0
- package/src/utils/normalization.ts +158 -0
- package/src/utils/save.ts +46 -0
- package/src/vis/future-react-vega.tsx +193 -0
- package/src/vis/gen-vega.tsx +52 -0
- package/src/vis/react-vega.tsx +398 -0
- package/src/visualSettings/index.tsx +252 -0
- package/src/visualSettings/menubar.tsx +109 -0
- package/src/vite-env.d.ts +1 -0
- package/src/workers/explainer.worker.js +78 -0
- package/src/workers/filter.worker.js +70 -0
package/dist/store/index.d.ts
CHANGED
|
@@ -5,6 +5,16 @@ interface GlobalStore {
|
|
|
5
5
|
commonStore: CommonStore;
|
|
6
6
|
vizStore: VizSpecStore;
|
|
7
7
|
}
|
|
8
|
-
export declare
|
|
8
|
+
export declare function destroyGWStore(): void;
|
|
9
|
+
export declare function rebootGWStore(): void;
|
|
10
|
+
export declare class StoreWrapper extends React.Component<{
|
|
11
|
+
keepAlive?: boolean;
|
|
12
|
+
}> {
|
|
13
|
+
constructor(props: {
|
|
14
|
+
keepAlive?: boolean;
|
|
15
|
+
});
|
|
16
|
+
componentWillUnmount(): void;
|
|
17
|
+
render(): JSX.Element;
|
|
18
|
+
}
|
|
9
19
|
export declare function useGlobalStore(): GlobalStore;
|
|
10
20
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kanaries/graphic-walker",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev:front_end": "vite --host",
|
|
6
6
|
"dev": "npm run dev:front_end",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"type": "tsc src/lib.ts --declaration --emitDeclarationOnly --jsx react --esModuleInterop --outDir dist"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"dist"
|
|
13
|
+
"dist",
|
|
14
|
+
"src"
|
|
14
15
|
],
|
|
15
16
|
"license": "AGPL",
|
|
16
17
|
"main": "./dist/graphic-walker.umd.js",
|
|
@@ -26,7 +27,8 @@
|
|
|
26
27
|
}
|
|
27
28
|
},
|
|
28
29
|
"prettier": {
|
|
29
|
-
"tabWidth": 4
|
|
30
|
+
"tabWidth": 4,
|
|
31
|
+
"printWidth": 120
|
|
30
32
|
},
|
|
31
33
|
"types": "./dist/index.d.ts",
|
|
32
34
|
"dependencies": {
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Specification } from 'visual-insights';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import { LightBulbIcon } from '@heroicons/react/24/outline'
|
|
5
|
+
import { toJS } from 'mobx';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { IMutField, IRow } from './interfaces';
|
|
8
|
+
import VisualSettings from './visualSettings';
|
|
9
|
+
import { Container, NestContainer } from './components/container';
|
|
10
|
+
import ClickMenu from './components/clickMenu';
|
|
11
|
+
import InsightBoard from './insightBoard/index';
|
|
12
|
+
import PosFields from './fields/posFields';
|
|
13
|
+
import AestheticFields from './fields/aestheticFields';
|
|
14
|
+
import DatasetFields from './fields/datasetFields/index';
|
|
15
|
+
import ReactiveRenderer from './renderer/index';
|
|
16
|
+
import DataSourceSegment from './dataSource/index';
|
|
17
|
+
import { useGlobalStore } from './store';
|
|
18
|
+
import { preAnalysis, destroyWorker } from './services'
|
|
19
|
+
import VisNav from './segments/visNav';
|
|
20
|
+
import { mergeLocaleRes, setLocaleLanguage } from './locales/i18n';
|
|
21
|
+
import Menubar from './visualSettings/menubar';
|
|
22
|
+
import FilterField from './fields/filterField';
|
|
23
|
+
import "tailwindcss/tailwind.css"
|
|
24
|
+
import './index.css'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export interface EditorProps {
|
|
28
|
+
dataSource?: IRow[];
|
|
29
|
+
rawFields?: IMutField[];
|
|
30
|
+
spec?: Specification;
|
|
31
|
+
hideDataSourceConfig?: boolean;
|
|
32
|
+
i18nLang?: string;
|
|
33
|
+
i18nResources?: { [lang: string]: Record<string, string | any> };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const App: React.FC<EditorProps> = props => {
|
|
37
|
+
const { dataSource = [], rawFields = [], spec, i18nLang = 'en-US', i18nResources, hideDataSourceConfig } = props;
|
|
38
|
+
const { commonStore, vizStore } = useGlobalStore();
|
|
39
|
+
const [insightReady, setInsightReady] = useState<boolean>(true);
|
|
40
|
+
|
|
41
|
+
const { currentDataset, datasets, vizEmbededMenu } = commonStore;
|
|
42
|
+
|
|
43
|
+
const { t, i18n } = useTranslation();
|
|
44
|
+
const curLang = i18n.language;
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (i18nResources) {
|
|
48
|
+
mergeLocaleRes(i18nResources);
|
|
49
|
+
}
|
|
50
|
+
}, [i18nResources]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (i18nLang !== curLang) {
|
|
54
|
+
setLocaleLanguage(i18nLang);
|
|
55
|
+
}
|
|
56
|
+
}, [i18nLang, curLang]);
|
|
57
|
+
|
|
58
|
+
// use as an embeding module, use outside datasource from props.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (dataSource.length > 0) {
|
|
61
|
+
commonStore.addAndUseDS({
|
|
62
|
+
name: 'context dataset',
|
|
63
|
+
dataSource: dataSource,
|
|
64
|
+
rawFields
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}, [dataSource, rawFields])
|
|
68
|
+
|
|
69
|
+
// do preparation analysis work when using a new dataset
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const ds = currentDataset;
|
|
72
|
+
if (ds && ds.dataSource.length > 0 && ds.rawFields.length > 0) {
|
|
73
|
+
setInsightReady(false)
|
|
74
|
+
preAnalysis({
|
|
75
|
+
dataSource: ds.dataSource,
|
|
76
|
+
fields: toJS(ds.rawFields)
|
|
77
|
+
}).then(() => {
|
|
78
|
+
setInsightReady(true);
|
|
79
|
+
|
|
80
|
+
if (spec) {
|
|
81
|
+
vizStore.renderSpec(spec);
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
return () => {
|
|
86
|
+
destroyWorker();
|
|
87
|
+
}
|
|
88
|
+
}, [currentDataset, spec]);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="App">
|
|
92
|
+
{ !hideDataSourceConfig && <DataSourceSegment preWorkDone={insightReady} />}
|
|
93
|
+
<div className='px-2 mx-2'>
|
|
94
|
+
<VisNav />
|
|
95
|
+
{/* <PureTabs tabs={[{label: 'a', key: 'a'}, {label: 'b', key: 'b'}]} selectedKey='a' onSelected={() => {}} /> */}
|
|
96
|
+
</div>
|
|
97
|
+
<Container style={{ marginTop: '0em', borderTop: 'none' }}>
|
|
98
|
+
<Menubar />
|
|
99
|
+
<VisualSettings />
|
|
100
|
+
<div className="grid grid-cols-12 xl:grid-cols-6">
|
|
101
|
+
<div className="col-span-3 xl:col-span-1">
|
|
102
|
+
<DatasetFields />
|
|
103
|
+
</div>
|
|
104
|
+
<div className="col-span-2 xl:col-span-1">
|
|
105
|
+
<FilterField />
|
|
106
|
+
<AestheticFields />
|
|
107
|
+
</div>
|
|
108
|
+
<div className="col-span-7 xl:col-span-4">
|
|
109
|
+
<div>
|
|
110
|
+
<PosFields />
|
|
111
|
+
</div>
|
|
112
|
+
<NestContainer style={{ minHeight: '600px', overflow: 'auto' }} onMouseLeave={() => {
|
|
113
|
+
vizEmbededMenu.show && commonStore.closeEmbededMenu();
|
|
114
|
+
}}>
|
|
115
|
+
{datasets.length > 0 && <ReactiveRenderer />}
|
|
116
|
+
<InsightBoard />
|
|
117
|
+
{vizEmbededMenu.show && (
|
|
118
|
+
<ClickMenu x={vizEmbededMenu.position[0]} y={vizEmbededMenu.position[1]}>
|
|
119
|
+
<div className="flex items-center whitespace-nowrap py-1 px-4 hover:bg-gray-100"
|
|
120
|
+
onClick={() => {
|
|
121
|
+
commonStore.closeEmbededMenu();
|
|
122
|
+
commonStore.setShowInsightBoard(true)
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
<span className="flex-1 pr-2">
|
|
126
|
+
{t('App.labels.data_interpretation')}
|
|
127
|
+
</span>
|
|
128
|
+
<LightBulbIcon className="ml-1 w-3 flex-grow-0 flex-shrink-0" />
|
|
129
|
+
</div>
|
|
130
|
+
</ClickMenu>
|
|
131
|
+
)}
|
|
132
|
+
</NestContainer>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</Container>
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default observer(App);
|
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
const MenuContainer = styled.div`
|
|
5
|
+
min-width: 100px;
|
|
6
|
+
background-color: #fff;
|
|
7
|
+
border: 1px solid #f0f0f0;
|
|
8
|
+
position: absolute;
|
|
9
|
+
z-index: 99;
|
|
10
|
+
cursor: pointer;
|
|
11
|
+
/* box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, 0.09); */
|
|
12
|
+
/* border-radius: 2px; */
|
|
13
|
+
padding: 4px;
|
|
14
|
+
`;
|
|
15
|
+
interface ClickMenuProps {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ClickMenu: React.FC<ClickMenuProps> = props => {
|
|
21
|
+
const { x, y, children } = props
|
|
22
|
+
return <MenuContainer className="shadow-lg text-sm" style={{ left: x + 'px', top: y + 'px' }}>
|
|
23
|
+
{
|
|
24
|
+
children
|
|
25
|
+
}
|
|
26
|
+
</MenuContainer>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default ClickMenu;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const Container = styled.div`
|
|
4
|
+
border: 1px solid #d9d9d9;
|
|
5
|
+
padding: 1em;
|
|
6
|
+
margin: 1em;
|
|
7
|
+
background-color: #fff;
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
export const NestContainer = styled.div`
|
|
11
|
+
border: 1px solid #d9d9d9;
|
|
12
|
+
padding: 0.4em;
|
|
13
|
+
font-size: 12px;
|
|
14
|
+
margin: 0.2em;
|
|
15
|
+
background-color: #fff;
|
|
16
|
+
`
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IMutField } from "../interfaces";
|
|
3
|
+
import { DocumentTextIcon, HashtagIcon, CalendarIcon } from '@heroicons/react/24/outline';
|
|
4
|
+
|
|
5
|
+
const DataTypeIcon: React.FC<{dataType: IMutField['semanticType']; analyticType: IMutField['analyticType']}> = props => {
|
|
6
|
+
const { dataType, analyticType } = props;
|
|
7
|
+
const color = analyticType === 'dimension' ? 'text-blue-500' : 'text-green-500'
|
|
8
|
+
const iconClassName = `w-3 inline-block mr-0.5 ${color}`
|
|
9
|
+
switch (dataType) {
|
|
10
|
+
case 'quantitative':
|
|
11
|
+
case 'ordinal':
|
|
12
|
+
return <HashtagIcon className={iconClassName} />
|
|
13
|
+
case 'temporal':
|
|
14
|
+
return <CalendarIcon className={iconClassName} />
|
|
15
|
+
default:
|
|
16
|
+
return <DocumentTextIcon className={iconClassName} />
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default DataTypeIcon;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const LiteForm = styled.div`
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
.item{
|
|
7
|
+
margin: 5px 8px;
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
>label {
|
|
11
|
+
margin-right: 4px;
|
|
12
|
+
line-height: 20px;
|
|
13
|
+
font-size: 12px;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import { XCircleIcon } from '@heroicons/react/24/outline';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const Background = styled.div({
|
|
7
|
+
position: 'fixed',
|
|
8
|
+
left: 0,
|
|
9
|
+
top: 0,
|
|
10
|
+
width: '100vw',
|
|
11
|
+
height: '100vh',
|
|
12
|
+
backdropFilter: 'blur(1px)',
|
|
13
|
+
zIndex: 25535,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const Container = styled.div`
|
|
17
|
+
width: 880px;
|
|
18
|
+
max-height: 800px;
|
|
19
|
+
overflow: auto;
|
|
20
|
+
> div.header {
|
|
21
|
+
background-color: #f0f0f0;
|
|
22
|
+
display: flex;
|
|
23
|
+
padding: 12px;
|
|
24
|
+
font-size: 14px;
|
|
25
|
+
align-items: center;
|
|
26
|
+
}
|
|
27
|
+
> div.container {
|
|
28
|
+
padding: 1em;
|
|
29
|
+
}
|
|
30
|
+
position: fixed;
|
|
31
|
+
left: 50%;
|
|
32
|
+
top: 50%;
|
|
33
|
+
transform: translate(-50%, -50%);
|
|
34
|
+
background-color: #fff;
|
|
35
|
+
/* box-shadow: 0px 0px 12px 3px rgba(0, 0, 0, 0.19); */
|
|
36
|
+
border-radius: 4px;
|
|
37
|
+
z-index: 999;
|
|
38
|
+
`;
|
|
39
|
+
interface ModalProps {
|
|
40
|
+
onClose?: () => void
|
|
41
|
+
title?: string;
|
|
42
|
+
}
|
|
43
|
+
const Modal: React.FC<ModalProps> = props => {
|
|
44
|
+
const { onClose, title } = props;
|
|
45
|
+
const prevMouseDownTimeRef = useRef(0);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Background
|
|
49
|
+
// This is a safer replacement of onClick handler.
|
|
50
|
+
// onClick also happens if the click event is begun by pressing mouse button
|
|
51
|
+
// at a different element and then released when the mouse is moved on the target element.
|
|
52
|
+
// This case is required to be prevented, especially disturbing when interacting
|
|
53
|
+
// with a Slider component.
|
|
54
|
+
onMouseDown={() => prevMouseDownTimeRef.current = Date.now()}
|
|
55
|
+
onMouseOut={() => prevMouseDownTimeRef.current = 0}
|
|
56
|
+
onMouseUp={() => {
|
|
57
|
+
if (Date.now() - prevMouseDownTimeRef.current < 1000) {
|
|
58
|
+
onClose?.();
|
|
59
|
+
}
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<Container
|
|
63
|
+
role="dialog"
|
|
64
|
+
className="shadow-lg"
|
|
65
|
+
onMouseDown={e => e.stopPropagation()}
|
|
66
|
+
>
|
|
67
|
+
<div className="header relative h-9">
|
|
68
|
+
<header className="font-bold">
|
|
69
|
+
{title}
|
|
70
|
+
</header>
|
|
71
|
+
<XCircleIcon
|
|
72
|
+
className="text-red-600 absolute right-2 w-6 cursor-pointer"
|
|
73
|
+
role="button"
|
|
74
|
+
tabIndex={0}
|
|
75
|
+
aria-label="close dialog"
|
|
76
|
+
onClick={onClose}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="container">{props.children}</div>
|
|
80
|
+
</Container>
|
|
81
|
+
</Background>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default Modal;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ArrowsPointingOutIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
|
2
|
+
import React, { useState, useEffect } from "react";
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
interface SizeSettingProps {
|
|
7
|
+
onWidthChange: (val: number) => void;
|
|
8
|
+
onHeightChange: (val: number) => void;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SizeSetting: React.FC<SizeSettingProps> = props => {
|
|
14
|
+
const { onWidthChange, onHeightChange, width, height } = props
|
|
15
|
+
const [show, setShow] = useState<boolean>(false);
|
|
16
|
+
const { t } = useTranslation('translation', { keyPrefix: 'main.tabpanel.settings.size_setting' });
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (show) {
|
|
20
|
+
const closeDialog = () => {
|
|
21
|
+
setShow(false);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
document.body.addEventListener('click', closeDialog);
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
document.body.removeEventListener('click', closeDialog);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}, [show]);
|
|
31
|
+
|
|
32
|
+
return <div className="leading-none cursor-pointer">
|
|
33
|
+
<ArrowsPointingOutIcon
|
|
34
|
+
role="button"
|
|
35
|
+
id="button:size_setting"
|
|
36
|
+
aria-describedby="button:size_setting:label"
|
|
37
|
+
tabIndex={0}
|
|
38
|
+
aria-haspopup="dialog"
|
|
39
|
+
onClick={() => {
|
|
40
|
+
setShow(v => !v)
|
|
41
|
+
}}
|
|
42
|
+
className="w-4 h-4 inline-block mr-0.5 text-gray-900"
|
|
43
|
+
/>
|
|
44
|
+
{
|
|
45
|
+
show && <div role="dialog" className="absolute z-auto bg-white p-4 border border-gray-200 shadow cursor-default" onClick={e => e.stopPropagation()} style={{ zIndex: 25535 }}>
|
|
46
|
+
<div>
|
|
47
|
+
<XMarkIcon
|
|
48
|
+
className="text-gray-900 absolute right-2 top-2 w-4 cursor-pointer hover:bg-red-100"
|
|
49
|
+
role="button"
|
|
50
|
+
tabIndex={0}
|
|
51
|
+
aria-label="close"
|
|
52
|
+
onClick={(e) => {
|
|
53
|
+
setShow(false);
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div className="mt-4 w-60">
|
|
60
|
+
<input className="w-full h-2 bg-blue-100 appearance-none"
|
|
61
|
+
style={{ cursor: 'ew-resize' }}
|
|
62
|
+
type="range"
|
|
63
|
+
name="width"
|
|
64
|
+
value={Math.sqrt(width / 1000)}
|
|
65
|
+
min="0" max="1" step="0.01"
|
|
66
|
+
onChange={(e) => {
|
|
67
|
+
onWidthChange(Math.round(Number(e.target.value) ** 2 * 1000))
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
<output className="text-sm ml-1" htmlFor="width">
|
|
71
|
+
{`${t('width')}: ${width}`}
|
|
72
|
+
</output>
|
|
73
|
+
</div>
|
|
74
|
+
<div className=" mt-2">
|
|
75
|
+
<input className="w-full h-2 bg-blue-100 appearance-none"
|
|
76
|
+
style={{ cursor: 'ew-resize' }}
|
|
77
|
+
type="range"
|
|
78
|
+
name="height"
|
|
79
|
+
value={Math.sqrt(height / 1000)}
|
|
80
|
+
min="0" max="1" step="0.01"
|
|
81
|
+
onChange={(e) => {
|
|
82
|
+
onHeightChange(Math.round(Number(e.target.value) ** 2 * 1000))
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
<output className="text-sm ml-1" htmlFor="height">
|
|
86
|
+
{`${t('height')}: ${height}`}
|
|
87
|
+
</output>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
</div>
|
|
91
|
+
}
|
|
92
|
+
</div>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default SizeSetting;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
function classNames(...classes: string[]) {
|
|
6
|
+
return classes.filter(Boolean).join(' ')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ITabOption {
|
|
10
|
+
label: string;
|
|
11
|
+
key: string;
|
|
12
|
+
options?: Record<string, any>;
|
|
13
|
+
}
|
|
14
|
+
interface PureTabsProps {
|
|
15
|
+
tabs: ITabOption[];
|
|
16
|
+
selectedKey: string;
|
|
17
|
+
onSelected: (selectedKey: string, index: number) => void;
|
|
18
|
+
allowEdit?: boolean;
|
|
19
|
+
onEditLabel?: (label: string, index: number) => void;
|
|
20
|
+
}
|
|
21
|
+
export default function PureTabs(props: PureTabsProps) {
|
|
22
|
+
const { tabs, selectedKey, onSelected, allowEdit, onEditLabel } = props;
|
|
23
|
+
const [editList, setEditList] = useState<boolean[]>([]);
|
|
24
|
+
const { t } = useTranslation();
|
|
25
|
+
|
|
26
|
+
const clearEditStatus = useCallback(() => {
|
|
27
|
+
setEditList(new Array(tabs.length).fill(false))
|
|
28
|
+
}, [tabs.length]);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
clearEditStatus
|
|
32
|
+
}, [clearEditStatus]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="border-b border-gray-200 overflow-x-auto overflow-y-hidden" onMouseLeave={clearEditStatus}>
|
|
36
|
+
<nav className="-mb-px flex h-8 border-gray-300 border-l" role="tablist" aria-label="Tabs">
|
|
37
|
+
{tabs.map((tab, tabIndex) => (
|
|
38
|
+
<span
|
|
39
|
+
role="tab"
|
|
40
|
+
tabIndex={0}
|
|
41
|
+
dangerouslySetInnerHTML={{
|
|
42
|
+
__html: t(tab.label, tab.options)
|
|
43
|
+
}}
|
|
44
|
+
onClick={() => {
|
|
45
|
+
onSelected(tab.key, tabIndex)
|
|
46
|
+
}}
|
|
47
|
+
onDoubleClick={() => {
|
|
48
|
+
setEditList(v => {
|
|
49
|
+
const nv = [...v];
|
|
50
|
+
nv[tabIndex] = true;
|
|
51
|
+
return nv
|
|
52
|
+
})
|
|
53
|
+
}}
|
|
54
|
+
contentEditable={editList[tabIndex]}
|
|
55
|
+
onInput={(e) => {
|
|
56
|
+
onEditLabel && onEditLabel(`${e.currentTarget.textContent}`, tabIndex)
|
|
57
|
+
}}
|
|
58
|
+
key={tab.key}
|
|
59
|
+
className={classNames(
|
|
60
|
+
tab.key === selectedKey
|
|
61
|
+
? "border-transparent text-black bg-gray-100"
|
|
62
|
+
: "text-gray-500 hover:text-gray-700 hover:border-gray-300",
|
|
63
|
+
"whitespace-nowrap border-gray-300 py-1 px-2 border-t border-r border-b pr-6 text-sm cursor-pointer"
|
|
64
|
+
)}
|
|
65
|
+
/>
|
|
66
|
+
))}
|
|
67
|
+
</nav>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { DraggableFieldState, IStackMode } from "./interfaces";
|
|
2
|
+
|
|
3
|
+
export const GEMO_TYPES: Readonly<string[]> = [
|
|
4
|
+
'auto',
|
|
5
|
+
'bar',
|
|
6
|
+
'line',
|
|
7
|
+
'area',
|
|
8
|
+
'trail',
|
|
9
|
+
'point',
|
|
10
|
+
'circle',
|
|
11
|
+
'tick',
|
|
12
|
+
'rect',
|
|
13
|
+
'arc',
|
|
14
|
+
'boxplot',
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export const STACK_MODE: Readonly<IStackMode[]> = [
|
|
18
|
+
'none',
|
|
19
|
+
'stack',
|
|
20
|
+
'normalize'
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
export const CHART_LAYOUT_TYPE: Readonly<string[]> = [
|
|
24
|
+
'auto',
|
|
25
|
+
'fixed',
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
export const COLORS = {
|
|
29
|
+
// tableau style
|
|
30
|
+
// dimension: 'rgb(73, 150, 178)',
|
|
31
|
+
// measure: 'rgb(0, 177, 128)',
|
|
32
|
+
// dimension: 'rgb(86, 170, 208)',
|
|
33
|
+
// measure: 'rgb(232, 149, 72)'
|
|
34
|
+
dimension: 'rgba(0, 0, 0, 0.9)',
|
|
35
|
+
measure: 'rgba(10, 0, 0, 0.6)',
|
|
36
|
+
black: '#141414',
|
|
37
|
+
white: '#fafafa'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const MAX_HISTORY_SIZE = 20;
|
|
41
|
+
|
|
42
|
+
export const CHANNEL_LIMIT = {
|
|
43
|
+
rows: Infinity,
|
|
44
|
+
columns: Infinity,
|
|
45
|
+
color: 1,
|
|
46
|
+
opacity: 1,
|
|
47
|
+
size: 1,
|
|
48
|
+
shape: 1,
|
|
49
|
+
theta: 1,
|
|
50
|
+
radius: 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const MetaFieldKeys: Array<keyof DraggableFieldState> = [
|
|
54
|
+
'dimensions',
|
|
55
|
+
'measures',
|
|
56
|
+
'fields'
|
|
57
|
+
]
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const COUNT_FIELD_ID = 'gw_count_fid';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const DemoDataAssets = process.env.NODE_ENV === 'production' ? {
|
|
2
|
+
CARS: "https://chspace.oss-cn-hongkong.aliyuncs.com/api/ds-cars-service.json",
|
|
3
|
+
STUDENTS: "https://chspace.oss-cn-hongkong.aliyuncs.com/api/ds-students-service.json",
|
|
4
|
+
BTC_GOLD: "https://chspace.oss-cn-hongkong.aliyuncs.com/api/ds_btc_gold_service.json",
|
|
5
|
+
BIKE_SHARING: 'https://chspace.oss-cn-hongkong.aliyuncs.com/api/ds-bikesharing-service.json',
|
|
6
|
+
CAR_SALES: 'https://chspace.oss-cn-hongkong.aliyuncs.com/api/ds-carsales-service.json',
|
|
7
|
+
COLLAGE: 'https://chspace.oss-cn-hongkong.aliyuncs.com/api/ds-collage-service.json',
|
|
8
|
+
TITANIC: 'https://chspace.oss-cn-hongkong.aliyuncs.com/api/ds-titanic-service.json',
|
|
9
|
+
KELPER: 'https://chspace.oss-cn-hongkong.aliyuncs.com/api/ds-kelper-service.json',
|
|
10
|
+
} : {
|
|
11
|
+
// CARS: "https://chspace.oss-cn-hongkong.aliyuncs.com/api/ds-cars-service.json",
|
|
12
|
+
CARS: "/datasets/ds-cars-service.json",
|
|
13
|
+
// STUDENTS: "https://chspace.oss-cn-hongkong.aliyuncs.com/datasets/ds-students-service.json",
|
|
14
|
+
STUDENTS: "/datasets/ds-students-service.json",
|
|
15
|
+
BTC_GOLD: "/datasets/ds_btc_gold_service.json",
|
|
16
|
+
BIKE_SHARING: '/datasets/ds-bikesharing-service.json',
|
|
17
|
+
CAR_SALES: '/datasets/ds-carsales-service.json',
|
|
18
|
+
COLLAGE: '/datasets/ds-collage-service.json',
|
|
19
|
+
TITANIC: '/datasets/ds-titanic-service.json',
|
|
20
|
+
KELPER: '/datasets/ds-kelper-service.json',
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
interface IPublicData {
|
|
24
|
+
key: string;
|
|
25
|
+
title: string;
|
|
26
|
+
desc?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const PUBLIC_DATA_LIST: IPublicData[] = [
|
|
30
|
+
{
|
|
31
|
+
key: "CARS",
|
|
32
|
+
title: "Cars",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "STUDENTS",
|
|
36
|
+
title: "Students' Performance"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "BIKE_SHARING",
|
|
40
|
+
title: "Bike Sharing"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "CAR_SALES",
|
|
44
|
+
title: "Car Sales"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "COLLAGE",
|
|
48
|
+
title: "Collage"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: "KELPER",
|
|
52
|
+
title: "NASA Kelper"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: 'BTC_GOLD',
|
|
56
|
+
title: "2022MCM Problem C: Trading Strategies"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "TITANIC",
|
|
60
|
+
title: "Titanic"
|
|
61
|
+
}
|
|
62
|
+
]
|