@kanaries/graphic-walker 0.2.12 → 0.2.13

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.
@@ -149,3 +149,7 @@ export interface IVisSpec {
149
149
  readonly encodings: DeepReadonly<DraggableFieldState>;
150
150
  readonly config: DeepReadonly<IVisualConfig>;
151
151
  }
152
+ export declare enum ISegmentKey {
153
+ vis = "vis",
154
+ data = "data"
155
+ }
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ declare const _default: React.FunctionComponent<{}>;
3
+ export default _default;
@@ -1,4 +1,4 @@
1
- import { DataSet, Filters, IDataSet, IDataSetInfo, IDataSource, IMutField, IRow } from '../interfaces';
1
+ import { DataSet, Filters, IDataSet, IDataSetInfo, IDataSource, IMutField, IRow, ISegmentKey } from '../interfaces';
2
2
  export declare class CommonStore {
3
3
  datasets: IDataSet[];
4
4
  dataSources: IDataSource[];
@@ -12,15 +12,21 @@ export declare class CommonStore {
12
12
  show: boolean;
13
13
  position: [number, number];
14
14
  };
15
+ showDataConfig: boolean;
15
16
  filters: Filters;
17
+ segmentKey: ISegmentKey;
16
18
  constructor();
17
19
  get currentDataset(): DataSet;
20
+ setSegmentKey(sk: ISegmentKey): void;
18
21
  setShowDSPanel(show: boolean): void;
22
+ setShowDataConfig(show: boolean): void;
19
23
  setShowInsightBoard(show: boolean): void;
20
24
  showEmbededMenu(position: [number, number]): void;
21
25
  closeEmbededMenu(): void;
22
26
  initTempDS(): void;
23
27
  updateTempFields(fields: IMutField[]): void;
28
+ updateCurrentDatasetMetas(fid: string, diffMeta: Partial<IMutField>): void;
29
+ updateTempDatasetMetas(fid: string, diffMeta: Partial<IMutField>): void;
24
30
  updateTempFieldAnalyticType(fieldKey: string, analyticType: IMutField['analyticType']): void;
25
31
  updateTempFieldSemanticType(fieldKey: string, semanticType: IMutField['semanticType']): void;
26
32
  updateTempName(name: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanaries/graphic-walker",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "scripts": {
5
5
  "dev:front_end": "vite --host",
6
6
  "dev": "npm run dev:front_end",
package/src/App.tsx CHANGED
@@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
4
4
  import { LightBulbIcon } from "@heroicons/react/24/outline";
5
5
  import { toJS } from "mobx";
6
6
  import { useTranslation } from "react-i18next";
7
- import { IMutField, IRow } from "./interfaces";
7
+ import { IMutField, IRow, ISegmentKey } from "./interfaces";
8
8
  import type { IReactVegaHandler } from "./vis/react-vega";
9
9
  import VisualSettings from "./visualSettings";
10
10
  import { Container, NestContainer } from "./components/container";
@@ -21,6 +21,8 @@ import VisNav from "./segments/visNav";
21
21
  import { mergeLocaleRes, setLocaleLanguage } from "./locales/i18n";
22
22
  import FilterField from "./fields/filterField";
23
23
  import { guardDataKeys } from "./utils/dataPrep";
24
+ import SegmentNav from "./segments/segmentNav";
25
+ import DatasetConfig from "./dataSource/datasetConfig";
24
26
 
25
27
  export interface IGWProps {
26
28
  dataSource?: IRow[];
@@ -37,11 +39,19 @@ export interface IGWProps {
37
39
  }
38
40
 
39
41
  const App: React.FC<IGWProps> = (props) => {
40
- const { dataSource = [], rawFields = [], spec, i18nLang = "en-US", i18nResources, hideDataSourceConfig, fieldKeyGuard = true } = props;
42
+ const {
43
+ dataSource = [],
44
+ rawFields = [],
45
+ spec,
46
+ i18nLang = "en-US",
47
+ i18nResources,
48
+ hideDataSourceConfig,
49
+ fieldKeyGuard = true,
50
+ } = props;
41
51
  const { commonStore, vizStore } = useGlobalStore();
42
52
  const [insightReady, setInsightReady] = useState<boolean>(true);
43
53
 
44
- const { currentDataset, datasets, vizEmbededMenu } = commonStore;
54
+ const { currentDataset, datasets, vizEmbededMenu, segmentKey } = commonStore;
45
55
 
46
56
  const { t, i18n } = useTranslation();
47
57
  const curLang = i18n.language;
@@ -114,48 +124,63 @@ const App: React.FC<IGWProps> = (props) => {
114
124
  <div className="">
115
125
  {!hideDataSourceConfig && <DataSourceSegment preWorkDone={insightReady} />}
116
126
  <div className="px-2 mx-2">
117
- <VisNav />
127
+ <SegmentNav />
128
+ {
129
+ segmentKey === ISegmentKey.vis && <VisNav />
130
+ }
118
131
  </div>
119
- <Container style={{ marginTop: "0em", borderTop: "none" }}>
120
- <VisualSettings rendererHandler={rendererRef} />
121
- <div className="md:grid md:grid-cols-12 xl:grid-cols-6">
122
- <div className="md:col-span-3 xl:col-span-1">
123
- <DatasetFields />
124
- </div>
125
- <div className="md:col-span-2 xl:col-span-1">
126
- <FilterField />
127
- <AestheticFields />
128
- </div>
129
- <div className="md:col-span-7 xl:col-span-4">
130
- <div>
131
- <PosFields />
132
+ {segmentKey === ISegmentKey.vis && (
133
+ <Container style={{ marginTop: "0em", borderTop: "none" }}>
134
+ <VisualSettings rendererHandler={rendererRef} />
135
+ <div className="md:grid md:grid-cols-12 xl:grid-cols-6">
136
+ <div className="md:col-span-3 xl:col-span-1">
137
+ <DatasetFields />
138
+ </div>
139
+ <div className="md:col-span-2 xl:col-span-1">
140
+ <FilterField />
141
+ <AestheticFields />
142
+ </div>
143
+ <div className="md:col-span-7 xl:col-span-4">
144
+ <div>
145
+ <PosFields />
146
+ </div>
147
+ <NestContainer
148
+ style={{ minHeight: "600px", overflow: "auto" }}
149
+ onMouseLeave={() => {
150
+ vizEmbededMenu.show && commonStore.closeEmbededMenu();
151
+ }}
152
+ onClick={() => {
153
+ vizEmbededMenu.show && commonStore.closeEmbededMenu();
154
+ }}
155
+ >
156
+ {datasets.length > 0 && <ReactiveRenderer ref={rendererRef} />}
157
+ <InsightBoard />
158
+ {vizEmbededMenu.show && (
159
+ <ClickMenu x={vizEmbededMenu.position[0]} y={vizEmbededMenu.position[1]}>
160
+ <div
161
+ className="flex items-center whitespace-nowrap py-1 px-4 hover:bg-gray-100"
162
+ onClick={() => {
163
+ commonStore.closeEmbededMenu();
164
+ commonStore.setShowInsightBoard(true);
165
+ }}
166
+ >
167
+ <span className="flex-1 pr-2">
168
+ {t("App.labels.data_interpretation")}
169
+ </span>
170
+ <LightBulbIcon className="ml-1 w-3 flex-grow-0 flex-shrink-0" />
171
+ </div>
172
+ </ClickMenu>
173
+ )}
174
+ </NestContainer>
132
175
  </div>
133
- <NestContainer
134
- style={{ minHeight: "600px", overflow: "auto" }}
135
- onMouseLeave={() => {
136
- vizEmbededMenu.show && commonStore.closeEmbededMenu();
137
- }}
138
- >
139
- {datasets.length > 0 && <ReactiveRenderer ref={rendererRef} />}
140
- <InsightBoard />
141
- {vizEmbededMenu.show && (
142
- <ClickMenu x={vizEmbededMenu.position[0]} y={vizEmbededMenu.position[1]}>
143
- <div
144
- className="flex items-center whitespace-nowrap py-1 px-4 hover:bg-gray-100"
145
- onClick={() => {
146
- commonStore.closeEmbededMenu();
147
- commonStore.setShowInsightBoard(true);
148
- }}
149
- >
150
- <span className="flex-1 pr-2">{t("App.labels.data_interpretation")}</span>
151
- <LightBulbIcon className="ml-1 w-3 flex-grow-0 flex-shrink-0" />
152
- </div>
153
- </ClickMenu>
154
- )}
155
- </NestContainer>
156
176
  </div>
157
- </div>
158
- </Container>
177
+ </Container>
178
+ )}
179
+ {segmentKey === ISegmentKey.data && (
180
+ <Container style={{ marginTop: "0em", borderTop: "none" }}>
181
+ <DatasetConfig />
182
+ </Container>
183
+ )}
159
184
  </div>
160
185
  </div>
161
186
  );
@@ -0,0 +1,187 @@
1
+ import React, { useMemo, useState } from "react";
2
+ import styled from "styled-components";
3
+ import { IMutField, IRow } from "../../interfaces";
4
+ import { useTranslation } from "react-i18next";
5
+ import Pagination from "./pagination";
6
+
7
+ interface DataTableProps {
8
+ size?: number;
9
+ metas: IMutField[];
10
+ data: IRow[];
11
+ onMetaChange: (fid: string, fIndex: number, meta: Partial<IMutField>) => void;
12
+ }
13
+ const Container = styled.div`
14
+ overflow-x: auto;
15
+ max-height: 660px;
16
+ overflow-y: auto;
17
+ table {
18
+ box-sizing: content-box;
19
+ border-collapse: collapse;
20
+ font-size: 12px;
21
+ thead {
22
+ th {
23
+ }
24
+ th.number {
25
+ border-top: 3px solid #5cdbd3;
26
+ }
27
+ th.text {
28
+ border-top: 3px solid #69c0ff;
29
+ }
30
+ }
31
+ tbody {
32
+ td {
33
+ }
34
+ td.number {
35
+ text-align: right;
36
+ }
37
+ td.text {
38
+ text-align: left;
39
+ }
40
+ }
41
+ }
42
+ `;
43
+ const ANALYTIC_TYPE_LIST = ["dimension", "measure"];
44
+ const SEMANTIC_TYPE_LIST = ["nominal", "ordinal", "quantitative", "temporal"];
45
+ // function getCellType(field: IMutField): 'number' | 'text' {
46
+ // return field.dataType === 'number' || field.dataType === 'integer' ? 'number' : 'text';
47
+ // }
48
+ function getHeaderType(field: IMutField): "number" | "text" {
49
+ return field.analyticType === "dimension" ? "text" : "number";
50
+ }
51
+
52
+ function getHeaderClassNames(field: IMutField) {
53
+ return field.analyticType === "dimension" ? "border-t-4 border-blue-400" : "border-t-4 border-teal-400";
54
+ }
55
+
56
+ function getSemanticColors(field: IMutField): string {
57
+ switch (field.semanticType) {
58
+ case "nominal":
59
+ return "bg-indigo-100 text-indigo-800";
60
+ case "ordinal":
61
+ return "bg-purple-100 text-purple-800";
62
+ case "quantitative":
63
+ return "bg-green-100 text-green-800";
64
+ case "temporal":
65
+ return "bg-yellow-100 text-yellow-800";
66
+ default:
67
+ return "bg-gray-400";
68
+ }
69
+ }
70
+
71
+ const DataTable: React.FC<DataTableProps> = (props) => {
72
+ const { size = 10, data, metas, onMetaChange } = props;
73
+ const [pageIndex, setPageIndex] = useState(0);
74
+ const { t } = useTranslation();
75
+
76
+ const analyticTypeList = useMemo<{ value: string; label: string }[]>(() => {
77
+ return ANALYTIC_TYPE_LIST.map((at) => ({
78
+ value: at,
79
+ label: t(`constant.analytic_type.${at}`),
80
+ }));
81
+ }, []);
82
+
83
+ const semanticTypeList = useMemo<{ value: string; label: string }[]>(() => {
84
+ return SEMANTIC_TYPE_LIST.map((st) => ({
85
+ value: st,
86
+ label: t(`constant.semantic_type.${st}`),
87
+ }));
88
+ }, []);
89
+
90
+ const from = pageIndex * size;
91
+ const to = Math.min((pageIndex + 1) * size, data.length - 1);
92
+
93
+ return (
94
+ <Container className="rounded border-gray-200 border">
95
+ <Pagination
96
+ total={data.length}
97
+ from={from + 1}
98
+ to={to + 1}
99
+ onNext={() => {
100
+ setPageIndex(Math.min(Math.ceil(data.length / size) - 1, pageIndex + 1));
101
+ }}
102
+ onPrev={() => {
103
+ setPageIndex(Math.max(0, pageIndex - 1));
104
+ }}
105
+ />
106
+ <table className="min-w-full divide-y divide-gray-30">
107
+ <thead className="bg-gray-50">
108
+ <tr className="divide-x divide-gray-200">
109
+ {metas.map((field, fIndex) => (
110
+ <th key={field.fid} className={""}>
111
+ <div
112
+ className={
113
+ getHeaderClassNames(field) +
114
+ " whitespace-nowrap py-3.5 px-6 text-left text-xs font-semibold text-gray-900 sm:pl-6"
115
+ }
116
+ >
117
+ <b>{field.name || field.fid}</b>
118
+ <div>
119
+ <select
120
+ className={
121
+ "px-2 py font-normal mt-2 rounded-full text-xs text-white " +
122
+ (field.analyticType === "dimension" ? "bg-blue-500" : "bg-teal-500")
123
+ }
124
+ // className="border-b border-gray-200 bg-gray-50 pl-0 mt-2 font-light"
125
+ value={field.analyticType}
126
+ onChange={(e) => {
127
+ onMetaChange(field.fid, fIndex, {
128
+ analyticType: e.target.value as IMutField["analyticType"],
129
+ });
130
+ }}
131
+ >
132
+ {analyticTypeList.map((type) => (
133
+ <option key={type.value} value={type.value}>
134
+ {type.label}
135
+ </option>
136
+ ))}
137
+ </select>
138
+ </div>
139
+ <div>
140
+ <select
141
+ className={
142
+ "inline-block px-2.5 py-0.5 text-xs font-medium mt-1 rounded-full text-xs text-white " +
143
+ getSemanticColors(field)
144
+ }
145
+ // className="border-b border-gray-200 bg-gray-50 pl-0 mt-2 font-light"
146
+ value={field.semanticType}
147
+ onChange={(e) => {
148
+ onMetaChange(field.fid, fIndex, {
149
+ semanticType: e.target.value as IMutField["semanticType"],
150
+ });
151
+ }}
152
+ >
153
+ {semanticTypeList.map((type) => (
154
+ <option key={type.value} value={type.value}>
155
+ {type.label}
156
+ </option>
157
+ ))}
158
+ </select>
159
+ </div>
160
+ </div>
161
+ </th>
162
+ ))}
163
+ </tr>
164
+ </thead>
165
+ <tbody className="divide-y divide-gray-200 bg-white">
166
+ {data.slice(from, to).map((row, index) => (
167
+ <tr className={"divide-x divide-gray-200 " + (index % 2 ? "bg-gray-50" : "")} key={index}>
168
+ {metas.map((field) => (
169
+ <td
170
+ key={field.fid + index}
171
+ className={
172
+ getHeaderType(field) +
173
+ " whitespace-nowrap py-2 pl-4 pr-3 text-xs text-gray-500 sm:pl-6"
174
+ }
175
+ >
176
+ {row[field.fid]}
177
+ </td>
178
+ ))}
179
+ </tr>
180
+ ))}
181
+ </tbody>
182
+ </table>
183
+ </Container>
184
+ );
185
+ };
186
+
187
+ export default DataTable;
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ interface PaginationProps {
4
+ from: number;
5
+ to: number;
6
+ total: number;
7
+ onPrev: () => void;
8
+ onNext: () => void;
9
+ }
10
+ export default function Pagination(props: PaginationProps) {
11
+ const { from , to, total, onNext, onPrev } = props;
12
+ const { t } = useTranslation();
13
+ return (
14
+ <nav
15
+ className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6"
16
+ aria-label="Pagination"
17
+ >
18
+ <div className="hidden sm:block">
19
+ <p className="text-sm text-gray-700">
20
+ Showing <span className="font-medium">{from}</span> to <span className="font-medium">{to}</span> of{" "}
21
+ <span className="font-medium">{total}</span> results
22
+ </p>
23
+ </div>
24
+ <div className="flex flex-1 justify-between sm:justify-end">
25
+ <button
26
+ onClick={() => {
27
+ onPrev();
28
+ }}
29
+ className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
30
+ >
31
+ {t('actions.prev')}
32
+ </button>
33
+ <button
34
+ onClick={() => {
35
+ onNext()
36
+ }}
37
+ className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
38
+ >
39
+ {t('actions.next')}
40
+ </button>
41
+ </div>
42
+ </nav>
43
+ );
44
+ }
@@ -0,0 +1,43 @@
1
+ import React, { ReactElement } from "react";
2
+
3
+ function classNames(...classes: string[]) {
4
+ return classes.filter(Boolean).join(' ')
5
+ }
6
+
7
+ export interface ITabOption {
8
+ label: string | ReactElement;
9
+ key: string;
10
+ }
11
+ interface DefaultProps {
12
+ tabs: ITabOption[];
13
+ selectedKey: string;
14
+ onSelected: (selectedKey: string, index: number) => void;
15
+ allowEdit?: boolean;
16
+ onEditLabel?: (label: string, index: number) => void;
17
+ }
18
+ export default function Default(props: DefaultProps) {
19
+ const { tabs, selectedKey, onSelected } = props;
20
+
21
+ return (
22
+ <div className="border-b border-gray-200 mb-2" >
23
+ <nav className="-mb-px flex space-x-8" role="tablist" aria-label="Tabs">
24
+ {tabs.map((tab, tabIndex) => (
25
+ <span
26
+ role="tab"
27
+ tabIndex={0}
28
+ onClick={() => {
29
+ onSelected(tab.key, tabIndex)
30
+ }}
31
+ key={tab.key}
32
+ className={classNames(
33
+ tab.key === selectedKey
34
+ ? 'border-indigo-500 text-indigo-600'
35
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
36
+ 'whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm'
37
+ )}
38
+ >{tab.label}</span>
39
+ ))}
40
+ </nav>
41
+ </div>
42
+ );
43
+ }
@@ -11,14 +11,14 @@ export interface ITabOption {
11
11
  key: string;
12
12
  options?: Record<string, any>;
13
13
  }
14
- interface PureTabsProps {
14
+ interface EditableTabsProps {
15
15
  tabs: ITabOption[];
16
16
  selectedKey: string;
17
17
  onSelected: (selectedKey: string, index: number) => void;
18
18
  allowEdit?: boolean;
19
19
  onEditLabel?: (label: string, index: number) => void;
20
20
  }
21
- export default function PureTabs(props: PureTabsProps) {
21
+ export default function EditableTabs(props: EditableTabsProps) {
22
22
  const { tabs, selectedKey, onSelected, allowEdit, onEditLabel } = props;
23
23
  const [editList, setEditList] = useState<boolean[]>([]);
24
24
  const { t } = useTranslation();
@@ -66,7 +66,7 @@ export default function PureTabs(props: PureTabsProps) {
66
66
  tab.key === selectedKey
67
67
  ? "text-black bg-gray-100"
68
68
  : "text-gray-500 hover:text-gray-700",
69
- "whitespace-nowrap border-gray-200 py-1 px-2 border-t border-r border-b pr-6 text-sm cursor-pointer"
69
+ "whitespace-nowrap border-gray-200 py-1 px-2 border-r border-t border-b pr-6 text-sm cursor-pointer"
70
70
  )}
71
71
  />
72
72
  ))}
@@ -3,7 +3,7 @@ import { useState } from "react";
3
3
  import CSVData from "./csvData";
4
4
  import PublicData from "./publicData";
5
5
  import { useTranslation } from "react-i18next";
6
- import PureTabs from "../../components/tabs/pureTab";
6
+ import PureTabs from "../../components/tabs/editableTab";
7
7
 
8
8
  const DataSelection: React.FC = (props) => {
9
9
  const [sourceType, setSourceType] = useState<"file" | "public">("file");
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import DatasetTable from "../../components/dataTable";
3
+ import { observer } from "mobx-react-lite";
4
+ import { useGlobalStore } from "../../store";
5
+
6
+ const DatasetConfig: React.FC = (props) => {
7
+ const { commonStore, vizStore } = useGlobalStore();
8
+ const { currentDataset } = commonStore;
9
+ const { dataSource, rawFields } = currentDataset;
10
+ return (
11
+ <div>
12
+ <DatasetTable size={100} data={dataSource} metas={rawFields}
13
+ onMetaChange={(fid, fIndex, diffMeta) => {
14
+ commonStore.updateCurrentDatasetMetas(fid, diffMeta)
15
+ }}
16
+ />
17
+ </div>
18
+ );
19
+ };
20
+
21
+ export default observer(DatasetConfig);