diff --git a/web/gui-v2/src/components/DetailView.jsx b/web/gui-v2/src/components/DetailView.jsx index 9c414310..14b36c80 100644 --- a/web/gui-v2/src/components/DetailView.jsx +++ b/web/gui-v2/src/components/DetailView.jsx @@ -100,10 +100,10 @@ const DetailView = ({ if ( companyData ) { const breadcrumbs = [ - + ETO PARAT , - + {companyData.name} ]; diff --git a/web/gui-v2/src/components/DetailViewChart.jsx b/web/gui-v2/src/components/DetailViewChart.jsx index f37b31c4..81cca27d 100644 --- a/web/gui-v2/src/components/DetailViewChart.jsx +++ b/web/gui-v2/src/components/DetailViewChart.jsx @@ -1,6 +1,7 @@ import React, { Suspense, lazy } from 'react'; import { css } from '@emotion/react'; +import SectionHeading from './SectionHeading'; import { fallback } from '../styles/common-styles'; const Plot = lazy(() => import('react-plotly.js')); @@ -16,27 +17,22 @@ const styles = { margin: 0.5rem auto 0; max-width: 1000px; `, - chartTitle: css` - font-family: GTZirkonMedium; - font-size: 24px; - margin-bottom: 0; - text-align: center; - `, }; const Chart = ({ config, data, + id, layout, title, }) => { return ( !isSSR && Loading graph...}> -

+ {title} -

+
0 ) { metadata.push({ title: "Stock tickers", - value: data.market_filt.map((e) => {e.market_key}), + value: data.market_filt.map((e) => {e.market_key}), }); } diff --git a/web/gui-v2/src/components/DetailViewMoreMetadataDialog.jsx b/web/gui-v2/src/components/DetailViewMoreMetadataDialog.jsx index 6a28c628..c0e8f114 100644 --- a/web/gui-v2/src/components/DetailViewMoreMetadataDialog.jsx +++ b/web/gui-v2/src/components/DetailViewMoreMetadataDialog.jsx @@ -67,13 +67,13 @@ const MoreMetadataDialog = ({ title: 'Crunchbase', value:
{data.crunchbase.crunchbase_url} - {data.child_crunchbase.map(e => {e.crunchbase_url})} + {data.child_crunchbase.map(e => {e.crunchbase_url})}
}, { title: 'LinkedIn', value:
- {data.linkedin.map(e => {e})} + {data.linkedin.map(e => {e})}
}, { title: 'In S&P 500?', value: data.in_sandp_500 ? 'Yes' : 'No' }, diff --git a/web/gui-v2/src/components/DetailViewPatents.jsx b/web/gui-v2/src/components/DetailViewPatents.jsx index e1281748..7c27f5e4 100644 --- a/web/gui-v2/src/components/DetailViewPatents.jsx +++ b/web/gui-v2/src/components/DetailViewPatents.jsx @@ -1,9 +1,34 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { css } from '@emotion/react'; + +import { Dropdown } from '@eto/eto-ui-components'; -import Chart from './DetailViewChart'; import HeaderWithLink from './HeaderWithLink'; +import StatGrid from './StatGrid'; +import TableSection from './TableSection'; +import TextAndBigStat from './TextAndBigStat'; +import TrendsChart from './TrendsChart'; +import { patentMap } from '../static_data/table_columns'; +import { commas } from '../util'; import { assemblePlotlyParams } from '../util/plotly-helpers'; +const styles = { + section: css` + margin: 2rem auto 1rem; + max-width: 808px; + + h3 { + margin-bottom: 0.5rem; + } + `, + trendsDropdown: css` + .MuiInputBase-input.MuiSelect-select { + align-items: center; + display: flex; + justify-content: center; + } + `, +}; const chartLayoutChanges = { legend: { @@ -16,62 +41,133 @@ const chartLayoutChanges = { }, }; -const PATENT_CHART_LEGEND_GROUPINGS = { - ai_patents: 'col-1', - Security__eg_cybersecurity: 'col-1', - Education: 'col-1', - Networks__eg_social_IOT_etc: 'col-1', - Business: 'col-1', - - Military: 'col-2', - Agricultural: 'col-2', - Life_Sciences: 'col-2', - Entertainment: 'col-2', - Transportation: 'col-2', - - Semiconductors: 'col-3', - Nanotechnology: 'col-3', - Energy_Management: 'col-3', - Banking_and_Finance: 'col-3', - Telecommunications: 'col-3', - - Computing_in_Government: 'col-4', - Industrial_and_Manufacturing: 'col-4', - Physical_Sciences_and_Engineering: 'col-4', - Document_Mgt_and_Publishing: 'col-4', - Personal_Devices_and_Computing: 'col-4', -}; - const DetailViewPatents = ({ data, }) => { - const patentsData = Object.entries(PATENT_CHART_LEGEND_GROUPINGS) - .map(([key, group]) => { - const { name, counts } = data.patents[key]; - return [name.replace(/ patents/i, ''), counts, { legendgroup: group }]; - }); - - const patentsChart = assemblePlotlyParams( - "AI patents over time", + const [aiSubfield, setAiSubfield] = useState("ai_patents"); + + const numYears = data.years.length; + const startIx = numYears - 7; + const endIx = numYears - 2; + + const yearSpanNdash = <>{data.years[startIx]}–{data.years[endIx]}; + // const yearSpanAnd = <>{data.years[startIx]} and {data.years[endIx]}; + + const statGridEntries = [ + { + key: "ai-patents", + stat: <>#{commas(data.patents.ai_patents.rank)}, + text: <>in PARAT for number of AI-related patents, + }, + { + key: "ai-patent-growth", + stat: <>NUM%, + text: <>growth in {data.name}'s AI patenting ({yearSpanNdash}), + }, + { + key: "ai-patent-applications", + stat: <>NUM, + text:
AI patent applications were filed by {data.name} ({yearSpanNdash})
, + }, + { + key: "ai-focused-percent", + stat: <>NUM%, + text: <>of {data.name}'s total patenting was AI-focused, + }, + ]; + + const patentTableColumns = [ + { display_name: "Subfield", key: "subfield" }, + { display_name: "Patents granted", key: "patents" }, + { display_name: <>Growth ({data.years[startIx]}–{data.years[endIx]}), key: "growth" }, + ]; + + const patentSubkeys = Object.keys(data.patents); + + // NOTE: for the time being, I'm hardcoding these to get data to display. The + // final implementation will require discussion and coordination. + const patentApplicationAreas = patentSubkeys.slice(0, 5).map((key) => { + const startVal = data.patents[key].counts[startIx]; + const endVal = data.patents[key].counts[endIx]; + const growth = `${Math.round((endVal - startVal) / startVal * 1000) / 10}%`; + return { + subfield: data.patents[key].name, + patents: data.patents[key].total, + growth, + }; + }); + + const patentIndustryAreas = patentSubkeys.slice(5, 10).map((key) => { + const startVal = data.patents[key].counts[startIx]; + const endVal = data.patents[key].counts[endIx]; + const growth = `${Math.round((endVal - startVal) / startVal * 1000) / 10}%`; + return { + subfield: data.patents[key].name, + patents: data.patents[key].total, + growth, + }; + }); + + // Temporarily using just a generic slice of patents + const aiSubfieldOptions = patentSubkeys.slice(0, 10).map(k => ({ text: patentMap[k], val: k })); + + const aiSubfieldChartData = assemblePlotlyParams( + "Trends in research....", data.years, - patentsData, + [ + [ + aiSubfieldOptions.find(e => e.val === aiSubfield)?.text, + data.patents[aiSubfield].counts + ], + ], chartLayoutChanges, ); - return ( <> -

- Radio telescope light years extraplanetary the sky calls to us billions - upon billions cosmic ocean. The only home we've ever known tesseract - tesseract dream of the mind's eye Apollonius of Perga take root and - flourish? Euclid realm of the galaxies inconspicuous motes of rock and - gas great turbulent clouds decipherment network of wormholes. -

+ Between {data.years[0]} and {data.years[data.years.length-1]}, {data.name} obtained} + bigText={<>{commas(data.patents.ai_patents.total)} AI patents} + /> + + + + Top application areas across {data.name}'s AI patents} + /> + + Top industry areas across {data.name}'s AI patents} + /> - + + Trends in {data.name}'s patenting in + + + } + /> ); }; diff --git a/web/gui-v2/src/components/DetailViewPublications.jsx b/web/gui-v2/src/components/DetailViewPublications.jsx index 539f4798..a7056104 100644 --- a/web/gui-v2/src/components/DetailViewPublications.jsx +++ b/web/gui-v2/src/components/DetailViewPublications.jsx @@ -1,16 +1,37 @@ -import React from 'react'; +import React, { useState } from 'react'; import { css } from '@emotion/react'; +import { Dropdown } from '@eto/eto-ui-components'; + import Chart from './DetailViewChart'; import HeaderWithLink from './HeaderWithLink'; -import StatBox from './StatBox'; -import StatWrapper from './StatWrapper'; +import StatGrid from './StatGrid'; +import TableSection from './TableSection'; +import TextAndBigStat from './TextAndBigStat'; +import TrendsChart from './TrendsChart'; +import { articleMap } from '../static_data/table_columns'; +import { commas } from '../util'; import { assemblePlotlyParams } from '../util/plotly-helpers'; const styles = { noTopMargin: css` margin-top: 0; `, + section: css` + margin: 2rem auto 1rem; + max-width: 808px; + + h3 { + margin-bottom: 0.5rem; + } + `, + trendsDropdown: css` + .MuiInputBase-input.MuiSelect-select { + align-items: center; + display: flex; + justify-content: center; + } + `, }; const chartLayoutChanges = { @@ -26,21 +47,92 @@ const chartLayoutChanges = { const DetailViewPublications = ({ data, }) => { - const allVsAi = assemblePlotlyParams( - "All publications vs topics over time", + const [aiSubfield, setAiSubfield] = useState("ai_publications"); + + const yearSpanNdash = <>{data.years[0]}–{data.years[data.years.length-1]}; + const yearSpanAnd = <>{data.years[0]} and {data.years[data.years.length-1]}; + + const averageCitations = Math.round(10 * data.articles.citation_counts.total / data.articles.all_publications.total) / 10; + const aiResearchPercent = Math.round(1000 * data.articles.ai_publications.total / data.articles.all_publications.total) / 10; + + const numYears = data.years.length; + const startIx = numYears - 7; + const endIx = numYears - 2; + + const statGridEntries = [ + { + key: "ai-papers", + stat: <>#{data.articles.ai_publications.rank}, + text: <>in PARAT for number of AI research articles, + }, + { + key: "average-citations", + stat: <>{averageCitations}, + text: <>citations per article on average (#RANK in PARAT, #RANK in the S&P 500), + }, + { + key: "highly-cited", + stat: <>NUMBER, + text: <>highly-cited articles (#RANK in PARAT, #RANK in the S&P 500), + }, + { + key: "ai-research-growth", + stat: <>NUM%, + text: <>growth in {data.name}'s public AI research ({yearSpanNdash}), + }, + { + key: "ai-top-conf", + stat: <>{commas(data.articles.ai_pubs_top_conf.total)}, + text: <>articles at top AI conferences (#{data.articles.ai_pubs_top_conf.rank} in PARAT, #RANK in the S&P 500), + }, + { + key: "ai-research-percent", + stat: <>{aiResearchPercent}%, + text: <>of {data.name}'s total public research was AI-focused, + }, + ]; + + const topAiResearchTopicsColumns = [ + { display_name: "Subfield", key: "subfield" }, + { display_name: "Articles", key: "articles" }, + { display_name: "Citations per article", key: "citations" }, + { display_name: <>Growth ({data.years[startIx]}–{data.years[endIx]}), key: "growth" }, + ]; + const topAiResearchTopics = Object.entries(data.articles) + .filter(([key, _val]) => ['cv_pubs', 'nlp_pubs', 'robotics_pubs'].includes(key)) + .map(([key, val]) => { + const startVal = val.counts[startIx]; + const endVal = val.counts[endIx]; + + return { + subfield: articleMap[key], + articles: val.total, + citations: "???", + growth: `${Math.round((endVal - startVal) / startVal * 1000) / 10}%`, + }; + }); + + const aiSubfieldOptions = [ + { text: "AI (all subtopics)", val: "ai_publications" }, + { text: "Computer vision", val: "cv_pubs" }, + { text: "Natural language processing", val: "nlp_pubs" }, + { text: "Robotics", val: "robotics_pubs" }, + ]; + + const aiSubfieldChartData = assemblePlotlyParams( + "Trends in research....", data.years, [ - ["All publications", data.articles.all_publications.counts], - ["AI publications", data.articles.ai_publications.counts], - ["CV publications", data.articles.cv_pubs.counts], - ["NLP publications", data.articles.nlp_pubs.counts], - ["Robotics publications", data.articles.robotics_pubs.counts], + [ + aiSubfieldOptions.find(e => e.val === aiSubfield)?.text, + data.articles[aiSubfield].counts + ], ], chartLayoutChanges, ); const topConfs = assemblePlotlyParams( - "AI top conference publications", + <>{data.name}'s top AI conference publications, data.years, [ ["AI top conference publications", data.articles.ai_pubs_top_conf.counts], @@ -48,43 +140,47 @@ const DetailViewPublications = ({ chartLayoutChanges, ); - const averageCitations = Math.round(10 * data.articles.citation_counts.total / data.articles.all_publications.total) / 10; - return ( <> -

- Radio telescope light years extraplanetary the sky calls to us billions - upon billions cosmic ocean. The only home we've ever known tesseract - tesseract dream of the mind's eye Apollonius of Perga take root and - flourish? Euclid realm of the galaxies inconspicuous motes of rock and - gas great turbulent clouds decipherment network of wormholes. -

- -

- The carbon in our apple pies circumnavigated venture worldlets Orion's - sword network of wormholes. Permanence of the stars another world - preserve and cherish that pale blue dot kindling the energy hidden in - matter muse about vastness is bearable only through love. Hearts of the - stars realm of the galaxies birth dispassionate extraterrestrial - observer vastness is bearable only through love not a sunrise but a - galaxyrise. Encyclopaedia galactica rich in heavy atoms made in the - interiors of collapsing stars descended from astronomers the only home - we've ever known. -

- -

- Brain is the seed of intelligence a mote of dust suspended in a sunbeam - light years ship of the imagination cosmic ocean muse about. Finite but - unbounded a still more glorious dawn awaits permanence of the stars - vanquish the impossible bits of moving fluff corpus callosum. Vanquish - the impossible preserve and cherish that pale blue dot citizens of - distant epochs inconspicuous motes of rock and gas. -

- - - + Between {yearSpanAnd}, {data.name} researchers released} + bigText={<>{commas(data.articles.ai_publications.total)} AI research articles} + /> + + + + {data.name}'s top AI research topics} + /> + + + Trends in {data.name}'s research in + + + } + /> + +
+ +
); }; diff --git a/web/gui-v2/src/components/DetailViewWorkforce.jsx b/web/gui-v2/src/components/DetailViewWorkforce.jsx index 0ac26b69..5d4843f6 100644 --- a/web/gui-v2/src/components/DetailViewWorkforce.jsx +++ b/web/gui-v2/src/components/DetailViewWorkforce.jsx @@ -4,27 +4,34 @@ import HeaderWithLink from './HeaderWithLink'; import StatBox from './StatBox'; import StatWrapper from './StatWrapper'; +const DetailViewWorkforce = ({ + data, +}) => { + const yearSpanText = <>{data.years[0]} to {data.years[data.years.length-1]}; + + const otherMetricsWorkforceKeys = ['ai_jobs', 'tt1_jobs']; -const DetailViewWorkforce = () => { return ( <> - - + { otherMetricsWorkforceKeys.map((key) => ( + + From {yearSpanText}, {data.name} here is some explanatory text + describing how they had NUMBER jobs of the specified type + (#{data.other_metrics[key].rank} rank in PARAT + {data.in_sandp_500 && <>, #NUMBER in the S&P500}) + + } + key={key} + label={data.other_metrics[key].name} + value={data.other_metrics[key].total} + /> + ))} -

- The carbon in our apple pies circumnavigated venture worldlets Orion's - sword network of wormholes. Permanence of the stars another world - preserve and cherish that pale blue dot kindling the energy hidden in - matter muse about vastness is bearable only through love. Hearts of the - stars realm of the galaxies birth dispassionate extraterrestrial - observer vastness is bearable only through love not a sunrise but a - galaxyrise. Encyclopaedia galactica rich in heavy atoms made in the - interiors of collapsing stars descended from astronomers the only home - we've ever known. -

); }; diff --git a/web/gui-v2/src/components/SectionHeading.jsx b/web/gui-v2/src/components/SectionHeading.jsx new file mode 100644 index 00000000..a6340cc0 --- /dev/null +++ b/web/gui-v2/src/components/SectionHeading.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { css } from '@emotion/react'; + +const styles = { + chartTitle: css` + font-family: GTZirkonMedium; + font-size: 24px; + margin-bottom: 0; + text-align: center; + `, +}; + +const SectionHeading = ({ children, id }) => { + return ( +

+ {children} +

+ ); +}; + +export default SectionHeading; diff --git a/web/gui-v2/src/components/StatBox.jsx b/web/gui-v2/src/components/StatBox.jsx index 62fce0a1..b9a7cd71 100644 --- a/web/gui-v2/src/components/StatBox.jsx +++ b/web/gui-v2/src/components/StatBox.jsx @@ -4,6 +4,11 @@ import { css } from '@emotion/react'; import { commas } from '../util'; const styles = { + outerWrapper: css` + column-gap: 2rem; + display: grid; + grid-template-columns: 240px 1fr; + `, statbox: css` align-items: center; background-color: var(--bright-blue-lighter); @@ -11,7 +16,7 @@ const styles = { color: var(--bright-blue); display: flex; flex-direction: column; - padding: 1rem 3rem; + padding: 1rem 1rem; `, label: css` font-size: 1.25rem; @@ -19,17 +24,25 @@ const styles = { value: css` font-size: 2.5rem; `, + description: css` + align-items: center; + display: flex; + `, }; const StatBox = ({ + description, label, value, }) => { return ( -
-
{commas(value)}
-
{label}
+
+
+
{commas(value)}
+
{label}
+
+
{description}
); }; diff --git a/web/gui-v2/src/components/StatGrid.jsx b/web/gui-v2/src/components/StatGrid.jsx new file mode 100644 index 00000000..f9e67206 --- /dev/null +++ b/web/gui-v2/src/components/StatGrid.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { css } from '@emotion/react'; + +import { breakpoints } from '@eto/eto-ui-components'; + +const styles = { + stats: css` + display: grid; + gap: 0.5rem; + grid-template-columns: minmax(0, 400px); + list-style: none; + margin: 1rem auto; + max-width: fit-content; + padding: 0; + + ${breakpoints.tablet_regular} { + grid-template-columns: repeat(2, minmax(0, 400px)); + } + + & > li { + align-content: center; + display: grid; + gap: 1rem; + grid-template-columns: 80px 1fr; + max-width: 400px; + padding: 0.5rem; + + & > div { + align-items: center; + display: flex; + + &:first-of-type { + font-family: GTZirkonMedium; + font-size: 200%; + justify-content: right; + } + } + } + `, +}; + + +/** + * A responsive grid of boxes, each presenting a statistic and an explanation. + * + * @param {object} props + * @param {Array<{stat: ReactNode, text: ReactNode}>} props.entries + * @returns {JSX.Element} + */ +const StatGrid = ({ + className: appliedClassName, + css: appliedCss, + entries, + id: appliedId, +}) => { + return ( +
    + { + entries.map((entry) => { + return ( +
  • +
    {entry.stat}
    +
    {entry.text}
    +
  • + ); + }) + } +
+ ); +}; + +export default StatGrid; diff --git a/web/gui-v2/src/components/StatWrapper.jsx b/web/gui-v2/src/components/StatWrapper.jsx index b868212f..32b8e8e1 100644 --- a/web/gui-v2/src/components/StatWrapper.jsx +++ b/web/gui-v2/src/components/StatWrapper.jsx @@ -4,8 +4,10 @@ import { css } from '@emotion/react'; const styles = { statWrapper: css` display: flex; + flex-direction: column; + gap: 2rem; justify-content: space-around; - margin: 1rem auto; + margin: 2rem auto; max-width: 720px; `, }; diff --git a/web/gui-v2/src/components/TableSection.jsx b/web/gui-v2/src/components/TableSection.jsx new file mode 100644 index 00000000..1839776f --- /dev/null +++ b/web/gui-v2/src/components/TableSection.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { css } from '@emotion/react'; + +import { Table } from '@eto/eto-ui-components'; + +import SectionHeading from './SectionHeading'; + +const styles = { + tableWrapper: css` + margin: 1rem auto; + max-width: 808px; + `, + table: css` + max-width: 808px; + `, +}; + +const TableSection = ({ + className: appliedClassName, + columns, + css: appliedCss, + data, + id: appliedId, + title, +}) => { + return ( +
+ + {title} + + + + ); +}; + +export default TableSection; diff --git a/web/gui-v2/src/components/TextAndBigStat.jsx b/web/gui-v2/src/components/TextAndBigStat.jsx new file mode 100644 index 00000000..a9d4b32a --- /dev/null +++ b/web/gui-v2/src/components/TextAndBigStat.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { css } from '@emotion/react'; + +const styles = css` + align-items: center; + column-gap: 0.5rem; + display: flex; + flex-wrap: wrap; + justify-content: center; + margin-top: 1rem; + + span { + font-size: 120%; + text-align: center; + } + + big { + font-family: GTZirkonRegular; + font-size: 180%; + } +`; + +const TextAndBigStat = ({ + bigText, + className: appliedClassName, + css: appliedCss, + id: appliedId, + smallText, +}) => { + return ( +
+ {smallText} + {bigText} +
+ ); +}; + +export default TextAndBigStat; diff --git a/web/gui-v2/src/components/TrendsChart.jsx b/web/gui-v2/src/components/TrendsChart.jsx new file mode 100644 index 00000000..201103fa --- /dev/null +++ b/web/gui-v2/src/components/TrendsChart.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { css } from '@emotion/react'; + +import Chart from './DetailViewChart'; + +const styles = { + chartWrapper: css` + h3 { + align-items: center; + display: flex; + flex-wrap: wrap; + justify-content: center; + margin: 0 auto; + width: fit-content; + + .dropdown .MuiFormControl-root { + margin: 0; + margin-left: 0.5rem; + } + } + `, +}; + +const TrendsChart = ({ + className: appliedClassName, + css: appliedCss, + id: appliedId, + title, + ...otherProps +}) => { + return ( +
+ +
+ ); +}; + +export default TrendsChart; diff --git a/web/gui-v2/src/components/TwoColumnTable.jsx b/web/gui-v2/src/components/TwoColumnTable.jsx index 0383ea89..0513c82a 100644 --- a/web/gui-v2/src/components/TwoColumnTable.jsx +++ b/web/gui-v2/src/components/TwoColumnTable.jsx @@ -56,7 +56,7 @@ const TwoColumnTable = ({ > {data.map((row) => ( - + diff --git a/web/gui-v2/src/static_data/table_columns.js b/web/gui-v2/src/static_data/table_columns.js index a354c609..ed8a4c4b 100644 --- a/web/gui-v2/src/static_data/table_columns.js +++ b/web/gui-v2/src/static_data/table_columns.js @@ -54,7 +54,7 @@ const generateSliderColDef = (dataKey, dataSubkey) => { } }; -export default [ +const columnDefinitions = [ { title: "Company", key: "name", @@ -219,3 +219,14 @@ export default [ ...generateSliderColDef("other_metrics", "tt1_jobs"), }, ]; +export default columnDefinitions; + +export const articleMap = Object.fromEntries(columnDefinitions + .filter(e => e.dataKey === 'articles') + .map(e => ([e.dataSubkey, e.title])) +); + +export const patentMap = Object.fromEntries(columnDefinitions + .filter(e => e.dataKey === 'patents') + .map(e => ([e.dataSubkey, e.title])) +);
{row.title} {row.value ?? None found}