From 18211f826e14fe7995d47c205231ade32dc9c1aa Mon Sep 17 00:00:00 2001 From: Kasper Welbers Date: Fri, 17 Nov 2023 15:32:53 +0100 Subject: [PATCH] add visualizations --- package.json | 4 +- public/port-0.0.0-py3-none-any.whl | Bin 10001 -> 10604 bytes .../py/dist/port-0.0.0-py3-none-any.whl | Bin 10001 -> 10604 bytes src/framework/processing/py/port/script.py | 188 +++++++++++++++++- src/framework/types/visualizations.ts | 5 +- .../react/ui/elements/figure.tsx | 10 +- .../ui/elements/figures/recharts_graph.tsx | 3 +- .../react/ui/elements/table_container.tsx | 1 + .../react/ui/prompts/consent_form.tsx | 3 +- .../prepareChartData.ts | 175 +++++++++++----- .../visualizationDataFunctions/util.ts | 144 +++++++++----- src/index.tsx | 2 +- 12 files changed, 422 insertions(+), 113 deletions(-) diff --git a/package.json b/package.json index 6945f87..dce1be5 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "release": "npm run build && npm run archive", "test": "react-scripts test", "lint": "npm run fix:ts", - "watch": "concurrently 'npm run dev:start' 'nodemon --ext py --exec \"npm run build:py\"'", + "watch": "concurrently 'npm run start:app' 'nodemon --ext py --exec \"npm run build:py\"'", "watch:ts": "tsc -watch" }, "browserslist": { @@ -76,4 +76,4 @@ "src/framework/processing/py_worker.js" ] } -} \ No newline at end of file +} diff --git a/public/port-0.0.0-py3-none-any.whl b/public/port-0.0.0-py3-none-any.whl index fcca411d1ffa1d045956a40a26d5b044868902a9..63bd6fe9744c1d1266bf995a2dac711e92905337 100644 GIT binary patch delta 6414 zcmV+p8S&!y$pZ-%5q(?761UKX8-^W0001RZ*p`mb7OL8aC9zkdF5SuZzH*p z|KFd2+k?4nZ8WmIKn{p;&S5f--3cbMgZ1nlZUv*!Qd3f|THQiF?6D04`5<>+E}!J; z!Dl~Y+4F#tQv#1f7K_DVvFcZBHaFdDi?SAJEz7znvg#n^0{nk7iMQ!wTNc|&#Fd~g z2aB@U*vfga*~EEL3DxQa|315_YPoq?y6~4bKg&Fk<$>t}3S(xS=(6i{yGS#s zfBs$-x&EF2vpU_Fy6shct-mkwO3=_ax_S0A5l??S zdHT1Lr)R+d(8&d;zN}cmdsIN~A0sgrz zaybE3Bj&k|bRn?Bi8Dmux33y7EP430$de-fmK&6^Zpz$kH_35SS$Zu)SHekL8BPxM zXrua?O-%gBEj5>~iFG_4$gv$U#ri43L*#tcI38r^{sOA&WuQvFZQU1^xu5e2z^Em3Z6 zcZyQgC9JNmB}@`!S0_`Kj2f&E&!-@P7`Pc$y53hh^CO`Aeo+m_8Y4%VH_{Ydr7}xwQ%@%iXk>Opo+|n6>N&&*I zBo2RUBSN4a3t|X5LHZ9+1%+rpi3CYEX)Y5nyP`Il_gGx6()mi%@mdmJ?c|Hu?+`8p zL}O=yfFjp5Rg>27ETis41pyMR0LVpC#Y>3+oTY?_8<$tu;l#?t>_8l}29yvPuyjPI z#>*=KPy=fUJ+6(}VTwcDe`XDvsUE!5_UM0H{NYp_ySB&@g|^uQzgD3aMdB=r7HJ79 zgerY&4%6w;ISH4dP)wm3uZpJ3u4u|p<$gmfnUr#y#d8_Lv^BLnh3H>NFs%f?ZPIyJ z=%1Qp5~sU4;-3(ypMVeP`S7T+ViqdEH3mpfgXQEtGli=ryZ|)u1Q8TDi;-=oRRw=c zBZO^3oKZ>G9`&?K0{mR2^-4p+34nw=#FitpxP`wGdEsHdldOo7aItIfPE0|_1~{?n zLcAqNri)7uL1X1FMsgTq0lr}@lBSGt6*$Y1{Ru7>1aWaG+v^TiKh`2c-5gg*OqQL< ztd5Bn6Z^}>3JuGJ=0jPsGa3_OGSGjXQ;xCaXb27TlOK1<9THdoW6}EQK|&f!cr+TF zvv#!ZL3Y)x#>5^Ms$l@!Ek|Plq<#VJuqn}^z*s!T>NGh%IUjl6ADcX*=HTLcMCIjJ zo&Is;+nN>}G9WO?B=k%GC4I&t8IAmAYIM-8Dd%zNl6x}N-b=Oe!lO*slc|3I+r}p3 zpcFQ7qJ9gQ7*;Mh#V@}`c|=zzG#_H@Q+ghCzKZK;#b9CpAsF}xVBlq1Vles}3>Tsf z;{)K}!V&zfe;T-upv9F()m+!8Dstw8 z6#h*iOms-9v&45Udx`%|zxbPY8`T9vw5$NPB+FE`LME3@fK)TbKzSk-!3}=7eMbNM z;rQ0fBts~+xOUJW<){ucfH*QP0xd|#o1MW4KTpJ20p1y_E3t|%q=SEL#-ad6UAi&B zRN`m=zpE_OVON74iMkMRb{Suprd2^cA2JJr?&d`z#br^hD=1#ZWdagZ6W3*>0KmZo zH7W0qwx#^&x*TjJ}&-bujYheWTWWt3Rql-*gjHm_eKf08IgS3YlKW1b^P{ zV+Mg{f0*X8;^X0W-yNzFBaX8#fE!zwP&Hu0+AX?eJZw+*iknvd)N~k}`%{6d6Vg5! z3{JVCn_@`Zgrw9BhYNL(YZoDHlr&Kv!?)-z(}e84g|m^ysWJd8qum$I6d z=@wTl*vHhh!crfm(0KK#;-lNgB3w7oy(4=8HTGf;{SK<==akrK@ii=cU zcI$Nb6J#JmmBxPzS_IFN9=_-HYw(V?H9Bf>U9VSxAG?SaT6qyb+GwFQc4*7hVgg#; z&arz0zQvBl_wT<`afdwcO-a*jS;~x{c(qEmm2KG~?UmB^iVqkE`a`xJa)^36r8271 z1z`C}8k_nrp}{y|-POf5omYpnp;Sf6ZU?oOZfN@)^{0O!SL8R{N0FZ@EJqVI_j`%T z4J#?zEm~sw{%FcR`+PTzS()cwV#xUO0RzTnyN9c!z2(&HBCQ%kIlM$TfpcB|EB573 z`7>>t+7ShWUzTOO(T?8flb$e5K9q4@!Ro^uCtnpefkclToN-HtrTnNPGxs(?&uYy~{$Qjf&nY@I4UV}6;bmN6X;}4a~ElTH*l~87)cmYn}HsYI?4~9-wDN}!s%m=o6gDa~qont#4^dqfvKa028 zH5~kBX@-lTj5Z*LI26Z6M@MSjk_A2obItE2c%B#e)uw1F&=(l86&2>^jG?XV{8sas za@{%egQUv{V(Uheq@F;$T~K-JsbDY~TGOp{`5yoN@&Pp}1^!Ii=!q4rHFQidk zZRKeI*>l`#xh#ujd)n354_FSXH)}TeDbEY45fBU{gmAZb^} z_{rj#%uWNlanehrMf1$CpEO=BF+Z(A1LS{>hRBlSH(3@ifDdKU6TJL|<-|j-ctA)Q zaKsM?F$3mj!^~e~na)?WV_E$%CRq%{lHO3FADs=vkcas8U0~92>+KDK=^)H(`P8mQ z?3%0ZB58v`CjhutMED=_!4J5u&mGqpw7l=I_k7QJRn(Me@0KLQqg<6&{n%g<2%({d}Z(1*5Vyr^nG92uRo90f405PZpV2uU|`lss+u@1 zJwN$y_sE%9mab*e@{CQHeIf=%uGJy&=6?2Yk<>={NBZ7&j+mk_;I^ZHzKYz&#(TY zIww9bLFZrDIZ>-unsXy~N@tf%@RlBEn|XR|B)Z204ecaDE*F?Cs;Pd`E&oK$!?SOpO3ocT>kSXD)wDWtlEl!?mm8 z@-!$KSR<-%#K6Qim}vlZEQi34tLR|>baVv&cXG}T>U;!oZJ(LgqZeEeKB96>J%I>B zLC_&_6pX}7s0`p#4H4|r2sRcms#(n7uak=J>WAvV-m@GJd#OOBMvTnPNmBlbr}qKQ z*yEj0F|#zYUYho*L>GU?2c7Tm<^#ukjMRGthOtLJ?5E9IPnY$DdiZ4?;K8bC6b8fR_Y2FrKzf3{-g0H&CqD+5ccaE@=$jK6_X7kV- zpHr z7dw{4(%AQ=aHoI7uF~;m>ekuSG<~{zm+c-09!~Lz6Kc{q;=lvwV2L}|EH!V>i8-J? zFje=1AYI|yx6KfSv^%1W(Dfrn9_FMWVh>2PPPzJu&BfseO) z?Y0ind>!C%Z{*Q+LHBw_<*bscXa~G+c32H0n_JzLDccJl7ZJb(}FbPG4ty zBt{Z|6U{(05i2PdH4v}ie|;14uNGVWwUXMtw;kGp*+uh6<4WhHh#j%yS4C@gaOn3Z z1P%2HXM}%G?022`qBklHhFt%Ew<6WgBXGm}3}2_knS7PgguYa7VA}%|nr7&LEm!V$>S1d5ix8ZndcSx? zyw!fcef!uYR~^&^c-)9D1E|80I28YWG@5{e%aecbd;dWIZpw#hRPo2OIz8^IH~cNp zV~k(#OzcuB6V(rP|d_CG7 z>7H|x<9;#3RW-1{;OBHb5a>QxjxQMrwdH@^Bgi~`cZ84rMe_B+$)`sX*PrhfQ_i@> zz$zLzCtrJ|jmEByej{(NcZn>+$ZtcK1??DlE zP&lV|#_n`q)2(i&=Jt`_((P;bmo?{E#MEdoq(=Pw+Z3XAusX)`HG}ubVXRVb$bcy< zvf^^^E;(S+wnPxt-fRcHzf}$9G!0UUa^!!O@r!+$j=y~4;qB6S7FU%xTc_I(#XA_r zSuMBWtB-R@ug{tngYl|7MT>)YsmAc0FU2cLl?H%W#Lh4}DA+F1-Zuia-Lv0yBT%EHkas=gzHo84XvN!SO zr&nbO6BR?Y+27+6QO0Q{d+VGr@#uf?_O3;$oGres2J5-J;@cd7m~W8m883bY^hq4T z{wC}#x?{_#fx)DU9u4+hE$Dt~e4TR%>A>);?~+n=Y`6?}grCm+XZw#%e+JZopB^hL z@MeiQE}KP@jm5ISc>^i%&0D&U>3;L2T814xkGy4SmDXCTRM=i+CBL|*{dj-0jh8J{ zY`TMj+(EEqLS(r(oFK5HMl#42`TPQ z<@gKmw7f{?aw1-2>3l8z@%MlKK`;4_zyGfQa=KFmLAIC`bzOjZ!JLstQi!~$`9hai z+;|$krT!+OZ<~-H*dq9~$d+_syCc1%&9627yOK4f2sjwt11q*m2`XCx1@C)7j?i3+ zc)N|uIEGB%c~$}0QUXC+Ce4W(t^uO}(;PX#kbi=72N=gWjvLU>s;+-PKVW#1)*x`PpgP320x)jGSv<@B`zC2sQTm0-eRYEtYz~ z&hr&4HG9L5x>&qHt^K@1nP=M|U$AtTseYg?sp%w5#>`|w(&---*Xj}<+xqiN$5rWm zOLg|Z)Y?>#=8HIXx0T%KbT^mKA_Mz!z56aB(Q}`@>#s9hPvCz5r^C^+nVqpCXV04H zg+O#^g{4s?pSqauF`qn@fcmFaE9b3U2+te18-uPt@I~*9e}6su>D{nHpwUT#C+H-J zZz`+|rVI5#RVl%n<MisWi+w52*;$vB~lLcFej zq|$IW5?QVsgUpK5@pDlWb(imJDh-VcJI8LU#dgNh>Y;ES_fStK4KNvidKQF0Wj7f2 zfw%_Yzu?Yc-Jro`AlN1NxHv~Ntc;dn^-UWNzfkF&!O(bHq`LY=BXdfWW#Zs&Zu7`s3*E0W4@k30g8rUFs7BImxi4PmLz#Wk~;d6xN zG0vFba+yMO1kevW++o}vSU*&(ccUevBIjVN-+O+b0UN3nyALRYLy;2%NGI^=5;E8sFrrqDgT7 zRqH&^%B6q403@}ru?_Ot&<V6^%blwj=YbL3zWH!-44jAaoD=GgxrS@UI01^}A~vGU*yzJk_Jl4f#iQd;SwN-4?(wMG%8? zs1RZlZD1)zk%EW%zXWqtv2Ca;vElb5E|(W0@n`ow>@9FL+ej)QQt=1H}=gCMqg4YTKKHctofGkIJB#n zhmMgBei~dVxc*{JoPU^7z8#i34*Zu+O;wTl+&xL-G#E-IG%Az<$rW}#2)XSUSDeIF zRcd^S#~KxU-jR+|z|$3L8}6>y`3^_EBAI@>?CO@8JD`ANlorMQ3gN~oz8@aQs1>q`(eoTxxa`RDN9{kt>mdY;x< z^YOI^b)<*-FKfGDG|s5JYBp}zhC9=9bYhjVdn|;9ozdw4^h7WNNjh@!CMp7$B$G}d7n7|f6&(Ko@pz>H004Lb000{R c0000000000005)`B_)&ICM^a7CjbBd04oEB%>V!Z delta 5808 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 delta 5808 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 uIAo>JqyYc`cme { + console.log('okdss') const [visualizationData, status] = useVisualizationData(table, visualization) const { title } = useMemo(() => { @@ -47,7 +48,7 @@ export const Figure = ({ const { errorMsg, noDataMsg } = useMemo(() => prepareCopy(locale), [locale]) - if ((visualizationData == null) && status === 'loading') { + if (visualizationData == null && status === 'loading') { return (
@@ -55,7 +56,9 @@ export const Figure = ({ ) } - if (status === 'error') { return
{errorMsg}
} + if (status === 'error') { + return
{errorMsg}
+ } const visualizationHeightTruthy = Boolean(visualization.height) const minHeight = visualizationHeightTruthy ? `${visualization.height ?? ''} px` : '20rem' @@ -95,8 +98,7 @@ const RenderVisualization = memo( } if (visualizationData.type === 'wordcloud') { - const textVisualizationData: TextVisualizationData = - visualizationData + const textVisualizationData: TextVisualizationData = visualizationData if (textVisualizationData.topTerms.length === 0) return fallback return } diff --git a/src/framework/visualisation/react/ui/elements/figures/recharts_graph.tsx b/src/framework/visualisation/react/ui/elements/figures/recharts_graph.tsx index 6d1d155..e69bfa0 100644 --- a/src/framework/visualisation/react/ui/elements/figures/recharts_graph.tsx +++ b/src/framework/visualisation/react/ui/elements/figures/recharts_graph.tsx @@ -23,6 +23,7 @@ interface Props { } export default function RechartsGraph ({ visualizationData }: Props): JSX.Element | null { + console.log(visualizationData) function tooltip (): JSX.Element { return ( - {axes(0)} + {axes(5)} {tooltip()} {legend()} {Object.values(visualizationData.yKeys).map((yKey: AxisSettings, i: number) => { diff --git a/src/framework/visualisation/react/ui/elements/table_container.tsx b/src/framework/visualisation/react/ui/elements/table_container.tsx index 9addec3..68b6f35 100644 --- a/src/framework/visualisation/react/ui/elements/table_container.tsx +++ b/src/framework/visualisation/react/ui/elements/table_container.tsx @@ -22,6 +22,7 @@ export const TableContainer = ({ updateTable, locale }: TableContainerProps): JSX.Element => { + console.log('refresh2') const tableVisualizations = table.visualizations != null ? table.visualizations : [] const [searchFilterIds, setSearchFilterIds] = useState>() const [search, setSearch] = useState('') diff --git a/src/framework/visualisation/react/ui/prompts/consent_form.tsx b/src/framework/visualisation/react/ui/prompts/consent_form.tsx index 0590a9d..b34ab4d 100644 --- a/src/framework/visualisation/react/ui/prompts/consent_form.tsx +++ b/src/framework/visualisation/react/ui/prompts/consent_form.tsx @@ -80,7 +80,8 @@ export const ConsentForm = (props: Props): JSX.Element => { function rows (data: any): PropsUITableRow[] { const result: PropsUITableRow[] = [] - for (let row = 0; row <= rowCount(data); row++) { + const n = rowCount(data) + for (let row = 0; row <= n; row++) { const id = `${row}` const cells = columnNames(data).map((column: string) => rowCell(data, column, row)) result.push({ __type__: 'PropsUITableRow', id, cells }) diff --git a/src/framework/visualisation/react/ui/workers/visualizationDataFunctions/prepareChartData.ts b/src/framework/visualisation/react/ui/workers/visualizationDataFunctions/prepareChartData.ts index f56d9a2..0aeba7d 100644 --- a/src/framework/visualisation/react/ui/workers/visualizationDataFunctions/prepareChartData.ts +++ b/src/framework/visualisation/react/ui/workers/visualizationDataFunctions/prepareChartData.ts @@ -10,7 +10,48 @@ export async function prepareChartData ( table: PropsUITable & TableContext, visualization: ChartVisualization ): Promise { - const visualizationData: ChartVisualizationData = { + if (table.body.rows.length === 0) return emptyVisualizationData(visualization) + + const aggregate = aggregateData(table, visualization) + return createVisualizationData(visualization, aggregate) +} + +function createVisualizationData ( + visualization: ChartVisualization, + aggregate: Record +): ChartVisualizationData { + const visualizationData = emptyVisualizationData(visualization) + + for (const aggdata of Object.values(aggregate)) { + for (const group of Object.keys(aggdata.values)) { + if (visualizationData.yKeys[group] === undefined) { + visualizationData.yKeys[group] = { + label: group, + secondAxis: aggdata.secondAxis, + tickerFormat: aggdata.tickerFormat + } + } + } + } + + visualizationData.data = Object.values(aggregate) + .sort((a: any, b: any) => (a.sortBy < b.sortBy ? -1 : b.sortBy < a.sortBy ? 1 : 0)) + .map((d) => { + for (const key of Object.keys(d.values)) d.values[key] = Math.round(d.values[key] * 100) / 100 + + return { + ...d.values, + [d.xLabel]: d.xValue, + __rowIds: d.rowIds, + __sortBy: d.sortBy + } + }) + + return visualizationData +} + +function emptyVisualizationData (visualization: ChartVisualization): ChartVisualizationData { + return { type: visualization.type, xKey: { label: @@ -21,34 +62,27 @@ export async function prepareChartData ( yKeys: {}, data: [] } +} - if (table.body.rows.length === 0) return visualizationData +function aggregateData ( + table: PropsUITable & TableContext, + visualization: ChartVisualization +): Record { + const aggregate: Record = {} - // First get the unique values of the x column + const { groupBy, xSortable } = prepareX(table, visualization) const rowIds = table.body.rows.map((row) => row.id) + const xLabel = + visualization.group.label !== undefined ? visualization.group.label : visualization.group.column - let groupBy = getTableColumn(table, visualization.group.column) - // KASPER CHECK: I think the first clause in the statement can go - // getTableColumn will return a string array or errs out - // so only check for length is still doing something - if (groupBy.length === 0) { - throw new Error(`X column ${table.id}.${visualization.group.column} not found`) - } - let xSortable: Array | null = null // separate variable allows using epoch time for sorting dates - - // ADD CODE TO TRANSFORM TO DATE, BUT THEN ALSO KEEP AN INDEX BASED ON THE DATE ORDER - if (visualization.group.dateFormat !== undefined) { - ;[groupBy, xSortable] = formatDate(groupBy, visualization.group.dateFormat) - } - - const aggregate: Record = {} for (const value of visualization.values) { + // loop over all y values + const aggFun = value.aggregate !== undefined ? value.aggregate : 'count' let tickerFormat: TickerFormat = 'default' if (aggFun === 'pct' || aggFun === 'count_pct') tickerFormat = 'percent' const yValues = getTableColumn(table, value.column) - // KASPER CHECK if (yValues.length === 0) throw new Error(`Y column ${table.id}.${value.column} not found`) // If group_by column is specified, the columns in the aggregated data will be the unique group_by columns @@ -57,37 +91,63 @@ export async function prepareChartData ( // if missing values should be treated as zero, we need to add the missing values after knowing all groups const addZeroes = value.addZeroes ?? false const groupSummary: Record = {} - const uniqueGroups = new Set([]) - for (let i = 0; i < groupBy.length; i++) { + // if addZeroes, prefill with all possible values + if (addZeroes && xSortable != null) { + for (const [uniqueValue, sortby] of Object.entries(xSortable)) { + if (aggregate[uniqueValue] !== undefined) continue + aggregate[uniqueValue] = { + sortBy: sortby, + rowIds: {}, + xLabel, + xValue: uniqueValue, + values: {}, + secondAxis: value.secondAxis, + tickerFormat + } + } + } + + for (let i = 0; i < rowIds.length; i++) { + // loop over rows of table const xValue = groupBy[i] + + if (visualization.group.range !== undefined) { + if ( + Number(xValue) < visualization.group.range[0] || + Number(xValue) > visualization.group.range[1] + ) { + continue + } + } + + // SHOULD GROUP BE IGNORED IF NOT IN group.levels? MAYBE NOT, BECAUSE + // THIS COULD HARM INFORMED CONSENT IF THE RESEARCHER IS UNAWARE OF CERTAIN GROUPS + // if (visualization.group.levels !== undefined) { + // // formatLevels has xSortable < 0 if no match with levels + // if (xSortable !== null && xSortable[i] < 0) continue + // } + const yValue = yValues[i] - const group = - yGroup != null ? yGroup[i] : value.label !== undefined ? value.label : value.column - if (addZeroes) uniqueGroups.add(group) - const sortBy = xSortable != null ? xSortable[i] : groupBy[i] + const label = value.label !== undefined ? value.label : value.column + const group = yGroup != null ? yGroup[i] : label + + const sortBy = xSortable != null ? xSortable[xValue] : groupBy[i] // calculate group summary statistics. This is used for the mean, pct and count_pct aggregations if (groupSummary[group] === undefined) groupSummary[group] = { n: 0, sum: 0 } if (aggFun === 'count_pct' || aggFun === 'mean') groupSummary[group].n += 1 if (aggFun === 'pct') groupSummary[group].sum += Number(yValue) ?? 0 - // add the AxisSettings for the yKeys in this loop, because we need to get the unique group values from the data (if group_by is used) - if (visualizationData.yKeys[group] === undefined) { - visualizationData.yKeys[group] = { - label: group, - secondAxis: value.secondAxis !== undefined, - tickerFormat - } - } - if (aggregate[xValue] === undefined) { aggregate[xValue] = { sortBy: sortBy, rowIds: {}, - xLabel: visualizationData.xKey.label, + xLabel, xValue: String(xValue), - values: {} + values: {}, + secondAxis: value.secondAxis, + tickerFormat } } if (aggregate[xValue].rowIds[group] === undefined) aggregate[xValue].rowIds[group] = [] @@ -100,6 +160,7 @@ export async function prepareChartData ( } } + // use groupSummary to calculate the mean, pct and count_pct aggregations Object.keys(groupSummary).forEach((group) => { for (const xValue of Object.keys(aggregate)) { if (aggregate[xValue].values[group] === undefined) { @@ -122,19 +183,35 @@ export async function prepareChartData ( }) } - visualizationData.data = Object.values(aggregate) - .sort((a: any, b: any) => (a.sortBy < b.sortBy ? -1 : b.sortBy < a.sortBy ? 1 : 0)) - .map((d) => { - for (const key of Object.keys(d.values)) d.values[key] = Math.round(d.values[key] * 100) / 100 - return { - ...d.values, - [d.xLabel]: d.xValue, - __rowIds: d.rowIds, - __sortBy: d.sortBy - } - }) + return aggregate +} - return visualizationData +function prepareX ( + table: PropsUITable & TableContext, + visualization: ChartVisualization +): { groupBy: string[], xSortable: Record | null } { + let groupBy = getTableColumn(table, visualization.group.column) + if (groupBy.length === 0) { + throw new Error(`X column ${table.id}.${visualization.group.column} not found`) + } + // let xSortable: Array | null = null // separate variable allows using epoch time for sorting dates + let xSortable: Record | null = null // map x values to sortable values + + // ADD CODE TO TRANSFORM TO DATE, BUT THEN ALSO KEEP AN INDEX BASED ON THE DATE ORDER + if (visualization.group.dateFormat !== undefined) { + ;[groupBy, xSortable] = formatDate(groupBy, visualization.group.dateFormat) + } + + if (visualization.group.levels !== undefined) { + xSortable = {} + + for (let i = 0; i < visualization.group.levels.length; i++) { + const level = visualization.group.levels[i] + xSortable[level] = i + } + } + + return { groupBy, xSortable } } export interface PrepareAggregatedData { @@ -143,4 +220,6 @@ export interface PrepareAggregatedData { values: Record rowIds: Record sortBy: number | string + secondAxis?: boolean + tickerFormat?: TickerFormat } diff --git a/src/framework/visualisation/react/ui/workers/visualizationDataFunctions/util.ts b/src/framework/visualisation/react/ui/workers/visualizationDataFunctions/util.ts index dcafe84..336fa4c 100644 --- a/src/framework/visualisation/react/ui/workers/visualizationDataFunctions/util.ts +++ b/src/framework/visualisation/react/ui/workers/visualizationDataFunctions/util.ts @@ -1,84 +1,128 @@ import { PropsUITable, TableContext } from '../../../../../types/elements' import { DateFormat } from '../../../../../types/visualizations' -export function autoFormatDate (dateNumbers: number[], minValues: number): DateFormat { - const minTime = Math.min(...dateNumbers) - const maxTime = Math.max(...dateNumbers) - - let autoFormat: DateFormat = 'hour' - if (maxTime - minTime > 1000 * 60 * 60 * 24 * minValues) autoFormat = 'day' - if (maxTime - minTime > 1000 * 60 * 60 * 24 * 30 * minValues) autoFormat = 'month' - if (maxTime - minTime > 1000 * 60 * 60 * 24 * 30 * 3 * minValues) autoFormat = 'quarter' - if (maxTime - minTime > 1000 * 60 * 60 * 24 * 365 * minValues) autoFormat = 'year' - - return autoFormat -} - export function formatDate ( dateString: string[], format: DateFormat, minValues: number = 10 -): [string[], number[] | null] { +): [string[], Record | null] { let formattedDate: string[] = dateString const dateNumbers = dateString.map((date) => new Date(date).getTime()) - let sortableDate: number[] | null = null + let domain: [number, number] | null = null + let formatter: (date: Date) => string = (date) => date.toISOString() if (format === 'auto') format = autoFormatDate(dateNumbers, minValues) - if (format === 'year') { - formattedDate = dateNumbers.map((date) => new Date(date).getFullYear().toString()) - sortableDate = dateNumbers - } + if (format === 'year') formatter = (date) => date.getFullYear().toString() + if (format === 'quarter') { - formattedDate = dateNumbers.map((date) => { - const year = new Date(date).getFullYear().toString() - const quarter = Math.floor(new Date(date).getMonth() / 3) + 1 + formatter = (date) => { + const year = date.getFullYear().toString() + const quarter = Math.floor(date.getMonth() / 3) + 1 return `${year}-Q${quarter}` - }) - sortableDate = dateNumbers + } } + if (format === 'month') { - formattedDate = dateNumbers.map((date) => { - const year = new Date(date).getFullYear().toString() - const month = new Date(date).toLocaleString('default', { month: 'short' }) - return year + '-' + month - }) - sortableDate = dateNumbers + formatter = (date) => { + const year = date.getFullYear().toString() + const month = date.toLocaleString('default', { month: 'short' }) + return `${year}-${month}` + } } + if (format === 'day') { - formattedDate = dateNumbers.map((date) => new Date(date).toISOString().split('T')[0]) - sortableDate = dateNumbers + formatter = (date) => { + const year = date.getFullYear().toString() + const month = date.toLocaleString('default', { month: 'short' }) + const day = date.getDate().toString() + return `${year}-${month}-${day}` + } } + if (format === 'hour') { - formattedDate = dateNumbers.map( - (date) => new Date(date).toISOString().split('T')[1].split(':')[0] - ) - sortableDate = dateNumbers + formatter = (date) => { + return date.toISOString().split('T')[1].split(':')[0] + } } + if (format === 'month_cycle') { - const formatter = new Intl.DateTimeFormat('default', { month: 'long' }) - formattedDate = dateNumbers.map((date) => formatter.format(new Date(date))) - sortableDate = dateNumbers.map((date) => new Date(date).getMonth()) + formatter = (date) => { + const intlFormatter = new Intl.DateTimeFormat('default', { month: 'long' }) + return intlFormatter.format(date) + } + // can be any year, starting at january + domain = [new Date('2000-01-01').getTime(), new Date('2001-01-01').getTime()] } if (format === 'weekday_cycle') { - const formatter = new Intl.DateTimeFormat('default', { weekday: 'long' }) - formattedDate = dateNumbers.map((date) => formatter.format(new Date(date))) - sortableDate = dateNumbers.map((date) => new Date(date).getDay()) - } - if (format === 'day_cycle') { - const formatter = new Intl.DateTimeFormat('default', { day: 'numeric' }) - formattedDate = dateNumbers.map((date) => formatter.format(new Date(date))) - sortableDate = dateNumbers.map((date) => new Date(date).getDay()) + formatter = (date) => { + const intlFormatter = new Intl.DateTimeFormat('default', { weekday: 'long' }) + return intlFormatter.format(date) + } + // can be any full week, starting at monday + domain = [new Date('2023-11-06').getTime(), new Date('2023-11-13').getTime()] } if (format === 'hour_cycle') { - const formatter = new Intl.DateTimeFormat('default', { hour: 'numeric' }) - formattedDate = dateNumbers.map((date) => formatter.format(new Date(date))) - sortableDate = dateNumbers.map((date) => new Date(date).getHours()) + formatter = (date) => { + const intlFormatter = new Intl.DateTimeFormat('default', { hour: 'numeric', hour12: false }) + return intlFormatter.format(date) + } + // can be any day, starting at midnight + domain = [new Date('2000-01-01').getTime(), new Date('2000-01-02').getTime()] } + formattedDate = dateNumbers.map((date) => formatter(new Date(date))) + if (domain == null) domain = [Math.min(...dateNumbers), Math.max(...dateNumbers)] + const sortableDate: Record | null = createSortable(domain, format, formatter) + return [formattedDate, sortableDate] } +function autoFormatDate (dateNumbers: number[], minValues: number): DateFormat { + const minTime = Math.min(...dateNumbers) + const maxTime = Math.max(...dateNumbers) + + let autoFormat: DateFormat = 'hour' + if (maxTime - minTime > 1000 * 60 * 60 * 24 * minValues) autoFormat = 'day' + if (maxTime - minTime > 1000 * 60 * 60 * 24 * 30 * minValues) autoFormat = 'month' + if (maxTime - minTime > 1000 * 60 * 60 * 24 * 30 * 3 * minValues) autoFormat = 'quarter' + if (maxTime - minTime > 1000 * 60 * 60 * 24 * 365 * minValues) autoFormat = 'year' + + return autoFormat +} + +function createSortable ( + domain: [number, number], + interval: string, + formatter: (date: Date) => string +): Record | null { + // creates a map of datestrings to sortby numbers. Also includes intervalls, so that + // addZeroes can be used. + const sortable: Record = {} + const [minTime, maxTime] = domain + + // intervalnumbers don't need to be exact. Just small enough that they never + // skip over an interval (e.g., month should be shortest possible month). + // Duplicate dates are ignored in set + let intervalNumber: number = 0 + if (interval === 'year') intervalNumber = 1000 * 60 * 60 * 24 * 364 + if (interval === 'quarter') intervalNumber = 1000 * 60 * 60 * 24 * 28 * 3 + if (['month', 'month_cycle'].includes(interval)) intervalNumber = 1000 * 60 * 60 * 24 * 28 + if (['day', 'weekday_cycle'].includes(interval)) intervalNumber = 1000 * 60 * 60 * 24 + if (['hour', 'hour_cycle'].includes(interval)) intervalNumber = 1000 * 60 * 60 + + if (intervalNumber > 0) { + for (let i = minTime; i <= maxTime; i += intervalNumber) { + const date = new Date(i) + const datestring = formatter(date) + if (sortable[datestring] !== undefined) continue + sortable[datestring] = i + } + } + + return sortable +} + export function tokenize (text: string): string[] { const tokens = text.split(' ') return tokens.filter((token) => /\p{L}/giu.test(token)) // only tokens with word characters diff --git a/src/index.tsx b/src/index.tsx index acf5e1b..168249e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,7 +19,7 @@ const run = (system: Storage): void => { assembly.processingEngine.start() } -if (process.env.REACT_APP_BUILD!=='standalone' && process.env.NODE_ENV === 'production') { +if (process.env.REACT_APP_BUILD !== 'standalone' && process.env.NODE_ENV === 'production') { // Setup embedded mode (requires to be embedded in iFrame) console.log('Initializing storage system') LiveStorage.create(window, run)